Skip to main content

rusty_stack/
hardware.rs

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    // Supported distributions include major distros and Arch-based derivatives
671    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
704/// Detect ROCm installation path by searching common locations
705/// Returns the first valid ROCm path found, or None if not found
706fn detect_rocm_path() -> Option<PathBuf> {
707    // Build search paths in priority order
708    let mut search_paths: Vec<PathBuf> = Vec::new();
709
710    // 1. Environment variable override (highest priority)
711    if let Ok(env_path) = std::env::var("ROCm_PATH") {
712        search_paths.push(PathBuf::from(env_path));
713    }
714
715    // 2. Standard location
716    search_paths.push(PathBuf::from("/opt/rocm"));
717
718    // First check explicit paths
719    for path in &search_paths {
720        if validate_rocm_path(path) {
721            return Some(path.clone());
722        }
723    }
724
725    // 3. Check for versioned ROCm directories in /opt (sorted by version, newest first)
726    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        // Sort by version (descending)
741        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); // Descending order
755                }
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    // 4. Check for Arch AUR package locations
768    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    // 5. Check for versioned paths in /usr/lib (Arch AUR)
777    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    // 6. Try to derive from rocminfo in PATH
787    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
805/// Validate that a path is a valid ROCm installation
806fn validate_rocm_path(path: &Path) -> bool {
807    if !path.exists() || !path.is_dir() {
808        return false;
809    }
810
811    // Should have at least bin or lib directory
812    path.join("bin").exists() || path.join("lib").exists()
813        // Or contain ROCm tools directly
814        || path.join("rocminfo").exists() || path.join("rocm-smi").exists()
815}
816
817/// Map GPU marketing name to correct gfx architecture.
818/// This corrects rocminfo bugs where RDNA3 cards report gfx1030 instead of gfx1100/gfx1101.
819fn get_correct_gfx_from_marketing_name(marketing_name: &str, rocminfo_gfx: &str) -> String {
820    let name_lower = marketing_name.to_lowercase();
821
822    // RDNA 3 (Navi 3x) - gfx1100/gfx1101
823    // These are commonly misreported as gfx1030 by buggy ROCm versions
824    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        // Could be XT (not XTX), still gfx1100
829        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    // RDNA 4 (Navi 4x) - gfx1200
851    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    // RDNA 2 (Navi 2x) - gfx1030/gfx1031/gfx1032
863    // These are correctly reported by rocminfo, but we verify
864    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    // CDNA (MI accelerators) - trust rocminfo for these
897    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    // Default: use rocminfo value if it looks valid (gfx10xx, gfx11xx, gfx12xx)
908    // But if it's gfx1030 from a known RDNA3 card, we should have caught it above
909    format!("gfx{}", rocminfo_gfx)
910}
911
912fn detect_gpu() -> GPUInfo {
913    let mut info = GPUInfo::default();
914
915    // Detect ROCm path first
916    let rocm_path = detect_rocm_path();
917
918    // Determine rocminfo path
919    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            // Track marketing name and device type to detect iGPUs
937            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                // Track marketing name to detect iGPUs (Agent section)
942                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                // Track device type to distinguish GPU from CPU
950                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                // Detect GPU architecture from Name field
967                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                        // Only accept pure gfx architectures (e.g., "gfx1100", "gfx1030")
971                        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                                // Detect iGPU by marketing name containing "Ryzen" or device being APU
978                                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                                // Only set architecture for dGPUs (not iGPUs, not CPUs)
984                                if !is_igpu
985                                    && current_device_type == "GPU"
986                                    && info.architecture.is_empty()
987                                {
988                                    // Use marketing name to get correct architecture
989                                    // rocminfo may report wrong gfx on some ROCm versions
990                                    let corrected_arch = get_correct_gfx_from_marketing_name(
991                                        &current_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        // Try detected ROCm path first
1009        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    // Determine rocm-smi path using detected ROCm path
1059    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}