1#[cfg(target_os = "linux")]
4use std::fs;
5use std::path::Path;
6use std::process::Command;
7
8pub struct GpuInfo {
13 pub index: u8,
14 pub name: String,
15 pub sm_major: u32,
16 pub sm_minor: u32,
17 pub total_memory_mb: u64,
18}
19
20impl GpuInfo {
21 pub fn sm_version(&self) -> String {
22 format!("sm_{}{}", self.sm_major, self.sm_minor)
23 }
24
25 pub fn vram_bytes(&self) -> u64 {
26 self.total_memory_mb * 1024 * 1024
27 }
28
29 pub fn short_name(&self) -> String {
30 self.name.replace("NVIDIA ", "").replace("GeForce ", "")
31 }
32}
33
34pub fn detect_gpus() -> Vec<GpuInfo> {
35 let output = match Command::new("nvidia-smi")
36 .args([
37 "--query-gpu=index,name,compute_cap,memory.total",
38 "--format=csv,noheader,nounits",
39 ])
40 .output()
41 {
42 Ok(o) if o.status.success() => o,
43 _ => return Vec::new(),
44 };
45
46 let stdout = String::from_utf8_lossy(&output.stdout);
47 stdout
48 .lines()
49 .filter_map(|line| {
50 let parts: Vec<&str> = line.splitn(4, ", ").collect();
51 if parts.len() < 4 {
52 return None;
53 }
54 let index: u8 = parts[0].trim().parse().ok()?;
55 let name = parts[1].trim().to_string();
56 let cap_parts: Vec<&str> = parts[2].trim().split('.').collect();
57 let sm_major: u32 = cap_parts.first()?.parse().ok()?;
58 let sm_minor: u32 = cap_parts.get(1)?.parse().ok()?;
59 let total_memory_mb: u64 = parts[3].trim().parse().ok()?;
60 Some(GpuInfo {
61 index,
62 name,
63 sm_major,
64 sm_minor,
65 total_memory_mb,
66 })
67 })
68 .collect()
69}
70
71pub fn nvidia_driver_version() -> Option<String> {
72 let output = Command::new("nvidia-smi")
73 .args(["--query-gpu=driver_version", "--format=csv,noheader"])
74 .output()
75 .ok()?;
76 if !output.status.success() {
77 return None;
78 }
79 let s = String::from_utf8_lossy(&output.stdout);
80 Some(s.lines().next()?.trim().to_string())
81}
82
83#[cfg(target_os = "linux")]
88pub fn cpu_model() -> Option<String> {
89 let info = fs::read_to_string("/proc/cpuinfo").ok()?;
90 for line in info.lines() {
91 if let Some(rest) = line.strip_prefix("model name") {
92 if let Some(val) = rest.split(':').nth(1) {
93 return Some(val.trim().to_string());
94 }
95 }
96 }
97 None
98}
99
100#[cfg(target_os = "macos")]
101pub fn cpu_model() -> Option<String> {
102 let out = Command::new("sysctl")
103 .args(["-n", "machdep.cpu.brand_string"])
104 .output()
105 .ok()?;
106 if !out.status.success() {
107 return None;
108 }
109 let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
110 if s.is_empty() { None } else { Some(s) }
111}
112
113#[cfg(target_os = "windows")]
114pub fn cpu_model() -> Option<String> {
115 let out = Command::new("wmic")
116 .args(["cpu", "get", "Name", "/value"])
117 .output()
118 .ok()?;
119 let s = String::from_utf8_lossy(&out.stdout);
120 for line in s.lines() {
121 if let Some(val) = line.strip_prefix("Name=") {
122 let v = val.trim();
123 if !v.is_empty() {
124 return Some(v.to_string());
125 }
126 }
127 }
128 None
129}
130
131#[cfg(target_os = "linux")]
132pub fn cpu_threads() -> usize {
133 fs::read_to_string("/proc/cpuinfo")
134 .ok()
135 .map(|s| s.lines().filter(|l| l.starts_with("processor")).count())
136 .unwrap_or(1)
137}
138
139#[cfg(target_os = "macos")]
140pub fn cpu_threads() -> usize {
141 Command::new("sysctl")
142 .args(["-n", "hw.logicalcpu"])
143 .output()
144 .ok()
145 .and_then(|o| {
146 String::from_utf8_lossy(&o.stdout)
147 .trim()
148 .parse()
149 .ok()
150 })
151 .unwrap_or(1)
152}
153
154#[cfg(target_os = "windows")]
155pub fn cpu_threads() -> usize {
156 std::env::var("NUMBER_OF_PROCESSORS")
157 .ok()
158 .and_then(|v| v.parse().ok())
159 .unwrap_or(1)
160}
161
162#[cfg(target_os = "linux")]
167pub fn ram_total_gb() -> u64 {
168 fs::read_to_string("/proc/meminfo")
169 .ok()
170 .and_then(|s| {
171 for line in s.lines() {
172 if let Some(rest) = line.strip_prefix("MemTotal:") {
173 let kb: u64 = rest.split_whitespace().next()?.parse().ok()?;
174 return Some(kb / (1024 * 1024));
175 }
176 }
177 None
178 })
179 .unwrap_or(0)
180}
181
182#[cfg(target_os = "macos")]
183pub fn ram_total_gb() -> u64 {
184 Command::new("sysctl")
185 .args(["-n", "hw.memsize"])
186 .output()
187 .ok()
188 .and_then(|o| {
189 let bytes: u64 = String::from_utf8_lossy(&o.stdout)
190 .trim()
191 .parse()
192 .ok()?;
193 Some(bytes / (1024 * 1024 * 1024))
194 })
195 .unwrap_or(0)
196}
197
198#[cfg(target_os = "windows")]
199pub fn ram_total_gb() -> u64 {
200 Command::new("wmic")
201 .args(["os", "get", "TotalVisibleMemorySize", "/value"])
202 .output()
203 .ok()
204 .and_then(|o| {
205 let s = String::from_utf8_lossy(&o.stdout);
206 for line in s.lines() {
207 if let Some(val) = line.strip_prefix("TotalVisibleMemorySize=") {
208 let kb: u64 = val.trim().parse().ok()?;
209 return Some(kb / (1024 * 1024));
210 }
211 }
212 None
213 })
214 .unwrap_or(0)
215}
216
217#[cfg(target_os = "linux")]
222pub fn os_version() -> Option<String> {
223 let uname = Command::new("uname").arg("-r").output().ok()?;
224 let kernel = String::from_utf8_lossy(&uname.stdout).trim().to_string();
225 let wsl = if kernel.contains("WSL") || kernel.contains("microsoft") {
226 " (WSL2)"
227 } else {
228 ""
229 };
230 Some(format!("Linux {}{}", kernel, wsl))
231}
232
233#[cfg(target_os = "macos")]
234pub fn os_version() -> Option<String> {
235 let out = Command::new("sw_vers")
236 .args(["-productVersion"])
237 .output()
238 .ok()?;
239 let ver = String::from_utf8_lossy(&out.stdout).trim().to_string();
240 if ver.is_empty() {
241 return None;
242 }
243 let arch = Command::new("uname")
244 .arg("-m")
245 .output()
246 .ok()
247 .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
248 .unwrap_or_default();
249 if arch.is_empty() {
250 Some(format!("macOS {}", ver))
251 } else {
252 Some(format!("macOS {} ({})", ver, arch))
253 }
254}
255
256#[cfg(target_os = "windows")]
257pub fn os_version() -> Option<String> {
258 let out = Command::new("cmd")
259 .args(["/C", "ver"])
260 .output()
261 .ok()?;
262 let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
263 if s.is_empty() { None } else { Some(s) }
264}
265
266pub fn is_inside_docker() -> bool {
271 Path::new("/.dockerenv").exists()
272}
273
274pub fn docker_version() -> Option<String> {
275 let out = Command::new("docker").arg("--version").output().ok()?;
276 if !out.status.success() {
277 return None;
278 }
279 let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
280 s.split("version ")
281 .nth(1)
282 .and_then(|v| v.split(',').next())
283 .map(|v| v.trim().to_string())
284}
285
286#[allow(dead_code)]
288pub fn has_cargo() -> bool {
289 Command::new("cargo")
290 .arg("--version")
291 .output()
292 .is_ok_and(|o| o.status.success())
293}
294
295#[allow(dead_code)]
301pub fn has_command(name: &str) -> bool {
302 Command::new(name)
303 .arg("--version")
304 .stdout(std::process::Stdio::null())
305 .stderr(std::process::Stdio::null())
306 .status()
307 .is_ok()
308}
309
310#[allow(dead_code)]
312pub fn platform_tag() -> Option<String> {
313 let os = std::env::consts::OS;
314 let arch = std::env::consts::ARCH;
315 match (os, arch) {
316 ("linux", "x86_64") => Some("linux-x86_64".into()),
317 ("macos", "aarch64") => Some("macos-arm64".into()),
318 ("windows", "x86_64") => Some("windows-x86_64".into()),
319 _ => None,
320 }
321}
322
323pub fn escape_json(s: &str) -> String {
324 s.replace('\\', "\\\\")
325 .replace('"', "\\\"")
326 .replace('\n', "\\n")
327}