1use crate::state::{
2 GPUInfo, HardwareState, PreflightCheck, PreflightResult, PreflightStatus, PreflightType,
3 SystemInfo,
4};
5use anyhow::Result;
6use std::fs;
7use std::io::Write;
8use std::path::{Path, PathBuf};
9use std::process::{Command, Stdio};
10use sysinfo::{Disks, System};
11
12pub fn detect_hardware() -> Result<HardwareState> {
13 let mut state = HardwareState {
14 status: "Detecting system info".into(),
15 progress: 0.2,
16 ..Default::default()
17 };
18
19 let mut sys = System::new_all();
20 sys.refresh_all();
21
22 state.system.os = System::name().unwrap_or_else(|| "Unknown".into());
23 state.system.kernel = System::kernel_version().unwrap_or_else(|| "Unknown".into());
24 state.system.distribution = System::long_os_version().unwrap_or_else(|| "Unknown".into());
25 if state.system.distribution.trim().is_empty() || state.system.distribution == "Unknown" {
26 if let Some(dist) = fallback_distribution() {
27 state.system.distribution = dist;
28 }
29 }
30 state.system.cpu_model = sys
31 .cpus()
32 .first()
33 .map(|c| c.brand().to_string())
34 .unwrap_or_default();
35 if state.system.cpu_model.trim().is_empty() {
36 if let Some(model) = read_cpu_model_name() {
37 state.system.cpu_model = model;
38 }
39 }
40 state.system.memory_gb = (sys.total_memory() as f32) / 1024.0 / 1024.0;
41 if state.system.memory_gb < 0.1 {
42 if let Some(mem) = fallback_memory_gb() {
43 state.system.memory_gb = mem;
44 }
45 }
46
47 let disks = Disks::new_with_refreshed_list();
48 let total_storage: u64 = disks.iter().map(|d| d.total_space()).sum();
49 state.system.storage_gb = total_storage as f32 / 1024.0 / 1024.0 / 1024.0;
50 let total_available: u64 = disks.iter().map(|d| d.available_space()).sum();
51 state.system.storage_available_gb = total_available as f32 / 1024.0 / 1024.0 / 1024.0;
52 if state.system.storage_gb < 0.1 || state.system.storage_available_gb < 0.1 {
53 if let Some((available, total)) = fallback_disk_space_gb() {
54 state.system.storage_available_gb = available;
55 state.system.storage_gb = total;
56 }
57 }
58
59 state.status = "Detecting GPU info".into();
60 state.progress = 0.6;
61 state.gpu = detect_gpu();
62
63 state.status = "Hardware detection complete".into();
64 state.progress = 1.0;
65
66 Ok(state)
67}
68
69pub fn run_preflight_checks(
70 system: &SystemInfo,
71 gpu: &GPUInfo,
72 sudo_password: Option<&str>,
73) -> PreflightResult {
74 let mut result = PreflightResult {
75 can_continue: true,
76 ..Default::default()
77 };
78
79 let checks = vec![
80 check_root_privileges(sudo_password),
81 check_disk_space(system),
82 check_network_connectivity(),
83 check_gpu_detection(gpu),
84 check_driver_compatibility(gpu),
85 check_cpu_compatibility(system),
86 check_memory_requirements(system),
87 check_package_manager(),
88 check_python_availability(),
89 check_system_dependencies(),
90 check_distribution_compatibility(system),
91 ];
92
93 for check in checks {
94 match check.status {
95 PreflightStatus::Passed => result.passed_count += 1,
96 PreflightStatus::Warning => result.warning_count += 1,
97 PreflightStatus::Failed => {
98 result.failed_count += 1;
99 if check.check_type == PreflightType::Critical {
100 result.can_continue = false;
101 }
102 }
103 }
104 result.total_score += check.score;
105 result.checks.push(check);
106 }
107
108 result.passed = result.failed_count == 0;
109 result.summary = if result.passed {
110 format!("Preflight checks passed (score: {})", result.total_score)
111 } else {
112 format!(
113 "Preflight checks reported issues ({} failed)",
114 result.failed_count
115 )
116 };
117
118 result
119}
120
121fn check_root_privileges(sudo_password: Option<&str>) -> PreflightCheck {
122 #[cfg(unix)]
123 let is_root = unsafe { libc::geteuid() == 0 };
124 #[cfg(not(unix))]
125 let is_root = false;
126
127 if is_root {
128 return PreflightCheck {
129 name: "Root privileges".into(),
130 status: PreflightStatus::Passed,
131 check_type: PreflightType::Critical,
132 message: "Running as root".into(),
133 details: "Installer has direct root access".into(),
134 score: 10,
135 };
136 }
137
138 let (status, message, details, score) = if let Some(password) = sudo_password {
139 match validate_sudo_password(password) {
140 Ok(true) => (
141 PreflightStatus::Passed,
142 "Sudo privileges validated".into(),
143 "Sudo password validated".into(),
144 10,
145 ),
146 Ok(false) => (
147 PreflightStatus::Failed,
148 "Sudo validation failed".into(),
149 "Check the sudo password".into(),
150 0,
151 ),
152 Err(err) => (
153 PreflightStatus::Warning,
154 "Sudo validation unavailable".into(),
155 format!("Sudo validation skipped: {}", err),
156 5,
157 ),
158 }
159 } else if check_cached_sudo() {
160 (
161 PreflightStatus::Passed,
162 "Sudo privileges validated".into(),
163 "Cached sudo credentials available".into(),
164 10,
165 )
166 } else {
167 (
168 PreflightStatus::Failed,
169 "Sudo privileges unavailable".into(),
170 "Enter sudo password on the welcome screen".into(),
171 0,
172 )
173 };
174
175 PreflightCheck {
176 name: "Root privileges".into(),
177 status,
178 check_type: PreflightType::Critical,
179 message,
180 details,
181 score,
182 }
183}
184
185fn validate_sudo_password(password: &str) -> Result<bool> {
186 let mut command = Command::new("sudo");
187 command
188 .arg("-S")
189 .arg("-p")
190 .arg("")
191 .arg("-k")
192 .arg("-v")
193 .stdin(Stdio::piped())
194 .stdout(Stdio::piped())
195 .stderr(Stdio::piped());
196
197 let mut child = command.spawn()?;
198 if let Some(mut stdin) = child.stdin.take() {
199 stdin.write_all(password.as_bytes())?;
200 stdin.write_all(b"\n")?;
201 }
202
203 let output = child.wait_with_output()?;
204 Ok(output.status.success())
205}
206
207fn check_cached_sudo() -> bool {
208 Command::new("sudo")
209 .arg("-n")
210 .arg("-v")
211 .stdout(Stdio::null())
212 .stderr(Stdio::null())
213 .status()
214 .map(|status| status.success())
215 .unwrap_or(false)
216}
217
218fn check_disk_space(system: &SystemInfo) -> PreflightCheck {
219 let free_gb = system.storage_available_gb;
220 let total_gb = system.storage_gb;
221
222 if total_gb < 0.1 {
223 return PreflightCheck {
224 name: "Disk space".into(),
225 status: PreflightStatus::Warning,
226 check_type: PreflightType::Critical,
227 message: "Disk space unavailable".into(),
228 details: "Unable to detect disk capacity".into(),
229 score: 2,
230 };
231 }
232
233 let (status, score, details) = if free_gb >= 100.0 {
234 (
235 PreflightStatus::Passed,
236 10,
237 "Sufficient disk space for full ML stack".into(),
238 )
239 } else if free_gb >= 50.0 {
240 (
241 PreflightStatus::Warning,
242 5,
243 "Limited disk space (100GB recommended)".into(),
244 )
245 } else {
246 (
247 PreflightStatus::Failed,
248 0,
249 "Insufficient disk space (minimum 50GB required)".into(),
250 )
251 };
252
253 PreflightCheck {
254 name: "Disk space".into(),
255 status,
256 check_type: PreflightType::Critical,
257 message: format!("{:.1} GB free of {:.1} GB", free_gb, total_gb),
258 details,
259 score,
260 }
261}
262
263fn check_network_connectivity() -> PreflightCheck {
264 let network_ok = Command::new("bash")
265 .arg("-c")
266 .arg("ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1")
267 .status()
268 .map(|s| s.success())
269 .unwrap_or(false);
270
271 PreflightCheck {
272 name: "Network connectivity".into(),
273 status: if network_ok {
274 PreflightStatus::Passed
275 } else {
276 PreflightStatus::Failed
277 },
278 check_type: PreflightType::Critical,
279 message: if network_ok {
280 "Network reachable".into()
281 } else {
282 "Network connectivity failed".into()
283 },
284 details: "Connectivity required for package downloads".into(),
285 score: if network_ok { 10 } else { 0 },
286 }
287}
288
289fn check_gpu_detection(gpu: &GPUInfo) -> PreflightCheck {
290 let ok = gpu.gpu_count > 0;
291 let score = if ok { 15 } else { 0 };
292 PreflightCheck {
293 name: "GPU detection".into(),
294 status: if ok {
295 PreflightStatus::Passed
296 } else {
297 PreflightStatus::Failed
298 },
299 check_type: PreflightType::Critical,
300 message: if ok {
301 format!("Detected {} AMD GPU(s)", gpu.gpu_count)
302 } else {
303 "No AMD GPU detected".into()
304 },
305 details: format!("Primary GPU: {}", gpu.model),
306 score,
307 }
308}
309
310fn check_driver_compatibility(gpu: &GPUInfo) -> PreflightCheck {
311 let ok = !gpu.rocm_version.is_empty();
312 PreflightCheck {
313 name: "Driver compatibility".into(),
314 status: if ok {
315 PreflightStatus::Passed
316 } else {
317 PreflightStatus::Warning
318 },
319 check_type: PreflightType::Warning,
320 message: if ok {
321 "ROCm drivers detected".into()
322 } else {
323 "ROCm drivers not detected".into()
324 },
325 details: if ok {
326 format!("ROCm version {}", gpu.rocm_version)
327 } else {
328 "Install ROCm drivers for GPU acceleration".into()
329 },
330 score: if ok { 10 } else { 5 },
331 }
332}
333
334fn check_cpu_compatibility(system: &SystemInfo) -> PreflightCheck {
335 let is_amd = is_amd_cpu(system);
336 PreflightCheck {
337 name: "CPU compatibility".into(),
338 status: if is_amd {
339 PreflightStatus::Passed
340 } else {
341 PreflightStatus::Warning
342 },
343 check_type: PreflightType::Warning,
344 message: if is_amd {
345 "AMD CPU detected".into()
346 } else {
347 "Non-AMD CPU detected".into()
348 },
349 details: if system.cpu_model.trim().is_empty() {
350 "CPU model unavailable".into()
351 } else {
352 system.cpu_model.clone()
353 },
354 score: if is_amd { 10 } else { 5 },
355 }
356}
357
358fn is_amd_cpu(system: &SystemInfo) -> bool {
359 let model = system.cpu_model.to_lowercase();
360 if model.contains("amd") || model.contains("ryzen") || model.contains("epyc") {
361 return true;
362 }
363
364 if let Ok(cpuinfo) = fs::read_to_string("/proc/cpuinfo") {
365 for line in cpuinfo.lines() {
366 let lower = line.to_lowercase();
367 if lower.starts_with("vendor_id") && lower.contains("authenticamd") {
368 return true;
369 }
370 if lower.starts_with("model name") && lower.contains("amd") {
371 return true;
372 }
373 }
374 }
375
376 false
377}
378
379fn read_cpu_model_name() -> Option<String> {
380 let cpuinfo = fs::read_to_string("/proc/cpuinfo").ok()?;
381 for line in cpuinfo.lines() {
382 let lower = line.to_lowercase();
383 if lower.starts_with("model name") {
384 if let Some(model) = line.split(':').nth(1) {
385 let trimmed = model.trim();
386 if !trimmed.is_empty() {
387 return Some(trimmed.to_string());
388 }
389 }
390 }
391 }
392 None
393}
394
395fn fallback_memory_gb() -> Option<f32> {
396 let meminfo = fs::read_to_string("/proc/meminfo").ok()?;
397 for line in meminfo.lines() {
398 if line.starts_with("MemTotal") {
399 let parts: Vec<&str> = line.split_whitespace().collect();
400 if parts.len() >= 2 {
401 if let Ok(kb) = parts[1].parse::<f32>() {
402 return Some(kb / 1024.0 / 1024.0);
403 }
404 }
405 }
406 }
407 None
408}
409
410fn fallback_disk_space_gb() -> Option<(f32, f32)> {
411 let output = Command::new("df")
412 .args(["-k", "--output=avail,size", "/"])
413 .output()
414 .ok()?;
415 if !output.status.success() {
416 return None;
417 }
418 let stdout = String::from_utf8_lossy(&output.stdout);
419 let last_line = stdout.lines().last()?;
420 let parts: Vec<&str> = last_line.split_whitespace().collect();
421 if parts.len() < 2 {
422 return None;
423 }
424 let avail_kb = parts[0].parse::<f32>().ok()?;
425 let total_kb = parts[1].parse::<f32>().ok()?;
426 Some((avail_kb / 1024.0 / 1024.0, total_kb / 1024.0 / 1024.0))
427}
428
429fn fallback_distribution() -> Option<String> {
430 let contents = fs::read_to_string("/etc/os-release").ok()?;
431 for line in contents.lines() {
432 if let Some(value) = line.strip_prefix("PRETTY_NAME=") {
433 let cleaned = value.trim().trim_matches('"');
434 if !cleaned.is_empty() {
435 return Some(cleaned.to_string());
436 }
437 }
438 }
439 None
440}
441
442fn detect_gpu_from_sysfs() -> Option<(usize, String)> {
443 let mut count = 0usize;
444 let mut model = String::new();
445 let entries = fs::read_dir("/sys/class/drm").ok()?;
446 for entry in entries.flatten() {
447 let name = entry.file_name();
448 let name = name.to_string_lossy();
449 if !name.starts_with("card") || name.contains("render") {
450 continue;
451 }
452 let vendor_path = entry.path().join("device/vendor");
453 let vendor = match fs::read_to_string(&vendor_path) {
454 Ok(value) => value,
455 Err(_) => continue,
456 };
457 if vendor.trim().eq_ignore_ascii_case("0x1002") {
458 count += 1;
459 if model.is_empty() {
460 let device_path = entry.path().join("device");
461 let uevent_path = device_path.join("uevent");
462 if let Ok(uevent) = fs::read_to_string(&uevent_path) {
463 for line in uevent.lines() {
464 if let Some(driver) = line.strip_prefix("DRIVER=") {
465 model = format!("AMD GPU ({})", driver);
466 break;
467 }
468 }
469 }
470 if model.is_empty() {
471 model = "AMD GPU".to_string();
472 }
473 }
474 }
475 }
476 if count > 0 {
477 Some((count, model))
478 } else {
479 None
480 }
481}
482
483fn check_memory_requirements(system: &SystemInfo) -> PreflightCheck {
484 let mem = system.memory_gb;
485
486 if mem < 0.1 {
487 return PreflightCheck {
488 name: "Memory requirements".into(),
489 status: PreflightStatus::Warning,
490 check_type: PreflightType::Warning,
491 message: "Memory unavailable".into(),
492 details: "Unable to detect system memory".into(),
493 score: 2,
494 };
495 }
496
497 let (status, score, details) = if mem >= 32.0 {
498 (
499 PreflightStatus::Passed,
500 10,
501 "Sufficient memory for large models".into(),
502 )
503 } else if mem >= 16.0 {
504 (
505 PreflightStatus::Warning,
506 7,
507 "Moderate memory (consider 32GB+)".into(),
508 )
509 } else {
510 (
511 PreflightStatus::Failed,
512 0,
513 "Insufficient memory (16GB minimum)".into(),
514 )
515 };
516
517 PreflightCheck {
518 name: "Memory requirements".into(),
519 status,
520 check_type: PreflightType::Warning,
521 message: format!("{:.1} GB RAM", mem),
522 details,
523 score,
524 }
525}
526
527fn check_package_manager() -> PreflightCheck {
528 let managers = ["apt", "dnf", "yum", "zypper", "pacman"];
529 let found = managers
530 .iter()
531 .find(|m| {
532 Command::new("bash")
533 .arg("-c")
534 .arg(format!("command -v {}", m))
535 .stdout(Stdio::null())
536 .stderr(Stdio::null())
537 .status()
538 .map(|s| s.success())
539 .unwrap_or(false)
540 })
541 .cloned();
542
543 PreflightCheck {
544 name: "Package manager".into(),
545 status: if found.is_some() {
546 PreflightStatus::Passed
547 } else {
548 PreflightStatus::Failed
549 },
550 check_type: PreflightType::Critical,
551 message: match found {
552 Some(manager) => format!("{} detected", manager),
553 None => "No supported package manager detected".into(),
554 },
555 details: "Required for dependency installation".into(),
556 score: if found.is_some() { 10 } else { 0 },
557 }
558}
559
560fn is_version_at_least(version: &str, min_version: &str) -> bool {
561 let v_parts: Vec<i32> = version
562 .split('.')
563 .filter_map(|s| s.parse::<i32>().ok())
564 .collect();
565 let m_parts: Vec<i32> = min_version
566 .split('.')
567 .filter_map(|s| s.parse::<i32>().ok())
568 .collect();
569
570 for i in 0..std::cmp::max(v_parts.len(), m_parts.len()) {
571 let v = *v_parts.get(i).unwrap_or(&0);
572 let m = *m_parts.get(i).unwrap_or(&0);
573 if v > m {
574 return true;
575 }
576 if v < m {
577 return false;
578 }
579 }
580 true
581}
582
583fn check_python_availability() -> PreflightCheck {
584 let output = Command::new("python3").arg("--version").output();
585 let (status, message, details, score) = if let Ok(output) = output {
586 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
587 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
588 let version_str = if stdout.is_empty() { stderr } else { stdout };
589 let version = version_str
590 .split_whitespace()
591 .nth(1)
592 .unwrap_or("")
593 .to_string();
594 if is_version_at_least(&version, "3.9") {
595 (
596 PreflightStatus::Passed,
597 "Python 3.9+ available".into(),
598 version,
599 10,
600 )
601 } else {
602 (
603 PreflightStatus::Warning,
604 "Python version too old".into(),
605 version,
606 5,
607 )
608 }
609 } else {
610 (
611 PreflightStatus::Failed,
612 "Python not available".into(),
613 "python3 missing".into(),
614 0,
615 )
616 };
617
618 PreflightCheck {
619 name: "Python availability".into(),
620 status,
621 check_type: PreflightType::Critical,
622 message,
623 details,
624 score,
625 }
626}
627
628fn check_system_dependencies() -> PreflightCheck {
629 let deps = [
630 "curl", "wget", "git", "cmake", "unzip", "gcc", "g++", "make",
631 ];
632 let mut missing = Vec::new();
633 for dep in deps {
634 let ok = Command::new("bash")
635 .arg("-c")
636 .arg(format!("command -v {}", dep))
637 .stdout(Stdio::null())
638 .stderr(Stdio::null())
639 .status()
640 .map(|s| s.success())
641 .unwrap_or(false);
642 if !ok {
643 missing.push(dep);
644 }
645 }
646
647 PreflightCheck {
648 name: "System dependencies".into(),
649 status: if missing.is_empty() {
650 PreflightStatus::Passed
651 } else {
652 PreflightStatus::Failed
653 },
654 check_type: PreflightType::Critical,
655 message: if missing.is_empty() {
656 "All dependencies available".into()
657 } else {
658 "Missing dependencies".into()
659 },
660 details: if missing.is_empty() {
661 "Required tools are installed".into()
662 } else {
663 format!("Missing: {}", missing.join(", "))
664 },
665 score: if missing.is_empty() { 10 } else { 0 },
666 }
667}
668
669fn check_distribution_compatibility(system: &SystemInfo) -> PreflightCheck {
670 let supported = [
672 "ubuntu",
673 "debian",
674 "linux mint",
675 "pop!_os",
676 "fedora",
677 "arch",
678 "cachyos",
679 "manjaro",
680 "endeavouros",
681 "garuda",
682 ];
683 let dist = system.distribution.to_lowercase();
684 let compatible = supported.iter().any(|name| dist.contains(name));
685
686 PreflightCheck {
687 name: "Distribution compatibility".into(),
688 status: if compatible {
689 PreflightStatus::Passed
690 } else {
691 PreflightStatus::Warning
692 },
693 check_type: PreflightType::Info,
694 message: if compatible {
695 format!("Compatible distribution: {}", system.distribution)
696 } else {
697 format!("Unofficial distribution: {}", system.distribution)
698 },
699 details: "Supported distributions offer best compatibility".into(),
700 score: if compatible { 5 } else { 2 },
701 }
702}
703
704fn detect_rocm_path() -> Option<PathBuf> {
707 let mut search_paths: Vec<PathBuf> = Vec::new();
709
710 if let Ok(env_path) = std::env::var("ROCm_PATH") {
712 search_paths.push(PathBuf::from(env_path));
713 }
714
715 search_paths.push(PathBuf::from("/opt/rocm"));
717
718 for path in &search_paths {
720 if validate_rocm_path(path) {
721 return Some(path.clone());
722 }
723 }
724
725 if let Ok(entries) = fs::read_dir("/opt") {
727 let mut versioned_paths: Vec<(String, PathBuf)> = entries
728 .filter_map(|e| e.ok())
729 .filter_map(|e| {
730 let name = e.file_name().to_string_lossy().to_string();
731 if name.starts_with("rocm-") {
732 let version = name.trim_start_matches("rocm-").to_string();
733 Some((version, e.path()))
734 } else {
735 None
736 }
737 })
738 .collect();
739
740 versioned_paths.sort_by(|a, b| {
742 let a_parts: Vec<i32> =
743 a.0.split('.')
744 .filter_map(|s: &str| s.parse().ok())
745 .collect();
746 let b_parts: Vec<i32> =
747 b.0.split('.')
748 .filter_map(|s: &str| s.parse().ok())
749 .collect();
750 for i in 0..std::cmp::max(a_parts.len(), b_parts.len()) {
751 let a_val = *a_parts.get(i).unwrap_or(&0);
752 let b_val = *b_parts.get(i).unwrap_or(&0);
753 if a_val != b_val {
754 return b_val.cmp(&a_val); }
756 }
757 std::cmp::Ordering::Equal
758 });
759
760 for (_version, path) in versioned_paths {
761 if validate_rocm_path(&path) {
762 return Some(path);
763 }
764 }
765 }
766
767 let arch_paths = ["/usr/lib/rocm", "/usr/local/rocm"];
769 for path_str in &arch_paths {
770 let path = PathBuf::from(path_str);
771 if validate_rocm_path(&path) {
772 return Some(path);
773 }
774 }
775
776 if let Ok(entries) = fs::read_dir("/usr/lib") {
778 for entry in entries.filter_map(|e| e.ok()) {
779 let name = entry.file_name().to_string_lossy().to_string();
780 if name.starts_with("rocm-") && validate_rocm_path(&entry.path()) {
781 return Some(entry.path());
782 }
783 }
784 }
785
786 if let Ok(output) = Command::new("which").arg("rocminfo").output() {
788 if output.status.success() {
789 let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
790 if !path.is_empty() {
791 if let Some(parent) = Path::new(&path).parent() {
792 if let Some(rocm_root) = parent.parent() {
793 if validate_rocm_path(rocm_root) {
794 return Some(rocm_root.to_path_buf());
795 }
796 }
797 }
798 }
799 }
800 }
801
802 None
803}
804
805fn validate_rocm_path(path: &Path) -> bool {
807 if !path.exists() || !path.is_dir() {
808 return false;
809 }
810
811 path.join("bin").exists() || path.join("lib").exists()
813 || path.join("rocminfo").exists() || path.join("rocm-smi").exists()
815}
816
817fn get_correct_gfx_from_marketing_name(marketing_name: &str, rocminfo_gfx: &str) -> String {
820 let name_lower = marketing_name.to_lowercase();
821
822 if name_lower.contains("7900 xtx") || name_lower.contains("7900xtx") {
825 return "gfx1100".to_string();
826 }
827 if name_lower.contains("7900 xt") || name_lower.contains("7900xt") {
828 return "gfx1100".to_string();
830 }
831 if name_lower.contains("7900 gre") || name_lower.contains("7900gre") {
832 return "gfx1100".to_string();
833 }
834 if name_lower.contains("7800 xt") || name_lower.contains("7800xt") {
835 return "gfx1101".to_string();
836 }
837 if name_lower.contains("7800 gre") || name_lower.contains("7800gre") {
838 return "gfx1101".to_string();
839 }
840 if name_lower.contains("7700 xt") || name_lower.contains("7700xt") {
841 return "gfx1101".to_string();
842 }
843 if name_lower.contains("7600 xt")
844 || name_lower.contains("7600xt")
845 || name_lower.contains("7600")
846 {
847 return "gfx1102".to_string();
848 }
849
850 if name_lower.contains("9070 xt")
852 || name_lower.contains("9070xt")
853 || name_lower.contains("9070 gre")
854 || name_lower.contains("9070gre")
855 {
856 return "gfx1200".to_string();
857 }
858 if name_lower.contains("9060") {
859 return "gfx1201".to_string();
860 }
861
862 if name_lower.contains("6900 xt")
865 || name_lower.contains("6900xt")
866 || name_lower.contains("6950 xt")
867 || name_lower.contains("6950xt")
868 {
869 return "gfx1030".to_string();
870 }
871 if name_lower.contains("6800 xt")
872 || name_lower.contains("6800xt")
873 || name_lower.contains("6800")
874 || name_lower.contains("6900")
875 {
876 return "gfx1030".to_string();
877 }
878 if name_lower.contains("6700 xt")
879 || name_lower.contains("6700xt")
880 || name_lower.contains("6750 xt")
881 || name_lower.contains("6750xt")
882 {
883 return "gfx1031".to_string();
884 }
885 if name_lower.contains("6600 xt")
886 || name_lower.contains("6600xt")
887 || name_lower.contains("6600")
888 || name_lower.contains("6650")
889 {
890 return "gfx1032".to_string();
891 }
892 if name_lower.contains("6500 xt") || name_lower.contains("6500xt") {
893 return "gfx1034".to_string();
894 }
895
896 if name_lower.contains("instinct")
898 || name_lower.contains("mi60")
899 || name_lower.contains("mi100")
900 || name_lower.contains("mi200")
901 || name_lower.contains("mi250")
902 || name_lower.contains("mi300")
903 {
904 return format!("gfx{}", rocminfo_gfx);
905 }
906
907 format!("gfx{}", rocminfo_gfx)
910}
911
912fn detect_gpu() -> GPUInfo {
913 let mut info = GPUInfo::default();
914
915 let rocm_path = detect_rocm_path();
917
918 let rocminfo_cmd = if let Some(ref path) = rocm_path {
920 let rocminfo_path = path.join("bin/rocminfo");
921 if rocminfo_path.exists() {
922 rocminfo_path.to_string_lossy().to_string()
923 } else {
924 "rocminfo".to_string()
925 }
926 } else if Path::new("/opt/rocm/bin/rocminfo").exists() {
927 "/opt/rocm/bin/rocminfo".to_string()
928 } else {
929 "rocminfo".to_string()
930 };
931
932 if let Ok(output) = Command::new(rocminfo_cmd).output() {
933 if output.status.success() {
934 let stdout = String::from_utf8_lossy(&output.stdout);
935 let mut gpu_count = 0usize;
936 let mut current_marketing_name = String::new();
938 let mut current_device_type = String::new();
939 for line in stdout.lines() {
940 let trimmed = line.trim();
941 if trimmed.starts_with("Marketing Name:") {
943 current_marketing_name = trimmed
944 .strip_prefix("Marketing Name:")
945 .unwrap_or("")
946 .trim()
947 .to_string();
948 }
949 if trimmed.starts_with("Device Type:") {
951 current_device_type = trimmed
952 .strip_prefix("Device Type:")
953 .unwrap_or("")
954 .trim()
955 .to_string();
956 }
957 if line.contains("Name:") {
958 let name = line.replace("Name:", "").trim().to_string();
959 if !name.is_empty() && !name.to_lowercase().contains("cpu") {
960 gpu_count += 1;
961 if info.model.is_empty() {
962 info.model = name;
963 }
964 }
965 }
966 if trimmed.starts_with("Name:") && trimmed.contains("gfx") {
968 if let Some(name_value) = trimmed.strip_prefix("Name:") {
969 let name_value = name_value.trim();
970 if let Some(after_gfx) = name_value.strip_prefix("gfx") {
972 let gfx_num: String = after_gfx
973 .chars()
974 .take_while(|c| c.is_ascii_digit())
975 .collect();
976 if !gfx_num.is_empty() && gfx_num.len() >= 3 {
977 let is_igpu = current_marketing_name
979 .to_lowercase()
980 .contains("ryzen")
981 || current_marketing_name.to_lowercase().contains("apu")
982 || current_marketing_name.to_lowercase().contains("integrated");
983 if !is_igpu
985 && current_device_type == "GPU"
986 && info.architecture.is_empty()
987 {
988 let corrected_arch = get_correct_gfx_from_marketing_name(
991 ¤t_marketing_name,
992 &gfx_num,
993 );
994 info.architecture = corrected_arch;
995 }
996 }
997 }
998 }
999 }
1000 }
1001 if gpu_count > 0 {
1002 info.gpu_count = gpu_count;
1003 }
1004 }
1005 }
1006
1007 if info.rocm_version.is_empty() {
1008 let version_path = if let Some(ref path) = rocm_path {
1010 path.join(".info/version")
1011 } else {
1012 PathBuf::from("/opt/rocm/.info/version")
1013 };
1014
1015 if let Ok(output) = Command::new("bash")
1016 .arg("-c")
1017 .arg(format!("cat {} 2>/dev/null", version_path.display()))
1018 .output()
1019 {
1020 if output.status.success() {
1021 info.rocm_version = String::from_utf8_lossy(&output.stdout).trim().to_string();
1022 }
1023 }
1024 }
1025
1026 if info.model.is_empty() {
1027 let lspci_cmd = if Path::new("/usr/bin/lspci").exists() {
1028 "/usr/bin/lspci"
1029 } else {
1030 "lspci"
1031 };
1032 if let Ok(output) = Command::new(lspci_cmd).output() {
1033 let stdout = String::from_utf8_lossy(&output.stdout);
1034 for line in stdout.lines() {
1035 if line.to_lowercase().contains("amd") && line.to_lowercase().contains("vga") {
1036 if info.model.is_empty() {
1037 info.model = line.trim().to_string();
1038 }
1039 info.gpu_count += 1;
1040 }
1041 }
1042 }
1043 }
1044
1045 if info.gpu_count == 0 {
1046 if let Some((count, model)) = detect_gpu_from_sysfs() {
1047 info.gpu_count = count;
1048 if info.model.is_empty() {
1049 info.model = model;
1050 }
1051 }
1052 }
1053
1054 if info.gpu_count == 0 && !info.model.is_empty() {
1055 info.gpu_count = 1;
1056 }
1057
1058 let rocm_smi_cmd = if let Some(ref path) = rocm_path {
1060 let rocm_smi_path = path.join("bin/rocm-smi");
1061 if rocm_smi_path.exists() {
1062 rocm_smi_path.to_string_lossy().to_string()
1063 } else {
1064 "rocm-smi".to_string()
1065 }
1066 } else if Path::new("/opt/rocm/bin/rocm-smi").exists() {
1067 "/opt/rocm/bin/rocm-smi".to_string()
1068 } else {
1069 "rocm-smi".to_string()
1070 };
1071
1072 if let Ok(output) = Command::new(&rocm_smi_cmd)
1073 .arg("--showmeminfo")
1074 .arg("vram")
1075 .arg("--csv")
1076 .output()
1077 {
1078 if output.status.success() {
1079 let stdout = String::from_utf8_lossy(&output.stdout);
1080 for line in stdout.lines().skip(1) {
1081 let parts: Vec<&str> = line.split(',').collect();
1082 if parts.len() >= 2 {
1083 if let Ok(value) = parts[1].trim().parse::<f32>() {
1084 info.memory_gb = value / 1024.0;
1085 break;
1086 }
1087 }
1088 }
1089 }
1090 }
1091
1092 if let Ok(output) = Command::new(&rocm_smi_cmd)
1093 .arg("--showtemp")
1094 .arg("--csv")
1095 .output()
1096 {
1097 if output.status.success() {
1098 let stdout = String::from_utf8_lossy(&output.stdout);
1099 for line in stdout.lines().skip(1) {
1100 let parts: Vec<&str> = line.split(',').collect();
1101 if parts.len() >= 2 {
1102 if let Ok(value) = parts[1].trim().parse::<f32>() {
1103 info.temperature_c = Some(value);
1104 break;
1105 }
1106 }
1107 }
1108 }
1109 }
1110
1111 if let Ok(output) = Command::new(&rocm_smi_cmd)
1112 .arg("--showpower")
1113 .arg("--csv")
1114 .output()
1115 {
1116 if output.status.success() {
1117 let stdout = String::from_utf8_lossy(&output.stdout);
1118 for line in stdout.lines().skip(1) {
1119 let parts: Vec<&str> = line.split(',').collect();
1120 if parts.len() >= 2 {
1121 if let Ok(value) = parts[1].trim().parse::<f32>() {
1122 info.power_watts = Some(value);
1123 break;
1124 }
1125 }
1126 }
1127 }
1128 }
1129
1130 info
1131}