1use std::fmt;
2use std::process::Command;
3
4use sysinfo::{Disks, System};
5
6use crate::manifest::CudaVersion;
7
8#[derive(Debug, Clone)]
11pub struct GpuInfo {
12 pub name: String,
13 pub vram_mb: u64,
14 pub driver_version: Option<String>,
15 pub cuda_version: Option<CudaVersion>,
16}
17
18#[derive(Debug, Clone)]
19pub struct DetectedHardware {
20 pub cpu_cores: u32,
21 pub ram_mb: u64,
22 pub disk_free_mb: u64,
23 pub gpus: Vec<GpuInfo>,
24}
25
26impl DetectedHardware {
27 pub fn detect() -> Self {
28 let mut sys = System::new();
29 sys.refresh_cpu_all();
30 sys.refresh_memory();
31
32 let cpu_cores = sys.cpus().len() as u32;
33 let ram_mb = sys.total_memory() / (1024 * 1024);
34
35 let disk_free_mb = {
36 let disks = Disks::new_with_refreshed_list();
37 disks
39 .iter()
40 .map(|d| d.available_space() / (1024 * 1024))
41 .max()
42 .unwrap_or(0)
43 };
44
45 let gpus = detect_gpus();
46
47 Self {
48 cpu_cores,
49 ram_mb,
50 disk_free_mb,
51 gpus,
52 }
53 }
54
55 pub fn ram_gb(&self) -> f64 {
56 self.ram_mb as f64 / 1024.0
57 }
58
59 pub fn disk_free_gb(&self) -> f64 {
60 self.disk_free_mb as f64 / 1024.0
61 }
62}
63
64#[derive(Debug, Clone)]
67pub enum HardwareMismatch {
68 NoGpu,
69 InsufficientVram {
70 required_gb: u32,
71 available_gb: u32,
72 },
73 CudaTooOld {
74 required: CudaVersion,
75 available: CudaVersion,
76 },
77 NoCuda {
78 required: CudaVersion,
79 },
80 InsufficientRam {
81 required_gb: f64,
82 available_gb: f64,
83 },
84 InsufficientDisk {
85 required_gb: f64,
86 available_gb: f64,
87 },
88}
89
90impl fmt::Display for HardwareMismatch {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 match self {
93 HardwareMismatch::NoGpu => {
94 write!(f, "NVIDIA GPU required but none detected")
95 }
96 HardwareMismatch::InsufficientVram {
97 required_gb,
98 available_gb,
99 } => {
100 write!(
101 f,
102 "GPU requires ≥{required_gb} GB VRAM, but best available is {available_gb} GB"
103 )
104 }
105 HardwareMismatch::CudaTooOld {
106 required,
107 available,
108 } => {
109 write!(f, "CUDA ≥{required} required, driver supports {available}")
110 }
111 HardwareMismatch::NoCuda { required } => {
112 write!(f, "CUDA ≥{required} required but no CUDA driver detected")
113 }
114 HardwareMismatch::InsufficientRam {
115 required_gb,
116 available_gb,
117 } => {
118 write!(
119 f,
120 "{required_gb:.0} GB RAM required, only {available_gb:.1} GB available"
121 )
122 }
123 HardwareMismatch::InsufficientDisk {
124 required_gb,
125 available_gb,
126 } => {
127 write!(
128 f,
129 "{required_gb:.0} GB free disk required, only {available_gb:.1} GB available"
130 )
131 }
132 }
133 }
134}
135
136fn detect_gpus() -> Vec<GpuInfo> {
139 let output = match Command::new("nvidia-smi")
140 .args([
141 "--query-gpu=name,memory.total,driver_version",
142 "--format=csv,noheader,nounits",
143 ])
144 .output()
145 {
146 Ok(o) if o.status.success() => o,
147 _ => return vec![],
148 };
149
150 let cuda_ver = detect_cuda_version();
151 let stdout = String::from_utf8_lossy(&output.stdout);
152
153 stdout
154 .lines()
155 .filter_map(|line| parse_gpu_csv(line, cuda_ver.clone()))
156 .collect()
157}
158
159fn parse_gpu_csv(line: &str, cuda_version: Option<CudaVersion>) -> Option<GpuInfo> {
160 let parts: Vec<&str> = line.splitn(3, ',').map(str::trim).collect();
161 if parts.len() < 2 {
162 return None;
163 }
164 let name = parts[0].to_string();
165 let vram_mb = parts[1].parse::<u64>().ok()?;
166 let driver_version = parts.get(2).map(|s| s.to_string());
167
168 Some(GpuInfo {
169 name,
170 vram_mb,
171 driver_version,
172 cuda_version,
173 })
174}
175
176fn detect_cuda_version() -> Option<CudaVersion> {
178 let output = Command::new("nvidia-smi").output().ok()?;
179 if !output.status.success() {
180 return None;
181 }
182 let stdout = String::from_utf8_lossy(&output.stdout);
183 for line in stdout.lines() {
184 if let Some(rest) = line.find("CUDA Version:").map(|i| &line[i + 13..])
185 && let Some(ver_str) = rest.split_whitespace().next()
186 {
187 return ver_str.parse().ok();
188 }
189 }
190 None
191}