1#[cfg(target_os = "linux")]
4use std::fs;
5use std::path::Path;
6use std::process::Command;
7
8pub use flodl_hw::{
25 GpuInfo, GpuVendor, detect_gpus_physical as detect_gpus, nvidia_driver_version,
26};
27
28#[cfg(target_os = "linux")]
33pub fn cpu_model() -> Option<String> {
34 let info = fs::read_to_string("/proc/cpuinfo").ok()?;
35 for line in info.lines() {
36 if let Some(rest) = line.strip_prefix("model name")
37 && let Some(val) = rest.split(':').nth(1)
38 {
39 return Some(val.trim().to_string());
40 }
41 }
42 None
43}
44
45#[cfg(target_os = "macos")]
46pub fn cpu_model() -> Option<String> {
47 let out = Command::new("sysctl")
48 .args(["-n", "machdep.cpu.brand_string"])
49 .output()
50 .ok()?;
51 if !out.status.success() {
52 return None;
53 }
54 let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
55 if s.is_empty() { None } else { Some(s) }
56}
57
58#[cfg(target_os = "windows")]
59pub fn cpu_model() -> Option<String> {
60 let out = Command::new("wmic")
61 .args(["cpu", "get", "Name", "/value"])
62 .output()
63 .ok()?;
64 let s = String::from_utf8_lossy(&out.stdout);
65 for line in s.lines() {
66 if let Some(val) = line.strip_prefix("Name=") {
67 let v = val.trim();
68 if !v.is_empty() {
69 return Some(v.to_string());
70 }
71 }
72 }
73 None
74}
75
76#[cfg(target_os = "linux")]
77pub fn cpu_threads() -> usize {
78 fs::read_to_string("/proc/cpuinfo")
79 .ok()
80 .map(|s| s.lines().filter(|l| l.starts_with("processor")).count())
81 .unwrap_or(1)
82}
83
84#[cfg(target_os = "macos")]
85pub fn cpu_threads() -> usize {
86 Command::new("sysctl")
87 .args(["-n", "hw.logicalcpu"])
88 .output()
89 .ok()
90 .and_then(|o| String::from_utf8_lossy(&o.stdout).trim().parse().ok())
91 .unwrap_or(1)
92}
93
94#[cfg(target_os = "windows")]
95pub fn cpu_threads() -> usize {
96 std::env::var("NUMBER_OF_PROCESSORS")
97 .ok()
98 .and_then(|v| v.parse().ok())
99 .unwrap_or(1)
100}
101
102#[cfg(target_os = "linux")]
107pub fn ram_total_gb() -> u64 {
108 fs::read_to_string("/proc/meminfo")
109 .ok()
110 .and_then(|s| {
111 for line in s.lines() {
112 if let Some(rest) = line.strip_prefix("MemTotal:") {
113 let kb: u64 = rest.split_whitespace().next()?.parse().ok()?;
114 return Some(kb / (1024 * 1024));
115 }
116 }
117 None
118 })
119 .unwrap_or(0)
120}
121
122#[cfg(target_os = "macos")]
123pub fn ram_total_gb() -> u64 {
124 Command::new("sysctl")
125 .args(["-n", "hw.memsize"])
126 .output()
127 .ok()
128 .and_then(|o| {
129 let bytes: u64 = String::from_utf8_lossy(&o.stdout).trim().parse().ok()?;
130 Some(bytes / (1024 * 1024 * 1024))
131 })
132 .unwrap_or(0)
133}
134
135#[cfg(target_os = "windows")]
136pub fn ram_total_gb() -> u64 {
137 Command::new("wmic")
138 .args(["os", "get", "TotalVisibleMemorySize", "/value"])
139 .output()
140 .ok()
141 .and_then(|o| {
142 let s = String::from_utf8_lossy(&o.stdout);
143 for line in s.lines() {
144 if let Some(val) = line.strip_prefix("TotalVisibleMemorySize=") {
145 let kb: u64 = val.trim().parse().ok()?;
146 return Some(kb / (1024 * 1024));
147 }
148 }
149 None
150 })
151 .unwrap_or(0)
152}
153
154#[cfg(target_os = "linux")]
159pub fn os_version() -> Option<String> {
160 let uname = Command::new("uname").arg("-r").output().ok()?;
161 let kernel = String::from_utf8_lossy(&uname.stdout).trim().to_string();
162 let wsl = if kernel.contains("WSL") || kernel.contains("microsoft") {
163 " (WSL2)"
164 } else {
165 ""
166 };
167 Some(format!("Linux {}{}", kernel, wsl))
168}
169
170#[cfg(target_os = "macos")]
171pub fn os_version() -> Option<String> {
172 let out = Command::new("sw_vers")
173 .args(["-productVersion"])
174 .output()
175 .ok()?;
176 let ver = String::from_utf8_lossy(&out.stdout).trim().to_string();
177 if ver.is_empty() {
178 return None;
179 }
180 let arch = Command::new("uname")
181 .arg("-m")
182 .output()
183 .ok()
184 .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
185 .unwrap_or_default();
186 if arch.is_empty() {
187 Some(format!("macOS {}", ver))
188 } else {
189 Some(format!("macOS {} ({})", ver, arch))
190 }
191}
192
193#[cfg(target_os = "windows")]
194pub fn os_version() -> Option<String> {
195 let out = Command::new("cmd").args(["/C", "ver"]).output().ok()?;
196 let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
197 if s.is_empty() { None } else { Some(s) }
198}
199
200pub fn is_inside_docker() -> bool {
205 Path::new("/.dockerenv").exists()
206}
207
208pub fn docker_version() -> Option<String> {
209 let out = Command::new("docker").arg("--version").output().ok()?;
210 if !out.status.success() {
211 return None;
212 }
213 let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
214 s.split("version ")
215 .nth(1)
216 .and_then(|v| v.split(',').next())
217 .map(|v| v.trim().to_string())
218}
219
220#[allow(dead_code)]
222pub fn has_cargo() -> bool {
223 Command::new("cargo")
224 .arg("--version")
225 .output()
226 .is_ok_and(|o| o.status.success())
227}
228
229#[allow(dead_code)]
235pub fn has_command(name: &str) -> bool {
236 Command::new(name)
237 .arg("--version")
238 .stdout(std::process::Stdio::null())
239 .stderr(std::process::Stdio::null())
240 .status()
241 .is_ok()
242}
243
244#[allow(dead_code)]
246pub fn platform_tag() -> Option<String> {
247 let os = std::env::consts::OS;
248 let arch = std::env::consts::ARCH;
249 match (os, arch) {
250 ("linux", "x86_64") => Some("linux-x86_64".into()),
251 ("macos", "aarch64") => Some("macos-arm64".into()),
252 ("windows", "x86_64") => Some("windows-x86_64".into()),
253 _ => None,
254 }
255}
256
257pub fn escape_json(s: &str) -> String {
263 let mut out = String::with_capacity(s.len());
264 for c in s.chars() {
265 match c {
266 '\\' => out.push_str("\\\\"),
267 '"' => out.push_str("\\\""),
268 '\n' => out.push_str("\\n"),
269 '\r' => out.push_str("\\r"),
270 '\t' => out.push_str("\\t"),
271 '\u{08}' => out.push_str("\\b"),
272 '\u{0C}' => out.push_str("\\f"),
273 c if (c as u32) < 0x20 => {
274 let _ = std::fmt::Write::write_fmt(&mut out, format_args!("\\u{:04x}", c as u32));
275 }
276 c => out.push(c),
277 }
278 }
279 out
280}
281
282pub fn arch_dir_name(archs: &str) -> String {
292 archs
293 .split(';')
294 .map(|tok| {
295 let tok = tok.trim();
296 match flodl_hw::GpuArch::parse(flodl_hw::GpuVendor::Amd, tok) {
300 Some(arch) => arch.to_string(),
301 None => format!("sm{}", tok.replace('.', "")),
302 }
303 })
304 .collect::<Vec<_>>()
305 .join("-")
306}
307
308#[cfg(test)]
309mod tests {
310 use super::{arch_dir_name, escape_json};
311
312 #[test]
313 fn escape_json_passes_through_plain_ascii() {
314 assert_eq!(escape_json("hello world 123"), "hello world 123");
315 }
316
317 #[test]
318 fn escape_json_escapes_quotes_and_backslashes() {
319 assert_eq!(escape_json(r#"C:\a\b"#), r#"C:\\a\\b"#);
322 assert_eq!(escape_json(r#"say "hi""#), r#"say \"hi\""#);
323 }
324
325 #[test]
326 fn escape_json_escapes_named_control_chars() {
327 assert_eq!(escape_json("a\nb\rc\td"), "a\\nb\\rc\\td");
328 assert_eq!(escape_json("\u{08}\u{0C}"), "\\b\\f");
329 }
330
331 #[test]
332 fn escape_json_uescapes_other_control_chars() {
333 assert_eq!(escape_json("\u{01}"), "\\u0001");
335 assert_eq!(escape_json("\u{1f}"), "\\u001f");
336 }
337
338 #[test]
339 fn escape_json_leaves_non_ascii_unescaped() {
340 assert_eq!(escape_json("café — 日本"), "café — 日本");
342 }
343
344 #[test]
345 fn arch_dir_name_single() {
346 assert_eq!(arch_dir_name("12.0"), "sm120");
347 }
348
349 #[test]
350 fn arch_dir_name_multi() {
351 assert_eq!(arch_dir_name("6.1;12.0"), "sm61-sm120");
352 }
353
354 #[test]
355 fn arch_dir_name_strips_all_dots() {
356 assert_eq!(arch_dir_name("7.5"), "sm75");
358 assert_eq!(arch_dir_name("8.0;8.6;9.0"), "sm80-sm86-sm90");
359 }
360}