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