1use crate::benchmark_logs::{
2 benchmark_log_directories, extract_benchmark_json_value, find_latest_log_in_dirs,
3};
4use crate::component_status::{
5 component_verification_commands, is_component_installed_by_id, modules_available,
6 python_interpreters, python_search_paths, verification_commands, VerificationCommand,
7};
8use crate::config::InstallerConfig;
9use crate::state::{Category, Component};
10use anyhow::{bail, Context, Result};
11use chrono::Local;
12use std::collections::HashMap;
13use std::io::{BufRead, BufReader, Read, Write};
14use std::path::{Path, PathBuf};
15use std::process::{Command, Stdio};
16use std::sync::{
17 atomic::{AtomicBool, Ordering},
18 mpsc::Receiver,
19 mpsc::Sender,
20 Arc, Mutex,
21};
22use std::time::Duration;
23use std::{fs, thread};
24
25#[derive(Debug, Clone)]
26pub enum InstallerEvent {
27 Log(String, bool),
28 Progress {
29 component_id: String,
30 progress: f32,
31 message: String,
32 },
33 ComponentStart {
34 component_id: String,
35 name: String,
36 },
37 ComponentComplete {
38 component_id: String,
39 success: bool,
40 message: String,
41 },
42 VerificationReport {
43 component_id: String,
44 lines: Vec<String>,
45 },
46 Finished {
47 success: bool,
48 },
49}
50
51pub fn run_installation(
52 components: Vec<Component>,
53 config: InstallerConfig,
54 sudo_password: Option<String>,
55 sender: Sender<InstallerEvent>,
56 input_rx: Receiver<String>,
57) {
58 run_installation_with_version(components, config, sudo_password, sender, input_rx, None);
59}
60
61pub fn run_installation_with_version(
68 components: Vec<Component>,
69 config: InstallerConfig,
70 sudo_password: Option<String>,
71 sender: Sender<InstallerEvent>,
72 input_rx: Receiver<String>,
73 target_version: Option<String>,
74) {
75 let scripts_dir = &config.scripts_dir;
76 let batch_mode = config.batch_mode;
77 let install_method = config.install_method.clone();
78 let total = components.len() as f32;
79 let mut index = 0usize;
80 let mut python_candidates = python_interpreters();
81 let user_home = resolve_mlstack_user_home();
82 let input_rx = Arc::new(Mutex::new(input_rx));
83
84 let _unseal_scope = if is_explicit_update_target(target_version.as_deref()) {
95 Some(ScopedEnvOverride::setting(&[
96 ("MLSTACK_FORCE_REINSTALL", "1"),
97 ("MLSTACK_UNSEAL_CORE", "1"),
98 ]))
99 } else {
100 None
101 };
102
103 match ensure_mlstack_env(&user_home, &install_method, false) {
104 Ok(EnvUpdate::Created) => {
105 let _ = sender.send(InstallerEvent::Log(
106 format!("Created .mlstack_env in {}", user_home),
107 false,
108 ));
109 }
110 Ok(EnvUpdate::Updated) => {
111 let _ = sender.send(InstallerEvent::Log(
112 format!("Updated .mlstack_env in {}", user_home),
113 false,
114 ));
115 }
116 Ok(EnvUpdate::Unchanged) => {}
117 Err(err) => {
118 let _ = sender.send(InstallerEvent::Log(
119 format!("Failed to ensure .mlstack_env: {}", err),
120 false,
121 ));
122 }
123 }
124
125 let any_component_needs_sudo = components.iter().any(|c| c.needs_sudo);
127
128 if needs_sudo() && any_component_needs_sudo {
129 if let Some(password) = sudo_password.clone() {
130 if let Err(err) = validate_sudo(password) {
131 let _ = sender.send(InstallerEvent::Log(
132 format!("Sudo validation failed: {}", err),
133 false,
134 ));
135 let _ = sender.send(InstallerEvent::Finished { success: false });
136 return;
137 }
138 } else if can_sudo_non_interactive() {
139 let _ = sender.send(InstallerEvent::Log(
140 "Using cached/passwordless sudo credentials".into(),
141 false,
142 ));
143 } else {
144 let _ = sender.send(InstallerEvent::Log(
145 "Sudo password missing; cannot continue".into(),
146 false,
147 ));
148 let _ = sender.send(InstallerEvent::Finished { success: false });
149 return;
150 }
151 }
152
153 let no_named_env = std::env::var("MLSTACK_ENV_NAME")
160 .map(|v| v.trim().is_empty())
161 .unwrap_or(true);
162 let no_explicit_python = std::env::var("MLSTACK_PYTHON_BIN")
163 .map(|v| v.trim().is_empty())
164 .unwrap_or(true);
165 let is_global_install = no_named_env && no_explicit_python;
169 if is_global_install {
170 let bootstrap = resolve_python_bin();
171 if let Ok(global_py) = crate::platform::environment::ensure_global_venv(&bootstrap) {
172 std::env::set_var(
173 "MLSTACK_PYTHON_BIN",
174 global_py.to_string_lossy().to_string(),
175 );
176 } else if std::env::var("MLSTACK_PYTHON_BIN").is_err() {
177 let _ = sender.send(InstallerEvent::Log(
180 "[native] could not create managed global venv (~/.mlstack/global); \
181 falling back to a discovered interpreter"
182 .into(),
183 false,
184 ));
185 }
186 }
187
188 let mut overall_success = true;
189 let env_exports = load_mlstack_env_exports(&user_home);
190 let persistent_python = env_exports
191 .get("MLSTACK_PYTHON_BIN")
192 .map(|v| v.trim().to_string())
193 .filter(|v| !v.is_empty());
194 let python_bin = std::env::var("MLSTACK_ENV_NAME")
198 .ok()
199 .map(|n| n.trim().to_string())
200 .filter(|n| !n.is_empty())
201 .map(|n| {
202 crate::platform::environment::mlstack_env_dir(&n)
203 .join("bin")
204 .join("python")
205 .to_string_lossy()
206 .to_string()
207 })
208 .or(persistent_python)
209 .unwrap_or_else(resolve_python_bin);
210 python_candidates.retain(|candidate| candidate != &python_bin);
211 python_candidates.insert(0, python_bin.clone());
212 let _ = sender.send(InstallerEvent::Log(
213 format!("Selected python interpreter: {}", python_bin),
214 false,
215 ));
216
217 let components = sort_components_by_dependencies(components);
221
222 for component in components {
223 index += 1;
224 let _ = sender.send(InstallerEvent::ComponentStart {
225 component_id: component.id.clone(),
226 name: component.name.clone(),
227 });
228
229 if component.category == Category::Maintenance && component.id.starts_with("verify-") {
230 let outcome = run_verification(&component, &python_candidates, &sender, &user_home);
231 let _ = sender.send(InstallerEvent::VerificationReport {
232 component_id: component.id.clone(),
233 lines: outcome.report_lines.clone(),
234 });
235 if !outcome.success {
236 overall_success = false;
237 }
238 let _ = sender.send(InstallerEvent::ComponentComplete {
239 component_id: component.id.clone(),
240 success: outcome.success,
241 message: if outcome.success {
242 format!("{} verification complete", component.name)
243 } else {
244 format!("{} verification completed with issues", component.name)
245 },
246 });
247
248 let overall_progress = index as f32 / total;
249 let _ = sender.send(InstallerEvent::Progress {
250 component_id: "__overall__".into(),
251 progress: overall_progress,
252 message: format!(
253 "Finished {} ({:.0}%)",
254 component.name,
255 overall_progress * 100.0
256 ),
257 });
258 continue;
259 }
260
261 let is_native = crate::installers::components::is_native_component(&component.id);
264
265 let mut install_success = true;
266 let mut error_msg = String::new();
267
268 if is_native {
269 let ctx = NativeInstallerContext {
274 sudo_password: sudo_password.clone(),
275 batch_mode,
276 install_method: &install_method,
277 sender: &sender,
278 user_home: &user_home,
279 env_exports: &env_exports,
280 vllm_version_lag: config.vllm_version_lag,
281 vllm_version_min_age_days: config.vllm_version_min_age_days,
282 target_version: target_version.clone(),
283 };
284 match registry_gate(&component, &sender) {
286 Ok(false) => { }
287 Ok(true) => {
288 if let Err(err) = run_native_installer(&component, &ctx) {
289 install_success = false;
290 let chain = err
291 .chain()
292 .map(|e| e.to_string())
293 .collect::<Vec<_>>()
294 .join(": ");
295 error_msg = format_user_friendly_error(&component.id, &chain);
296 let err_label = if component.category == Category::Performance {
298 "benchmarks"
299 } else {
300 "installation"
301 };
302 let _ = sender.send(InstallerEvent::Log(
303 format!("[ERROR] {} {} failed: {}", component.name, err_label, chain),
304 false,
305 ));
306 }
307 }
308 Err(blocked) => {
309 install_success = false;
310 error_msg = format_user_friendly_error(&component.id, &blocked.to_string());
311 let _ = sender.send(InstallerEvent::Log(
312 format!("[ERROR] {} blocked: {}", component.name, blocked),
313 false,
314 ));
315 }
316 }
317 } else {
318 let script_path = format!("{}/{}", scripts_dir, component.script);
321 if fs::metadata(&script_path).is_err() {
322 overall_success = false;
323 let _ = sender.send(InstallerEvent::ComponentComplete {
324 component_id: component.id.clone(),
325 success: false,
326 message: format!("Script not found: {}", script_path),
327 });
328 continue;
329 }
330
331 if let Err(err) = run_script(
332 &component,
333 &script_path,
334 sudo_password.clone(),
335 batch_mode,
336 &install_method,
337 &sender,
338 Arc::clone(&input_rx),
339 ) {
340 install_success = false;
341 let chain = err
342 .chain()
343 .map(|e| e.to_string())
344 .collect::<Vec<_>>()
345 .join(": ");
346 error_msg = format_user_friendly_error(&component.id, &chain);
347 let err_label = if component.category == Category::Performance {
349 "benchmarks"
350 } else {
351 "installation"
352 };
353 let _ = sender.send(InstallerEvent::Log(
354 format!("[ERROR] {} {} failed: {}", component.name, err_label, chain),
355 false,
356 ));
357 }
358 }
359
360 let verify_label = if component.category == Category::Performance {
361 "Validating"
362 } else {
363 "Verifying"
364 };
365 let _ = sender.send(InstallerEvent::Progress {
366 component_id: component.id.clone(),
367 progress: 0.8,
368 message: format!("{} {}", verify_label, component.name),
369 });
370
371 let verification_outcome = if component.category == Category::Performance {
378 VerificationOutcome {
379 success: install_success,
380 report_lines: vec![format!(
381 "Benchmark {}",
382 if install_success {
383 "completed successfully"
384 } else {
385 "reported failures"
386 }
387 )],
388 }
389 } else if is_native_maintenance_action(&component) {
390 native_maintenance_action_outcome(&component, install_success)
391 } else {
392 run_component_verification(&component, &python_candidates, &sender, &user_home)
393 };
394 let _ = sender.send(InstallerEvent::VerificationReport {
395 component_id: component.id.clone(),
396 lines: verification_outcome.report_lines.clone(),
397 });
398
399 if !verification_outcome.success {
400 overall_success = false;
401 }
402
403 let final_success = install_success && verification_outcome.success;
412 if !install_success && verification_outcome.success {
413 let _ = sender.send(InstallerEvent::Log(
414 format!(
415 "[{}] install step failed — NOT marking installed despite passing \
416 verification (verification likely ran against pre-existing/stale artifacts)",
417 component.name
418 ),
419 true,
420 ));
421 }
422 if !final_success {
423 overall_success = false;
424 } else {
425 registry_record(&component);
428 }
429
430 if component.id == "permanent-env"
432 || component.id == "enhanced-env"
433 || component.id == "basic-env"
434 {
435 let _ = sender.send(InstallerEvent::Log(
436 format!(
437 "[DEBUG] Component {}: install_success={}, verification_success={}, final={}",
438 component.id, install_success, verification_outcome.success, final_success
439 ),
440 false,
441 ));
442 if !install_success {
443 let _ = sender.send(InstallerEvent::Log(
444 format!("[DEBUG] Install error: {}", error_msg),
445 false,
446 ));
447 }
448 }
449
450 let completion_msg = if !install_success {
451 error_msg
452 } else if component.category == Category::Performance {
453 if verification_outcome.success {
454 format!("{} benchmarks completed", component.name)
455 } else {
456 format!("{} benchmarks completed with errors", component.name)
457 }
458 } else if verification_outcome.success {
459 format!("{} completed", component.name)
460 } else {
461 format!("{} completed with verification errors", component.name)
462 };
463
464 let _ = sender.send(InstallerEvent::ComponentComplete {
465 component_id: component.id.clone(),
466 success: final_success,
467 message: completion_msg,
468 });
469
470 let overall_progress = index as f32 / total;
471 let _ = sender.send(InstallerEvent::Progress {
472 component_id: "__overall__".into(),
473 progress: overall_progress,
474 message: format!(
475 "Finished {} ({:.0}%)",
476 component.name,
477 overall_progress * 100.0
478 ),
479 });
480 }
481
482 match ensure_mlstack_env(&user_home, &install_method, false) {
490 Ok(EnvUpdate::Created) | Ok(EnvUpdate::Updated) => {
491 let _ = sender.send(InstallerEvent::Log(
492 "[post-install] Refreshed ~/.mlstack_env from installed state \
493 (ROCM_VERSION / GPU_ARCH / etc.)"
494 .into(),
495 false,
496 ));
497 }
498 Ok(EnvUpdate::Unchanged) => {}
499 Err(e) => {
500 let _ = sender.send(InstallerEvent::Log(
501 format!("[post-install] Warning: could not refresh ~/.mlstack_env: {e}"),
502 false,
503 ));
504 }
505 }
506
507 if is_global_install {
512 if let Err(e) = crate::platform::environment::offer_mlstack_env_source() {
513 let _ = sender.send(InstallerEvent::Log(
514 format!(
515 "[post-install] Warning: could not auto-source ~/.mlstack_env into shell rc: {e}"
516 ),
517 false,
518 ));
519 }
520 }
521
522 if overall_success && config.star_repos {
523 let _ = star_repo(&config, &sender);
524 }
525
526 if std::path::Path::new("/tmp/mlstack-reboot-required").exists() {
528 let _ = sender.send(InstallerEvent::Log(
529 "⚠ REBOOT REQUIRED: ROCm packages were reinstalled. A reboot is needed for kernel module changes to take effect.".to_string(),
530 false,
531 ));
532 let _ = std::fs::remove_file("/tmp/mlstack-reboot-required");
533 }
534
535 {
540 use crate::installers::common::fix_pip_cache_ownership;
541 let result = fix_pip_cache_ownership(&user_home);
542 match &result {
543 crate::installers::common::PipCacheFixResult::Fixed => {
544 let _ = sender.send(InstallerEvent::Log(
545 "[post-install] Fixed pip cache directory ownership (~/.cache/pip)".into(),
546 false,
547 ));
548 }
549 crate::installers::common::PipCacheFixResult::FixFailed(err) => {
550 let _ = sender.send(InstallerEvent::Log(
551 format!(
552 "[post-install] Warning: could not fix pip cache ownership: {}",
553 err
554 ),
555 false,
556 ));
557 }
558 _ => {
559 }
561 }
562 }
563
564 let _ = sender.send(InstallerEvent::Finished {
565 success: overall_success,
566 });
567}
568
569fn validate_sudo(password: String) -> Result<()> {
570 let mut child = Command::new("sudo")
571 .arg("-S")
572 .arg("-v")
573 .stdin(Stdio::piped())
574 .stdout(Stdio::null())
575 .stderr(Stdio::piped())
576 .spawn()
577 .context("Failed to spawn sudo")?;
578
579 let mut stdin = child.stdin.take().context("Failed to open stdin")?;
580 stdin.write_all(password.as_bytes())?;
581 stdin.write_all(b"\n")?;
582 stdin.flush()?;
583 drop(stdin);
584
585 let output = child.wait_with_output()?;
586 if output.status.success() {
587 return Ok(());
588 }
589
590 let stderr = String::from_utf8_lossy(&output.stderr);
591 let detail = stderr.lines().next().unwrap_or("sudo validation failed");
592 anyhow::bail!(detail.to_string())
593}
594
595fn can_sudo_non_interactive() -> bool {
596 Command::new("sudo")
597 .arg("-n")
598 .arg("true")
599 .stdin(Stdio::null())
600 .stdout(Stdio::null())
601 .stderr(Stdio::null())
602 .status()
603 .map(|s| s.success())
604 .unwrap_or(false)
605}
606
607#[derive(Debug, Clone, Copy, PartialEq, Eq)]
608enum EnvUpdate {
609 Created,
610 Updated,
611 Unchanged,
612}
613
614struct VerificationCheckResult {
615 status: VerificationResult,
616 output: Vec<String>,
617}
618
619#[derive(Debug, Clone)]
620struct VerificationOutcome {
621 success: bool,
622 report_lines: Vec<String>,
623}
624
625fn fix_libdrm_amdgpu_ids() {
635 let target = Path::new("/usr/share/libdrm/amdgpu.ids");
636 let link = Path::new("/opt/amdgpu/share/libdrm/amdgpu.ids");
637
638 if !target.exists() {
639 return;
640 }
641 if link.exists() {
642 return; }
644
645 if let Some(parent) = link.parent() {
647 if fs::create_dir_all(parent).is_ok() {
648 #[cfg(unix)]
649 {
650 let relative = compute_relative_symlink(link, target);
651 if std::os::unix::fs::symlink(&relative, link).is_ok() {
652 tracing::info!(
653 "Created libdrm symlink: {} -> {}",
654 link.display(),
655 relative.display()
656 );
657 return;
658 }
659 }
660 #[cfg(not(unix))]
661 {
662 let _ = (link, target);
663 }
664 }
665 }
666
667 let result = Command::new("sudo")
673 .args(["-n", "mkdir", "-p", "/opt/amdgpu/share/libdrm/"])
674 .stdin(std::process::Stdio::null())
675 .output();
676
677 if let Ok(out) = result {
678 if out.status.success() {
679 let result2 = Command::new("sudo")
680 .args([
681 "-n",
682 "ln",
683 "-sf",
684 "/usr/share/libdrm/amdgpu.ids",
685 "/opt/amdgpu/share/libdrm/amdgpu.ids",
686 ])
687 .stdin(std::process::Stdio::null())
688 .output();
689 if let Ok(out2) = result2 {
690 if out2.status.success() {
691 tracing::info!(
692 "Created libdrm symlink via sudo: {} -> {}",
693 link.display(),
694 target.display()
695 );
696 return;
697 }
698 }
699 }
700 }
701
702 tracing::warn!(
704 "Cannot create /opt/amdgpu/share/libdrm/amdgpu.ids symlink (need sudo). \
705 Run manually: sudo mkdir -p /opt/amdgpu/share/libdrm && \
706 sudo ln -sf /usr/share/libdrm/amdgpu.ids /opt/amdgpu/share/libdrm/amdgpu.ids"
707 );
708}
709
710fn compute_relative_symlink(link: &Path, target: &Path) -> PathBuf {
712 let mut ups = 0u32;
714 for _ in link.parent().unwrap_or(Path::new(".")).ancestors().skip(1) {
715 ups += 1;
716 }
717 let mut result = PathBuf::new();
718 for _ in 0..ups.saturating_sub(1) {
719 result.push("..");
720 }
721 result.push(target.strip_prefix("/").unwrap_or(target));
722 result
723}
724
725fn ensure_mlstack_env(user_home: &str, install_method: &str, force: bool) -> Result<EnvUpdate> {
726 let env_path = PathBuf::from(user_home).join(".mlstack_env");
727 let normalized_install_method = match install_method.trim().to_ascii_lowercase().as_str() {
728 "global" => "global",
729 "venv" => "venv",
730 "auto" => "auto",
731 _ => "auto",
732 };
733 let env_exports = load_mlstack_env_exports(user_home);
734 let persistent_python = env_exports
735 .get("MLSTACK_PYTHON_BIN")
736 .map(|v| v.trim().to_string())
737 .filter(|v| !v.is_empty());
738 let python_bin = std::env::var("MLSTACK_ENV_NAME")
742 .ok()
743 .map(|n| n.trim().to_string())
744 .filter(|n| !n.is_empty())
745 .map(|n| {
746 crate::platform::environment::mlstack_env_dir(&n)
747 .join("bin")
748 .join("python")
749 .to_string_lossy()
750 .to_string()
751 })
752 .or(persistent_python)
753 .unwrap_or_else(|| {
754 let global_py = crate::platform::environment::mlstack_global_python();
756 if global_py.exists() {
757 global_py.to_string_lossy().to_string()
758 } else {
759 resolve_python_bin()
760 }
761 });
762
763 let dgpus = crate::gpu::detect_discrete_amd_gpus();
765 if dgpus.is_empty() {
766 bail!("cannot detect AMD dGPU arch via sysfs — ensure amdgpu driver loaded");
767 }
768 let primary = &dgpus[0];
769 let gpu_arch = primary.gfx_arch.as_ref().ok_or_else(|| {
770 anyhow::anyhow!(
771 "could not resolve gfx arch for PCI device {} — set GPU_ARCH manually",
772 primary.pci_device_id
773 )
774 })?;
775 let discrete_indices = if dgpus.iter().all(|g| g.rocm_index.is_some()) {
787 dgpus
788 .iter()
789 .map(|g| g.rocm_index.expect("checked Some above").to_string())
790 .collect::<Vec<_>>()
791 .join(",")
792 } else {
793 dgpus
794 .iter()
795 .enumerate()
796 .map(|(i, _)| i.to_string())
797 .collect::<Vec<_>>()
798 .join(",")
799 };
800 let primary_gpu = "0";
804 let gpu_vram_gb_csv = dgpus
809 .iter()
810 .map(|g| match g.vram_bytes {
811 Some(b) => format!("{:.0}", b as f64 / (1024.0 * 1024.0 * 1024.0)),
812 None => "0".to_string(),
813 })
814 .collect::<Vec<_>>()
815 .join(",");
816 let (gpu_names_csv, gpu_cus_csv) = gpu_identity_from_rocminfo(&dgpus);
817 let gpu_pairs = [
818 ("MLSTACK_GPU_NAMES", gpu_names_csv.as_str()),
819 ("MLSTACK_GPU_VRAM_GB", gpu_vram_gb_csv.as_str()),
820 ("MLSTACK_GPU_CUS", gpu_cus_csv.as_str()),
821 ];
822
823 let hsa_override =
827 if crate::bootstrap::env_setup::rocm_version_is_therock(&detect_rocm_version())
828 || std::env::var("ROCM_CHANNEL")
829 .ok()
830 .and_then(|c| crate::installers::components::rocm::RocmChannel::parse(&c))
831 .map(|c| c.is_therock() && rocm_sdk_venv_active())
832 .unwrap_or(false)
833 {
834 String::new()
835 } else {
836 crate::bootstrap::env_setup::GpuArchInfo::from_arch(gpu_arch).hsa_override_gfx_version
837 };
838
839 if env_path.exists() && !force {
845 let contents = fs::read_to_string(&env_path).context("Failed to read .mlstack_env")?;
846 let rocm_version = detect_rocm_version();
847 let rocm_home = "/opt/rocm";
848 let rocm_lib = "/opt/rocm/lib";
849 let defaults = [
850 ("ROCM_VERSION", rocm_version.as_str()),
851 ("ROCM_CHANNEL", "latest"),
852 ("GPU_ARCH", gpu_arch),
853 ("PYTORCH_ROCM_ARCH", gpu_arch),
854 ("GPU_ARCHS", gpu_arch),
855 ("ROCM_HOME", rocm_home),
856 ("ROCM_PATH", rocm_home),
857 ("HIP_PATH", rocm_home),
858 ("HSA_OVERRIDE_GFX_VERSION", hsa_override.as_str()),
859 ("HIP_VISIBLE_DEVICES", discrete_indices.as_str()),
860 ("CUDA_VISIBLE_DEVICES", discrete_indices.as_str()),
861 ("ROCR_VISIBLE_DEVICES", discrete_indices.as_str()),
866 ("PYTORCH_ROCM_DEVICE", primary_gpu),
867 ("MLSTACK_PYTHON_BIN", python_bin.as_str()),
868 ("UV_PYTHON", python_bin.as_str()),
869 ("MLSTACK_INSTALL_METHOD", normalized_install_method),
870 ("INSTALL_METHOD", normalized_install_method),
871 ("PYTHONPATH", rocm_lib),
872 ("PYTORCH_CUDA_ALLOC_CONF", "max_split_size_mb:512"),
878 ("PYTORCH_HIP_ALLOC_CONF", "max_split_size_mb:512"),
882 ("AMDGPU_ASIC_ID_TABLE_PATH", "/usr/share/libdrm/amdgpu.ids"),
884 ("AMDGPU_ASIC_ID_TABLE_PATHS", "/usr/share/libdrm"),
885 ];
886 let (sanitized, _changed) = sanitize_mlstack_env(
887 &contents,
888 &defaults,
889 discrete_indices.as_str(),
890 rocm_home,
891 python_bin.as_str(),
892 normalized_install_method,
893 hsa_override.as_str(),
894 );
895 let updated = ensure_gpu_metadata_exports(&sanitized, &gpu_pairs);
898 if updated != contents {
899 fs::write(&env_path, &updated).context("Failed to update .mlstack_env")?;
900 return Ok(EnvUpdate::Updated);
901 }
902 return Ok(EnvUpdate::Unchanged);
903 }
904
905 if let Some(parent) = env_path.parent() {
906 fs::create_dir_all(parent).context("Failed to create home directory")?;
907 }
908
909 let rocm_version = detect_rocm_version();
910 let rocm_home = "/opt/rocm";
911
912 let mut content = crate::bootstrap::env_setup::generate_env_file_content(
916 &discrete_indices,
917 rocm_home,
918 &rocm_version,
919 "latest",
920 gpu_arch,
921 &hsa_override,
922 &python_bin,
923 );
924 content.push_str(&format!(
925 "\n# Rusty-Stack install metadata\nexport MLSTACK_INSTALL_METHOD={}\nexport INSTALL_METHOD={}\n",
926 normalized_install_method, normalized_install_method,
927 ));
928 content = ensure_gpu_metadata_exports(&content, &gpu_pairs);
930
931 fs::write(&env_path, &content).context("Failed to create .mlstack_env")?;
932
933 let fish_dir = PathBuf::from(user_home).join(".config/fish/conf.d");
936 if fs::create_dir_all(&fish_dir).is_ok() {
937 let fish_content = crate::bootstrap::env_setup::generate_fish_env_file_content(
938 &discrete_indices,
939 rocm_home,
940 &rocm_version,
941 "latest",
942 gpu_arch,
943 &hsa_override,
944 &python_bin,
945 );
946 let fish_path = fish_dir.join("mlstack_env.fish");
947 fs::write(&fish_path, fish_content)
948 .context("Failed to write fish conf.d/mlstack_env.fish")?;
949 }
950
951 fix_libdrm_amdgpu_ids();
955
956 Ok(EnvUpdate::Created)
957}
958
959fn gpu_identity_from_rocminfo(dgpus: &[crate::gpu::AmdGpu]) -> (String, String) {
966 let count = dgpus.len();
967 let text =
968 match std::process::Command::new(crate::installers::common::utils::resolve_rocminfo_path())
969 .output()
970 {
971 Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).to_string(),
972 _ => return gpu_identity_fallback(dgpus),
973 };
974 let mut entries: Vec<(String, u32)> = Vec::new();
975 let mut name = String::new();
976 let mut cus: u32 = 0;
977 let mut is_gpu = false;
978 let mut started = false;
979 for line in text.lines() {
980 let t = line.trim();
981 if t.starts_with("Agent ") && t.contains(char::is_numeric) {
982 if started && is_gpu && !name.is_empty() && !crate::gpu::is_integrated_gpu_name(&name) {
983 entries.push((name.clone(), cus));
984 }
985 name.clear();
986 cus = 0;
987 is_gpu = false;
988 started = false;
989 } else if let Some(v) = t.strip_prefix("Marketing Name:") {
990 name = v.trim().to_string();
991 started = true;
992 } else if let Some(v) = t.strip_prefix("Device Type:") {
993 is_gpu = v.trim().contains("GPU");
994 started = true;
995 } else if let Some(v) = t.strip_prefix("Compute Unit:") {
996 cus = v.trim().parse().unwrap_or(0);
997 started = true;
998 }
999 }
1000 if started && is_gpu && !name.is_empty() && !crate::gpu::is_integrated_gpu_name(&name) {
1001 entries.push((name, cus));
1002 }
1003 entries.truncate(count);
1004 if entries.len() == count {
1005 let names = entries
1006 .iter()
1007 .map(|(n, _)| n.clone())
1008 .collect::<Vec<_>>()
1009 .join(",");
1010 let cus = entries
1011 .iter()
1012 .map(|(_, c)| c.to_string())
1013 .collect::<Vec<_>>()
1014 .join(",");
1015 (names, cus)
1016 } else {
1017 gpu_identity_fallback(dgpus)
1018 }
1019}
1020
1021fn gpu_identity_fallback(dgpus: &[crate::gpu::AmdGpu]) -> (String, String) {
1024 let names = dgpus
1025 .iter()
1026 .map(|g| {
1027 g.marketing_name
1028 .clone()
1029 .unwrap_or_else(|| format!("AMD GPU {}", g.pci_device_id))
1030 })
1031 .collect::<Vec<_>>()
1032 .join(",");
1033 let cus = dgpus
1034 .iter()
1035 .map(|_| "0".to_string())
1036 .collect::<Vec<_>>()
1037 .join(",");
1038 (names, cus)
1039}
1040
1041fn ensure_gpu_metadata_exports(contents: &str, pairs: &[(&str, &str)]) -> String {
1049 let mut lines: Vec<String> = contents.lines().map(|l| l.to_string()).collect();
1050 for (key, value) in pairs {
1051 let quoted = format!("export {key}=\"{value}\"");
1052 let prefix = format!("export {key}=");
1053 match lines
1054 .iter()
1055 .position(|l| l.trim_start().starts_with(&prefix))
1056 {
1057 Some(idx) => lines[idx] = quoted,
1058 None => lines.push(quoted),
1059 }
1060 }
1061 let mut out = lines.join("\n");
1062 if !out.ends_with('\n') {
1063 out.push('\n');
1064 }
1065 out
1066}
1067
1068fn sanitize_mlstack_env(
1069 contents: &str,
1070 defaults: &[(&str, &str)],
1071 gpu_list: &str,
1072 rocm_home: &str,
1073 python_bin: &str,
1074 install_method: &str,
1075 hsa_override: &str,
1076) -> (String, bool) {
1077 let mut changed = false;
1078 let mut lines = Vec::new();
1079 let mut seen = std::collections::HashSet::new();
1080 let default_for = |key: &str| -> &str {
1085 defaults
1086 .iter()
1087 .find(|(k, _)| *k == key)
1088 .map(|(_, v)| *v)
1089 .unwrap_or("")
1090 };
1091
1092 for line in contents.lines() {
1093 let trimmed = line.trim_start();
1094 if let Some(key) = extract_env_key(trimmed) {
1095 seen.insert(key.to_string());
1096 }
1097 if hsa_override.is_empty() && trimmed.starts_with("export HSA_OVERRIDE_GFX_VERSION=") {
1107 changed = true;
1108 continue;
1109 }
1110 if trimmed.starts_with("export TORCH_CUDA_ARCH_LIST=")
1111 || trimmed.starts_with("export PYTORCH_ALLOC_CONF=")
1112 {
1113 changed = true;
1114 continue;
1115 }
1116 let (line, changed_hip) = normalize_env_value(line, "HIP_PATH", "/opt/rocm");
1117 let (line, changed_rocm) = normalize_env_value(&line, "ROCM_PATH", "/opt/rocm");
1118 let (line, changed_python) = normalize_env_value(&line, "MLSTACK_PYTHON_BIN", python_bin);
1119 let (line, changed_uv_python) = normalize_env_value(&line, "UV_PYTHON", python_bin);
1120 let (line, changed_method) =
1121 normalize_env_value(&line, "MLSTACK_INSTALL_METHOD", install_method);
1122 let (line, changed_method_alias) =
1123 normalize_env_value(&line, "INSTALL_METHOD", install_method);
1124 let (line, changed_hsa) = if hsa_override.is_empty() {
1125 (line, false)
1126 } else {
1127 normalize_env_value(&line, "HSA_OVERRIDE_GFX_VERSION", hsa_override)
1128 };
1129 let (line, changed_hip_vis) =
1130 normalize_visible_devices(&line, "HIP_VISIBLE_DEVICES", gpu_list);
1131 let (line, changed_cuda_vis) =
1132 normalize_visible_devices(&line, "CUDA_VISIBLE_DEVICES", gpu_list);
1133 let (line, changed_rocr_vis) =
1134 normalize_visible_devices(&line, "ROCR_VISIBLE_DEVICES", gpu_list);
1135 let (line, changed_rocm_ver) =
1139 normalize_env_value(&line, "ROCM_VERSION", default_for("ROCM_VERSION"));
1140 let (line, changed_gpu_arch) =
1141 normalize_env_value(&line, "GPU_ARCH", default_for("GPU_ARCH"));
1142 let (line, changed_pytorch_arch) =
1143 normalize_env_value(&line, "PYTORCH_ROCM_ARCH", default_for("PYTORCH_ROCM_ARCH"));
1144 let (line, changed_gpu_archs) =
1145 normalize_env_value(&line, "GPU_ARCHS", default_for("GPU_ARCHS"));
1146 let (line, changed_rocm_home) =
1147 normalize_env_value(&line, "ROCM_HOME", default_for("ROCM_HOME"));
1148 let (line, changed_pytorch_dev) = normalize_env_value(
1149 &line,
1150 "PYTORCH_ROCM_DEVICE",
1151 default_for("PYTORCH_ROCM_DEVICE"),
1152 );
1153 let (line, changed_pythonpath) = if line.starts_with("export PYTHONPATH=") {
1154 let desired = format!("export PYTHONPATH={}/lib:$PYTHONPATH", rocm_home);
1155 if line.trim() != desired {
1156 (desired, true)
1157 } else {
1158 (line, false)
1159 }
1160 } else {
1161 (line, false)
1162 };
1163
1164 let (line, changed_path) = if line.starts_with("export PATH=") {
1166 let desired = format!("export PATH=\"/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:{}/bin:{}/hip/bin:$PATH\"", rocm_home, rocm_home);
1167 if line.trim() != desired {
1168 (desired, true)
1169 } else {
1170 (line, false)
1171 }
1172 } else {
1173 (line, false)
1174 };
1175
1176 let (line, changed_ld) = if line.starts_with("export LD_LIBRARY_PATH=") {
1177 let desired = format!(
1178 "export LD_LIBRARY_PATH=\"$HOME/.mlstack/libmpi-compat:$HOME/.mlstack/libmpi-compat-user-$(id -u):{}/lib:{}/hip/lib:{}/opencl/lib:$LD_LIBRARY_PATH\"",
1179 rocm_home, rocm_home, rocm_home
1180 );
1181 if line.trim() != desired {
1182 (desired, true)
1183 } else {
1184 (line, false)
1185 }
1186 } else {
1187 (line, false)
1188 };
1189
1190 changed |= changed_hip
1191 || changed_rocm
1192 || changed_python
1193 || changed_uv_python
1194 || changed_method
1195 || changed_method_alias
1196 || changed_hsa
1197 || changed_hip_vis
1198 || changed_cuda_vis
1199 || changed_rocr_vis
1200 || changed_rocm_ver
1201 || changed_gpu_arch
1202 || changed_pytorch_arch
1203 || changed_gpu_archs
1204 || changed_rocm_home
1205 || changed_pytorch_dev
1206 || changed_pythonpath
1207 || changed_path
1208 || changed_ld;
1209 lines.push(line);
1210 }
1211
1212 for (key, value) in defaults {
1213 if !seen.contains(*key) {
1214 lines.push(format!("export {}={}", key, value));
1215 changed = true;
1216 }
1217 }
1218
1219 let mut output = lines.join("\n");
1220 output.push('\n');
1221 (output, changed)
1222}
1223
1224fn extract_env_key(line: &str) -> Option<&str> {
1225 let export_idx = line.rfind("export ")?;
1227 let rest = &line[export_idx + 7..]; rest.split('=').next().map(str::trim)
1229}
1230
1231fn normalize_env_value(line: &str, key: &str, desired: &str) -> (String, bool) {
1232 let marker = format!("export {}=", key);
1233 if let Some(export_idx) = line.rfind(&marker) {
1235 let after_marker = &line[export_idx + marker.len()..];
1236 let current = after_marker.trim().trim_matches('"');
1237 let current = strip_shell_suffixes_from_value(current);
1239 if current != desired {
1240 return (format!("export {}={}", key, desired), true);
1242 }
1243 }
1244 (line.to_string(), false)
1245}
1246
1247fn normalize_visible_devices(line: &str, key: &str, desired: &str) -> (String, bool) {
1248 let desired = desired.trim();
1249 if desired.is_empty() {
1250 return (line.to_string(), false);
1251 }
1252
1253 let marker = format!("export {}=", key);
1254 if let Some(export_idx) = line.rfind(&marker) {
1256 let after_marker = &line[export_idx + marker.len()..];
1257 let current = after_marker.trim().trim_matches('"');
1258 let current = strip_shell_suffixes_from_value(current);
1260
1261 if current != desired {
1267 return (format!("export {}={}", key, desired), true);
1269 }
1270 }
1271 (line.to_string(), false)
1272}
1273
1274fn strip_hsa_override_for_therock(command: &mut Command, env_exports: &HashMap<String, String>) {
1278 let version = env_exports
1279 .get("ROCM_VERSION")
1280 .cloned()
1281 .unwrap_or_else(detect_rocm_version);
1282 if crate::bootstrap::env_setup::rocm_version_is_therock(&version)
1283 || crate::bootstrap::env_setup::rocm_version_is_therock(&detect_rocm_version())
1284 {
1285 command.env_remove("HSA_OVERRIDE_GFX_VERSION");
1286 }
1287}
1288
1289fn rocm_sdk_venv_python() -> Option<std::path::PathBuf> {
1292 let home = std::env::var("HOME").ok()?;
1293 let entries = match fs::read_dir(format!("{home}/.mlstack/rocm")) {
1294 Ok(d) => d,
1295 Err(_) => return None,
1296 };
1297 entries
1298 .flatten()
1299 .map(|e| e.path().join("bin/python"))
1300 .find(|p| p.exists())
1301}
1302
1303fn rocm_sdk_venv_active() -> bool {
1304 rocm_sdk_venv_python().is_some()
1305}
1306
1307fn detect_rocm_version() -> String {
1308 if let Ok(version) = fs::read_to_string("/opt/rocm/.info/version") {
1309 return version.trim().to_string();
1310 }
1311 if let Some(py) = rocm_sdk_venv_python() {
1313 if let Ok(out) = Command::new(py)
1314 .args(["-c", "import importlib.metadata as m; print(m.version('rocm'))"])
1315 .output()
1316 {
1317 let v = String::from_utf8_lossy(&out.stdout).trim().to_string();
1318 if out.status.success() && !v.is_empty() {
1319 return v;
1320 }
1321 }
1322 }
1323 "7.2.0".to_string()
1324}
1325
1326fn run_verification(
1327 component: &Component,
1328 python_candidates: &[String],
1329 sender: &Sender<InstallerEvent>,
1330 user_home: &str,
1331) -> VerificationOutcome {
1332 let steps = verification_commands(&component.id, python_candidates);
1333 let mut results: HashMap<String, VerificationCheckResult> = HashMap::new();
1334 let mut executed = 0usize;
1335 let mut failed = 0usize;
1336
1337 for step in &steps {
1338 if let Some((benchmark_ok, benchmark_output)) = verify_benchmark_target(&step.target_id) {
1339 executed += 1;
1340 if benchmark_ok {
1341 results.insert(
1342 step.target_id.clone(),
1343 VerificationCheckResult {
1344 status: VerificationResult::Verified,
1345 output: benchmark_output,
1346 },
1347 );
1348 } else {
1349 failed += 1;
1350 results.insert(
1351 step.target_id.clone(),
1352 VerificationCheckResult {
1353 status: VerificationResult::Failed,
1354 output: benchmark_output,
1355 },
1356 );
1357 let _ = sender.send(InstallerEvent::Log(
1358 format!("Verification failed for {}", step.label),
1359 false,
1360 ));
1361 }
1362 continue;
1363 }
1364
1365 if !is_component_installed_by_id(&step.target_id, python_candidates) {
1366 results.insert(
1367 step.target_id.clone(),
1368 VerificationCheckResult {
1369 status: VerificationResult::Missing,
1370 output: vec!["Component not found in path or site-packages".to_string()],
1371 },
1372 );
1373 let _ = sender.send(InstallerEvent::Log(
1374 format!("Skipping {} (not installed)", step.label),
1375 false,
1376 ));
1377 continue;
1378 }
1379 if !modules_available(&step.modules, python_candidates) {
1380 results.insert(
1381 step.target_id.clone(),
1382 VerificationCheckResult {
1383 status: VerificationResult::Missing,
1384 output: vec![format!(
1385 "Required Python modules missing: {}",
1386 step.modules.join(", ")
1387 )],
1388 },
1389 );
1390 let _ = sender.send(InstallerEvent::Log(
1391 format!("Skipping {} (module not available)", step.label),
1392 false,
1393 ));
1394 continue;
1395 }
1396 executed += 1;
1397 let _ = sender.send(InstallerEvent::Log(
1398 format!("Verifying {}...", step.label),
1399 false,
1400 ));
1401 match run_verification_command(step, sender) {
1402 Ok((success, output)) => {
1403 if success {
1404 results.insert(
1405 step.target_id.clone(),
1406 VerificationCheckResult {
1407 status: VerificationResult::Verified,
1408 output,
1409 },
1410 );
1411 } else {
1412 failed += 1;
1413 results.insert(
1414 step.target_id.clone(),
1415 VerificationCheckResult {
1416 status: VerificationResult::Failed,
1417 output,
1418 },
1419 );
1420 let _ = sender.send(InstallerEvent::Log(
1421 format!("Verification failed for {}", step.label),
1422 false,
1423 ));
1424 }
1425 }
1426 Err(err) => {
1427 failed += 1;
1428 results.insert(
1429 step.target_id.clone(),
1430 VerificationCheckResult {
1431 status: VerificationResult::Failed,
1432 output: vec![err.to_string()],
1433 },
1434 );
1435 let _ = sender.send(InstallerEvent::Log(
1436 format!("Failed to execute verification for {}: {}", step.label, err),
1437 false,
1438 ));
1439 }
1440 }
1441 }
1442
1443 let report_lines = build_verification_report(python_candidates, user_home, &results, &steps);
1444 let success = executed > 0 && failed == 0;
1445
1446 if executed == 0 {
1447 let _ = sender.send(InstallerEvent::Log(
1448 "No installed components found to verify".into(),
1449 false,
1450 ));
1451 }
1452
1453 if failed > 0 {
1454 let _ = sender.send(InstallerEvent::Log(
1455 format!("Verification completed with {} failed checks", failed),
1456 false,
1457 ));
1458 }
1459
1460 VerificationOutcome {
1461 success,
1462 report_lines,
1463 }
1464}
1465
1466fn build_verification_report(
1467 python_candidates: &[String],
1468 user_home: &str,
1469 results: &HashMap<String, VerificationCheckResult>,
1470 steps: &[VerificationCommand],
1471) -> Vec<String> {
1472 let mut lines = Vec::new();
1473 lines.push("Environment:".to_string());
1474 for (key, value) in collect_env_info(user_home) {
1475 lines.push(format!(" {}: {}", key, value));
1476 }
1477
1478 lines.push("".to_string());
1479 lines.push("Python Interpreters:".to_string());
1480 for py in python_candidates {
1481 lines.push(format!(" - {}", py));
1482 }
1483
1484 lines.push("".to_string());
1485 lines.push("Checks:".to_string());
1486 for step in steps {
1487 if let Some(res) = results.get(&step.target_id) {
1488 lines.push(format!(" - {}: {}", step.label, res.status.label()));
1489 for out_line in &res.output {
1490 lines.push(format!(" > {}", out_line));
1491 }
1492 } else {
1493 lines.push(format!(" - {}: Skipped", step.label));
1494 }
1495 }
1496
1497 lines
1498}
1499
1500fn run_component_verification(
1501 component: &Component,
1502 python_candidates: &[String],
1503 sender: &Sender<InstallerEvent>,
1504 user_home: &str,
1505) -> VerificationOutcome {
1506 let steps = component_verification_commands(&component.id, python_candidates);
1507 let mut results: HashMap<String, VerificationCheckResult> = HashMap::new();
1508 let mut executed = 0usize;
1509 let mut failed = 0usize;
1510
1511 for step in &steps {
1512 if let Some((benchmark_ok, benchmark_output)) = verify_benchmark_target(&step.target_id) {
1513 executed += 1;
1514 if benchmark_ok {
1515 results.insert(
1516 step.target_id.clone(),
1517 VerificationCheckResult {
1518 status: VerificationResult::Verified,
1519 output: benchmark_output,
1520 },
1521 );
1522 } else {
1523 failed += 1;
1524 results.insert(
1525 step.target_id.clone(),
1526 VerificationCheckResult {
1527 status: VerificationResult::Failed,
1528 output: benchmark_output,
1529 },
1530 );
1531 }
1532 continue;
1533 }
1534
1535 if !modules_available(&step.modules, python_candidates) {
1536 results.insert(
1537 step.target_id.clone(),
1538 VerificationCheckResult {
1539 status: VerificationResult::Missing,
1540 output: vec![format!(
1541 "Required Python modules missing: {}",
1542 step.modules.join(", ")
1543 )],
1544 },
1545 );
1546 continue;
1547 }
1548 executed += 1;
1549 match run_verification_command(step, sender) {
1550 Ok((success, output)) => {
1551 if success {
1552 results.insert(
1553 step.target_id.clone(),
1554 VerificationCheckResult {
1555 status: VerificationResult::Verified,
1556 output,
1557 },
1558 );
1559 } else {
1560 failed += 1;
1561 results.insert(
1562 step.target_id.clone(),
1563 VerificationCheckResult {
1564 status: VerificationResult::Failed,
1565 output,
1566 },
1567 );
1568 }
1569 }
1570 Err(err) => {
1571 failed += 1;
1572 results.insert(
1573 step.target_id.clone(),
1574 VerificationCheckResult {
1575 status: VerificationResult::Failed,
1576 output: vec![err.to_string()],
1577 },
1578 );
1579 }
1580 }
1581 }
1582
1583 let report_lines = build_component_report(component, user_home, &results, &steps);
1584 let success = executed > 0 && failed == 0;
1585
1586 VerificationOutcome {
1587 success,
1588 report_lines,
1589 }
1590}
1591
1592fn is_native_maintenance_action(component: &Component) -> bool {
1593 component.category == Category::Maintenance && !component.id.starts_with("verify-")
1594}
1595
1596fn native_maintenance_action_outcome(
1597 component: &Component,
1598 install_success: bool,
1599) -> VerificationOutcome {
1600 VerificationOutcome {
1601 success: install_success,
1602 report_lines: vec![format!(
1603 "{} native action {}",
1604 component.name,
1605 if install_success {
1606 "reported success"
1607 } else {
1608 "reported failure"
1609 }
1610 )],
1611 }
1612}
1613
1614fn build_component_report(
1615 component: &Component,
1616 user_home: &str,
1617 results: &HashMap<String, VerificationCheckResult>,
1618 steps: &[VerificationCommand],
1619) -> Vec<String> {
1620 let mut lines = Vec::new();
1621 lines.push(format!("Component: {}", component.name));
1622
1623 for (key, value) in collect_env_info(user_home) {
1624 lines.push(format!(" {}: {}", key, value));
1625 }
1626
1627 if let Some(benchmark_lines) = extract_benchmark_results(&component.id) {
1629 lines.push("".to_string());
1630 lines.push("Benchmark Results:".to_string());
1631 lines.extend(benchmark_lines);
1632 }
1633
1634 lines.push("".to_string());
1635 lines.push("Verification Checks:".to_string());
1636
1637 for step in steps {
1638 if let Some(res) = results.get(&step.target_id) {
1639 lines.push(format!(" - {}: {}", step.label, res.status.label()));
1640 for out_line in &res.output {
1641 lines.push(format!(" > {}", out_line));
1642 }
1643 } else {
1644 lines.push(format!(" - {}: Skipped", step.label));
1645 }
1646 }
1647
1648 lines
1649}
1650
1651fn extract_benchmark_results(component_id: &str) -> Option<Vec<String>> {
1652 let log_dirs = benchmark_log_directories();
1653 if log_dirs.is_empty() {
1654 return None;
1655 }
1656
1657 let pattern = match component_id {
1659 "mlperf-inference" => "all_benchmarks",
1660 "rocm-benchmarks" => "gpu-capability_benchmarks",
1661 "gpu-memory-bandwidth" => "memory-bandwidth_benchmarks",
1662 "rocm-smi-bench" => "gpu-capability_benchmarks",
1663 "pytorch-performance" => "pytorch_performance",
1664 "vllm-performance" => "vllm_benchmarks",
1665 "deepspeed-performance" => "deepspeed_benchmarks",
1666 "megatron-performance" => "megatron_benchmarks",
1667 "onnx-performance" => "onnx_benchmarks",
1668 "all-benchmarks" => "all_benchmarks",
1669 _ => return None,
1670 };
1671
1672 let log_file = find_latest_log_in_dirs(&log_dirs, pattern)?;
1673 let contents = std::fs::read_to_string(log_file).ok()?;
1674
1675 let Some(json) = extract_benchmark_json_value(&contents) else {
1676 return Some(vec![" No metrics found in log".to_string()]);
1677 };
1678
1679 let mut result_lines = benchmark_result_lines_from_json(&json);
1680 if result_lines.is_empty() {
1681 result_lines.push(" No metrics found in log".to_string());
1682 }
1683
1684 Some(result_lines)
1685}
1686
1687fn benchmark_result_lines_from_json(json: &serde_json::Value) -> Vec<String> {
1688 let mut result_lines = Vec::new();
1689
1690 if let Some(results) = json.get("results").and_then(|r| r.as_object()) {
1693 let nested_suite = results.values().any(|value| {
1694 value.as_object().is_some_and(|obj| {
1695 obj.contains_key("success")
1696 || obj.contains_key("metrics")
1697 || obj.contains_key("execution_time_ms")
1698 })
1699 });
1700 if nested_suite {
1701 for (bench_name, bench_data) in results {
1702 if bench_data.is_object() {
1703 let status = bench_data
1704 .get("success")
1705 .and_then(|v| v.as_bool())
1706 .map(|ok| if ok { "SUCCESS" } else { "FAILED" })
1707 .unwrap_or("UNKNOWN");
1708 let time_ms = bench_data
1709 .get("execution_time_ms")
1710 .and_then(|v| v.as_u64())
1711 .unwrap_or(0);
1712 result_lines.push(format!(
1713 " === {} ({}, {} ms) ===",
1714 bench_name.replace('_', " ").to_uppercase(),
1715 status,
1716 time_ms
1717 ));
1718 if let Some(metrics) = bench_data.get("metrics").and_then(|m| m.as_object()) {
1719 append_benchmark_metric_lines(&mut result_lines, metrics, " ");
1720 }
1721 append_benchmark_error_lines(&mut result_lines, bench_data, " ");
1722 result_lines.push(String::new());
1723 }
1724 }
1725 } else {
1726 append_benchmark_metric_lines(&mut result_lines, results, " ");
1727 append_benchmark_error_lines(&mut result_lines, json, " ");
1728 }
1729 } else if let Some(metrics) = json.get("metrics").and_then(|m| m.as_object()) {
1730 append_benchmark_metric_lines(&mut result_lines, metrics, " ");
1732 append_benchmark_error_lines(&mut result_lines, json, " ");
1733 }
1734
1735 result_lines
1736}
1737
1738fn append_benchmark_metric_lines(
1739 lines: &mut Vec<String>,
1740 metrics: &serde_json::Map<String, serde_json::Value>,
1741 indent: &str,
1742) {
1743 for (key, value) in metrics {
1744 let value_str = match value {
1745 serde_json::Value::Number(n) => n.to_string(),
1746 serde_json::Value::String(s) => s.clone(),
1747 serde_json::Value::Bool(b) => b.to_string(),
1748 serde_json::Value::Array(arr) => format!("{} values", arr.len()),
1749 _ => continue,
1750 };
1751 let formatted_key = key.replace('_', " ").to_uppercase();
1752 lines.push(format!("{}{}: {}", indent, formatted_key, value_str));
1753 }
1754}
1755
1756fn append_benchmark_error_lines(lines: &mut Vec<String>, value: &serde_json::Value, indent: &str) {
1757 if let Some(errors) = value.get("errors").and_then(|v| v.as_array()) {
1758 for err in errors.iter().filter_map(|v| v.as_str()).take(3) {
1759 lines.push(format!("{}ERROR: {}", indent, err));
1760 }
1761 }
1762}
1763
1764fn benchmark_pattern_for_target(target_id: &str) -> Option<&'static str> {
1765 match target_id {
1766 "mlperf-inference" => Some("all_benchmarks"),
1767 "rocm-benchmarks" => Some("gpu-capability_benchmarks"),
1768 "gpu-memory-bandwidth" => Some("memory-bandwidth_benchmarks"),
1769 "rocm-smi-bench" => Some("gpu-capability_benchmarks"),
1770 "pytorch-performance" => Some("pytorch_performance"),
1771 "vllm-performance" => Some("vllm_benchmarks"),
1772 "deepspeed-performance" => Some("deepspeed_benchmarks"),
1773 "megatron-performance" => Some("megatron_benchmarks"),
1774 "onnx-performance" => Some("onnx_benchmarks"),
1775 "all-benchmarks" => Some("all_benchmarks"),
1776 _ => None,
1777 }
1778}
1779
1780fn benchmark_success_and_errors(value: &serde_json::Value) -> (bool, Vec<String>) {
1781 if let Some(success) = value.get("success").and_then(|v| v.as_bool()) {
1782 let mut details = Vec::new();
1783 if !success {
1784 if let Some(reason) = value.get("reason").and_then(|v| v.as_str()) {
1785 let reason = reason.trim();
1786 if !reason.is_empty() {
1787 details.push(reason.to_string());
1788 }
1789 }
1790 if let Some(errors) = value.get("errors").and_then(|v| v.as_array()) {
1791 for err in errors.iter().filter_map(|entry| entry.as_str()).take(4) {
1792 let trimmed = err.trim();
1793 if !trimmed.is_empty() {
1794 details.push(trimmed.to_string());
1795 }
1796 }
1797 }
1798 if details.is_empty() {
1799 details.push("Benchmark JSON reported success=false".to_string());
1800 }
1801 }
1802 return (success, details);
1803 }
1804
1805 if let Some(results) = value.get("results").and_then(|v| v.as_object()) {
1806 let mut failed = Vec::new();
1807 let mut seen_result = false;
1808 for (name, item) in results {
1809 if let Some(item_success) = item.get("success").and_then(|v| v.as_bool()) {
1810 seen_result = true;
1811 if !item_success {
1812 let mut reason = item
1813 .get("reason")
1814 .and_then(|v| v.as_str())
1815 .map(|s| s.trim().to_string())
1816 .filter(|s| !s.is_empty())
1817 .unwrap_or_else(|| format!("{name}: success=false"));
1818 if reason == format!("{name}: success=false") {
1819 if let Some(errors) = item.get("errors").and_then(|v| v.as_array()) {
1820 if let Some(first) = errors.iter().filter_map(|v| v.as_str()).next() {
1821 let first = first.trim();
1822 if !first.is_empty() {
1823 reason = format!("{name}: {first}");
1824 }
1825 }
1826 }
1827 }
1828 failed.push(reason);
1829 }
1830 }
1831 }
1832 if seen_result {
1833 return (failed.is_empty(), failed);
1834 }
1835 }
1836
1837 (
1838 false,
1839 vec!["Benchmark JSON does not contain a recognizable success field".to_string()],
1840 )
1841}
1842
1843fn verify_benchmark_target(target_id: &str) -> Option<(bool, Vec<String>)> {
1844 let pattern = benchmark_pattern_for_target(target_id)?;
1845 let log_dirs = benchmark_log_directories();
1846 if log_dirs.is_empty() {
1847 return Some((
1848 false,
1849 vec!["No benchmark log directories found".to_string()],
1850 ));
1851 }
1852
1853 let Some(log_file) = find_latest_log_in_dirs(&log_dirs, pattern) else {
1854 return Some((
1855 false,
1856 vec![format!("No benchmark logs found for pattern '{pattern}'")],
1857 ));
1858 };
1859
1860 let mut output = vec![format!("Latest benchmark log: {}", log_file.display())];
1861 let contents = match std::fs::read_to_string(&log_file) {
1862 Ok(contents) => contents,
1863 Err(err) => {
1864 output.push(format!("Could not read benchmark log: {err}"));
1865 return Some((false, output));
1866 }
1867 };
1868
1869 let Some(json) = extract_benchmark_json_value(&contents) else {
1870 output.push("Could not parse benchmark JSON payload from log".to_string());
1871 return Some((false, output));
1872 };
1873
1874 let (success, mut details) = benchmark_success_and_errors(&json);
1875 output.append(&mut details);
1876 Some((success, output))
1877}
1878
1879fn collect_env_info(user_home: &str) -> Vec<(String, String)> {
1880 let mut values = load_mlstack_env_exports(user_home);
1881
1882 let keys = [
1883 "ROCM_VERSION",
1884 "ROCM_CHANNEL",
1885 "GPU_ARCH",
1886 "GPU_ARCHS",
1887 "PYTORCH_ROCM_ARCH",
1888 "HIP_VISIBLE_DEVICES",
1889 "CUDA_VISIBLE_DEVICES",
1890 "ROCR_VISIBLE_DEVICES",
1891 "PYTORCH_ROCM_DEVICE",
1892 "HSA_OVERRIDE_GFX_VERSION",
1893 "ROCM_HOME",
1894 "ROCM_PATH",
1895 "HIP_PATH",
1896 "MLSTACK_PYTHON_BIN",
1897 "PYTHONPATH",
1898 ];
1899
1900 for key in keys {
1901 if !values.contains_key(key) {
1902 if let Ok(value) = std::env::var(key) {
1903 values.insert(key.to_string(), value);
1904 }
1905 }
1906 }
1907
1908 keys.iter()
1909 .map(|key| {
1910 let default_val = if *key == "ROCM_HOME" || *key == "ROCM_PATH" || *key == "HIP_PATH" {
1911 "/opt/rocm".to_string()
1912 } else {
1913 "unknown".to_string()
1914 };
1915 (
1916 key.to_string(),
1917 values.get(*key).cloned().unwrap_or(default_val),
1918 )
1919 })
1920 .collect()
1921}
1922
1923fn load_mlstack_env_exports(user_home: &str) -> HashMap<String, String> {
1924 let mut values: HashMap<String, String> = HashMap::new();
1925 for home in candidate_mlstack_homes(user_home) {
1926 let env_path = PathBuf::from(home).join(".mlstack_env");
1927 if let Ok(contents) = fs::read_to_string(&env_path) {
1928 for line in contents.lines() {
1929 if let Some((key, value)) = parse_env_export(line) {
1930 values.entry(key).or_insert(value);
1931 }
1932 }
1933 }
1934 }
1935
1936 values
1937}
1938
1939fn parse_env_export(line: &str) -> Option<(String, String)> {
1940 let export_idx = line.rfind("export ")?;
1941 let rest = &line[export_idx + 7..];
1942 let mut parts = rest.splitn(2, '=');
1943 let key = parts.next()?.trim().to_string();
1944 let value = parts.next()?.trim().trim_matches('"').to_string();
1945 let value = strip_shell_suffixes_from_value(&value);
1947 Some((key, value))
1948}
1949
1950fn strip_shell_suffixes_from_value(value: &str) -> String {
1958 let mut v = value.to_string();
1959 if let Some(idx) = v.find("; fi") {
1961 v.truncate(idx);
1962 }
1963 if let Some(idx) = v.find("&& ") {
1965 v.truncate(idx);
1966 }
1967 v.trim().to_string()
1968}
1969
1970fn detect_onnx_rocm_provider(python: &str) -> Option<String> {
1971 let output = Command::new(python)
1972 .arg("-c")
1973 .arg(
1974 "import onnxruntime, pathlib; base=pathlib.Path(onnxruntime.__file__).parent; libs=list(base.rglob('libonnxruntime_providers_rocm.so')); print(libs[0] if libs else '')",
1975 )
1976 .output()
1977 .ok()?;
1978 if !output.status.success() {
1979 return None;
1980 }
1981 let value = String::from_utf8_lossy(&output.stdout).trim().to_string();
1982 if value.is_empty() {
1983 None
1984 } else {
1985 Some(value)
1986 }
1987}
1988
1989fn push_unique_env_path(paths: &mut Vec<String>, value: &str) {
1990 let candidate = value.trim();
1991 if candidate.is_empty() {
1992 return;
1993 }
1994 if paths.iter().any(|existing| existing == candidate) {
1995 return;
1996 }
1997 paths.push(candidate.to_string());
1998}
1999
2000fn user_home_from_passwd(user_name: &str) -> Option<String> {
2001 let target = user_name.trim();
2002 if target.is_empty() {
2003 return None;
2004 }
2005 let passwd = fs::read_to_string("/etc/passwd").ok()?;
2006 passwd.lines().find_map(|line| {
2007 if line.trim().is_empty() || line.starts_with('#') {
2008 return None;
2009 }
2010 let fields: Vec<&str> = line.split(':').collect();
2011 if fields.len() < 6 || fields[0] != target {
2012 return None;
2013 }
2014 let home = fields[5].trim();
2015 if home.is_empty() {
2016 None
2017 } else {
2018 Some(home.to_string())
2019 }
2020 })
2021}
2022
2023fn passwd_homes_with_mlstack() -> Vec<String> {
2024 let mut homes = Vec::new();
2025 let Ok(passwd) = fs::read_to_string("/etc/passwd") else {
2026 return homes;
2027 };
2028
2029 for line in passwd.lines() {
2030 if line.trim().is_empty() || line.starts_with('#') {
2031 continue;
2032 }
2033 let fields: Vec<&str> = line.split(':').collect();
2034 if fields.len() < 6 {
2035 continue;
2036 }
2037 let home = fields[5].trim();
2038 if home.is_empty() {
2039 continue;
2040 }
2041 if Path::new(home).join(".mlstack").is_dir()
2042 || Path::new(home).join(".mlstack_env").is_file()
2043 {
2044 push_unique_env_path(&mut homes, home);
2045 }
2046 }
2047
2048 homes
2049}
2050
2051fn pin_pip_requirement(args: Vec<String>, name: &str, version: Option<&str>) -> Vec<String> {
2057 let Some(v) = version.map(str::trim).filter(|v| !v.is_empty()) else {
2058 return args;
2059 };
2060 if !is_explicit_update_target(Some(v)) {
2065 return args;
2066 }
2067 args.into_iter()
2068 .filter(|a| a != "--upgrade")
2069 .map(|a| {
2070 let base = a
2073 .split(['=', '<', '>', '!', '~', '[', ' ', '\t'])
2074 .next()
2075 .unwrap_or(&a)
2076 .trim();
2077 if base == name {
2078 format!("{name}=={v}")
2079 } else {
2080 a
2081 }
2082 })
2083 .collect()
2084}
2085
2086pub(crate) fn candidate_mlstack_homes(preferred_home: &str) -> Vec<String> {
2087 let mut homes = Vec::new();
2088 push_unique_env_path(&mut homes, preferred_home);
2089
2090 if let Ok(value) = std::env::var("MLSTACK_USER_HOME") {
2091 push_unique_env_path(&mut homes, &value);
2092 }
2093 if let Ok(value) = std::env::var("HOME") {
2094 push_unique_env_path(&mut homes, &value);
2095 }
2096
2097 for key in ["SUDO_USER", "USER", "LOGNAME"] {
2098 if let Ok(user_name) = std::env::var(key) {
2099 if let Some(home) = user_home_from_passwd(&user_name) {
2100 push_unique_env_path(&mut homes, &home);
2101 }
2102 }
2103 }
2104
2105 for home in passwd_homes_with_mlstack() {
2106 push_unique_env_path(&mut homes, &home);
2107 }
2108
2109 homes
2110}
2111
2112fn resolve_mlstack_user_home() -> String {
2113 let fallback = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
2114 let candidates = candidate_mlstack_homes(&fallback);
2115
2116 for home in &candidates {
2117 if Path::new(home).join(".mlstack_env").is_file() {
2118 return home.clone();
2119 }
2120 }
2121 for home in &candidates {
2122 if Path::new(home).join(".mlstack").is_dir() {
2123 return home.clone();
2124 }
2125 }
2126
2127 candidates.into_iter().next().unwrap_or(fallback)
2128}
2129
2130fn mlstack_mpi_compat_dirs(user_home: &str) -> Vec<String> {
2131 let mut dirs = Vec::new();
2132 for home in candidate_mlstack_homes(user_home) {
2133 let mlstack_dir = PathBuf::from(home).join(".mlstack");
2134
2135 let primary = mlstack_dir.join("libmpi-compat");
2136 if primary.is_dir() {
2137 dirs.push(primary.to_string_lossy().to_string());
2138 }
2139
2140 if let Ok(entries) = fs::read_dir(&mlstack_dir) {
2141 for entry in entries.flatten() {
2142 let path = entry.path();
2143 if !path.is_dir() {
2144 continue;
2145 }
2146 let Some(name) = path.file_name().and_then(|value| value.to_str()) else {
2147 continue;
2148 };
2149 if name.starts_with("libmpi-compat-user-") {
2150 dirs.push(path.to_string_lossy().to_string());
2151 }
2152 }
2153 }
2154 }
2155
2156 dirs.sort();
2157 dirs.dedup();
2158 dirs
2159}
2160
2161fn build_verification_ld_library_path(user_home: &str) -> String {
2162 let mut paths: Vec<String> = Vec::new();
2163
2164 for compat_dir in mlstack_mpi_compat_dirs(user_home) {
2165 push_unique_env_path(&mut paths, &compat_dir);
2166 }
2167
2168 for rocm_path in ["/opt/rocm/lib", "/opt/rocm/hip/lib", "/opt/rocm/opencl/lib"] {
2169 if Path::new(rocm_path).exists() {
2170 push_unique_env_path(&mut paths, rocm_path);
2171 }
2172 }
2173
2174 if let Ok(existing) = std::env::var("LD_LIBRARY_PATH") {
2175 for part in existing.split(':') {
2176 push_unique_env_path(&mut paths, part);
2177 }
2178 }
2179
2180 paths.join(":")
2181}
2182
2183fn run_verification_command(
2184 step: &VerificationCommand,
2185 sender: &Sender<InstallerEvent>,
2186) -> Result<(bool, Vec<String>)> {
2187 let user_home = resolve_mlstack_user_home();
2188 let user_name = std::env::var("USER").unwrap_or_else(|_| "user".into());
2189 let env_exports = load_mlstack_env_exports(&user_home);
2190
2191 let mut command = Command::new(&step.program);
2192 command.args(&step.args);
2193
2194 let extra_paths = python_search_paths();
2195 let mut pythonpath = std::env::var("PYTHONPATH").unwrap_or_default();
2196 if !extra_paths.is_empty() {
2197 if !pythonpath.is_empty() {
2198 pythonpath.push(':');
2199 }
2200 pythonpath.push_str(&extra_paths.join(":"));
2201 }
2202
2203 pythonpath = pythonpath
2205 .split(':')
2206 .filter(|part| !part.starts_with("/tmp/"))
2207 .collect::<Vec<_>>()
2208 .join(":");
2209
2210 if !pythonpath.is_empty() {
2211 command.env("PYTHONPATH", pythonpath);
2212 }
2213
2214 let verification_ld = build_verification_ld_library_path(&user_home);
2215 if !verification_ld.is_empty() {
2216 command.env("LD_LIBRARY_PATH", verification_ld);
2217 }
2218
2219 if step.target_id == "onnx" {
2220 if let Some(lib) = detect_onnx_rocm_provider(&step.program) {
2221 command.env("ORT_ROCM_EP_PROVIDER_PATH", lib);
2222 }
2223 }
2224
2225 let selected_python = if !step.modules.is_empty() {
2226 step.program.clone()
2227 } else {
2228 resolve_python_bin()
2229 };
2230 let gpu_arch = env_exports
2231 .get("GPU_ARCH")
2232 .map(|v| v.trim().to_string())
2233 .filter(|v| !v.is_empty())
2234 .unwrap_or_else(|| {
2235 detect_gpu_arch().unwrap_or_else(|| {
2236 eprintln!("Warning: no GPU arch detected, falling back to gfx1100");
2237 "gfx1100".to_string()
2238 })
2239 });
2240 let py_rocm_arch = env_exports
2241 .get("PYTORCH_ROCM_ARCH")
2242 .map(|v| v.trim().to_string())
2243 .filter(|v| !v.is_empty())
2244 .unwrap_or_else(|| gpu_arch.clone());
2245 let mut gpu_archs = env_exports
2246 .get("GPU_ARCHS")
2247 .map(|v| v.trim().to_string())
2248 .filter(|v| !v.is_empty())
2249 .unwrap_or_else(|| gpu_arch.clone());
2250 if step.target_id == "aiter" {
2251 gpu_archs = py_rocm_arch
2252 .split(';')
2253 .next()
2254 .map(|v| v.trim().to_string())
2255 .filter(|v| !v.is_empty())
2256 .unwrap_or_else(|| gpu_arch.clone());
2257 }
2258 let mut hip_visible = env_exports
2259 .get("HIP_VISIBLE_DEVICES")
2260 .map(|v| v.trim().to_string())
2261 .filter(|v| !v.is_empty());
2262 let mut cuda_visible = env_exports
2263 .get("CUDA_VISIBLE_DEVICES")
2264 .map(|v| v.trim().to_string())
2265 .filter(|v| !v.is_empty())
2266 .or_else(|| hip_visible.clone());
2267 let mut rocr_visible = env_exports
2268 .get("ROCR_VISIBLE_DEVICES")
2269 .map(|v| v.trim().to_string())
2270 .filter(|v| !v.is_empty())
2271 .or_else(|| hip_visible.clone());
2272 if step.target_id == "aiter" {
2273 let base_list = hip_visible
2274 .clone()
2275 .or_else(|| cuda_visible.clone())
2276 .unwrap_or_else(detect_gpu_list);
2277 let ordered = prioritize_gpu_list_for_arch(&base_list, &gpu_archs);
2278 hip_visible = Some(ordered.clone());
2279 cuda_visible = Some(ordered.clone());
2280 rocr_visible = Some(ordered);
2281 }
2282 let hsa_override = env_exports
2283 .get("HSA_OVERRIDE_GFX_VERSION")
2284 .map(|v| v.trim().to_string())
2285 .filter(|v| !v.is_empty());
2286
2287 command
2288 .env("HOME", &user_home)
2289 .env("USER", &user_name)
2290 .env("LOGNAME", &user_name)
2291 .env("MLSTACK_USER_HOME", &user_home)
2292 .env("MLSTACK_PYTHON_BIN", &selected_python)
2293 .env("UV_PYTHON", &selected_python)
2294 .env("PYTHONWARNINGS", "ignore")
2295 .env("PYTHONUNBUFFERED", "1")
2296 .env("ROCM_HOME", "/opt/rocm")
2297 .env("HIP_PATH", "/opt/rocm")
2298 .env("GPU_ARCH", &gpu_arch)
2299 .env("PYTORCH_ROCM_ARCH", &py_rocm_arch)
2300 .env("GPU_ARCHS", &gpu_archs)
2301 .stdout(Stdio::piped())
2302 .stderr(Stdio::piped());
2303
2304 if let Some(value) = hip_visible {
2305 command.env("HIP_VISIBLE_DEVICES", value);
2306 }
2307 if let Some(value) = cuda_visible {
2308 command.env("CUDA_VISIBLE_DEVICES", value);
2309 }
2310 if let Some(value) = rocr_visible {
2311 command.env("ROCR_VISIBLE_DEVICES", value);
2312 }
2313 if let Some(value) = hsa_override {
2314 command.env("HSA_OVERRIDE_GFX_VERSION", value);
2315 }
2316 strip_hsa_override_for_therock(&mut command, &env_exports);
2317 if step.target_id == "aiter" {
2318 let aiter_jit_dir = PathBuf::from(&user_home)
2319 .join(".mlstack")
2320 .join("aiter")
2321 .join("jit");
2322 let _ = fs::create_dir_all(&aiter_jit_dir);
2323 command.env("AITER_JIT_DIR", aiter_jit_dir);
2324 }
2325
2326 if step.target_id == "mpi4py" {
2327 command
2329 .env("OMPI_MCA_btl", "^smcuda")
2330 .env("OMPI_MCA_opal_cuda_support", "0")
2331 .env("OMPI_MCA_opal_accelerator_rocm_enable", "0");
2332 }
2333
2334 let output = command
2335 .output()
2336 .with_context(|| format!("Failed to run verification for {}", step.label))?;
2337
2338 let mut captured_output = Vec::new();
2339 const MAX_CAPTURED_OUTPUT_LINES: usize = 24;
2340
2341 let stdout = String::from_utf8_lossy(&output.stdout);
2342 for raw in stdout.replace('\r', "\n").lines() {
2343 let clean_line = sanitize_output_line(raw);
2344 if should_suppress_verification_line(&clean_line, &step.target_id) {
2345 continue;
2346 }
2347 if !clean_line.trim().is_empty() && captured_output.len() < MAX_CAPTURED_OUTPUT_LINES {
2348 let line = format!("{}: {}", step.label, clean_line);
2349 let _ = sender.send(InstallerEvent::Log(line.clone(), false));
2350 captured_output.push(clean_line);
2351 }
2352 }
2353
2354 let stderr = String::from_utf8_lossy(&output.stderr);
2355 for raw in stderr.replace('\r', "\n").lines() {
2356 let clean_line = sanitize_output_line(raw);
2357 if should_suppress_verification_line(&clean_line, &step.target_id) {
2358 continue;
2359 }
2360 if !clean_line.trim().is_empty() && captured_output.len() < MAX_CAPTURED_OUTPUT_LINES {
2361 let line = format!("{}: {}", step.label, clean_line);
2362 let _ = sender.send(InstallerEvent::Log(line.clone(), false));
2363 captured_output.push(clean_line);
2364 }
2365 }
2366
2367 if output.status.success() && captured_output.len() == MAX_CAPTURED_OUTPUT_LINES {
2368 captured_output.push("Output truncated".to_string());
2369 }
2370
2371 Ok((output.status.success(), captured_output))
2372}
2373
2374fn sort_components_by_dependencies(components: Vec<Component>) -> Vec<Component> {
2387 use crate::installers::components::topological_sort;
2388
2389 let mut installer_comps: Vec<Component> = Vec::new();
2391 let mut other_comps: Vec<Component> = Vec::new();
2392
2393 for comp in components {
2394 if comp.category == Category::Maintenance || comp.category == Category::Performance {
2395 other_comps.push(comp);
2396 } else {
2397 installer_comps.push(comp);
2398 }
2399 }
2400
2401 let ids: Vec<String> = installer_comps.iter().map(|c| c.id.clone()).collect();
2403
2404 match topological_sort(&ids) {
2405 Ok(sorted_ids) => {
2406 let mut reordered = Vec::with_capacity(installer_comps.len());
2408 for id in &sorted_ids {
2409 if let Some(pos) = installer_comps.iter().position(|c| c.id == *id) {
2410 reordered.push(installer_comps.remove(pos));
2411 }
2412 }
2413 reordered.extend(installer_comps);
2415
2416 reordered.extend(other_comps);
2418 reordered
2419 }
2420 Err(_) => {
2421 installer_comps.extend(other_comps);
2424 installer_comps
2425 }
2426 }
2427}
2428
2429struct NativeInstallerContext<'a> {
2436 sudo_password: Option<String>,
2437 batch_mode: bool,
2438 install_method: &'a str,
2439 sender: &'a Sender<InstallerEvent>,
2440 user_home: &'a str,
2441 env_exports: &'a HashMap<String, String>,
2442 vllm_version_lag: u32,
2444 vllm_version_min_age_days: u32,
2445 target_version: Option<String>,
2447}
2448
2449enum NativeCommand {
2459 Shell {
2461 program: String,
2462 args: Vec<String>,
2463 env: Vec<(String, String)>,
2464 working_dir: Option<PathBuf>,
2465 },
2466 Pip { program: String, args: Vec<String> },
2468 System { program: String, args: Vec<String> },
2470 Package { program: String, args: Vec<String> },
2472}
2473
2474impl NativeCommand {
2475 fn from_shell_cmd(program: &str, args: &[String], env: &[(String, String)]) -> Self {
2479 NativeCommand::Shell {
2480 program: program.to_string(),
2481 args: args.to_vec(),
2482 env: env.to_vec(),
2483 working_dir: None,
2484 }
2485 }
2486
2487 fn from_shell_cmd_with_dir(
2489 program: &str,
2490 args: &[String],
2491 env: &[(String, String)],
2492 working_dir: Option<PathBuf>,
2493 ) -> Self {
2494 NativeCommand::Shell {
2495 program: program.to_string(),
2496 args: args.to_vec(),
2497 env: env.to_vec(),
2498 working_dir,
2499 }
2500 }
2501}
2502
2503fn is_up_to_date_output(output: &str) -> bool {
2523 let lower = output.to_lowercase();
2524 lower.contains("up to date -- skipping")
2525 || lower.contains("up to date, skipping")
2526 || lower.contains("up to date -- reinstalling")
2527 || lower.contains("there is nothing to do")
2528 || lower.contains("already installed")
2529 || (lower.contains("warning:") && lower.contains("up to date"))
2530}
2531
2532fn safe_rocm_path(inherited: &str) -> String {
2544 const ROCM_BINS: &[&str] = &["/opt/rocm/bin", "/opt/rocm/hip/bin"];
2545 if !Path::new("/opt/rocm/bin").exists() {
2546 return inherited.to_string();
2547 }
2548 let mut parts: Vec<&str> = ROCM_BINS.to_vec();
2549 for entry in inherited.split(':') {
2550 if !entry.is_empty() && !ROCM_BINS.contains(&entry) {
2551 parts.push(entry);
2552 }
2553 }
2554 parts.join(":")
2555}
2556
2557fn execute_native_command(
2563 cmd: &NativeCommand,
2564 sudo_pw: Option<&str>,
2565 sender: &Sender<InstallerEvent>,
2566 component_name: &str,
2567) -> Result<()> {
2568 let (program, args, envs, working_dir) = match cmd {
2569 NativeCommand::Shell {
2570 program,
2571 args,
2572 env,
2573 working_dir,
2574 } => (
2575 program.clone(),
2576 args.clone(),
2577 env.clone(),
2578 working_dir.clone(),
2579 ),
2580 NativeCommand::Pip { program, args } => (program.clone(), args.clone(), vec![], None),
2581 NativeCommand::System { program, args } => (program.clone(), args.clone(), vec![], None),
2582 NativeCommand::Package { program, args } => (program.clone(), args.clone(), vec![], None),
2583 };
2584
2585 let invokes_pip = program == "uv"
2594 || program.ends_with("pip")
2595 || program.ends_with("pip3")
2596 || args.iter().any(|a| a == "pip");
2597 let is_install = args.iter().any(|a| a == "install");
2598 if invokes_pip && is_install {
2599 let mut skip_next = false;
2600 for a in &args {
2601 if skip_next {
2602 skip_next = false;
2606 continue;
2607 }
2608 if a == "-r" || a == "--requirement" || a == "-c" || a == "--constraint" {
2609 skip_next = true;
2610 continue;
2611 }
2612 if a.starts_with('-') || a == "install" || a == "." || a == "-e" || a.is_empty() {
2613 continue;
2614 }
2615 if crate::installers::common::nvidia_blocklist::is_nvidia_cuda_runtime_package(a) {
2616 let _ = sender.send(InstallerEvent::Log(
2617 format!("[native] BLOCKED nvidia/cuda package in {component_name}: {a}"),
2618 true,
2619 ));
2620 anyhow::bail!(
2621 "NVIDIA/CUDA contamination blocked: component '{component_name}' attempted \
2622 to pip install '{a}' — violates the No-CUDA hard-prime tenet"
2623 );
2624 }
2625 if a.starts_with("http")
2626 && crate::installers::common::nvidia_blocklist::is_cuda_wheel_url(a)
2627 {
2628 let _ = sender.send(InstallerEvent::Log(
2629 format!("[native] BLOCKED cuda wheel URL in {component_name}: {a}"),
2630 true,
2631 ));
2632 anyhow::bail!("NVIDIA/CUDA wheel URL blocked in {component_name}: {a}");
2633 }
2634 }
2635 }
2636
2637 let already_sudo = program == "sudo";
2640 let mut command = if let Some(_pw) = sudo_pw {
2641 if already_sudo {
2642 let mut c = Command::new(&program);
2644 c.args(&args);
2645 c
2646 } else {
2647 let mut c = Command::new("sudo");
2648 c.arg("-S").arg("-p").arg("").arg("--").arg(&program);
2649 c.args(&args);
2650 let _ = sender.send(InstallerEvent::Log(
2652 format!("[native] Running with sudo: {} {}", program, args.join(" ")),
2653 false,
2654 ));
2655 c
2656 }
2657 } else {
2658 let mut c = Command::new(&program);
2659 c.args(&args);
2660 c
2661 };
2662
2663 for (key, value) in &envs {
2665 command.env(key, value);
2666 }
2667
2668 command.env(
2677 "PATH",
2678 safe_rocm_path(&std::env::var("PATH").unwrap_or_default()),
2679 );
2680
2681 if let Some(ref dir) = working_dir {
2683 command.current_dir(dir);
2684 }
2685
2686 command
2688 .stdin(Stdio::piped())
2689 .stdout(Stdio::piped())
2690 .stderr(Stdio::piped());
2691
2692 let cmd_str = if args.is_empty() {
2694 program.clone()
2695 } else {
2696 format!("{} {}", program, args.join(" "))
2697 };
2698 let _ = sender.send(InstallerEvent::Log(
2699 format!("[native] $ {}", cmd_str),
2700 false,
2701 ));
2702
2703 let mut child = command.spawn().with_context(|| {
2704 let mut hint = String::new();
2705 if cmd_str.starts_with("sudo") {
2707 hint = " — ensure sudo is installed and you have the correct permissions".to_string();
2708 } else if cmd_str.starts_with("pip")
2709 || cmd_str.starts_with("uv")
2710 || cmd_str.starts_with("python")
2711 {
2712 hint = " — ensure Python/pip is installed and accessible".to_string();
2713 } else if cmd_str.starts_with("git") {
2714 hint = " — ensure git is installed".to_string();
2715 } else if cmd_str.starts_with("cmake") || cmd_str.starts_with("make") {
2716 hint = " — ensure build tools (cmake, make, gcc) are installed".to_string();
2717 } else if cmd_str.starts_with("rocminfo") || cmd_str.starts_with("rocm-smi") {
2718 hint = " — ensure ROCm is installed and /opt/rocm/bin is in PATH".to_string();
2719 }
2720 format!(
2721 "Failed to spawn command '{}' for {}.{}",
2722 cmd_str, component_name, hint
2723 )
2724 })?;
2725
2726 if let Some(pw) = sudo_pw {
2729 if let Some(mut stdin) = child.stdin.take() {
2730 let _ = stdin.write_all(pw.as_bytes());
2731 let _ = stdin.write_all(b"\n");
2732 let _ = stdin.flush();
2733 drop(stdin);
2734 }
2735 } else {
2736 drop(child.stdin.take());
2739 }
2740
2741 let sender_stdout = sender.clone();
2744 let stdout_stream = match child.stdout.take() {
2745 Some(s) => s,
2746 None => {
2747 let _ = sender.send(InstallerEvent::Log(
2748 format!(
2749 "[ERROR] {} — failed to capture stdout from spawned process",
2750 component_name
2751 ),
2752 false,
2753 ));
2754 bail!(
2755 "{} — failed to capture stdout from spawned process. \
2756 This usually indicates a system resource issue.",
2757 component_name
2758 );
2759 }
2760 };
2761 let stdout_captured: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
2762 let stdout_captured_clone = stdout_captured.clone();
2763 let stdout_handle = thread::spawn(move || {
2764 let reader = BufReader::new(stdout_stream);
2765 for line in reader.lines() {
2766 match line {
2767 Ok(l) => {
2768 let clean = strip_ansi_codes(&l);
2769 if !clean.trim().is_empty() {
2770 let _ = sender_stdout.send(InstallerEvent::Log(clean.clone(), false));
2771 if let Ok(mut buf) = stdout_captured_clone.lock() {
2772 buf.push_str(&clean);
2773 buf.push('\n');
2774 }
2775 }
2776 }
2777 Err(_) => break,
2778 }
2779 }
2780 });
2781
2782 let sender_stderr = sender.clone();
2783 let stderr_stream = match child.stderr.take() {
2784 Some(s) => s,
2785 None => {
2786 let _ = sender.send(InstallerEvent::Log(
2787 format!(
2788 "[ERROR] {} — failed to capture stderr from spawned process",
2789 component_name
2790 ),
2791 false,
2792 ));
2793 bail!(
2794 "{} — failed to capture stderr from spawned process. \
2795 This usually indicates a system resource issue.",
2796 component_name
2797 );
2798 }
2799 };
2800 let stderr_captured: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
2801 let stderr_captured_clone = stderr_captured.clone();
2802 let stderr_handle = thread::spawn(move || {
2803 let reader = BufReader::new(stderr_stream);
2804 for line in reader.lines() {
2805 match line {
2806 Ok(l) => {
2807 let clean = strip_ansi_codes(&l);
2808 if !clean.trim().is_empty() {
2809 let _ = sender_stderr.send(InstallerEvent::Log(clean.clone(), false));
2810 if let Ok(mut buf) = stderr_captured_clone.lock() {
2811 buf.push_str(&clean);
2812 buf.push('\n');
2813 }
2814 }
2815 }
2816 Err(_) => break,
2817 }
2818 }
2819 });
2820
2821 let status = child.wait().with_context(|| {
2823 format!(
2824 "{} — command process wait failed. \
2825 The process may have been killed or the system is under heavy load.",
2826 component_name
2827 )
2828 })?;
2829 let _ = stdout_handle.join();
2830 let _ = stderr_handle.join();
2831
2832 if !status.success() {
2833 let combined_output = {
2838 let stdout_buf = stdout_captured.lock().unwrap_or_else(|e| e.into_inner());
2839 let stderr_buf = stderr_captured.lock().unwrap_or_else(|e| e.into_inner());
2840 format!("{}{}", *stdout_buf, *stderr_buf)
2841 };
2842
2843 if is_up_to_date_output(&combined_output) {
2844 let _ = sender.send(InstallerEvent::Log(
2845 format!(
2846 "[native] {} — package already up to date, treating as success",
2847 component_name
2848 ),
2849 false,
2850 ));
2851 return Ok(());
2852 }
2853
2854 let code = status.code().unwrap_or(-1);
2855 let error_detail = match code {
2856 1 => "General error — check the command output above for details".to_string(),
2857 2 => "Misuse of shell builtins — check command arguments".to_string(),
2858 126 => "Command not executable — check file permissions".to_string(),
2859 127 => "Command not found — ensure the required tool is installed".to_string(),
2860 130 => "Process terminated by Ctrl+C (SIGINT)".to_string(),
2861 137 => "Process killed (SIGKILL) — possibly out of memory".to_string(),
2862 139 => "Segmentation fault — this is a bug in the invoked program".to_string(),
2863 n if n > 128 => format!(
2864 "Process terminated by signal {} — check system logs",
2865 n - 128
2866 ),
2867 _ => format!(
2868 "Command exited with code {} — check the output above for details",
2869 code
2870 ),
2871 };
2872 let _ = sender.send(InstallerEvent::Log(
2873 format!(
2874 "[ERROR] {} failed: {} (exit code {} at {})",
2875 component_name,
2876 error_detail,
2877 code,
2878 Local::now().format("%H:%M:%S")
2879 ),
2880 false,
2881 ));
2882 bail!(
2883 "{} failed: {} (exit code {})",
2884 component_name,
2885 error_detail,
2886 code
2887 );
2888 }
2889
2890 Ok(())
2891}
2892
2893fn build_common_env(ctx: &NativeInstallerContext) -> Vec<(String, String)> {
2902 let mut env = Vec::new();
2903
2904 env.push(("HOME".into(), ctx.user_home.to_string()));
2906 if let Ok(user) = std::env::var("USER") {
2907 env.push(("USER".into(), user.clone()));
2908 env.push(("LOGNAME".into(), user));
2909 }
2910 env.push(("MLSTACK_USER_HOME".into(), ctx.user_home.to_string()));
2911 env.push((
2912 "MLSTACK_BATCH_MODE".into(),
2913 if ctx.batch_mode { "1" } else { "0" }.into(),
2914 ));
2915 env.push((
2916 "MLSTACK_INSTALL_METHOD".into(),
2917 ctx.install_method.to_string(),
2918 ));
2919 env.push(("INSTALL_METHOD".into(), ctx.install_method.to_string()));
2920 env.push(("RUSTY_STACK".into(), "true".into()));
2921
2922 for key in &[
2924 "ROCM_VERSION",
2925 "ROCM_CHANNEL",
2926 "GPU_ARCH",
2927 "GPU_ARCHS",
2928 "PYTORCH_ROCM_ARCH",
2929 "HIP_VISIBLE_DEVICES",
2930 "CUDA_VISIBLE_DEVICES",
2931 "ROCR_VISIBLE_DEVICES",
2932 "PYTORCH_ROCM_DEVICE",
2933 "HSA_OVERRIDE_GFX_VERSION",
2934 "MLSTACK_PYTHON_BIN",
2935 "UV_PYTHON",
2936 ] {
2937 if let Some(value) = ctx.env_exports.get(*key) {
2938 let trimmed = value.trim();
2939 if !trimmed.is_empty() {
2940 env.push((key.to_string(), trimmed.to_string()));
2941 }
2942 }
2943 }
2944
2945 env.push(("ROCM_HOME".into(), "/opt/rocm".into()));
2947 env.push(("ROCM_PATH".into(), "/opt/rocm".into()));
2948 env.push(("HIP_PATH".into(), "/opt/rocm".into()));
2949 env.push(("HIP_ROOT_DIR".into(), "/opt/rocm".into()));
2950 env.push(("ROCM_ROOT".into(), "/opt/rocm".into()));
2951 env.push(("HIPCC_BIN_DIR".into(), "/opt/rocm/bin".into()));
2952
2953 env.push(("PIP_BREAK_SYSTEM_PACKAGES".into(), "1".into()));
2955 env.push(("PIP_ROOT_USER_ACTION".into(), "ignore".into()));
2956 env.push(("UV_PIP_BREAK_SYSTEM_PACKAGES".into(), "1".into()));
2957 env.push(("UV_SYSTEM_PYTHON".into(), "1".into()));
2958 env.push(("PYTHONUNBUFFERED".into(), "1".into()));
2959
2960 env
2961}
2962
2963fn is_existing_git_repo(target_dir: &str) -> bool {
2973 let path = Path::new(target_dir);
2974 match std::fs::symlink_metadata(target_dir) {
2977 Ok(meta) if meta.is_dir() => {
2978 path.join(".git")
2980 .symlink_metadata()
2981 .map(|m| m.is_dir() || m.is_file())
2982 .unwrap_or(false)
2983 }
2984 _ => false,
2985 }
2986}
2987
2988fn fix_directory_ownership(dir: &str, sudo_pw: Option<&str>, sender: &Sender<InstallerEvent>) {
3005 let stat_output = std::process::Command::new("stat")
3007 .args(["-c", "%U", dir])
3008 .output();
3009
3010 let needs_fix = match stat_output {
3011 Ok(out) if out.status.success() => {
3012 let owner = String::from_utf8_lossy(&out.stdout).trim().to_string();
3013 owner == "root"
3014 }
3015 _ => true, };
3017
3018 if !needs_fix {
3019 return;
3020 }
3021
3022 let real_user = std::env::var("SUDO_USER")
3023 .or_else(|_| std::env::var("USER"))
3024 .unwrap_or_else(|_| "scooter".to_string());
3025
3026 let _ = sender.send(InstallerEvent::Log(
3027 format!("[native] Fixing ownership of {} -> {}", dir, real_user),
3028 false,
3029 ));
3030
3031 if sudo_pw.is_some() {
3032 let chown_cmd = NativeCommand::from_shell_cmd(
3033 "chown",
3034 &[
3035 "-R".to_string(),
3036 format!("{}:{}", real_user, real_user),
3037 dir.to_string(),
3038 ],
3039 &[],
3040 );
3041 let _ = execute_native_command(&chown_cmd, sudo_pw, sender, "ownership-fix");
3042 }
3043 }
3045
3046fn git_clone_or_pull(
3047 repo_url: &str,
3048 target_dir: &str,
3049 extra_clone_args: &[&str],
3050 sudo_pw: Option<&str>,
3051 sender: &Sender<InstallerEvent>,
3052 component_name: &str,
3053) -> Result<()> {
3054 if is_existing_git_repo(target_dir) {
3055 let _ = sender.send(InstallerEvent::Log(
3056 format!(
3057 "[native] {} — directory '{}' already exists, pulling updates instead of cloning",
3058 component_name, target_dir
3059 ),
3060 false,
3061 ));
3062
3063 let pull_cmd = NativeCommand::from_shell_cmd(
3065 "git",
3066 &[
3067 "-C".to_string(),
3068 target_dir.to_string(),
3069 "pull".to_string(),
3070 "--ff-only".to_string(),
3071 ],
3072 &[],
3073 );
3074 let pull_result = execute_native_command(&pull_cmd, sudo_pw, sender, component_name);
3075
3076 if pull_result.is_ok() {
3077 return Ok(());
3078 }
3079
3080 let _ = sender.send(InstallerEvent::Log(
3082 format!(
3083 "[native] {} — git pull --ff-only failed, trying fetch + reset --hard origin/HEAD",
3084 component_name
3085 ),
3086 false,
3087 ));
3088
3089 let fetch_cmd = NativeCommand::from_shell_cmd(
3090 "git",
3091 &[
3092 "-C".to_string(),
3093 target_dir.to_string(),
3094 "fetch".to_string(),
3095 "--all".to_string(),
3096 ],
3097 &[],
3098 );
3099 let fetch_ok = execute_native_command(&fetch_cmd, sudo_pw, sender, component_name).is_ok();
3100
3101 let reset_ok = if fetch_ok {
3102 let reset_cmd = NativeCommand::from_shell_cmd(
3103 "git",
3104 &[
3105 "-C".to_string(),
3106 target_dir.to_string(),
3107 "reset".to_string(),
3108 "--hard".to_string(),
3109 "origin/HEAD".to_string(),
3110 ],
3111 &[],
3112 );
3113 execute_native_command(&reset_cmd, sudo_pw, sender, component_name).is_ok()
3114 } else {
3115 false
3116 };
3117
3118 if fetch_ok && reset_ok {
3119 return Ok(());
3120 }
3121
3122 let _ = sender.send(InstallerEvent::Log(
3126 format!(
3127 "[native] {} — existing .git is corrupt/unusable (pull+fetch+reset failed); removing and re-cloning",
3128 component_name
3129 ),
3130 false,
3131 ));
3132 if std::fs::remove_dir_all(target_dir).is_err() {
3133 let rm_cmd = NativeCommand::from_shell_cmd(
3134 "rm",
3135 &["-rf".to_string(), target_dir.to_string()],
3136 &[],
3137 );
3138 let _ = execute_native_command(&rm_cmd, sudo_pw, sender, component_name);
3139 }
3140 if Path::new(target_dir).join(".git").exists() {
3147 bail!(
3148 "{} — could not remove corrupt repo at '{}' (.git still present); \
3149 aborting re-clone to avoid infinite recursion",
3150 component_name,
3151 target_dir
3152 );
3153 }
3154 git_clone_or_pull(
3155 repo_url,
3156 target_dir,
3157 extra_clone_args,
3158 sudo_pw,
3159 sender,
3160 component_name,
3161 )
3162 } else {
3163 let target_path = Path::new(target_dir);
3168 if target_path.is_dir() && !target_path.join(".git").exists() {
3169 let _ = sender.send(InstallerEvent::Log(
3170 format!(
3171 "[native] {} — directory '{}' exists but is not a git repo; removing incomplete clone",
3172 component_name, target_dir
3173 ),
3174 false,
3175 ));
3176 if std::fs::remove_dir_all(target_dir).is_err() {
3178 let rm_cmd = NativeCommand::from_shell_cmd(
3180 "rm",
3181 &["-rf".to_string(), target_dir.to_string()],
3182 &[],
3183 );
3184 let rm_result = execute_native_command(&rm_cmd, sudo_pw, sender, component_name);
3185 if let Err(e) = rm_result {
3186 let _ = sender.send(InstallerEvent::Log(
3187 format!(
3188 "[native] [WARN] {} — could not remove stale dir: {}. Clone may fail.",
3189 component_name, e
3190 ),
3191 false,
3192 ));
3193 }
3194 }
3195 }
3196
3197 let _ = sender.send(InstallerEvent::Log(
3198 format!(
3199 "[native] {} — cloning '{}' into '{}'",
3200 component_name, repo_url, target_dir
3201 ),
3202 false,
3203 ));
3204
3205 let mut args: Vec<String> = vec!["clone".to_string()];
3206 for extra in extra_clone_args {
3207 args.push(extra.to_string());
3208 }
3209 args.push(repo_url.to_string());
3210 args.push(target_dir.to_string());
3211
3212 let clone_cmd = NativeCommand::from_shell_cmd("git", &args, &[]);
3213 execute_native_command(&clone_cmd, sudo_pw, sender, component_name)
3214 }
3215}
3216
3217fn filter_cuda_requirements(req_path: &str) -> std::io::Result<String> {
3248 crate::installers::common::nvidia_blocklist::filter_requirements_file(req_path)
3249}
3250
3251fn is_force_reinstall() -> bool {
3256 std::env::var("MLSTACK_FORCE_REINSTALL")
3257 .or_else(|_| std::env::var("FORCE"))
3258 .map(|v| {
3259 matches!(
3260 v.trim().to_ascii_lowercase().as_str(),
3261 "1" | "true" | "yes" | "on"
3262 )
3263 })
3264 .unwrap_or(false)
3265}
3266
3267fn triton_installed_satisfies_target(python_bin: &str, target: Option<&str>) -> bool {
3282 let target = match target {
3283 Some(t) => t.trim(),
3284 None => return true, };
3286 if target.is_empty() || target == "latest" || target == "installed" {
3288 return true;
3289 }
3290 if is_force_reinstall() {
3292 return false;
3293 }
3294 let installed = std::process::Command::new(python_bin)
3296 .arg("-c")
3297 .arg(
3298 "try:\n import triton, importlib.metadata as im\n \
3299 print(getattr(triton, '__version__', None) or im.version('triton') \
3300 or im.version('triton-rocm') or im.version('pytorch-triton'))\n\
3301 except Exception:\n pass",
3302 )
3303 .output()
3304 .ok()
3305 .and_then(|o| {
3306 if o.status.success() {
3307 String::from_utf8(o.stdout).ok()
3308 } else {
3309 None
3310 }
3311 })
3312 .map(|s| s.trim().to_string())
3313 .filter(|s| !s.is_empty());
3314 match installed {
3315 Some(ver) => version_satisfies(&ver, target),
3316 None => false, }
3318}
3319
3320fn version_satisfies(installed: &str, target: &str) -> bool {
3326 fn parts(v: &str) -> Vec<u64> {
3327 v.split('-')
3328 .next()
3329 .unwrap_or("")
3330 .split('.')
3331 .filter_map(|s| s.parse::<u64>().ok())
3332 .collect()
3333 }
3334 let a = parts(installed.trim());
3335 let b = parts(target.trim());
3336 if a.is_empty() || b.is_empty() {
3337 return false;
3339 }
3340 for (ai, bi) in a.iter().zip(b.iter()) {
3341 match ai.cmp(bi) {
3342 std::cmp::Ordering::Greater => return true,
3343 std::cmp::Ordering::Less => return false,
3344 std::cmp::Ordering::Equal => continue,
3345 }
3346 }
3347 a.len() >= b.len()
3350}
3351
3352fn build_pip_uninstall(packages: &[&str]) -> NativeCommand {
3357 let use_uv = crate::installers::common::utils::command_exists("uv");
3358 if use_uv {
3359 let mut args = vec!["pip".to_string(), "uninstall".to_string()];
3360 for pkg in packages {
3361 args.push(pkg.to_string());
3362 }
3363 NativeCommand::Pip {
3364 program: "uv".to_string(),
3365 args,
3366 }
3367 } else {
3368 let mut args = vec!["uninstall".to_string(), "-y".to_string()];
3369 for pkg in packages {
3370 args.push(pkg.to_string());
3371 }
3372 NativeCommand::Pip {
3373 program: "pip3".to_string(),
3374 args,
3375 }
3376 }
3377}
3378
3379fn purge_component_packages(
3385 packages: &[&str],
3386 sender: &Sender<InstallerEvent>,
3387 component_name: &str,
3388) {
3389 let _ = sender.send(InstallerEvent::Log(
3390 format!(
3391 "[native] {} — purging packages: {}",
3392 component_name,
3393 packages.join(", ")
3394 ),
3395 false,
3396 ));
3397
3398 let uninstall_cmd = build_pip_uninstall(packages);
3400 let _ = execute_native_command(&uninstall_cmd, None, sender, component_name);
3401}
3402
3403fn purge_pytorch(sender: &Sender<InstallerEvent>, component_name: &str) {
3405 purge_component_packages(
3406 &[
3407 "torch",
3408 "torchvision",
3409 "torchaudio",
3410 "pytorch-triton-rocm",
3411 "pytorch-triton",
3412 ],
3413 sender,
3414 component_name,
3415 );
3416}
3417
3418fn purge_triton(sender: &Sender<InstallerEvent>, component_name: &str) {
3420 purge_component_packages(
3421 &["triton", "triton-rocm", "pytorch-triton-rocm"],
3422 sender,
3423 component_name,
3424 );
3425}
3426
3427fn purge_aiter(sender: &Sender<InstallerEvent>, component_name: &str) {
3429 purge_component_packages(&["aiter", "amd-aiter"], sender, component_name);
3430}
3431
3432fn purge_flash_attn(sender: &Sender<InstallerEvent>, component_name: &str) {
3434 purge_component_packages(
3435 &["flash-attn", "flash_attn", "flash_attention_amd"],
3436 sender,
3437 component_name,
3438 );
3439}
3440
3441fn purge_vllm(sender: &Sender<InstallerEvent>, component_name: &str) {
3443 purge_component_packages(&["vllm"], sender, component_name);
3444}
3445
3446fn update_rocm_channel(target: Option<&str>) -> crate::installers::components::rocm::RocmChannel {
3457 use crate::installers::components::rocm::RocmChannel;
3458 if is_explicit_update_target(target) {
3459 if let Some(channel) = target.and_then(RocmChannel::for_version_to_channel) {
3460 return channel;
3461 }
3462 }
3465 RocmChannel::configured()
3466}
3467
3468struct ScopedEnvOverride {
3476 previous: Vec<(String, Option<String>)>,
3477}
3478
3479impl ScopedEnvOverride {
3480 fn setting(entries: &[(&str, &str)]) -> Self {
3481 let previous = entries
3482 .iter()
3483 .map(|(key, value)| {
3484 let old = std::env::var(key).ok();
3485 std::env::set_var(key, value);
3486 ((*key).to_string(), old)
3487 })
3488 .collect();
3489 Self { previous }
3490 }
3491}
3492
3493impl Drop for ScopedEnvOverride {
3494 fn drop(&mut self) {
3495 for (key, old) in &self.previous {
3496 match old {
3497 Some(value) => std::env::set_var(key, value),
3498 None => std::env::remove_var(key),
3499 }
3500 }
3501 }
3502}
3503
3504fn is_explicit_update_target(target: Option<&str>) -> bool {
3510 match target {
3511 Some(t) => {
3512 let t = t.trim();
3513 !t.is_empty() && !t.eq_ignore_ascii_case("latest") && !t.eq_ignore_ascii_case("installed")
3514 }
3515 None => false,
3516 }
3517}
3518
3519fn unseal_core_requested() -> bool {
3522 std::env::var("MLSTACK_UNSEAL_CORE")
3523 .map(|v| {
3524 matches!(
3525 v.trim().to_ascii_lowercase().as_str(),
3526 "1" | "true" | "yes" | "on"
3527 )
3528 })
3529 .unwrap_or(false)
3530}
3531
3532fn registry_gate(component: &Component, sender: &Sender<InstallerEvent>) -> Result<bool> {
3541 let registry = crate::core::registry::InstalledComponentRegistry::load();
3542 if !registry.is_sealed(&component.id) {
3543 return Ok(true);
3544 }
3545 if is_force_reinstall() && !unseal_core_requested() {
3546 let _ = sender.send(InstallerEvent::Log(
3553 format!(
3554 "[native] {} is a sealed CORE component — force ignored, reusing the installed copy. \
3555 Set MLSTACK_UNSEAL_CORE=1 to intentionally override it.",
3556 component.name
3557 ),
3558 false,
3559 ));
3560 return Ok(false);
3561 }
3562 if !is_force_reinstall() {
3563 let _ = sender.send(InstallerEvent::Log(
3564 format!(
3565 "[native] {} already installed (sealed core) — reusing, NOT reinstalling",
3566 component.name
3567 ),
3568 false,
3569 ));
3570 return Ok(false);
3571 }
3572 Ok(true)
3573}
3574
3575fn registry_record(component: &Component) {
3581 use crate::core::registry::{InstalledComponent, InstalledComponentRegistry};
3582 let mut registry = InstalledComponentRegistry::load();
3583 let record = InstalledComponent {
3584 id: component.id.clone(),
3585 version: component_version(&component.id),
3586 source_index: component_source_index(&component.id),
3587 location: install_target_location(),
3588 sealed: crate::core::registry::should_seal_component(&component.id),
3589 installed_at: Some(chrono::Utc::now().to_rfc3339()),
3590 pip_packages: component_pip_packages(&component.id),
3591 };
3592 registry.mark_installed(record);
3593 if let Err(e) = registry.save() {
3594 eprintln!("[WARN] Failed to save installed component registry: {e}");
3598 }
3599}
3600
3601fn component_pip_packages(id: &str) -> Vec<String> {
3605 match id {
3606 "pytorch" => vec!["torch", "torchvision", "torchaudio", "pytorch-triton-rocm"],
3607 "triton" => vec!["triton", "pytorch-triton-rocm"],
3608 "vllm" | "vllm-multi" => vec!["vllm"],
3609 "deepspeed" => vec!["deepspeed", "einops"],
3610 "megatron" => vec!["megatron-core"],
3611 "bitsandbytes" => vec!["bitsandbytes"],
3612 "flash-attn" | "flash-attn-triton" | "flash-attn-ck" | "flash_attention" => {
3613 vec!["flash-attn", "flash_attn"]
3614 }
3615 "aiter" => vec!["aiter"],
3616 "onnx" | "onnxruntime" => vec!["onnxruntime", "onnxruntime-rocm"],
3617 "migraphx" => vec!["migraphx"],
3618 "mpi4py" => vec!["mpi4py"],
3619 "wandb" => vec!["wandb"],
3620 "fastvideo" => vec!["fastvideo"],
3621 _ => Vec::new(),
3622 }
3623 .into_iter()
3624 .map(String::from)
3625 .collect()
3626}
3627
3628fn install_target_location() -> Option<String> {
3632 if let Ok(name) = std::env::var("MLSTACK_ENV_NAME") {
3633 let name = name.trim();
3634 if !name.is_empty() {
3635 return Some(
3636 crate::platform::environment::mlstack_env_dir(name)
3637 .to_string_lossy()
3638 .into_owned(),
3639 );
3640 }
3641 }
3642 let global = crate::platform::environment::mlstack_global_dir();
3643 if global.exists() {
3644 return Some(global.to_string_lossy().into_owned());
3645 }
3646 std::env::var("MLSTACK_PYTHON_BIN")
3647 .ok()
3648 .map(|s| s.trim().to_string())
3649 .filter(|s| !s.is_empty())
3650}
3651
3652fn component_source_index(id: &str) -> Option<String> {
3658 match id {
3659 "pytorch" => Some("https://download.pytorch.org/whl/rocm".to_string()),
3660 "triton" => Some("triton-rocm (bundled with PyTorch)".to_string()),
3661 "rocm" => Some("/opt/rocm".to_string()),
3662 _ => None,
3663 }
3664}
3665
3666fn component_version(id: &str) -> Option<String> {
3670 match id {
3671 "rocm" => {
3672 let v = detect_rocm_version();
3673 if v.is_empty() {
3674 None
3675 } else {
3676 Some(v)
3677 }
3678 }
3679 _ => None,
3680 }
3681}
3682
3683fn ensure_user_in_groups(
3700 groups: &[&str],
3701 sudo_pw: Option<&str>,
3702 sender: &Sender<InstallerEvent>,
3703 component_name: &str,
3704) {
3705 use std::process::Command;
3706
3707 let user = std::env::var("SUDO_USER")
3710 .ok()
3711 .map(|u| u.trim().to_string())
3712 .filter(|u| !u.is_empty())
3713 .or_else(|| {
3714 std::env::var("USER")
3715 .ok()
3716 .map(|u| u.trim().to_string())
3717 .filter(|u| !u.is_empty())
3718 });
3719 let user = match user {
3720 Some(u) => u,
3721 None => {
3722 let _ = sender.send(InstallerEvent::Log(
3723 format!(
3724 "[native] {component_name} — could not determine target user; skipping \
3725 group check. Add yourself manually: sudo usermod -aG {} $USER",
3726 groups.join(",")
3727 ),
3728 false,
3729 ));
3730 return;
3731 }
3732 };
3733
3734 let current_groups: Vec<String> = Command::new("id")
3737 .args(["-nG", &user])
3738 .output()
3739 .ok()
3740 .filter(|o| o.status.success())
3741 .map(|o| {
3742 String::from_utf8_lossy(&o.stdout)
3743 .split_whitespace()
3744 .map(|s| s.to_string())
3745 .collect()
3746 })
3747 .unwrap_or_default();
3748
3749 let mut to_add: Vec<&str> = Vec::new();
3751 for g in groups {
3752 let exists = Command::new("getent")
3753 .args(["group", g])
3754 .output()
3755 .map(|o| o.status.success())
3756 .unwrap_or(false);
3757 if !exists {
3758 let _ = sender.send(InstallerEvent::Log(
3759 format!(
3760 "[native] {component_name} — group '{g}' not present on this system; skipping"
3761 ),
3762 false,
3763 ));
3764 continue;
3765 }
3766 if !current_groups.iter().any(|c| c == g) {
3767 to_add.push(*g);
3768 }
3769 }
3770
3771 if to_add.is_empty() {
3772 let _ = sender.send(InstallerEvent::Log(
3773 format!(
3774 "[native] {component_name} — user '{user}' already in all required group(s); nothing to add"
3775 ),
3776 false,
3777 ));
3778 return;
3779 }
3780
3781 let csv = to_add.join(",");
3782
3783 let askpass = sudo_pw.filter(|p| !p.is_empty()).and_then(|p| {
3785 match crate::installers::common::askpass::Askpass::new(p) {
3786 Ok(g) => Some(g),
3787 Err(e) => {
3788 let _ = sender.send(InstallerEvent::Log(
3789 format!(
3790 "[native] {component_name} — askpass setup failed ({e}); cannot add \
3791 groups non-interactively"
3792 ),
3793 true,
3794 ));
3795 None
3796 }
3797 }
3798 });
3799
3800 let added = if let Some(ap) = askpass {
3801 let args = vec![
3804 "-A".to_string(),
3805 "usermod".to_string(),
3806 "-aG".to_string(),
3807 csv.clone(),
3808 user.clone(),
3809 ];
3810 let envs = vec![("SUDO_ASKPASS".to_string(), ap.path().to_string())];
3811 let cmd = NativeCommand::from_shell_cmd("sudo", &args, &envs);
3812 execute_native_command(&cmd, None, sender, component_name).is_ok()
3813 } else {
3814 let _ = sender.send(InstallerEvent::Log(
3815 format!(
3816 "[native] {component_name} — no sudo password available; please run: \
3817 sudo usermod -aG {csv} {user}"
3818 ),
3819 true,
3820 ));
3821 return;
3822 };
3823
3824 if added {
3825 let _ = sender.send(InstallerEvent::Log(
3826 format!(
3827 "[native] {component_name} — added user '{user}' to group(s): {csv}. \
3828 A re-login (or reboot) is required for the new groups to take effect."
3829 ),
3830 false,
3831 ));
3832 }
3833}
3834
3835fn run_rccl_remediation(ctx: &NativeInstallerContext, component_name: &str) -> Result<()> {
3837 let sender = ctx.sender;
3838 let rccl_remediation = crate::installers::components::rccl::RcclInstaller::new(
3839 ctx.user_home,
3840 resolve_python_bin(),
3841 ctx.env_exports
3842 .get("ROCM_PATH")
3843 .cloned()
3844 .unwrap_or_else(|| "/opt/rocm".to_string()),
3845 ctx.env_exports
3846 .get("ROCM_VERSION")
3847 .cloned()
3848 .unwrap_or_else(|| "7.2.0".to_string()),
3849 ctx.env_exports
3850 .get("ROCM_CHANNEL")
3851 .cloned()
3852 .unwrap_or_else(|| "latest".to_string()),
3853 ctx.env_exports
3854 .get("ROCR_VISIBLE_DEVICES")
3855 .cloned()
3856 .or_else(|| ctx.env_exports.get("HIP_VISIBLE_DEVICES").cloned()),
3857 );
3858
3859 let _ = sender.send(InstallerEvent::Log(
3860 "[native] Probing RCCL multi-GPU collectives...".into(),
3861 false,
3862 ));
3863 if let Some(build) = rccl_remediation.prepare_remediation()? {
3864 let _ = sender.send(InstallerEvent::Log(
3865 "[native] Known RCCL collective failure detected; building sealed overlay...".into(),
3866 false,
3867 ));
3868 execute_native_command(
3869 &NativeCommand::from_shell_cmd(&build.program, &build.args, &build.env),
3870 None,
3871 sender,
3872 component_name,
3873 )?;
3874 rccl_remediation.activate_and_verify()?;
3875 let _ = sender.send(InstallerEvent::Log(
3876 "[native] RCCL sealed overlay activated and validated on both GPUs".into(),
3877 false,
3878 ));
3879 } else {
3880 let _ = sender.send(InstallerEvent::Log(
3881 "[native] RCCL probe healthy or existing sealed overlay revalidated; no repair needed"
3882 .into(),
3883 false,
3884 ));
3885 }
3886 Ok(())
3887}
3888
3889fn run_native_installer(component: &Component, ctx: &NativeInstallerContext) -> Result<()> {
3897 let sender = ctx.sender;
3898 let _ = sender.send(InstallerEvent::Log(
3899 format!("[native] Installing {} via Rust module...", component.name),
3900 false,
3901 ));
3902
3903 let _ = sender.send(InstallerEvent::Progress {
3904 component_id: component.id.clone(),
3905 progress: 0.1,
3906 message: format!("Starting {} installation", component.name),
3907 });
3908
3909 let component_needs_sudo = component.needs_sudo && needs_sudo();
3911 let cached_sudo =
3912 component_needs_sudo && ctx.sudo_password.is_none() && can_sudo_non_interactive();
3913 let sudo_pw: Option<&str> = if component_needs_sudo {
3914 ctx.sudo_password
3915 .as_deref()
3916 .or(if cached_sudo { Some("") } else { None })
3917 } else {
3918 None
3919 };
3920
3921 if component_needs_sudo && sudo_pw.is_none() {
3923 bail!("{} requires sudo but no password provided", component.name);
3924 }
3925
3926 let common_env = build_common_env(ctx);
3928
3929 match component.id.as_str() {
3933 "permanent-env" => {
3935 use crate::installers::components::permanent_env::PermanentEnvInstaller;
3936 match ensure_mlstack_env(ctx.user_home, ctx.install_method, is_force_reinstall()) {
3947 Ok(EnvUpdate::Created) | Ok(EnvUpdate::Updated) => {
3948 let _ = sender.send(InstallerEvent::Log(
3949 "[native] Permanent ROCm Env — regenerated ~/.mlstack_env from current state".into(),
3950 false,
3951 ));
3952 }
3953 Ok(EnvUpdate::Unchanged) => {
3954 let _ = sender.send(InstallerEvent::Log(
3955 "[native] Permanent ROCm Env — ~/.mlstack_env already up to date".into(),
3956 false,
3957 ));
3958 }
3959 Err(e) => {
3960 let _ = sender.send(InstallerEvent::Log(
3961 format!(
3962 "[native] Permanent ROCm Env — could not regenerate ~/.mlstack_env: {e}"
3963 ),
3964 true,
3965 ));
3966 }
3967 }
3968 crate::platform::environment::write_activate_snippets()?;
3970 crate::platform::environment::offer_global_prepend(true)?;
3971 let inst = PermanentEnvInstaller::default();
3972 let cmd = inst.build_mkdir_triton_dirs_command();
3973 execute_native_command(
3974 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
3975 sudo_pw,
3976 sender,
3977 &component.name,
3978 )?;
3979 }
3980
3981 "rocm" => {
3983 use crate::installers::components::rocm::{RocmConfig, RocmInstaller};
3984 let rocm_force = is_force_reinstall();
3985 use crate::installers::components::rocm::RocmChannel;
3986 let rocm_channel = update_rocm_channel(ctx.target_version.as_deref());
3987 let inst = RocmInstaller::new(RocmConfig {
3988 force_reinstall: rocm_force,
3989 channel: rocm_channel,
3990 gfx_arch: detect_gpu_arch().unwrap_or_else(|| "gfx1100".to_string()),
3991 legacy_fallback: std::env::var("ROCM_LEGACY_VERSION")
3992 .map(|v| v.trim() == "7.2.3")
3993 .unwrap_or(false),
3994 ..RocmConfig::default()
3995 });
3996 let distro = crate::installers::common::DistroFacade::detect();
3997 let is_arch = distro.is_arch_family();
3998 if rocm_channel.is_therock() && (is_arch || rocm_channel == RocmChannel::Preview) {
3999 let home = std::env::var("HOME").unwrap_or_default();
4002 let venv = format!("{home}/.mlstack/rocm/{}", inst.effective_version());
4003 let index = inst
4004 .pip_index_url()
4005 .ok_or_else(|| anyhow::anyhow!("TheRock channel has no pip index"))?
4006 .to_string();
4007 if !std::process::Command::new("uv")
4008 .arg("--version")
4009 .output()
4010 .map(|o| o.status.success())
4011 .unwrap_or(false)
4012 {
4013 bail!(
4014 "`uv` is required to install ROCm {} into a user-space environment; install it from https://docs.astral.sh/uv/ and retry",
4015 inst.effective_version()
4016 );
4017 }
4018 let venv_python =
4019 std::env::var("ROCM_VENV_PYTHON").unwrap_or_else(|_| "3.12".to_string());
4020 let req = inst.pip_requirement();
4021 let steps: Vec<(&str, Vec<String>)> = vec![
4022 (
4023 "uv",
4024 vec![
4025 "venv".into(),
4026 "--python".into(),
4027 venv_python.clone(),
4028 venv.clone(),
4029 ],
4030 ),
4031 (
4032 "uv",
4033 vec![
4034 "pip".into(),
4035 "install".into(),
4036 "--python".into(),
4037 format!("{venv}/bin/python"),
4038 "--index-url".into(),
4039 index,
4040 "--extra-index-url".into(),
4041 "https://pypi.org/simple".into(),
4042 "--index-strategy".into(),
4043 "unsafe-best-match".into(),
4044 req,
4045 ],
4046 ),
4047 ];
4048 for (prog, args) in steps {
4049 execute_native_command(
4050 &NativeCommand::Pip {
4051 program: prog.to_string(),
4052 args,
4053 },
4054 None,
4055 sender,
4056 &component.name,
4057 )?;
4058 }
4059 let _ = sender.send(InstallerEvent::Log(
4060 format!(
4061 "[native] ROCm {} installed to {venv}",
4062 inst.effective_version()
4063 ),
4064 false,
4065 ));
4066 } else if is_arch {
4067 let all_pkgs = inst.pacman_rocm_packages();
4073 let force_reinstall = is_force_reinstall();
4074 let pkgs_to_install = if force_reinstall {
4075 all_pkgs.clone()
4076 } else {
4077 let need_install = RocmInstaller::filter_already_installed_pacman(&all_pkgs);
4078 if need_install.is_empty() {
4079 let _ = sender.send(InstallerEvent::Log(
4080 format!(
4081 "[native] {} — all ROCm packages already installed, skipping",
4082 component.name
4083 ),
4084 false,
4085 ));
4086 return Ok(());
4087 }
4088 need_install
4089 };
4090
4091 let _ = sender.send(InstallerEvent::Log(
4092 format!(
4093 "[native] {} — installing {} of {} ROCm packages via yay \
4094 (user-space, non-interactive): {}",
4095 component.name,
4096 pkgs_to_install.len(),
4097 all_pkgs.len(),
4098 pkgs_to_install.join(", ")
4099 ),
4100 false,
4101 ));
4102
4103 let mut args = vec!["-S".to_string()];
4113 if !force_reinstall {
4114 args.push("--needed".to_string());
4115 }
4116 args.push("--noconfirm".to_string());
4117
4118 let askpass = sudo_pw.filter(|p| !p.is_empty()).and_then(|p| {
4120 match crate::installers::common::askpass::Askpass::new(p) {
4121 Ok(g) => Some(g),
4122 Err(e) => {
4123 let _ = sender.send(InstallerEvent::Log(
4124 format!(
4125 "[native] {} — askpass setup failed ({e}); yay's \
4126 privileged steps may not authenticate",
4127 component.name
4128 ),
4129 true,
4130 ));
4131 None
4132 }
4133 }
4134 });
4135 let mut envs: Vec<(String, String)> = Vec::new();
4136 if let Some(ref ap) = askpass {
4137 envs.push(("SUDO_ASKPASS".to_string(), ap.path().to_string()));
4138 args.push("--sudoflags=-A".to_string());
4139 } else {
4140 let _ = sender.send(InstallerEvent::Log(
4141 format!(
4142 "[native] {} — no sudo password supplied; yay will only \
4143 authenticate if sudo is cached / passwordless",
4144 component.name
4145 ),
4146 false,
4147 ));
4148 }
4149 args.extend(pkgs_to_install);
4150
4151 let yay_cmd = NativeCommand::Shell {
4152 program: "yay".to_string(),
4153 args,
4154 env: envs,
4155 working_dir: None,
4156 };
4157 execute_native_command(&yay_cmd, None, sender, &component.name)?;
4160 } else {
4161 let therock = rocm_channel.is_therock();
4162 let commands = if distro.uses_apt() {
4163 if therock {
4164 inst.therock_apt_commands(&distro)
4165 .map_err(|e| anyhow::anyhow!(e))?
4166 } else {
4167 inst.apt_install_commands()
4168 }
4169 } else if distro.uses_dnf() || distro.uses_yum() {
4170 if therock {
4171 inst.therock_rpm_commands(&distro)
4172 .map_err(|e| anyhow::anyhow!(e))?
4173 } else {
4174 let rhel_ver = distro.version().split('.').next().unwrap_or("9");
4175 inst.dnf_install_commands(rhel_ver)
4176 }
4177 } else if distro.uses_zypper() {
4178 if therock {
4179 inst.therock_rpm_commands(&distro)
4180 .map_err(|e| anyhow::anyhow!(e))?
4181 } else {
4182 inst.zypper_install_commands()
4183 }
4184 } else {
4185 bail!(
4186 "Unsupported distribution for ROCm installation: {} ({})",
4187 distro.name(),
4188 distro.id()
4189 );
4190 };
4191 for pkg_cmd in &commands {
4192 let native_cmd = NativeCommand::Package {
4193 program: pkg_cmd.program.clone(),
4194 args: pkg_cmd.args.clone(),
4195 };
4196 execute_native_command(&native_cmd, sudo_pw, sender, &component.name)?;
4199 }
4200 }
4201
4202 if is_force_reinstall() {
4204 let marker_path = std::path::Path::new("/tmp/mlstack-reboot-required");
4205 let _ = std::fs::write(
4206 marker_path,
4207 "ROCm packages were reinstalled. Reboot required for kernel module changes.\n",
4208 );
4209 let _ = sender.send(InstallerEvent::Log(
4210 "[native] ROCm — reboot recommended for kernel module changes".to_string(),
4211 false,
4212 ));
4213 }
4214
4215 ensure_user_in_groups(&["render", "video"], sudo_pw, sender, &component.name);
4220 }
4221
4222 "pytorch" => {
4224 use crate::installers::components::pytorch::{PyTorchConfig, PyTorchInstaller};
4225 let force = is_force_reinstall();
4226 let python_bin = resolve_python_bin();
4227 let config = PyTorchConfig {
4228 python_bin: python_bin.clone(),
4229 force_reinstall: force,
4230 ..PyTorchConfig::default()
4231 };
4232 let inst = PyTorchInstaller::new(config);
4233 let use_uv = crate::installers::common::utils::command_exists("uv");
4234 let rocm_mm = ctx
4235 .env_exports
4236 .get("ROCM_VERSION")
4237 .map(|v| {
4238 let parts: Vec<&str> = v.split('.').collect();
4239 if parts.len() >= 2 {
4240 format!("{}.{}", parts[0], parts[1])
4241 } else {
4242 v.clone()
4243 }
4244 })
4245 .unwrap_or_else(|| "7.2".to_string());
4246
4247 if force {
4249 purge_pytorch(sender, &component.name);
4250 }
4251
4252 let install_cmd =
4261 inst.build_install_command(&rocm_mm, use_uv, ctx.target_version.as_deref());
4262 execute_native_command(
4263 &NativeCommand::Pip {
4264 program: install_cmd.program.clone(),
4265 args: install_cmd.args.clone(),
4266 },
4267 None,
4268 sender,
4269 &component.name,
4270 )?;
4271
4272 let deps_cmd = inst.build_common_deps_command(use_uv);
4277 let _ = common_env.clone(); execute_native_command(
4279 &NativeCommand::Pip {
4280 program: deps_cmd.program.clone(),
4281 args: deps_cmd.args.clone(),
4282 },
4283 None, sender,
4285 &component.name,
4286 )?;
4287
4288 let managed_python = resolve_python_bin();
4293 let managed_install =
4294 crate::installers::common::managed_python_install_command(managed_python.clone());
4295 execute_native_command(
4296 &NativeCommand::Pip {
4297 program: managed_install.program,
4298 args: managed_install.args,
4299 },
4300 None,
4301 sender,
4302 &component.name,
4303 )?;
4304
4305 let managed_verify =
4306 crate::installers::common::managed_python_verify_command(managed_python);
4307 execute_native_command(
4308 &NativeCommand::from_shell_cmd(&managed_verify.program, &managed_verify.args, &[]),
4309 None,
4310 sender,
4311 &component.name,
4312 )?;
4313 }
4314
4315 "triton" => {
4317 use crate::installers::components::triton::{TritonConfig, TritonInstaller};
4318
4319 let python_bin = resolve_python_bin();
4336 let triton_present = std::process::Command::new(&python_bin)
4337 .arg("-c")
4338 .arg("import triton")
4339 .status()
4340 .map(|s| s.success())
4341 .unwrap_or(false);
4342 let triton_satisfies_target = triton_present
4343 && triton_installed_satisfies_target(&python_bin, ctx.target_version.as_deref());
4344 if triton_satisfies_target {
4345 let _ = sender.send(InstallerEvent::Log(
4346 "[native] Triton — already importable (triton-rocm bundled with PyTorch) \
4347 and satisfies the requested target; skipping the redundant from-source \
4348 ROCm/triton build"
4349 .into(),
4350 false,
4351 ));
4352 return Ok(());
4353 }
4354
4355 let triton_force = is_force_reinstall();
4356 let inst = TritonInstaller::new(TritonConfig {
4357 python_bin: resolve_python_bin(),
4358 ..Default::default()
4359 });
4360 let target_dir = format!("{}/.mlstack/triton", ctx.user_home);
4361
4362 if triton_force {
4364 purge_triton(sender, &component.name);
4365 }
4366
4367 git_clone_or_pull(
4371 inst.git_clone_url(),
4372 &target_dir,
4373 &[],
4374 sudo_pw,
4375 sender,
4376 &component.name,
4377 )?;
4378
4379 fix_directory_ownership(&target_dir, sudo_pw, sender);
4382
4383 let safe_dir_cmd = NativeCommand::from_shell_cmd(
4385 "git",
4386 &[
4387 "config".to_string(),
4388 "--global".to_string(),
4389 "--add".to_string(),
4390 "safe.directory".to_string(),
4391 target_dir.clone(),
4392 ],
4393 &[],
4394 );
4395 let _ = execute_native_command(&safe_dir_cmd, None, sender, &component.name);
4396
4397 let branch = inst.select_branch().to_string();
4399 let fetch_branch_cmd = NativeCommand::from_shell_cmd(
4400 "git",
4401 &[
4402 "-C".to_string(),
4403 target_dir.clone(),
4404 "fetch".to_string(),
4405 "origin".to_string(),
4406 branch.clone(),
4407 ],
4408 &[],
4409 );
4410 let _ = execute_native_command(&fetch_branch_cmd, None, sender, &component.name);
4411
4412 let checkout_cmd = inst.build_git_checkout_command();
4414 let mut checkout_args = vec!["-C".to_string(), target_dir.clone()];
4415 checkout_args.extend(checkout_cmd.args.iter().cloned());
4416 let checkout_result = execute_native_command(
4417 &NativeCommand::from_shell_cmd(
4418 &checkout_cmd.program,
4419 &checkout_args,
4420 &checkout_cmd.env,
4421 ),
4422 None,
4423 sender,
4424 &component.name,
4425 );
4426 if let Err(err) = checkout_result {
4427 let _ = sender.send(InstallerEvent::Log(
4428 format!(
4429 "[native] [WARN] {} git checkout failed (non-fatal): {}",
4430 component.name, err
4431 ),
4432 false,
4433 ));
4434 }
4435
4436 let cmd = inst.build_prerequisites_command();
4438 execute_native_command(
4439 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
4440 None,
4441 sender,
4442 &component.name,
4443 )?;
4444
4445 let cmd = inst.build_pip_install_command(&target_dir);
4447 execute_native_command(
4448 &NativeCommand::from_shell_cmd_with_dir(
4449 &cmd.program,
4450 &cmd.args,
4451 &cmd.env,
4452 cmd.working_dir.clone(),
4453 ),
4454 None,
4455 sender,
4456 &component.name,
4457 )?;
4458 }
4459
4460 "mpi4py" => {
4462 use crate::installers::components::mpi4py::{Mpi4PyConfig, Mpi4PyInstaller};
4463 let force = is_force_reinstall();
4464 let inst = Mpi4PyInstaller::new(Mpi4PyConfig {
4465 python_bin: resolve_python_bin(),
4466 force_reinstall: force,
4467 ..Mpi4PyConfig::default()
4468 });
4469 let distro = crate::installers::common::DistroFacade::detect();
4470
4471 if force {
4473 purge_component_packages(&["mpi4py"], sender, &component.name);
4474 }
4475
4476 let cmd = inst.build_system_mpi_install_command(&distro);
4478 execute_native_command(
4479 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
4480 sudo_pw,
4481 sender,
4482 &component.name,
4483 )?;
4484
4485 let mpi_path = std::path::PathBuf::from("/usr");
4487 let cmd = inst.build_pip_install_command(&mpi_path);
4488 execute_native_command(
4489 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
4490 None,
4491 sender,
4492 &component.name,
4493 )?;
4494 }
4495
4496 "deepspeed" => {
4498 use crate::installers::components::deepspeed::{DeepSpeedConfig, DeepSpeedInstaller};
4499 let force = is_force_reinstall();
4500 let config = DeepSpeedConfig {
4501 python_bin: resolve_python_bin(),
4502 force_reinstall: force,
4503 ..DeepSpeedConfig::default()
4504 };
4505 let inst = DeepSpeedInstaller::new(config);
4506
4507 if force {
4509 purge_component_packages(&["deepspeed"], sender, &component.name);
4510 }
4511
4512 let deps_cmd = inst.build_deps_install_command();
4514 execute_native_command(
4515 &NativeCommand::Pip {
4516 program: deps_cmd.program.clone(),
4517 args: deps_cmd.args.clone(),
4518 },
4519 None,
4520 sender,
4521 &component.name,
4522 )?;
4523
4524 let install_cmd = inst.build_install_command();
4526 let install_args = pin_pip_requirement(
4527 install_cmd.args.clone(),
4528 "deepspeed",
4529 ctx.target_version.as_deref(),
4530 );
4531 execute_native_command(
4532 &NativeCommand::Pip {
4533 program: install_cmd.program.clone(),
4534 args: install_args,
4535 },
4536 None,
4537 sender,
4538 &component.name,
4539 )?;
4540 }
4541
4542 "ml-stack-core" => {
4544 use crate::installers::components::ml_stack::{MlStackConfig, MlStackInstaller};
4545 let inst = MlStackInstaller::new(MlStackConfig {
4546 python_bin: resolve_python_bin(),
4547 ..Default::default()
4548 });
4549 let distro = crate::installers::common::DistroFacade::detect();
4550
4551 let migraphx_cmd = inst.build_migraphx_install_command(&distro);
4553 execute_native_command(
4554 &NativeCommand::System {
4555 program: migraphx_cmd.program.clone(),
4556 args: migraphx_cmd.args.clone(),
4557 },
4558 sudo_pw,
4559 sender,
4560 &component.name,
4561 )?;
4562
4563 let rccl_cmd = inst.build_rccl_install_command(&distro);
4564 execute_native_command(
4565 &NativeCommand::System {
4566 program: rccl_cmd.program.clone(),
4567 args: rccl_cmd.args.clone(),
4568 },
4569 sudo_pw,
4570 sender,
4571 &component.name,
4572 )?;
4573
4574 run_rccl_remediation(ctx, &component.name)?;
4577
4578 let mpi_cmd = inst.build_mpi_install_command(&distro);
4579 execute_native_command(
4580 &NativeCommand::System {
4581 program: mpi_cmd.program.clone(),
4582 args: mpi_cmd.args.clone(),
4583 },
4584 sudo_pw,
4585 sender,
4586 &component.name,
4587 )?;
4588
4589 let megatron_dir = format!("{}/.mlstack/src/Megatron-LM", ctx.user_home);
4594 let megatron_clone = inst.build_megatron_clone_command(&megatron_dir);
4595 execute_native_command(
4596 &NativeCommand::System {
4597 program: megatron_clone.program.clone(),
4598 args: megatron_clone.args.clone(),
4599 },
4600 sudo_pw,
4601 sender,
4602 &component.name,
4603 )?;
4604
4605 let megatron_install = inst.build_megatron_install_command();
4606 execute_native_command(
4607 &NativeCommand::from_shell_cmd_with_dir(
4608 &megatron_install.program,
4609 &megatron_install.args,
4610 &[],
4611 Some(PathBuf::from(&megatron_dir)),
4612 ),
4613 None,
4614 sender,
4615 &component.name,
4616 )?;
4617 }
4618
4619 "flash-attn" | "flash-attn-triton" | "flash-attn-ck" => {
4629 let force = is_force_reinstall();
4630 let is_triton = matches!(component.id.as_str(), "flash-attn" | "flash-attn-triton");
4633 let backend_name = if is_triton { "triton" } else { "ck" };
4634
4635 if force {
4637 purge_flash_attn(sender, &component.name);
4638 }
4639
4640 let target_dir = format!("{}/.mlstack/flash-attention", ctx.user_home);
4641
4642 git_clone_or_pull(
4643 "https://github.com/ROCm/flash-attention.git",
4644 &target_dir,
4645 &[],
4646 sudo_pw,
4647 sender,
4648 &component.name,
4649 )?;
4650
4651 fix_directory_ownership(&target_dir, sudo_pw, sender);
4653
4654 let safe_dir_cmd = NativeCommand::from_shell_cmd(
4656 "git",
4657 &[
4658 "config".to_string(),
4659 "--global".to_string(),
4660 "--add".to_string(),
4661 "safe.directory".to_string(),
4662 target_dir.clone(),
4663 ],
4664 &[],
4665 );
4666 let _ = execute_native_command(&safe_dir_cmd, None, sender, &component.name);
4667
4668 let gpu_arch = ctx
4670 .env_exports
4671 .get("GPU_ARCH")
4672 .cloned()
4673 .unwrap_or_else(|| "gfx1100".to_string());
4674 let hsa_version = ctx
4675 .env_exports
4676 .get("HSA_OVERRIDE_GFX_VERSION")
4677 .cloned()
4678 .unwrap_or_else(|| "11.0.0".to_string());
4679
4680 let fa_deps_args = vec![
4686 "-m".to_string(),
4687 "pip".to_string(),
4688 "install".to_string(),
4689 "--break-system-packages".to_string(),
4690 "einops".to_string(),
4691 "ninja".to_string(),
4692 "packaging".to_string(),
4693 "psutil".to_string(),
4694 ];
4695 execute_native_command(
4696 &NativeCommand::Shell {
4697 program: resolve_python_bin(),
4698 args: fa_deps_args,
4699 env: vec![],
4700 working_dir: None,
4701 },
4702 None, sender,
4704 &component.name,
4705 )?;
4706
4707 let mut pip_args = vec![
4714 "-m".to_string(),
4715 "pip".to_string(),
4716 "install".to_string(),
4717 "-v".to_string(),
4718 "--no-build-isolation".to_string(),
4719 "--no-deps".to_string(),
4720 "--break-system-packages".to_string(),
4721 ".".to_string(),
4722 ];
4723 if force {
4724 pip_args.push("--force-reinstall".to_string());
4725 }
4726
4727 let mut pip_env = vec![
4728 ("GPU_ARCH".to_string(), gpu_arch.clone()),
4729 ("PYTORCH_ROCM_ARCH".to_string(), gpu_arch),
4730 ("ROCM_PATH".to_string(), "/opt/rocm".to_string()),
4731 ("HSA_OVERRIDE_GFX_VERSION".to_string(), hsa_version),
4732 ];
4733 if is_triton {
4734 pip_env.push((
4736 "FLASH_ATTENTION_TRITON_AMD_ENABLE".to_string(),
4737 "TRUE".to_string(),
4738 ));
4739 } else {
4740 let jobs = std::thread::available_parallelism()
4744 .map(|n| n.get())
4745 .unwrap_or(4)
4746 .min(8);
4747 pip_env.push(("MAX_JOBS".to_string(), jobs.to_string()));
4748 }
4749
4750 execute_native_command(
4751 &NativeCommand::Shell {
4752 program: resolve_python_bin(),
4755 args: pip_args,
4756 env: pip_env,
4757 working_dir: Some(PathBuf::from(&target_dir)),
4758 },
4759 None, sender,
4761 &component.name,
4762 )?;
4763
4764 let marker = PathBuf::from(&target_dir).join(".backend");
4770 std::fs::write(&marker, backend_name).map_err(|err| {
4771 anyhow::anyhow!(
4772 "{}: failed to write Flash Attention backend marker {}: {} \
4773 — package installed but untrackable; failing install",
4774 component.name,
4775 marker.display(),
4776 err
4777 )
4778 })?;
4779 }
4780
4781 "repair-stack" => {
4785 use crate::installers::components::repair::RepairInstaller;
4786 let repair_ids = RepairInstaller::native_component_ids();
4787
4788 let _ = sender.send(InstallerEvent::Log(
4789 "[native] Starting ML Stack repair (8 steps via native installers)...".to_string(),
4790 false,
4791 ));
4792
4793 for step_id in &repair_ids {
4794 let sub_component = Component {
4797 id: (*step_id).to_string(),
4798 name: (*step_id).to_string(),
4799 description: String::new(),
4800 script: String::new(),
4801 category: Category::Core,
4802 required: true,
4803 selected: true,
4804 installed: false,
4805 progress: 0.0,
4806 estimate: String::new(),
4807 needs_sudo: component.needs_sudo,
4808 experimental: false,
4809 note: None,
4810 };
4811
4812 let _ = sender.send(InstallerEvent::Progress {
4813 component_id: component.id.clone(),
4814 progress: 0.5,
4815 message: format!("Repairing {}...", step_id),
4816 });
4817
4818 if let Err(err) = run_native_installer(&sub_component, ctx) {
4819 let _ = sender.send(InstallerEvent::Log(
4821 format!("[native] Repair step '{}' failed: {}", step_id, err),
4822 false,
4823 ));
4824 }
4825 }
4826 }
4827
4828 "rccl-repair" => {
4829 run_rccl_remediation(ctx, &component.name)?;
4830 }
4831
4832 "megatron" => {
4834 use crate::installers::components::megatron::{MegatronConfig, MegatronInstaller};
4835 let force = is_force_reinstall();
4836 let inst = MegatronInstaller::new(MegatronConfig {
4837 python_bin: resolve_python_bin(),
4838 force_reinstall: force,
4839 ..MegatronConfig::default()
4840 });
4841
4842 if force {
4844 purge_component_packages(&["megatron-core", "megatron"], sender, &component.name);
4845 }
4846
4847 for dep_pkg in &["ninja", "packaging", "wheel"] {
4849 let cmd = inst.build_dep_install_command(dep_pkg);
4850 execute_native_command(
4851 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
4852 None,
4853 sender,
4854 &component.name,
4855 )?;
4856 }
4857
4858 let clone_cmd = inst.build_git_clone_command();
4860 let clone_target_str = clone_cmd.args.last().cloned().unwrap_or_default();
4861 git_clone_or_pull(
4862 "https://github.com/NVIDIA/Megatron-LM.git",
4863 &clone_target_str,
4864 &[],
4865 sudo_pw,
4866 sender,
4867 &component.name,
4868 )?;
4869
4870 let run_user = std::env::var("SUDO_USER")
4876 .or_else(|_| std::env::var("USER"))
4877 .unwrap_or_default();
4878 if !run_user.is_empty() {
4879 let chown_cmd = NativeCommand::from_shell_cmd(
4880 "chown",
4881 &[
4882 "-R".to_string(),
4883 format!("{run_user}:{run_user}"),
4884 clone_target_str.clone(),
4885 ],
4886 &[],
4887 );
4888 let _ = execute_native_command(&chown_cmd, sudo_pw, sender, &component.name);
4889 }
4890
4891 let clone_target_path = PathBuf::from(&clone_target_str);
4895 if let Ok(entries) = std::fs::read_dir(&clone_target_path) {
4896 for entry in entries.flatten() {
4897 let name = entry.file_name();
4898 let name_str = name.to_string_lossy();
4899 if name_str.ends_with(".egg-info") {
4900 let egg_path = entry.path();
4901 if std::fs::remove_dir_all(&egg_path).is_err() {
4903 let rm_cmd = NativeCommand::from_shell_cmd(
4905 "rm",
4906 &["-rf".to_string(), egg_path.to_string_lossy().to_string()],
4907 &[],
4908 );
4909 let _ =
4910 execute_native_command(&rm_cmd, sudo_pw, sender, &component.name);
4911 }
4912 }
4913 }
4914 }
4915
4916 let cmd = inst.build_pip_install_command();
4918 execute_native_command(
4919 &NativeCommand::from_shell_cmd_with_dir(
4920 &cmd.program,
4921 &cmd.args,
4922 &cmd.env,
4923 cmd.working_dir.clone(),
4924 ),
4925 None,
4926 sender,
4927 &component.name,
4928 )?;
4929
4930 let requirements_candidates = [
4932 clone_target_path
4933 .join("requirements")
4934 .join("requirements.txt"),
4935 clone_target_path.join("requirements.txt"),
4936 clone_target_path
4937 .join("requirements")
4938 .join("requirements_amd.txt"),
4939 ];
4940 for req_path in requirements_candidates.iter().filter(|p| p.exists()) {
4941 let filtered = match filter_cuda_requirements(&req_path.to_string_lossy()) {
4942 Ok(f) => f,
4943 Err(e) => {
4944 let _ = sender.send(InstallerEvent::Log(
4945 format!(
4946 "[native] WARN: could not read Megatron requirements {}: {e} — skipping requirements install",
4947 req_path.display()
4948 ),
4949 true,
4950 ));
4951 anyhow::bail!(
4952 "failed to read Megatron requirements {}: {e}",
4953 req_path.display()
4954 );
4955 }
4956 };
4957 if filtered.is_empty() {
4958 continue;
4959 }
4960 let tmp_dir = std::env::temp_dir().join("rusty-stack-megatron");
4961 let _ = std::fs::create_dir_all(&tmp_dir);
4962 let filtered_path = tmp_dir.join("requirements_filtered.txt");
4963 std::fs::write(&filtered_path, &filtered).with_context(|| {
4964 format!(
4965 "Failed to write filtered Megatron requirements to {:?}",
4966 filtered_path
4967 )
4968 })?;
4969 let filtered_path_str = filtered_path.to_string_lossy().to_string();
4970 let filtered_cmd = inst.build_requirements_install_command(&filtered_path_str);
4971 execute_native_command(
4972 &NativeCommand::from_shell_cmd(
4973 &filtered_cmd.program,
4974 &filtered_cmd.args,
4975 &filtered_cmd.env,
4976 ),
4977 None,
4978 sender,
4979 &component.name,
4980 )?;
4981 let _ = std::fs::remove_file(&filtered_path);
4982 break;
4983 }
4984 }
4985
4986 "vllm" => {
4988 use crate::installers::components::vllm_multi::{VllmConfig, VllmInstaller};
4989 let vllm_force = is_force_reinstall();
4990 let inst = VllmInstaller::new(VllmConfig {
4991 python_bin: resolve_python_bin(),
4992 ..Default::default()
4993 });
4994
4995 if vllm_force {
4997 purge_vllm(sender, &component.name);
4998 }
4999
5000 let cmd = inst.build_tools_install_command();
5003 execute_native_command(
5004 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
5005 None,
5006 sender,
5007 &component.name,
5008 )?;
5009
5010 let vllm_version = match ctx.target_version.as_deref() {
5018 Some(v) if is_explicit_update_target(Some(v)) => v.trim().to_string(),
5019 _ => inst.resolve_version_with_policy(
5020 ctx.vllm_version_lag,
5021 ctx.vllm_version_min_age_days,
5022 ),
5023 };
5024 let _ = sender.send(InstallerEvent::Log(
5025 format!(
5026 "[native] vLLM: selected {vllm_version} (gate: lag={}, min_age_days={}) — \
5027 building from source (ROCm, env torch)",
5028 ctx.vllm_version_lag, ctx.vllm_version_min_age_days
5029 ),
5030 false,
5031 ));
5032 let cmd = inst.build_source_install_command(&vllm_version);
5037 execute_native_command(
5038 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
5039 None,
5040 sender,
5041 &component.name,
5042 )?;
5043
5044 let cmd = inst.build_versioned_deps_command();
5048 execute_native_command(
5049 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
5050 None,
5051 sender,
5052 &component.name,
5053 )?;
5054
5055 let cmd = inst.build_deps_install_command();
5057 execute_native_command(
5058 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
5059 None,
5060 sender,
5061 &component.name,
5062 )?;
5063
5064 let cmd = inst.build_integrity_check_command();
5068 execute_native_command(
5069 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
5070 None,
5071 sender,
5072 &component.name,
5073 )?;
5074 }
5075
5076 "aiter" => {
5078 use crate::installers::components::aiter::{AiterConfig, AiterInstaller};
5079 let detected_gpu_arch = ctx
5082 .env_exports
5083 .get("GPU_ARCH")
5084 .map(|v| v.trim().to_string())
5085 .filter(|v| !v.is_empty())
5086 .unwrap_or_else(|| detect_gpu_arch().unwrap_or_else(|| "gfx1100".to_string()));
5087 let inst = AiterInstaller::new(AiterConfig {
5088 python_bin: resolve_python_bin(),
5089 gpu_arch: detected_gpu_arch,
5090 ..Default::default()
5091 });
5092 let target_dir = format!("{}/.mlstack/aiter", ctx.user_home);
5093
5094 if is_force_reinstall() {
5096 purge_aiter(sender, &component.name);
5097 }
5098
5099 git_clone_or_pull(
5101 inst.git_clone_url(),
5102 &target_dir,
5103 &["--recursive"],
5104 sudo_pw,
5105 sender,
5106 &component.name,
5107 )?;
5108
5109 let run_user = std::env::var("SUDO_USER")
5113 .or_else(|_| std::env::var("USER"))
5114 .unwrap_or_default();
5115 if !run_user.is_empty() {
5116 let chown_cmd = NativeCommand::from_shell_cmd(
5117 "chown",
5118 &[
5119 "-R".to_string(),
5120 format!("{run_user}:{run_user}"),
5121 target_dir.clone(),
5122 ],
5123 &[],
5124 );
5125 let _ = execute_native_command(&chown_cmd, sudo_pw, sender, &component.name);
5126 }
5127
5128 let cmd = inst.build_deps_install_command();
5130 execute_native_command(
5131 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
5132 sudo_pw,
5133 sender,
5134 &component.name,
5135 )?;
5136
5137 let cmd = inst.build_pip_install_command(&target_dir);
5139 execute_native_command(
5140 &NativeCommand::from_shell_cmd_with_dir(
5141 &cmd.program,
5142 &cmd.args,
5143 &cmd.env,
5144 cmd.working_dir.clone(),
5145 ),
5146 None,
5147 sender,
5148 &component.name,
5149 )?;
5150
5151 let run_user = std::env::var("SUDO_USER")
5155 .or_else(|_| std::env::var("USER"))
5156 .unwrap_or_default();
5157 if !run_user.is_empty() {
5158 let user_home = &ctx.user_home;
5159 for aiter_dir in [
5160 format!("{user_home}/.aiter"),
5161 format!("{user_home}/.mlstack/aiter"),
5162 ] {
5163 let dir_path = PathBuf::from(&aiter_dir);
5164 if dir_path.exists() {
5165 let needs_fix = std::fs::read_dir(&dir_path)
5167 .map(|mut entries| entries.next().is_some())
5168 .unwrap_or(false)
5169 && std::fs::write(dir_path.join(".permission_check"), b"").is_err();
5170 if needs_fix {
5171 let chown_cmd = NativeCommand::from_shell_cmd(
5172 "chown",
5173 &[
5174 "-R".to_string(),
5175 format!("{run_user}:{run_user}"),
5176 aiter_dir.clone(),
5177 ],
5178 &[],
5179 );
5180 let _ = execute_native_command(
5181 &chown_cmd,
5182 sudo_pw,
5183 sender,
5184 &component.name,
5185 );
5186 } else {
5187 let _ = std::fs::remove_file(dir_path.join(".permission_check"));
5189 }
5190 }
5191 }
5192 }
5193 }
5194 "vllm-studio" => {
5195 use crate::installers::components::vllm_studio::{
5196 VllmStudioConfig, VllmStudioInstaller,
5197 };
5198 let inst = VllmStudioInstaller::new(VllmStudioConfig {
5199 python_bin: resolve_python_bin(),
5200 ..Default::default()
5201 });
5202
5203 let clone_cmd = inst.build_git_clone_command();
5205 let target_dir = clone_cmd.args.last().cloned().unwrap_or_default();
5206 git_clone_or_pull(
5207 inst.repo_url(),
5208 &target_dir,
5209 &[],
5210 None,
5211 sender,
5212 &component.name,
5213 )?;
5214
5215 let pkg_mgr = if crate::installers::common::utils::command_exists("npm") {
5217 "npm"
5218 } else {
5219 "yarn"
5220 };
5221 let cmd = inst.build_pkg_install_command(pkg_mgr);
5222 execute_native_command(
5223 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
5224 None,
5225 sender,
5226 &component.name,
5227 )?;
5228
5229 let frontend_dir = PathBuf::from(&target_dir).join("frontend");
5232 let cmd = inst.build_frontend_build_command(pkg_mgr);
5233 let build_result = execute_native_command(
5234 &NativeCommand::from_shell_cmd_with_dir(
5235 &cmd.program,
5236 &cmd.args,
5237 &cmd.env,
5238 Some(frontend_dir),
5239 ),
5240 None,
5241 sender,
5242 &component.name,
5243 );
5244 if let Err(err) = build_result {
5245 let _ = sender.send(InstallerEvent::Log(
5246 format!(
5247 "[native] [WARN] {} frontend build failed (non-fatal): {}",
5248 component.name, err
5249 ),
5250 false,
5251 ));
5252 }
5253 }
5254
5255 "comfyui" => {
5257 use crate::installers::components::comfyui::{ComfyuiConfig, ComfyuiInstaller};
5258 let inst = ComfyuiInstaller::new(ComfyuiConfig {
5259 python_bin: resolve_python_bin(),
5260 ..Default::default()
5261 });
5262
5263 let cmd = inst.build_pytorch_check_command();
5265 execute_native_command(
5266 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
5267 None,
5268 sender,
5269 &component.name,
5270 )?;
5271
5272 let clone_cmd = inst.build_git_clone_command();
5274 let target_dir = clone_cmd.args.last().cloned().unwrap_or_default();
5275 git_clone_or_pull(
5276 inst.repo_url(),
5277 &target_dir,
5278 &[],
5279 None,
5280 sender,
5281 &component.name,
5282 )?;
5283
5284 let req_path = format!("{}/requirements.txt", target_dir);
5290 if std::path::Path::new(&req_path).exists() {
5291 let filtered = match filter_cuda_requirements(&req_path) {
5292 Ok(f) => f,
5293 Err(e) => {
5294 let _ = sender.send(InstallerEvent::Log(
5295 format!(
5296 "[native] WARN: could not read ComfyUI requirements {req_path}: {e} — skipping requirements install"
5297 ),
5298 true,
5299 ));
5300 anyhow::bail!("failed to read ComfyUI requirements {req_path}: {e}");
5301 }
5302 };
5303 if !filtered.is_empty() {
5304 let tmp_dir = std::env::temp_dir().join("rusty-stack-comfyui");
5305 let _ = std::fs::create_dir_all(&tmp_dir);
5306 let filtered_path = tmp_dir.join("requirements_filtered.txt");
5307 std::fs::write(&filtered_path, &filtered).with_context(|| {
5308 format!(
5309 "Failed to write filtered requirements to {:?}",
5310 filtered_path
5311 )
5312 })?;
5313 let filtered_path_str = filtered_path.to_string_lossy().to_string();
5314 let _ = sender.send(InstallerEvent::Log(
5315 format!(
5316 "[native] Installing filtered requirements (CUDA deps excluded) from {}",
5317 req_path
5318 ),
5319 false,
5320 ));
5321 let cmd = inst.build_pip_install_command(&filtered_path_str);
5322 execute_native_command(
5323 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
5324 None,
5325 sender,
5326 &component.name,
5327 )?;
5328 let _ = std::fs::remove_file(&filtered_path);
5329 } else {
5330 let _ = sender.send(InstallerEvent::Log(
5331 "[native] [WARN] All ComfyUI requirements were filtered out, nothing to install".to_string(),
5332 false,
5333 ));
5334 }
5335 } else {
5336 let _ = sender.send(InstallerEvent::Log(
5337 "[native] [WARN] ComfyUI requirements.txt not found, skipping dependency installation".to_string(),
5338 false,
5339 ));
5340 }
5341 }
5342
5343 "textgen" => {
5345 use crate::installers::components::textgen::{TextgenConfig, TextgenInstaller};
5346 let textgen_python = resolve_python_bin();
5349 let inst = TextgenInstaller::new(TextgenConfig {
5354 python_bin: textgen_python.clone(),
5355 use_uv: false,
5356 break_system_packages: true,
5357 ..Default::default()
5358 });
5359
5360 let clone_cmd = inst.build_git_clone_command();
5362 let target_dir = clone_cmd.args.last().cloned().unwrap_or_default();
5363 git_clone_or_pull(
5364 inst.repo_url(),
5365 &target_dir,
5366 &[],
5367 None,
5368 sender,
5369 &component.name,
5370 )?;
5371
5372 let amd_req_full = format!("{}/requirements/full/requirements_amd.txt", target_dir);
5377 let amd_req_subdir = format!("{}/requirements/requirements_amd.txt", target_dir);
5378 let amd_req_path = if std::path::Path::new(&amd_req_full).exists() {
5379 amd_req_full
5380 } else if std::path::Path::new(&amd_req_subdir).exists() {
5381 amd_req_subdir
5382 } else {
5383 String::new()
5384 };
5385 if !amd_req_path.is_empty() {
5386 let _ = sender.send(InstallerEvent::Log(
5387 "[native] Installing AMD-specific requirements first (ROCm wheels)".to_string(),
5388 false,
5389 ));
5390 let cmd = inst.build_amd_requirements_command(&amd_req_path);
5391 execute_native_command(
5392 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
5393 None,
5394 sender,
5395 &component.name,
5396 )?;
5397 }
5398
5399 let req_path_root = format!("{}/requirements.txt", target_dir);
5406 let req_path_full = format!("{}/requirements/full/requirements.txt", target_dir);
5407 let req_path_subdir = format!("{}/requirements/requirements.txt", target_dir);
5408 let raw_req_path = if std::path::Path::new(&req_path_full).exists() {
5409 req_path_full
5410 } else if std::path::Path::new(&req_path_root).exists() {
5411 req_path_root
5412 } else if std::path::Path::new(&req_path_subdir).exists() {
5413 req_path_subdir
5414 } else {
5415 String::new()
5416 };
5417
5418 if !raw_req_path.is_empty() {
5419 let filtered = match filter_cuda_requirements(&raw_req_path) {
5421 Ok(f) => f,
5422 Err(e) => {
5423 let _ = sender.send(InstallerEvent::Log(
5424 format!(
5425 "[native] WARN: could not read textgen requirements {raw_req_path}: {e} — skipping requirements install"
5426 ),
5427 true,
5428 ));
5429 anyhow::bail!("failed to read textgen requirements {raw_req_path}: {e}");
5430 }
5431 };
5432 if !filtered.is_empty() {
5433 let tmp_dir = std::env::temp_dir().join("rusty-stack-textgen");
5435 let _ = std::fs::create_dir_all(&tmp_dir);
5436 let filtered_path = tmp_dir.join("requirements_filtered.txt");
5437 std::fs::write(&filtered_path, &filtered).with_context(|| {
5438 format!(
5439 "Failed to write filtered requirements to {:?}",
5440 filtered_path
5441 )
5442 })?;
5443 let filtered_path_str = filtered_path.to_string_lossy().to_string();
5444 let _ = sender.send(InstallerEvent::Log(
5445 format!(
5446 "[native] Installing filtered requirements (CUDA deps excluded) from {}",
5447 raw_req_path
5448 ),
5449 false,
5450 ));
5451 let cmd = inst.build_pip_install_command(&filtered_path_str);
5452 execute_native_command(
5453 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
5454 None,
5455 sender,
5456 &component.name,
5457 )?;
5458 let _ = std::fs::remove_file(&filtered_path);
5459 } else {
5460 let _ = sender.send(InstallerEvent::Log(
5461 "[native] [WARN] All requirements were filtered out, nothing to install"
5462 .to_string(),
5463 false,
5464 ));
5465 }
5466 } else {
5467 let _ = sender.send(InstallerEvent::Log(
5468 "[native] [WARN] requirements.txt not found, skipping dependency installation"
5469 .to_string(),
5470 false,
5471 ));
5472 }
5473 }
5474
5475 "onnx" => {
5477 use crate::installers::components::onnxruntime::{
5478 install_method_from_env, prebuilt_version_from_env, OnnxInstallMethod,
5479 OnnxRuntimeConfig, OnnxRuntimeInstaller,
5480 };
5481
5482 let detected_rocm_version = detect_rocm_version();
5483 let detected_gpu_arch = detect_gpu_arch().unwrap_or_else(|| {
5484 eprintln!("Warning: no GPU arch detected for ONNX, falling back to gfx1100");
5485 "gfx1100".to_string()
5486 });
5487
5488 let install_method = install_method_from_env();
5495 let prebuilt_version = prebuilt_version_from_env();
5496
5497 let config = OnnxRuntimeConfig {
5499 rocm_version: Some(detected_rocm_version.clone()),
5500 rocm_release: Some({
5501 let parts: Vec<&str> = detected_rocm_version.split('.').collect();
5504 if parts.len() >= 3 {
5505 detected_rocm_version.clone()
5506 } else if parts.len() >= 2 && parts[0] == "7" && parts[1] == "2" {
5507 "7.2.4".to_string()
5508 } else if parts.len() >= 2 {
5509 format!("{}.{}.3", parts[0], parts[1])
5510 } else {
5511 "7.2.4".to_string()
5512 }
5513 }),
5514 gpu_arch: Some(detected_gpu_arch),
5515 python_bin: resolve_python_bin(),
5516 install_method,
5517 runtime_version: ctx.target_version.clone(),
5518 prebuilt_version,
5519 ..Default::default()
5520 };
5521 let inst = OnnxRuntimeInstaller::new(config);
5522
5523 let cmd = inst.build_uninstall_command();
5525 let _ = execute_native_command(
5526 &NativeCommand::from_shell_cmd_with_dir(
5527 &cmd.program,
5528 &cmd.args,
5529 &cmd.env,
5530 cmd.working_dir.clone(),
5531 ),
5532 None,
5533 sender,
5534 &component.name,
5535 );
5536
5537 match install_method {
5539 OnnxInstallMethod::MigraphxWheel => {
5540 let _ = sender.send(InstallerEvent::Log(
5544 format!(
5545 "[native] {} — installing onnxruntime-migraphx=={} from PyPI (ROCm {})",
5546 component.name,
5547 inst.prebuilt_version(),
5548 detected_rocm_version
5549 ),
5550 false,
5551 ));
5552 let cmd = inst.build_migraphx_install_command();
5553 execute_native_command(
5554 &NativeCommand::from_shell_cmd_with_dir(
5555 &cmd.program,
5556 &cmd.args,
5557 &cmd.env,
5558 cmd.working_dir.clone(),
5559 ),
5560 None,
5561 sender,
5562 &component.name,
5563 )?;
5564 }
5565 OnnxInstallMethod::PrebuiltWheel => {
5566 let _ = sender.send(InstallerEvent::Log(
5569 format!(
5570 "[native] {} — installing prebuilt onnxruntime-rocm from PyPI \
5571 (legacy; may be ABI-incompatible with ROCm {})",
5572 component.name, detected_rocm_version
5573 ),
5574 false,
5575 ));
5576 let cmd = inst.build_prebuilt_install_command();
5577 execute_native_command(
5578 &NativeCommand::from_shell_cmd_with_dir(
5579 &cmd.program,
5580 &cmd.args,
5581 &cmd.env,
5582 cmd.working_dir.clone(),
5583 ),
5584 None,
5585 sender,
5586 &component.name,
5587 )?;
5588 }
5589 OnnxInstallMethod::SourceBuild => {
5590 let _ = sender.send(InstallerEvent::Log(
5592 format!(
5593 "[native] {} — building from source (target {}, ROCm {}). Heavy build.",
5594 component.name,
5595 inst.runtime_version(),
5596 detected_rocm_version
5597 ),
5598 false,
5599 ));
5600 let rocm_env = crate::installers::common::RocmEnv::from_known(
5601 Some(std::path::PathBuf::from("/opt/rocm")),
5602 detected_rocm_version.clone(),
5603 );
5604 for cmd in [
5605 inst.build_git_clone_command(),
5606 inst.build_git_checkout_command(),
5607 inst.build_git_submodule_command(),
5608 inst.build_build_command(&rocm_env),
5609 inst.build_wheel_install_command(),
5610 ] {
5611 execute_native_command(
5612 &NativeCommand::from_shell_cmd_with_dir(
5613 &cmd.program,
5614 &cmd.args,
5615 &cmd.env,
5616 cmd.working_dir.clone(),
5617 ),
5618 None,
5619 sender,
5620 &component.name,
5621 )?;
5622 }
5623 }
5624 }
5625
5626 let cmd = inst.build_provider_validation_command();
5628 execute_native_command(
5629 &NativeCommand::from_shell_cmd_with_dir(
5630 &cmd.program,
5631 &cmd.args,
5632 &cmd.env,
5633 cmd.working_dir.clone(),
5634 ),
5635 None,
5636 sender,
5637 &component.name,
5638 )?;
5639
5640 let model_dirs = [
5642 dirs::home_dir().map(|h| h.join(".mlstack/models")),
5643 dirs::home_dir().map(|h| h.join(".local/share/models")),
5644 ];
5645 for model_dir in model_dirs.iter().flatten() {
5646 if model_dir.exists() {
5647 if let Ok(entries) = std::fs::read_dir(model_dir) {
5648 for entry in entries.flatten() {
5649 let path = entry.path();
5650 if path.extension().map(|e| e == "onnx").unwrap_or(false) {
5651 let model_path = path.to_string_lossy().to_string();
5652 if model_path.ends_with(".onnx.optimized") {
5654 continue;
5655 }
5656 let _ = sender.send(InstallerEvent::Log(
5657 format!(
5658 "[native] {} — optimizing model: {}",
5659 component.name, model_path
5660 ),
5661 false,
5662 ));
5663 let cmd = inst.build_model_optimizer_command(&model_path);
5664 let _ = execute_native_command(
5665 &NativeCommand::from_shell_cmd_with_dir(
5666 &cmd.program,
5667 &cmd.args,
5668 &cmd.env,
5669 cmd.working_dir.clone(),
5670 ),
5671 None,
5672 sender,
5673 &component.name,
5674 );
5675 }
5676 }
5677 }
5678 }
5679 }
5680
5681 persist_onnx_install_status(
5682 inst.prebuilt_version(),
5683 &["MIGraphXExecutionProvider", "CPUExecutionProvider"],
5684 );
5685
5686 let _ = sender.send(InstallerEvent::Log(
5687 format!("[native] {} — ONNX Runtime (MIGraphX) installed successfully. \
5688 Source build available via OnnxInstallMethod::SourceBuild for ROCMExecutionProvider.",
5689 component.name),
5690 false,
5691 ));
5692 }
5693
5694 "bitsandbytes" => {
5696 use crate::installers::components::bitsandbytes_multi::{
5697 BitsAndBytesConfig, BitsAndBytesInstaller,
5698 };
5699 let inst = BitsAndBytesInstaller::new(BitsAndBytesConfig {
5700 python_bin: resolve_python_bin(),
5701 ..Default::default()
5702 });
5703 let target_dir = format!("{}/.mlstack/bitsandbytes", ctx.user_home);
5704
5705 if is_force_reinstall() {
5707 purge_component_packages(&["bitsandbytes"], sender, &component.name);
5708 }
5709
5710 git_clone_or_pull(
5712 "https://github.com/ROCm/bitsandbytes.git",
5713 &target_dir,
5714 &["--recursive"],
5715 sudo_pw,
5716 sender,
5717 &component.name,
5718 )?;
5719
5720 let cmd = inst.build_pypi_install_command();
5722 let cmd_args = pin_pip_requirement(
5723 cmd.args.clone(),
5724 "bitsandbytes",
5725 ctx.target_version.as_deref(),
5726 );
5727 execute_native_command(
5728 &NativeCommand::from_shell_cmd(&cmd.program, &cmd_args, &cmd.env),
5729 None,
5730 sender,
5731 &component.name,
5732 )?;
5733 }
5734
5735 "rocm-smi" => {
5737 use crate::installers::components::rocm_smi::RocmSmiInstaller;
5738 let inst = RocmSmiInstaller::new();
5739 let distro = crate::installers::common::DistroFacade::detect();
5740 let target_dir = format!("{}/.mlstack/rocm_smi", ctx.user_home);
5741
5742 git_clone_or_pull(
5744 inst.git_clone_url(),
5745 &target_dir,
5746 &[],
5747 sudo_pw,
5748 sender,
5749 &component.name,
5750 )?;
5751
5752 if !(distro.family() == crate::platform::detection::DistroFamily::Arch
5754 && crate::installers::common::utils::command_exists("rocm-smi"))
5755 {
5756 let cmd = inst.build_package_install_command(&distro);
5757 execute_native_command(
5758 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
5759 sudo_pw,
5760 sender,
5761 &component.name,
5762 )?;
5763 } else {
5764 let _ = sender.send(InstallerEvent::Log(
5765 format!(
5766 "[native] {} — rocm-smi already on PATH, skipping package install",
5767 component.name
5768 ),
5769 false,
5770 ));
5771 }
5772 }
5773
5774 "migraphx" => {
5776 use crate::installers::components::migraphx_multi::{
5777 MigraphxConfig, MigraphxInstaller,
5778 };
5779 let inst = MigraphxInstaller::new(MigraphxConfig {
5780 python_bin: resolve_python_bin(),
5781 ..Default::default()
5782 });
5783 let distro = crate::installers::common::DistroFacade::detect();
5784 let packages = inst.required_packages(&distro);
5785
5786 let cmd = inst.build_package_install_command(&distro, &packages);
5788 execute_native_command(
5789 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
5790 sudo_pw,
5791 sender,
5792 &component.name,
5793 )?;
5794
5795 if inst.should_skip_pip_install(&distro) {
5797 if let Some(msg) = inst.build_limitation_message(&distro) {
5798 let _ = sender.send(InstallerEvent::Log(
5799 format!("[native] [INFO] {}", msg),
5800 false,
5801 ));
5802 }
5803 } else {
5804 let cmd = inst.build_pip_install_command();
5805 execute_native_command(
5806 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
5807 None,
5808 sender,
5809 &component.name,
5810 )?;
5811 }
5812
5813 match crate::installers::components::migraphx_multi::migraphx_driver_status() {
5818 Ok(()) => {}
5819 Err(missing) => {
5820 let detail = if missing.is_empty() {
5821 "migraphx-driver would not run".to_string()
5822 } else {
5823 format!("missing shared libraries: {}", missing.join(", "))
5824 };
5825 let _ = sender.send(InstallerEvent::Log(
5826 format!(
5827 "[native] [WARN] {} installed but NOT functional — {detail}. \
5828 This is usually a library version desync from a partial upgrade. \
5829 Run `sudo pacman -Syu` (Arch/CachyOS) or the equivalent system \
5830 update to sync dependencies, then re-verify.",
5831 component.name
5832 ),
5833 true,
5834 ));
5835 }
5836 }
5837 }
5838
5839 "pytorch-profiler" => {
5841 use crate::installers::components::pytorch_profiler::{
5842 PytorchProfilerConfig, PytorchProfilerInstaller,
5843 };
5844 let inst = PytorchProfilerInstaller::new(PytorchProfilerConfig {
5845 python_bin: resolve_python_bin(),
5846 ..Default::default()
5847 });
5848
5849 let cmd = inst.build_install_command();
5851 execute_native_command(
5852 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
5853 None,
5854 sender,
5855 &component.name,
5856 )?;
5857 }
5858
5859 "wandb" => {
5861 use crate::installers::components::wandb::{WandbConfig, WandbInstaller};
5862 let inst = WandbInstaller::new(WandbConfig {
5863 python_bin: resolve_python_bin(),
5864 ..Default::default()
5865 });
5866
5867 let cmd = inst.build_install_command();
5869 let cmd_args = pin_pip_requirement(
5870 cmd.args.clone(),
5871 "wandb",
5872 ctx.target_version.as_deref(),
5873 );
5874 execute_native_command(
5875 &NativeCommand::from_shell_cmd(&cmd.program, &cmd_args, &cmd.env),
5876 None,
5877 sender,
5878 &component.name,
5879 )?;
5880 }
5881
5882 "amdgpu-drivers" => {
5884 use crate::installers::components::amdgpu_drivers::{AmdgpuConfig, AmdgpuInstaller};
5885 let inst = AmdgpuInstaller::new(AmdgpuConfig::default());
5886 let distro = crate::installers::common::DistroFacade::detect();
5887 let packages = inst.required_packages(&distro);
5888
5889 let cmd = inst.build_package_install_command(&distro, &packages);
5891 execute_native_command(
5892 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
5893 sudo_pw,
5894 sender,
5895 &component.name,
5896 )?;
5897
5898 let cmd = inst.build_verify_command();
5906 execute_native_command(
5907 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
5908 None,
5909 sender,
5910 &component.name,
5911 )?;
5912
5913 ensure_user_in_groups(&["render", "video"], sudo_pw, sender, &component.name);
5916 }
5917
5918 "migraphx-python" => {
5920 use crate::installers::components::migraphx_python::{
5921 build_python_source_requested, source_branch, MigraphxPythonConfig,
5922 MigraphxPythonInstaller,
5923 };
5924 let inst = MigraphxPythonInstaller::new(MigraphxPythonConfig {
5925 python_bin: resolve_python_bin(),
5926 ..Default::default()
5927 });
5928
5929 let distro = crate::installers::common::DistroFacade::detect();
5931 if !inst.is_available_on_distro(&distro) {
5932 if build_python_source_requested() {
5933 let _ = sender.send(InstallerEvent::Log(
5936 "[native] MLSTACK_MIGRAPHX_BUILD_PYTHON=1 — building Python \
5937 bindings from source (AMDMIGraphX). Heavy build (~20-40 min)."
5938 .into(),
5939 false,
5940 ));
5941 let workdir = std::env::temp_dir()
5942 .join("rusty-migraphx-python")
5943 .to_string_lossy()
5944 .to_string();
5945 let _ = std::fs::remove_dir_all(&workdir);
5946 let gpu_arch =
5947 std::env::var("GPU_ARCH").unwrap_or_else(|_| "gfx1100".to_string());
5948 let cmds = inst.build_source_commands(
5949 &workdir,
5950 "/opt/rocm",
5951 &gpu_arch,
5952 &source_branch(),
5953 );
5954 for cmd in cmds {
5955 execute_native_command(
5956 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
5957 None,
5958 sender,
5959 &component.name,
5960 )?;
5961 }
5962 } else if let Some(msg) = inst.build_unavailable_message(&distro) {
5963 let _ = sender.send(InstallerEvent::Log(
5964 format!("[native] [SKIP] {}", msg),
5965 false,
5966 ));
5967 }
5968 } else {
5969 let cmd = inst.build_install_command();
5971 execute_native_command(
5972 &NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env),
5973 None,
5974 sender,
5975 &component.name,
5976 )?;
5977 }
5978 }
5979
5980 "enhanced-env" => {
5982 let _ = sender.send(InstallerEvent::Log(
5986 "[native] Enhanced environment setup handled during pre-install phase".into(),
5987 false,
5988 ));
5989 }
5990
5991 "mlperf-inference"
5993 | "rocm-benchmarks"
5994 | "gpu-memory-bandwidth"
5995 | "rocm-smi-bench"
5996 | "pytorch-performance"
5997 | "vllm-performance"
5998 | "deepspeed-performance"
5999 | "megatron-performance"
6000 | "onnx-performance"
6001 | "rusty-llama-performance"
6002 | "flash-attention-ck-performance"
6003 | "all-benchmarks" => {
6004 run_native_benchmark(&component.id, sender)?;
6005 }
6006
6007 "fastvideo" => {
6009 use crate::installers::components::fastvideo::{
6010 detect_discrete_gpu_archs_from_sysfs, fastvideo_build_warning, post_checkout_plan,
6011 resolve_gpu_archs, resolve_install_user, select_visible_gpu_archs, FastVideoConfig,
6012 FastVideoInstallStep, FastVideoInstaller,
6013 };
6014 let detected_archs =
6015 detect_discrete_gpu_archs_from_sysfs(PathBuf::from("/sys/class/drm").as_path());
6016 let selected_archs = select_visible_gpu_archs(
6017 &detected_archs,
6018 ctx.env_exports
6019 .get("ROCR_VISIBLE_DEVICES")
6020 .map(String::as_str),
6021 ctx.env_exports
6022 .get("HIP_VISIBLE_DEVICES")
6023 .map(String::as_str),
6024 ctx.env_exports
6025 .get("CUDA_VISIBLE_DEVICES")
6026 .map(String::as_str),
6027 );
6028 let gpu_archs = resolve_gpu_archs(
6029 &selected_archs,
6030 ctx.env_exports.get("GPU_ARCHS").map(String::as_str),
6031 ctx.env_exports.get("GPU_ARCH").map(String::as_str),
6032 None,
6033 );
6034 let python_bin = ctx
6035 .env_exports
6036 .get("MLSTACK_PYTHON_BIN")
6037 .map(|v| v.trim().to_string())
6038 .filter(|v| !v.is_empty())
6039 .unwrap_or_else(resolve_python_bin);
6040 let inst = FastVideoInstaller::new(FastVideoConfig {
6041 gpu_archs: gpu_archs.clone(),
6042 python_bin: python_bin.clone(),
6043 });
6044 let build_dir = PathBuf::from("/tmp/FastVideo_ROCm_build");
6045
6046 let initial_cleanup = inst.cleanup();
6049 execute_native_command(
6050 &NativeCommand::from_shell_cmd(
6051 &initial_cleanup.program,
6052 &initial_cleanup.args,
6053 &initial_cleanup.env,
6054 ),
6055 sudo_pw,
6056 sender,
6057 &component.name,
6058 )?;
6059
6060 let mkdir_cmd = inst.mkdir_build_dir();
6062 execute_native_command(
6063 &NativeCommand::from_shell_cmd(&mkdir_cmd.program, &mkdir_cmd.args, &mkdir_cmd.env),
6064 sudo_pw,
6065 sender,
6066 &component.name,
6067 )?;
6068
6069 let clone_cmd = inst.git_clone();
6071 execute_native_command(
6072 &NativeCommand::from_shell_cmd_with_dir(
6073 &clone_cmd.program,
6074 &clone_cmd.args,
6075 &clone_cmd.env,
6076 clone_cmd.working_dir,
6077 ),
6078 sudo_pw,
6079 sender,
6080 &component.name,
6081 )?;
6082
6083 let checkout_cmd = inst.git_checkout();
6085 execute_native_command(
6086 &NativeCommand::from_shell_cmd_with_dir(
6087 &checkout_cmd.program,
6088 &checkout_cmd.args,
6089 &checkout_cmd.env,
6090 Some(build_dir.clone()),
6091 ),
6092 sudo_pw,
6093 sender,
6094 &component.name,
6095 )?;
6096
6097 let sudo_user = std::env::var("SUDO_USER").ok();
6098 let user = std::env::var("USER").ok();
6099 let run_user = resolve_install_user(sudo_user.as_deref(), user.as_deref())
6100 .context("FastVideo install user unavailable from SUDO_USER or USER")?;
6101
6102 for step in post_checkout_plan() {
6103 let step_sudo = if step.requires_sudo() { sudo_pw } else { None };
6104 if step == FastVideoInstallStep::KernelInstall {
6105 let snapshot = inst.distribution_snapshot(
6106 "before",
6107 crate::installers::common::nvidia_blocklist::NVIDIA_RUNTIME_PREFIXES,
6108 );
6109 execute_native_command(
6110 &NativeCommand::from_shell_cmd(
6111 &snapshot.program,
6112 &snapshot.args,
6113 &snapshot.env,
6114 ),
6115 None,
6116 sender,
6117 &component.name,
6118 )?;
6119 }
6120
6121 let command = match step {
6122 FastVideoInstallStep::Chown => {
6123 let cmd = inst.chown_build_dir(&run_user);
6124 NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env)
6125 }
6126 FastVideoInstallStep::ManagedDependenciesPreflight => {
6127 let cmd = inst.managed_dependencies_preflight();
6128 NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env)
6129 }
6130 FastVideoInstallStep::FlashAttentionPreflight => {
6131 let cmd = inst.flash_attention_preflight();
6132 NativeCommand::from_shell_cmd(&cmd.program, &cmd.args, &cmd.env)
6133 }
6134 FastVideoInstallStep::SourcePolicyPreflight => {
6135 let cmd = inst.source_policy_preflight();
6136 NativeCommand::from_shell_cmd_with_dir(
6137 &cmd.program,
6138 &cmd.args,
6139 &cmd.env,
6140 cmd.working_dir,
6141 )
6142 }
6143 FastVideoInstallStep::KernelInstall => {
6144 let _ = sender.send(InstallerEvent::Log(
6145 fastvideo_build_warning(&gpu_archs),
6146 false,
6147 ));
6148 let cmd = inst.pip_install_kernel();
6149 NativeCommand::from_shell_cmd_with_dir(
6150 &cmd.program,
6151 &cmd.args,
6152 &cmd.env,
6153 cmd.working_dir,
6154 )
6155 }
6156 FastVideoInstallStep::PackageInstall => {
6157 let cmd = inst.pip_install_package();
6158 NativeCommand::from_shell_cmd_with_dir(
6159 &cmd.program,
6160 &cmd.args,
6161 &cmd.env,
6162 cmd.working_dir,
6163 )
6164 }
6165 };
6166 execute_native_command(&command, step_sudo, sender, &component.name)?;
6167 }
6168
6169 let snapshot = inst.distribution_snapshot(
6170 "after",
6171 crate::installers::common::nvidia_blocklist::NVIDIA_RUNTIME_PREFIXES,
6172 );
6173 execute_native_command(
6174 &NativeCommand::from_shell_cmd(&snapshot.program, &snapshot.args, &snapshot.env),
6175 None,
6176 sender,
6177 &component.name,
6178 )?;
6179
6180 let cleanup_cmd = inst.cleanup();
6182 execute_native_command(
6183 &NativeCommand::from_shell_cmd(
6184 &cleanup_cmd.program,
6185 &cleanup_cmd.args,
6186 &cleanup_cmd.env,
6187 ),
6188 sudo_pw,
6189 sender,
6190 &component.name,
6191 )?;
6192 }
6193 "llama-cpp" => {
6195 use crate::installers::components::llama_cpp::{LlamaCppConfig, LlamaCppInstaller};
6196 let gpu_arch = ctx
6197 .env_exports
6198 .get("GPU_ARCH")
6199 .map(|v| v.trim().to_string())
6200 .filter(|v| !v.is_empty())
6201 .unwrap_or_else(|| {
6202 detect_gpu_arch().unwrap_or_else(|| {
6203 eprintln!("Warning: no GPU arch detected, falling back to gfx1100");
6204 "gfx1100".to_string()
6205 })
6206 });
6207 let channel = ctx
6208 .env_exports
6209 .get("ROCM_CHANNEL")
6210 .map(|v| v.trim().to_string())
6211 .filter(|v| !v.is_empty())
6212 .unwrap_or_else(|| "latest".to_string());
6213 let inst = LlamaCppInstaller::new(LlamaCppConfig {
6214 gpu_arch: gpu_arch.clone(),
6215 channel: channel.clone(),
6216 ..Default::default()
6217 });
6218 let home = ctx.user_home;
6219 let manifest = inst.check_latest_release();
6220 let strategy = inst.resolve_install_strategy(home, manifest.as_ref());
6221 match &strategy {
6222 crate::installers::components::llama_cpp::InstallStrategy::Prebuilt(plan) => {
6223 let _ = sender.send(InstallerEvent::Log(
6224 format!(
6225 "llama-cpp install strategy selected: prebuilt ({})",
6226 plan.arch
6227 ),
6228 false,
6229 ));
6230 if let Err(err) = inst.download_prebuilt_binary(plan) {
6231 let _ = sender.send(InstallerEvent::Log(
6232 format!(
6233 "llama-cpp prebuilt download failed; falling back to source: {}",
6234 err
6235 ),
6236 false,
6237 ));
6238 inst.run_source_install(home).map_err(|source_err| {
6239 anyhow::anyhow!(
6240 "llama-cpp prebuilt install failed and source fallback failed: {}",
6241 source_err
6242 )
6243 })?;
6244 }
6245 }
6246 crate::installers::components::llama_cpp::InstallStrategy::Source(plan) => {
6247 let _ = sender.send(InstallerEvent::Log(
6248 format!(
6249 "llama-cpp install strategy selected: source ({})",
6250 plan.reason
6251 ),
6252 false,
6253 ));
6254 inst.run_source_install(home).map_err(|err| {
6255 anyhow::anyhow!("llama-cpp source install failed: {}", err)
6256 })?;
6257 }
6258 }
6259
6260 let clone_dir = inst.build_dir_path().to_string();
6264 let verification =
6265 crate::installers::components::llama_cpp::verify_installed_binary(home, &clone_dir);
6266 let _ = sender.send(InstallerEvent::Log(
6267 format!(
6268 "llama-cpp post-install verification: {}",
6269 verification.summary
6270 ),
6271 false,
6272 ));
6273 if verification.stronger_check_passed {
6274 let _ = sender.send(InstallerEvent::Log(
6275 format!(
6276 "llama-cpp llama-bench: prefill={:.0} t/s, decode={:.0} t/s",
6277 verification.bench_prefill_tps.unwrap_or(0.0),
6278 verification.bench_decode_tps.unwrap_or(0.0),
6279 ),
6280 false,
6281 ));
6282 }
6283 if verification.rdna3_is_valid.unwrap_or(false) {
6284 let _ = sender.send(InstallerEvent::Log(
6285 format!(
6286 "llama-cpp RDNA3 proof: device=RDNA3, WMMA={:.0} t/s",
6287 verification.rdna3_wmma_tps.unwrap_or(0.0),
6288 ),
6289 false,
6290 ));
6291 }
6292 }
6293
6294 _ => {
6296 bail!(
6297 "No native installer dispatch for component '{}'. \
6298 This should not happen — is_native_component() should return false for this ID.",
6299 component.id
6300 );
6301 }
6302 }
6303
6304 let _ = sender.send(InstallerEvent::Progress {
6305 component_id: component.id.clone(),
6306 progress: 0.9,
6307 message: format!("{} installation complete", component.name),
6308 });
6309
6310 Ok(())
6311}
6312
6313fn run_script(
6314 component: &Component,
6315 script_path: &str,
6316 sudo_password: Option<String>,
6317 batch_mode: bool,
6318 install_method: &str,
6319 sender: &Sender<InstallerEvent>,
6320 input_rx: Arc<Mutex<Receiver<String>>>,
6321) -> Result<()> {
6322 let user_home = resolve_mlstack_user_home();
6323 let user_name = std::env::var("USER").unwrap_or_else(|_| "user".into());
6324 let preserve_env = "HOME,USER,LOGNAME,PATH,MLSTACK_USER_HOME,MLSTACK_SKIP_TORCH_INSTALL,MLSTACK_PYTHON_BIN,MLSTACK_ENV_NAME,MLSTACK_INSTALL_METHOD,INSTALL_METHOD,PIP_BREAK_SYSTEM_PACKAGES,PIP_ROOT_USER_ACTION,UV_PIP_BREAK_SYSTEM_PACKAGES,UV_SYSTEM_PYTHON,PYTHONPATH,LD_LIBRARY_PATH,ROCM_HOME,ROCM_PATH,ROCM_VERSION,ROCM_CHANNEL,GPU_ARCH,GPU_ARCHS,PYTORCH_ROCM_ARCH,HSA_OVERRIDE_GFX_VERSION,HIP_VISIBLE_DEVICES,CUDA_VISIBLE_DEVICES,ROCR_VISIBLE_DEVICES,AITER_JIT_DIR,HIP_PATH,HIP_ROOT_DIR,ROCM_ROOT,HIPCC_BIN_DIR,VLLM_TARGET_DEVICE,VLLM_USE_ROCM,USE_ROCM,VLLM_VERSION,UV_PYTHON,DS_ACCELERATOR,FORCE,PYTORCH_REINSTALL,MLSTACK_FORCE_REINSTALL,MLSTACK_SUDO_PASSWORD,MLSTACK_TARGET_UID,MLSTACK_TARGET_GID";
6325
6326 let venv_bin = PathBuf::from(&user_home).join("rocm_venv").join("bin");
6327 let local_bin = PathBuf::from(&user_home).join(".local").join("bin");
6328 let mut path = std::env::var("PATH").unwrap_or_default();
6329
6330 if local_bin.exists() {
6332 let local_str = local_bin.to_string_lossy();
6333 if !path.split(':').any(|entry| entry == local_str) {
6334 if path.is_empty() {
6335 path = local_str.to_string();
6336 } else {
6337 path = format!("{}:{}", path, local_str);
6338 }
6339 }
6340 }
6341
6342 if venv_bin.exists() {
6343 let venv_str = venv_bin.to_string_lossy();
6344 if !path.split(':').any(|entry| entry == venv_str) {
6345 if path.is_empty() {
6346 path = venv_str.to_string();
6347 } else {
6348 path = format!("{}:{}", venv_str, path);
6349 }
6350 }
6351 }
6352
6353 let env_exports = load_mlstack_env_exports(&user_home);
6354 let persistent_python = env_exports
6355 .get("MLSTACK_PYTHON_BIN")
6356 .map(|v| v.trim().to_string())
6357 .filter(|v| !v.is_empty());
6358 let python_bin = std::env::var("MLSTACK_ENV_NAME")
6362 .ok()
6363 .map(|n| n.trim().to_string())
6364 .filter(|n| !n.is_empty())
6365 .map(|n| {
6366 crate::platform::environment::mlstack_env_dir(&n)
6367 .join("bin")
6368 .join("python")
6369 .to_string_lossy()
6370 .to_string()
6371 })
6372 .or(persistent_python)
6373 .unwrap_or_else(resolve_python_bin);
6374 let force_reinstall = is_force_reinstall();
6375 let force_env_value = if force_reinstall { "true" } else { "false" };
6376 let sudo_password_env = sudo_password.clone();
6377 let user_home_meta = fs::metadata(&user_home).ok();
6378 #[cfg(unix)]
6379 let (mlstack_target_uid, mlstack_target_gid) = {
6380 use std::os::unix::fs::MetadataExt;
6381 (
6382 user_home_meta.as_ref().map(|meta| meta.uid().to_string()),
6383 user_home_meta.as_ref().map(|meta| meta.gid().to_string()),
6384 )
6385 };
6386 #[cfg(not(unix))]
6387 let (mlstack_target_uid, mlstack_target_gid): (Option<String>, Option<String>) = (None, None);
6388
6389 let component_needs_sudo = component.needs_sudo && needs_sudo();
6391
6392 let mut command = if component_needs_sudo {
6393 let mut cmd = Command::new("sudo");
6394 cmd.arg("-S")
6395 .arg("-p")
6396 .arg("")
6397 .arg(format!("--preserve-env={}", preserve_env))
6398 .arg("bash")
6399 .arg(script_path);
6400 cmd
6401 } else {
6402 let mut cmd = Command::new("bash");
6403 cmd.arg(script_path);
6404 cmd
6405 };
6406
6407 let mut pythonpath = std::env::var("PYTHONPATH").unwrap_or_default();
6408 let rocm_lib = "/opt/rocm/lib";
6409 if !pythonpath.split(':').any(|entry| entry == rocm_lib) {
6410 if pythonpath.is_empty() {
6411 pythonpath = rocm_lib.to_string();
6412 } else {
6413 pythonpath = format!("{}:{}", rocm_lib, pythonpath);
6414 }
6415 }
6416
6417 pythonpath = pythonpath
6419 .split(':')
6420 .filter(|part| !part.starts_with("/tmp/"))
6421 .collect::<Vec<_>>()
6422 .join(":");
6423
6424 let rocm_version = env_exports
6425 .get("ROCM_VERSION")
6426 .map(|v| v.trim().to_string())
6427 .filter(|v| !v.is_empty())
6428 .unwrap_or_else(detect_rocm_version);
6429 let gpu_arch = env_exports
6430 .get("GPU_ARCH")
6431 .map(|v| v.trim().to_string())
6432 .filter(|v| !v.is_empty())
6433 .or_else(|| {
6434 env_exports
6435 .get("PYTORCH_ROCM_ARCH")
6436 .map(|v| v.split(';').next().unwrap_or("").trim().to_string())
6437 .filter(|v| !v.is_empty())
6438 })
6439 .unwrap_or_else(|| {
6440 detect_gpu_arch().unwrap_or_else(|| {
6441 eprintln!("Warning: no GPU arch detected, falling back to gfx1100");
6442 "gfx1100".to_string()
6443 })
6444 });
6445 let py_rocm_arch = env_exports
6446 .get("PYTORCH_ROCM_ARCH")
6447 .map(|v| v.trim().to_string())
6448 .filter(|v| !v.is_empty())
6449 .unwrap_or_else(|| gpu_arch.clone());
6450 let mut gpu_archs = env_exports
6451 .get("GPU_ARCHS")
6452 .map(|v| v.trim().to_string())
6453 .filter(|v| !v.is_empty())
6454 .unwrap_or_else(|| gpu_arch.clone());
6455 let mut gpu_list = env_exports
6456 .get("HIP_VISIBLE_DEVICES")
6457 .map(|v| v.trim().to_string())
6458 .filter(|v| !v.is_empty())
6459 .or_else(|| {
6460 env_exports
6461 .get("CUDA_VISIBLE_DEVICES")
6462 .map(|v| v.trim().to_string())
6463 .filter(|v| !v.is_empty())
6464 })
6465 .unwrap_or_else(detect_gpu_list);
6466 let hsa_override = env_exports
6467 .get("HSA_OVERRIDE_GFX_VERSION")
6468 .map(|v| v.trim().to_string())
6469 .filter(|v| !v.is_empty());
6470 let aiter_jit_dir = PathBuf::from(&user_home)
6471 .join(".mlstack")
6472 .join("aiter")
6473 .join("jit");
6474
6475 if component.id == "aiter" {
6476 gpu_archs = py_rocm_arch
6477 .split(';')
6478 .next()
6479 .map(|v| v.trim().to_string())
6480 .filter(|v| !v.is_empty())
6481 .unwrap_or_else(|| gpu_arch.clone());
6482 gpu_list = prioritize_gpu_list_for_arch(&gpu_list, &gpu_archs);
6483 let _ = fs::create_dir_all(&aiter_jit_dir);
6484 }
6485
6486 command
6487 .stdin(Stdio::piped())
6488 .stdout(Stdio::piped())
6489 .stderr(Stdio::piped())
6490 .env("HOME", &user_home)
6491 .env("USER", &user_name)
6492 .env("LOGNAME", &user_name)
6493 .env("PATH", &path)
6494 .env("PYTHONPATH", &pythonpath)
6495 .env("MLSTACK_USER_HOME", &user_home)
6496 .env("MLSTACK_PYTHON_BIN", &python_bin)
6497 .env("UV_PYTHON", &python_bin)
6498 .env("ROCM_HOME", "/opt/rocm")
6499 .env("ROCM_PATH", "/opt/rocm")
6500 .env("ROCM_VERSION", &rocm_version)
6501 .env("ROCM_CHANNEL", "latest")
6502 .env("GPU_ARCH", &gpu_arch)
6503 .env("GPU_ARCHS", &gpu_archs)
6504 .env("PYTORCH_ROCM_ARCH", &py_rocm_arch)
6505 .env("HIP_PATH", "/opt/rocm")
6506 .env("HIP_ROOT_DIR", "/opt/rocm")
6507 .env("ROCM_ROOT", "/opt/rocm")
6508 .env("HIPCC_BIN_DIR", "/opt/rocm/bin")
6509 .env("HIP_VISIBLE_DEVICES", &gpu_list)
6510 .env("CUDA_VISIBLE_DEVICES", &gpu_list)
6511 .env("ROCR_VISIBLE_DEVICES", &gpu_list)
6512 .env("MLSTACK_BATCH_MODE", if batch_mode { "1" } else { "0" })
6513 .env("MLSTACK_INSTALL_METHOD", install_method)
6514 .env("INSTALL_METHOD", install_method)
6515 .env("PIP_BREAK_SYSTEM_PACKAGES", "1")
6516 .env("PIP_ROOT_USER_ACTION", "ignore")
6517 .env("UV_PIP_BREAK_SYSTEM_PACKAGES", "1")
6518 .env("UV_SYSTEM_PYTHON", "1")
6519 .env("FORCE", force_env_value)
6520 .env("PYTORCH_REINSTALL", force_env_value)
6521 .env("MLSTACK_FORCE_REINSTALL", force_env_value)
6522 .env(
6523 "MLSTACK_SKIP_TORCH_INSTALL",
6524 if component.id == "pytorch" { "0" } else { "1" },
6525 )
6526 .env("RUSTY_STACK", "true");
6527
6528 if let Some(uid) = mlstack_target_uid.as_deref() {
6529 command.env("MLSTACK_TARGET_UID", uid);
6530 }
6531 if let Some(gid) = mlstack_target_gid.as_deref() {
6532 command.env("MLSTACK_TARGET_GID", gid);
6533 }
6534
6535 if let Some(password) = sudo_password_env {
6536 command.env("MLSTACK_SUDO_PASSWORD", password);
6537 }
6538
6539 if force_reinstall {
6540 if let Ok(script_contents) = fs::read_to_string(script_path) {
6541 let supports_force_flag = script_contents.contains("--force)")
6542 || script_contents.contains("--force ")
6543 || script_contents.contains("\"--force\"")
6544 || script_contents.contains("'--force'");
6545 if supports_force_flag {
6546 command.arg("--force");
6547 }
6548 }
6549 }
6550
6551 if let Some(value) = hsa_override {
6552 command.env("HSA_OVERRIDE_GFX_VERSION", value);
6553 }
6554 strip_hsa_override_for_therock(&mut command, &env_exports);
6555 if component.id == "aiter" {
6556 command.env("AITER_JIT_DIR", aiter_jit_dir);
6557 }
6558
6559 let mut child = command.spawn().with_context(|| {
6560 format!(
6561 "Failed to spawn installer script '{}' for {}. \
6562 Ensure bash is installed and the script file is accessible.",
6563 script_path, component.name
6564 )
6565 })?;
6566
6567 let mut child_stdin = child.stdin.take().with_context(|| {
6568 format!(
6569 "Failed to open stdin for script '{}' — \
6570 this usually indicates a system resource issue.",
6571 script_path
6572 )
6573 })?;
6574 if let Some(password) = sudo_password {
6575 let _ = child_stdin.write_all(password.as_bytes());
6576 let _ = child_stdin.write_all(b"\n");
6577 let _ = child_stdin.flush();
6578 }
6579
6580 if let Some(default_input) = default_component_input(&component.id) {
6582 let _ = child_stdin.write_all(default_input.as_bytes());
6583 let _ = child_stdin.flush();
6584 }
6585
6586 let stop_input = Arc::new(AtomicBool::new(false));
6587
6588 let stop_input_clone = Arc::clone(&stop_input);
6590 let input_thread = thread::spawn(move || {
6591 while !stop_input_clone.load(Ordering::Relaxed) {
6592 let rx = match input_rx.lock() {
6593 Ok(guard) => guard,
6594 Err(_) => break, };
6596 match rx.recv_timeout(Duration::from_millis(100)) {
6597 Ok(input) => {
6598 let _ = child_stdin.write_all(input.as_bytes());
6599 let _ = child_stdin.flush();
6600 }
6601 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
6602 continue;
6603 }
6604 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
6605 }
6606 }
6607 });
6608
6609 let sender_stdout = sender.clone();
6610 let component_id = component.id.clone();
6611 let component_name = component_name(component);
6612 let stdout_stream = match child.stdout.take() {
6613 Some(s) => s,
6614 None => {
6615 stop_input.store(true, Ordering::Relaxed);
6616 let _ = input_thread.join();
6617 let _ = sender.send(InstallerEvent::Log(
6618 format!(
6619 "[ERROR] {} — failed to capture stdout from script process",
6620 component.name
6621 ),
6622 false,
6623 ));
6624 bail!(
6625 "{} — failed to capture stdout from script process. \
6626 This usually indicates a system resource issue.",
6627 component.name
6628 );
6629 }
6630 };
6631 let stdout_handle = thread::spawn(move || {
6632 let mut reader = BufReader::new(stdout_stream);
6633 let mut buffer = Vec::new();
6634 let mut chunk = [0u8; 1024];
6635
6636 while let Ok(n) = reader.read(&mut chunk) {
6637 if n == 0 {
6638 break;
6639 }
6640 for &b in chunk.iter().take(n) {
6641 if b == b'\n' || b == b'\r' {
6642 let is_transient = b == b'\r';
6643 if !buffer.is_empty() {
6644 let line = String::from_utf8_lossy(&buffer).to_string();
6645 let clean_line = strip_ansi_codes(&line);
6646 if !clean_line.trim().is_empty() {
6647 let _ = sender_stdout
6648 .send(InstallerEvent::Log(clean_line.clone(), is_transient));
6649 if let Some(progress) = parse_progress(&clean_line) {
6650 let _ = sender_stdout.send(InstallerEvent::Progress {
6651 component_id: component_id.clone(),
6652 progress,
6653 message: format!(
6654 "{}: {}%",
6655 component_name,
6656 (progress * 100.0) as i32
6657 ),
6658 });
6659 }
6660 }
6661 buffer.clear();
6662 }
6663 } else {
6664 buffer.push(b);
6665 if buffer.len() > 8192 {
6666 buffer.clear();
6667 }
6668 }
6669 }
6670 }
6671 });
6672
6673 let sender_stderr = sender.clone();
6674 let stderr_stream = match child.stderr.take() {
6675 Some(s) => s,
6676 None => {
6677 stop_input.store(true, Ordering::Relaxed);
6678 let _ = input_thread.join();
6679 let _ = stdout_handle.join();
6680 let _ = sender.send(InstallerEvent::Log(
6681 format!(
6682 "[ERROR] {} — failed to capture stderr from script process",
6683 component.name
6684 ),
6685 false,
6686 ));
6687 bail!(
6688 "{} — failed to capture stderr from script process. \
6689 This usually indicates a system resource issue.",
6690 component.name
6691 );
6692 }
6693 };
6694 let stderr_handle = thread::spawn(move || {
6695 let mut reader = BufReader::new(stderr_stream);
6696 let mut buffer = Vec::new();
6697 let mut chunk = [0u8; 1024];
6698
6699 while let Ok(n) = reader.read(&mut chunk) {
6700 if n == 0 {
6701 break;
6702 }
6703 for &b in chunk.iter().take(n) {
6704 if b == b'\n' || b == b'\r' {
6705 let is_transient = b == b'\r';
6706 if !buffer.is_empty() {
6707 let line = String::from_utf8_lossy(&buffer).to_string();
6708 let clean_line = strip_ansi_codes(&line);
6709 if !clean_line.trim().is_empty() {
6710 let _ =
6711 sender_stderr.send(InstallerEvent::Log(clean_line, is_transient));
6712 }
6713 buffer.clear();
6714 }
6715 } else {
6716 buffer.push(b);
6717 if buffer.len() > 8192 {
6718 buffer.clear();
6719 }
6720 }
6721 }
6722 }
6723 });
6724
6725 let status = child.wait().with_context(|| {
6726 format!(
6727 "{} — script process wait failed. \
6728 The process may have been killed or the system is under heavy load.",
6729 component.name
6730 )
6731 })?;
6732 stop_input.store(true, Ordering::Relaxed);
6733 let _ = input_thread.join();
6734 let _ = stdout_handle.join();
6735 let _ = stderr_handle.join();
6736
6737 if !status.success() {
6738 let code = status.code().unwrap_or(-1);
6739 let error_detail = match code {
6740 1 => "General error — check the script output above for details".to_string(),
6741 2 => "Misuse of shell builtins — check script arguments".to_string(),
6742 126 => "Script not executable — check file permissions (chmod +x)".to_string(),
6743 127 => "Command not found in script — ensure required tools are installed".to_string(),
6744 130 => "Script terminated by Ctrl+C (SIGINT)".to_string(),
6745 137 => "Script killed (SIGKILL) — possibly out of memory".to_string(),
6746 139 => "Segmentation fault in script subprocess".to_string(),
6747 n if n > 128 => format!(
6748 "Script process terminated by signal {} — check system logs",
6749 n - 128
6750 ),
6751 _ => format!(
6752 "Script exited with code {} — check the output above for details",
6753 code
6754 ),
6755 };
6756 let _ = sender.send(InstallerEvent::Log(
6757 format!(
6758 "[ERROR] {} failed: {} (exit code {} at {})",
6759 component.name,
6760 error_detail,
6761 code,
6762 Local::now().format("%H:%M:%S")
6763 ),
6764 false,
6765 ));
6766 bail!(
6767 "{} failed: {} (exit code {})",
6768 component.name,
6769 error_detail,
6770 code
6771 );
6772 }
6773
6774 Ok(())
6775}
6776
6777fn star_repo(config: &InstallerConfig, sender: &Sender<InstallerEvent>) -> Result<()> {
6778 if !config.star_repos {
6779 return Ok(());
6780 }
6781
6782 let repo_url = "https://github.com/scooter-lacroix/Stan-s-ML-Stack";
6783
6784 let gh_check = Command::new("gh")
6786 .arg("auth")
6787 .arg("status")
6788 .stdout(Stdio::null())
6789 .stderr(Stdio::null())
6790 .status();
6791
6792 if let Ok(status) = gh_check {
6793 if status.success() {
6794 let _ = sender.send(InstallerEvent::Log(
6795 "Starring ML Stack repository on GitHub...".into(),
6796 false,
6797 ));
6798 let star_status = Command::new("gh")
6799 .arg("repo")
6800 .arg("star")
6801 .arg(repo_url)
6802 .stdout(Stdio::null())
6803 .stderr(Stdio::null())
6804 .status();
6805
6806 if let Ok(s) = star_status {
6807 if s.success() {
6808 let _ = sender.send(InstallerEvent::Log(
6809 "Thank you for starring the repo! ✦".into(),
6810 false,
6811 ));
6812 }
6813 }
6814 }
6815 }
6816
6817 Ok(())
6818}
6819
6820fn component_name(comp: &Component) -> String {
6821 comp.name.clone()
6822}
6823
6824fn format_user_friendly_error(component_id: &str, raw_error: &str) -> String {
6833 let error_lower = raw_error.to_lowercase();
6834
6835 match component_id {
6838 "permanent-env" => {
6839 if error_lower.contains("rocminfo") {
6840 return format!(
6841 "{}: Failed to detect GPU — rocminfo command failed. \
6842 Ensure ROCm is properly installed and /opt/rocm/bin is in your PATH. \
6843 Original error: {}",
6844 component_id, raw_error
6845 );
6846 }
6847 return format!(
6848 "{}: Environment setup failed. \
6849 Ensure ROCm is installed and GPU is accessible. \
6850 Original error: {}",
6851 component_id, raw_error
6852 );
6853 }
6854 "rocm" if error_lower.contains("no such file") || error_lower.contains("not found") => {
6855 return format!(
6856 "{}: ROCm installation failed — required packages or repositories not found. \
6857 Ensure your system's package manager is configured correctly and ROCm repositories are added. \
6858 Original error: {}",
6859 component_id, raw_error
6860 );
6861 }
6862 "pytorch" => {
6863 if error_lower.contains("no such file") || error_lower.contains("not found") {
6864 return format!(
6865 "{}: PyTorch installation failed — required packages not found. \
6866 Ensure ROCm is installed first and Python 3.10+ is available. \
6867 Original error: {}",
6868 component_id, raw_error
6869 );
6870 }
6871 return format!(
6872 "{}: PyTorch installation failed. \
6873 Ensure ROCm is installed first, Python 3.10-3.13 is available, \
6874 and pip/uv can reach the PyTorch package index. \
6875 Original error: {}",
6876 component_id, raw_error
6877 );
6878 }
6879 "triton" | "flash-attn" | "flash-attn-triton" | "flash-attn-ck" | "vllm" | "aiter" => {
6880 return format!(
6881 "{}: Installation failed. \
6882 Ensure PyTorch and ROCm are properly installed first \
6883 (these are required dependencies). \
6884 Original error: {}",
6885 component_id, raw_error
6886 );
6887 }
6888 "megatron" => {
6889 return format!(
6890 "{}: Megatron-LM installation failed. \
6891 Ensure PyTorch and MPI4Py are installed first (required dependencies). \
6892 Original error: {}",
6893 component_id, raw_error
6894 );
6895 }
6896 "deepspeed" => {
6897 return format!(
6898 "{}: DeepSpeed installation failed. \
6899 Ensure PyTorch is installed first (required dependency). \
6900 Original error: {}",
6901 component_id, raw_error
6902 );
6903 }
6904 _ => {}
6905 }
6906
6907 if error_lower.contains("permission denied") || error_lower.contains("sudo") {
6910 return format!(
6911 "{}: Permission denied. Try running with elevated privileges or check file/directory permissions. \
6912 Original error: {}",
6913 component_id, raw_error
6914 );
6915 }
6916
6917 if error_lower.contains("network")
6918 || error_lower.contains("connection")
6919 || error_lower.contains("timeout")
6920 || error_lower.contains("resolve")
6921 {
6922 return format!(
6923 "{}: Network error — failed to download packages or clone repositories. \
6924 Check your internet connection and try again. \
6925 Original error: {}",
6926 component_id, raw_error
6927 );
6928 }
6929
6930 if error_lower.contains("out of memory")
6931 || error_lower.contains("cannot allocate")
6932 || error_lower.contains("killed")
6933 {
6934 return format!(
6935 "{}: Out of memory — the build process was killed due to insufficient memory. \
6936 Try closing other applications or increasing swap space. \
6937 Original error: {}",
6938 component_id, raw_error
6939 );
6940 }
6941
6942 if error_lower.contains("exit code") {
6943 return raw_error.to_string();
6945 }
6946
6947 if error_lower.contains("no such file") || error_lower.contains("not found") {
6948 return format!(
6949 "{}: Required file or command not found. \
6950 Ensure all dependencies are installed before this component. \
6951 Original error: {}",
6952 component_id, raw_error
6953 );
6954 }
6955
6956 format!(
6959 "{}: Installation failed. Check the log output above for details. \
6960 Original error: {}",
6961 component_id, raw_error
6962 )
6963}
6964
6965pub(crate) fn detect_gpu_arch() -> Option<String> {
6970 crate::gpu::detect_discrete_amd_gpus()
6971 .first()
6972 .and_then(|g| g.gfx_arch.clone())
6973}
6974
6975pub(crate) fn detect_gpu_list() -> String {
6998 let gpus = detect_discrete_gpus();
6999 if gpus.is_empty() {
7000 "0".to_string()
7001 } else {
7002 gpus.join(",")
7003 }
7004}
7005
7006pub(crate) fn detect_discrete_gpus() -> Vec<String> {
7014 crate::gpu::detect_discrete_amd_gpus()
7015 .iter()
7016 .enumerate()
7017 .map(|(i, _)| i.to_string())
7018 .collect()
7019}
7020
7021fn detect_gpu_arch_by_id() -> HashMap<String, String> {
7023 let mut mapping: HashMap<String, String> = HashMap::new();
7024
7025 let rocminfo_path = crate::installers::common::utils::resolve_rocminfo_path();
7026 let Ok(output) = Command::new(&rocminfo_path).output() else {
7027 return mapping;
7028 };
7029 let stdout = String::from_utf8_lossy(&output.stdout);
7030 let mut current_gpu_id: Option<String> = None;
7031
7032 for line in stdout.lines() {
7033 let line = line.trim();
7034 if line.contains("GPU ID:") {
7035 if let Some(id_part) = line.split(':').nth(1) {
7036 let id: String = id_part.chars().filter(|c| c.is_ascii_digit()).collect();
7037 if !id.is_empty() {
7038 current_gpu_id = Some(id);
7039 }
7040 }
7041 continue;
7042 }
7043
7044 if !line.contains("Name:") || !line.contains("gfx") {
7045 continue;
7046 }
7047
7048 if let Some(id) = current_gpu_id.clone() {
7049 for token in line.split_whitespace() {
7050 let cleaned = token.trim_matches(|c: char| !c.is_alphanumeric());
7051 if cleaned.starts_with("gfx") {
7052 mapping.entry(id).or_insert_with(|| cleaned.to_string());
7053 break;
7054 }
7055 }
7056 }
7057 }
7058
7059 mapping
7060}
7061
7062fn prioritize_gpu_list_for_arch(gpu_list: &str, target_arch: &str) -> String {
7065 let target_arch = target_arch.trim();
7066 if target_arch.is_empty() {
7067 return gpu_list.to_string();
7068 }
7069
7070 let arch_by_id = detect_gpu_arch_by_id();
7071 if arch_by_id.is_empty() {
7072 return gpu_list.to_string();
7073 }
7074
7075 let mut ids: Vec<String> = gpu_list
7076 .split(',')
7077 .map(|v| v.trim().to_string())
7078 .filter(|v| !v.is_empty())
7079 .collect();
7080 if ids.len() < 2 {
7081 return gpu_list.to_string();
7082 }
7083
7084 let preferred_pos = ids.iter().position(|id| {
7085 arch_by_id
7086 .get(id)
7087 .map(|v| v == target_arch)
7088 .unwrap_or(false)
7089 });
7090 if let Some(pos) = preferred_pos {
7091 if pos > 0 {
7092 let preferred = ids.remove(pos);
7093 ids.insert(0, preferred);
7094 return ids.join(",");
7095 }
7096 }
7097
7098 gpu_list.to_string()
7099}
7100
7101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7102enum VerificationResult {
7103 Verified,
7104 Failed,
7105 Missing,
7106}
7107
7108impl VerificationResult {
7109 fn label(&self) -> &'static str {
7110 match self {
7111 VerificationResult::Verified => "Verified",
7112 VerificationResult::Failed => "Failed",
7113 VerificationResult::Missing => "Missing",
7114 }
7115 }
7116}
7117
7118fn resolve_python_bin() -> String {
7119 if let Ok(value) = std::env::var("MLSTACK_PYTHON_BIN") {
7120 let value = value.trim();
7121 if !value.is_empty() {
7122 return value.to_string();
7123 }
7124 }
7125
7126 if let Ok(value) = std::env::var("UV_PYTHON") {
7127 let value = value.trim();
7128 if !value.is_empty() {
7129 return value.to_string();
7130 }
7131 }
7132
7133 let global_py = crate::platform::environment::mlstack_global_python();
7138 if global_py.exists() {
7139 return global_py.to_string_lossy().to_string();
7140 }
7141
7142 let home = std::env::var("HOME").unwrap_or_default();
7143
7144 let uv_python_dir = PathBuf::from(&home).join(".local/share/uv/python");
7149 if let Ok(entries) = std::fs::read_dir(&uv_python_dir) {
7150 let mut uv_pythons: Vec<PathBuf> = entries
7151 .filter_map(|e| e.ok())
7152 .filter_map(|e| {
7153 let bin = e.path().join("bin/python3");
7154 if bin.exists() {
7155 Some(bin)
7156 } else {
7157 None
7158 }
7159 })
7160 .collect();
7161 uv_pythons.sort_by(|a, b| {
7163 let va = python_version_tuple(a);
7164 let vb = python_version_tuple(b);
7165 python_ml_priority(&va).cmp(&python_ml_priority(&vb))
7166 });
7167 for python in &uv_pythons {
7169 if python_has_rocm_torch(&python.to_string_lossy()) {
7170 return python.to_string_lossy().to_string();
7171 }
7172 }
7173 if let Some(first) = uv_pythons.first() {
7175 return first.to_string_lossy().to_string();
7176 }
7177 }
7178
7179 let local_bin_python = format!("{home}/.local/bin/python3");
7181 if Path::new(&local_bin_python).exists() {
7182 if python_has_rocm_torch(&local_bin_python) {
7183 return local_bin_python;
7184 }
7185 return local_bin_python;
7186 }
7187
7188 let candidates = [
7190 Path::new(&home)
7191 .join("rocm_venv")
7192 .join("bin")
7193 .join("python"),
7194 PathBuf::from("/usr/local/bin/python3"),
7195 PathBuf::from("/usr/bin/python3"),
7196 PathBuf::from("python3"),
7197 PathBuf::from("python"),
7198 ];
7199
7200 for candidate in &candidates {
7201 let candidate_str = candidate.to_string_lossy();
7202 if candidate_str.is_empty() {
7203 continue;
7204 }
7205 if candidate.is_absolute() && !candidate.exists() {
7206 continue;
7207 }
7208 if python_has_rocm_torch(&candidate_str) {
7209 return candidate_str.to_string();
7210 }
7211 }
7212
7213 for candidate in &candidates {
7214 let candidate_str = candidate.to_string_lossy();
7215 if candidate.is_absolute() && !candidate.exists() {
7216 continue;
7217 }
7218 return candidate_str.to_string();
7219 }
7220
7221 "python3".to_string()
7222}
7223
7224fn python_version_tuple(path: &Path) -> (u32, u32) {
7226 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
7227 let parent = path
7229 .parent()
7230 .and_then(|p| p.parent())
7231 .and_then(|p| p.file_name())
7232 .and_then(|n| n.to_str())
7233 .unwrap_or("");
7234 if let Some(v) = parse_cpython_version(parent) {
7236 return v;
7237 }
7238 if let Some(rest) = name.strip_prefix("python") {
7240 let parts: Vec<&str> = rest.split('.').collect();
7241 if parts.len() >= 2 {
7242 if let (Ok(major), Ok(minor)) = (parts[0].parse::<u32>(), parts[1].parse::<u32>()) {
7243 return (major, minor);
7244 }
7245 }
7246 }
7247 (0, 0)
7248}
7249
7250fn parse_cpython_version(dir_name: &str) -> Option<(u32, u32)> {
7252 let rest = dir_name.strip_prefix("cpython-")?;
7253 let parts: Vec<&str> = rest.split('.').collect();
7254 if parts.len() >= 2 {
7255 let major = parts[0].parse::<u32>().ok()?;
7256 let minor = parts[1].parse::<u32>().ok()?;
7257 return Some((major, minor));
7258 }
7259 None
7260}
7261
7262fn python_ml_priority(version: &(u32, u32)) -> (i32, u32) {
7264 match version {
7265 (3, 12) => (0, version.1), (3, 13) => (1, version.1),
7267 (3, 11) => (2, version.1),
7268 (3, 10) => (3, version.1),
7269 _ => (4, version.1), }
7271}
7272
7273fn python_has_rocm_torch(python: &str) -> bool {
7274 Command::new(python)
7275 .arg("-c")
7276 .arg(
7277 "import torch, sys; ok=hasattr(torch,'__version__') and hasattr(torch,'version') and getattr(torch.version,'hip', None); sys.exit(0 if ok else 1)",
7278 )
7279 .stdout(Stdio::null())
7280 .stderr(Stdio::null())
7281 .status()
7282 .map(|status| status.success())
7283 .unwrap_or(false)
7284}
7285
7286fn default_component_input(component_id: &str) -> Option<&'static str> {
7287 match component_id {
7288 "ml-stack-core" => Some("9\n0\n"),
7289 _ => None,
7290 }
7291}
7292
7293fn parse_progress(line: &str) -> Option<f32> {
7294 let lower = line.to_lowercase();
7295 if let Some(percent_idx) = lower.find('%') {
7296 let prefix = &lower[..percent_idx];
7297 let digits: String = prefix
7298 .chars()
7299 .rev()
7300 .take_while(|c| c.is_ascii_digit())
7301 .collect();
7302 if !digits.is_empty() {
7303 let value: String = digits.chars().rev().collect();
7304 if let Ok(percent) = value.parse::<f32>() {
7305 return Some((percent / 100.0).min(1.0));
7306 }
7307 }
7308 }
7309 if lower.contains("progress") && lower.contains("100") {
7310 return Some(1.0);
7311 }
7312 if let Some(bracket) = line.find('[') {
7316 if let Some(close) = line[bracket..].find(']') {
7317 let inside = &line[bracket + 1..bracket + close];
7318 if let Some(slash) = inside.find('/') {
7319 let (n_str, m_str) = inside.split_at(slash);
7320 if let (Ok(n), Ok(m)) = (
7321 n_str.trim().parse::<f32>(),
7322 m_str[1..].trim().parse::<f32>(),
7323 ) {
7324 if m > 0.0 && n <= m {
7325 return Some((n / m).min(1.0));
7326 }
7327 }
7328 }
7329 }
7330 }
7331 None
7332}
7333
7334fn needs_sudo() -> bool {
7335 #[cfg(unix)]
7336 {
7337 unsafe { libc::geteuid() != 0 }
7338 }
7339 #[cfg(not(unix))]
7340 {
7341 false
7342 }
7343}
7344
7345fn strip_ansi_codes(s: &str) -> String {
7346 let mut clean = String::with_capacity(s.len());
7347 let mut in_escape = false;
7348 for c in s.chars() {
7349 if c == '\x1b' {
7350 in_escape = true;
7351 } else if in_escape {
7352 if ('@'..='~').contains(&c) || c == 'm' {
7353 in_escape = false;
7354 }
7355 } else {
7356 clean.push(c);
7357 }
7358 }
7359 clean
7360}
7361
7362fn sanitize_output_line(line: &str) -> String {
7363 let stripped = strip_ansi_codes(line);
7364 let mut clean = String::with_capacity(stripped.len());
7365 for c in stripped.chars() {
7366 if c == '\t' {
7367 clean.push(' ');
7368 } else if !c.is_control() {
7369 clean.push(c);
7370 }
7371 }
7372 clean
7373}
7374
7375fn should_suppress_verification_line(line: &str, target_id: &str) -> bool {
7376 let trimmed = line.trim();
7377 if trimmed.is_empty() {
7378 return true;
7379 }
7380
7381 let lower = trimmed.to_lowercase();
7382 if is_exception_or_traceback_line(&lower) {
7383 return false;
7384 }
7385
7386 if lower.contains("amdgpu.ids")
7387 || lower.contains("tool-lib")
7388 || lower.contains("cannot load tool")
7389 || lower.contains("libdrm/amdgpu.ids: no such file or directory")
7390 {
7391 return true;
7392 }
7393
7394 if (target_id == "rocm" || target_id == "rocm-smi")
7395 && (lower.starts_with("======")
7396 || lower.starts_with("====")
7397 || lower.starts_with("---")
7398 || lower.contains("the info is not accurate")
7399 || lower.contains("================================")
7400 || lower.starts_with('{')
7401 || lower.starts_with('[')
7402 || lower.starts_with(']')
7403 || lower.starts_with('}')
7404 || lower.starts_with("\"card")
7405 || lower.starts_with("device ")
7406 || lower.starts_with("*****"))
7407 {
7408 return true;
7409 }
7410
7411 false
7412}
7413
7414fn is_exception_or_traceback_line(lowercase_line: &str) -> bool {
7415 lowercase_line.contains("traceback")
7416 || lowercase_line.contains("exception")
7417 || lowercase_line.contains("segmentation fault")
7418 || lowercase_line.contains("fatal:")
7419 || lowercase_line.contains("error:")
7420 || lowercase_line.starts_with("error ")
7421}
7422
7423fn run_native_benchmark(component_id: &str, sender: &Sender<InstallerEvent>) -> Result<()> {
7428 use crate::benchmark_runners;
7429
7430 let bench_name = match component_id {
7431 "mlperf-inference" => "all",
7432 "rocm-benchmarks" => "gpu-capability",
7433 "gpu-memory-bandwidth" => "memory-bandwidth",
7434 "rocm-smi-bench" => "gpu-capability",
7435 "pytorch-performance" => "pytorch",
7436 "vllm-performance" => "vllm",
7437 "deepspeed-performance" => "deepspeed",
7438 "megatron-performance" => "megatron",
7439 "onnx-performance" => "onnx",
7440 "rusty-llama-performance" => "llama-cpp",
7441 "flash-attention-ck-performance" => "flash-attention-ck",
7442 "all-benchmarks" => "all",
7443 _ => bail!("Unknown benchmark component: {}", component_id),
7444 };
7445
7446 let _ = sender.send(InstallerEvent::Log(
7447 format!("[native] Running {} benchmark...", component_id),
7448 false,
7449 ));
7450
7451 let _ = sender.send(InstallerEvent::Progress {
7452 component_id: component_id.to_string(),
7453 progress: 0.3,
7454 message: format!("Running {} benchmark", component_id),
7455 });
7456
7457 match benchmark_runners::run_benchmark(bench_name) {
7458 Ok(output) => {
7459 let _ = sender.send(InstallerEvent::Log(
7460 format!(
7461 "[native] {} benchmark completed: {} ({} ms)",
7462 component_id,
7463 if output.success { "SUCCESS" } else { "FAILED" },
7464 output.execution_time_ms
7465 ),
7466 false,
7467 ));
7468 persist_benchmark_result(bench_name, &output);
7473 if !output.errors.is_empty() {
7474 for err in &output.errors {
7475 let _ = sender.send(InstallerEvent::Log(
7476 format!("[native] {} error: {}", component_id, err),
7477 false,
7478 ));
7479 }
7480 }
7481 if !output.success {
7482 let error_details = output.errors.join("; ");
7483 bail!(
7484 "{} benchmark failed: {}",
7485 component_id,
7486 if error_details.is_empty() {
7487 "unknown error".to_string()
7488 } else {
7489 error_details
7490 }
7491 );
7492 }
7493 Ok(())
7494 }
7495 Err(err) => {
7496 let _ = sender.send(InstallerEvent::Log(
7497 format!("[native] {} benchmark error: {}", component_id, err),
7498 false,
7499 ));
7500 bail!("{} benchmark error: {}", component_id, err)
7501 }
7502 }
7503}
7504
7505fn persist_onnx_install_status(version: &str, providers: &[&str]) {
7518 let provider = providers
7519 .iter()
7520 .find(|p| matches!(**p, "MIGraphXExecutionProvider" | "ROCMExecutionProvider"))
7521 .copied()
7522 .unwrap_or("none");
7523 persist_benchmark_result(
7524 "onnx",
7525 &crate::benchmark_runners::BenchmarkOutput {
7526 name: "onnx".to_string(),
7527 success: true,
7528 execution_time_ms: 0,
7529 results: serde_json::json!({
7530 "ort_version": version,
7531 "provider": provider,
7532 "providers_available": providers,
7533 "provider_priority": ["MIGraphXExecutionProvider", "ROCMExecutionProvider"],
7534 "install_status": "provider_ready",
7535 "benchmark_status": "not_run",
7536 "graph_opt_level": "ORT_ENABLE_ALL",
7537 "session_create_ms": 0.0,
7538 "inference_latency_p50_ms": 0.0,
7539 "inference_latency_p95_ms": 0.0,
7540 "inference_latency_p99_ms": 0.0,
7541 "throughput_inf_per_sec": 0.0,
7542 "peak_rss_mb": 0.0,
7543 "quantized_supported": false,
7544 "inference_samples": []
7545 }),
7546 errors: Vec::new(),
7547 },
7548 );
7549}
7550
7551fn persist_benchmark_result(bench_name: &str, output: &crate::benchmark_runners::BenchmarkOutput) {
7552 let metrics = output
7553 .results
7554 .get("metrics")
7555 .cloned()
7556 .unwrap_or_else(|| output.results.clone());
7557 let payload = serde_json::json!({
7558 "name": output.name.clone(),
7559 "success": output.success,
7560 "execution_time_ms": output.execution_time_ms as u64,
7561 "results": metrics,
7562 "errors": serde_json::to_value(&output.errors).unwrap_or_default(),
7563 });
7564 let body = match serde_json::to_string_pretty(&payload) {
7565 Ok(s) => s,
7566 Err(_) => return,
7567 };
7568 let ts = std::time::SystemTime::now()
7569 .duration_since(std::time::UNIX_EPOCH)
7570 .map(|d| d.as_secs())
7571 .unwrap_or(0);
7572 let fname = format!("{bench_name}_benchmarks_{ts}.json");
7573
7574 let mut candidates: Vec<std::path::PathBuf> =
7575 crate::benchmark_logs::benchmark_log_directories();
7576 if let Some(home) = std::env::var("HOME").ok().filter(|s| !s.is_empty()) {
7577 candidates.push(
7578 std::path::Path::new(&home)
7579 .join(".rusty-stack")
7580 .join("logs"),
7581 );
7582 }
7583 let tmp = std::env::var("TMPDIR").unwrap_or_else(|_| "/tmp".to_string());
7584 candidates.push(std::path::Path::new(&tmp).join("rusty-stack").join("logs"));
7585
7586 for dir in candidates {
7587 let _ = std::fs::create_dir_all(&dir);
7588 if std::fs::write(dir.join(&fname), &body).is_ok() {
7589 return;
7590 }
7591 }
7592}
7593
7594#[cfg(test)]
7595mod tests {
7596 #[test]
7597 fn pin_pip_requirement_pins_target_and_drops_upgrade() {
7598 let a: Vec<String> = ["-m", "pip", "install", "--upgrade", "deepspeed", "einops"]
7599 .iter().map(|s| s.to_string()).collect();
7600 let out = super::pin_pip_requirement(a.clone(), "deepspeed", Some(" 0.19.7 "));
7601 assert_eq!(out, ["-m", "pip", "install", "deepspeed==0.19.7", "einops"]);
7602 assert_eq!(super::pin_pip_requirement(a.clone(), "deepspeed", None), a);
7603 assert_eq!(super::pin_pip_requirement(a.clone(), "deepspeed", Some("")), a);
7604 }
7605
7606 #[test]
7607 fn pin_pip_requirement_overrides_existing_specifier() {
7608 let a: Vec<String> = ["-m", "pip", "install", "deepspeed>=0.18", "einops"]
7611 .iter().map(|s| s.to_string()).collect();
7612 let out = super::pin_pip_requirement(a, "deepspeed", Some("0.19.7"));
7613 assert_eq!(out, ["-m", "pip", "install", "deepspeed==0.19.7", "einops"]);
7614 let b: Vec<String> = ["-m", "pip", "install", "deepspeed[extra]==0.18", "deepspeed-tools"]
7616 .iter().map(|s| s.to_string()).collect();
7617 let out = super::pin_pip_requirement(b, "deepspeed", Some("0.19.7"));
7618 assert_eq!(out, ["-m", "pip", "install", "deepspeed==0.19.7", "deepspeed-tools"]);
7619 let c: Vec<String> = ["-m", "pip", "install", "--upgrade", "deepspeed"]
7622 .iter().map(|s| s.to_string()).collect();
7623 assert_eq!(super::pin_pip_requirement(c.clone(), "deepspeed", Some("latest")), c);
7624 assert_eq!(super::pin_pip_requirement(c.clone(), "deepspeed", Some("installed")), c);
7625 assert_eq!(super::pin_pip_requirement(c.clone(), "deepspeed", Some(" Latest ")), c);
7626 }
7627
7628 #[test]
7629 fn explicit_update_target_predicate_and_gate_stay_paired() {
7630 let _guard = crate::test_support::lock_env();
7631 std::env::remove_var("MLSTACK_UNSEAL_CORE");
7632 assert!(!super::is_explicit_update_target(None));
7634 assert!(!super::is_explicit_update_target(Some("")));
7635 assert!(!super::is_explicit_update_target(Some(" ")));
7636 assert!(!super::is_explicit_update_target(Some("latest")));
7637 assert!(!super::is_explicit_update_target(Some("Installed")));
7638 assert!(super::is_explicit_update_target(Some("7.14.1")));
7639 assert!(super::is_explicit_update_target(Some(" 0.19.7 ")));
7640 assert!(!super::unseal_core_requested());
7643 }
7644
7645 #[test]
7646 fn update_rocm_channel_maps_planned_target_and_matches_planner_precedence() {
7647 use crate::installers::components::rocm::RocmChannel;
7648 let _guard = crate::test_support::lock_env();
7649 std::env::set_var("ROCM_CHANNEL", "bogus");
7651 assert_eq!(super::update_rocm_channel(Some("7.14.1")), RocmChannel::Latest);
7652 assert_eq!(super::update_rocm_channel(Some("7.2.4")), RocmChannel::Legacy);
7653 assert_eq!(super::update_rocm_channel(None), RocmChannel::Latest);
7657 assert_eq!(super::update_rocm_channel(Some("latest")), RocmChannel::Latest);
7658 std::env::remove_var("ROCM_CHANNEL");
7659 }
7660
7661 #[test]
7662 fn scoped_env_override_restores_previous_values_on_drop() {
7663 let _guard = crate::test_support::lock_env();
7664 std::env::set_var("MLSTACK_FORCE_REINSTALL", "user-set");
7665 std::env::remove_var("MLSTACK_UNSEAL_CORE");
7666 {
7667 let _scope = super::ScopedEnvOverride::setting(&[
7668 ("MLSTACK_FORCE_REINSTALL", "1"),
7669 ("MLSTACK_UNSEAL_CORE", "1"),
7670 ]);
7671 assert_eq!(std::env::var("MLSTACK_FORCE_REINSTALL").as_deref(), Ok("1"));
7672 assert_eq!(std::env::var("MLSTACK_UNSEAL_CORE").as_deref(), Ok("1"));
7673 }
7674 assert_eq!(
7677 std::env::var("MLSTACK_FORCE_REINSTALL").as_deref(),
7678 Ok("user-set")
7679 );
7680 assert!(std::env::var("MLSTACK_UNSEAL_CORE").is_err());
7681 std::env::remove_var("MLSTACK_FORCE_REINSTALL");
7682 }
7683
7684 use super::*;
7685 use std::sync::Mutex;
7686
7687 static ENV_LOCK: Mutex<()> = Mutex::new(());
7688
7689 #[test]
7695 fn test_safe_rocm_path_prepends_rocm_bin() {
7696 if !Path::new("/opt/rocm/bin").exists() {
7701 eprintln!("skip: /opt/rocm/bin absent (CI host) — prepend behavior is pass-through");
7702 return;
7703 }
7704 let p = safe_rocm_path("/home/scooter/.mlstack/global/bin:/usr/bin:/bin");
7705 let rocm = p.find("/opt/rocm/bin").unwrap();
7706 let shim = p.find("/home/scooter/.mlstack/global/bin").unwrap();
7707 assert!(
7708 rocm < shim,
7709 "/opt/rocm/bin must precede the venv bin: got {p}"
7710 );
7711 assert!(p.contains("/opt/rocm/hip/bin"));
7713 assert!(p.contains("/usr/bin") && p.contains("/bin"));
7714 }
7715
7716 #[test]
7717 fn test_safe_rocm_path_dedupes_rocm_entries() {
7718 if !Path::new("/opt/rocm/bin").exists() {
7722 eprintln!("skip: /opt/rocm/bin absent (CI host)");
7723 return;
7724 }
7725 let p = safe_rocm_path("/opt/rocm/bin:/usr/bin:/opt/rocm/hip/bin");
7726 assert_eq!(
7727 p.matches("/opt/rocm/bin").count(),
7728 1,
7729 "rocm/bin must appear exactly once: got {p}"
7730 );
7731 assert_eq!(p.matches("/opt/rocm/hip/bin").count(), 1);
7732 }
7733
7734 #[test]
7735 fn test_safe_rocm_path_passes_through_when_rocm_absent() {
7736 let inherited = "/usr/local/bin:/usr/bin";
7742 if Path::new("/opt/rocm/bin").exists() {
7743 assert!(safe_rocm_path(inherited).starts_with("/opt/rocm/bin"));
7744 } else {
7745 assert_eq!(safe_rocm_path(inherited), inherited);
7746 }
7747 }
7748
7749 #[test]
7758 fn test_format_user_friendly_error_permission_denied() {
7759 let msg = format_user_friendly_error("rocm", "Permission denied: /opt/rocm");
7760 assert!(
7761 msg.contains("Permission denied"),
7762 "Should mention permission issue"
7763 );
7764 assert!(
7765 msg.contains("elevated privileges"),
7766 "Should suggest running with sudo"
7767 );
7768 }
7769
7770 #[test]
7771 fn test_native_maintenance_actions_use_native_verdict() {
7772 let repair = Component {
7773 id: "rccl-repair".into(),
7774 name: "Repair RCCL Multi-GPU".into(),
7775 description: String::new(),
7776 script: String::new(),
7777 category: Category::Maintenance,
7778 required: false,
7779 selected: true,
7780 installed: false,
7781 progress: 0.0,
7782 estimate: String::new(),
7783 needs_sudo: false,
7784 experimental: false,
7785 note: None,
7786 };
7787 let verify = Component {
7788 id: "verify-basic".into(),
7789 name: "Verify Installation".into(),
7790 description: String::new(),
7791 script: String::new(),
7792 category: Category::Maintenance,
7793 required: false,
7794 selected: true,
7795 installed: false,
7796 progress: 0.0,
7797 estimate: String::new(),
7798 needs_sudo: false,
7799 experimental: false,
7800 note: None,
7801 };
7802 let normal = Component {
7803 id: "pytorch".into(),
7804 name: "PyTorch with ROCm".into(),
7805 description: String::new(),
7806 script: String::new(),
7807 category: Category::Foundation,
7808 required: true,
7809 selected: true,
7810 installed: false,
7811 progress: 0.0,
7812 estimate: String::new(),
7813 needs_sudo: false,
7814 experimental: false,
7815 note: None,
7816 };
7817
7818 assert!(is_native_maintenance_action(&repair));
7819 assert!(!is_native_maintenance_action(&verify));
7820 assert!(!is_native_maintenance_action(&normal));
7821
7822 let success = native_maintenance_action_outcome(&repair, true);
7823 assert!(success.success);
7824 assert!(success.report_lines[0].contains("reported success"));
7825
7826 let failure = native_maintenance_action_outcome(&repair, false);
7827 assert!(!failure.success);
7828 assert!(failure.report_lines[0].contains("reported failure"));
7829 }
7830
7831 #[test]
7832 fn test_benchmark_patterns_match_native_output_filenames() {
7833 assert_eq!(
7834 benchmark_pattern_for_target("rocm-benchmarks"),
7835 Some("gpu-capability_benchmarks")
7836 );
7837 assert_eq!(
7838 benchmark_pattern_for_target("gpu-memory-bandwidth"),
7839 Some("memory-bandwidth_benchmarks")
7840 );
7841 assert_eq!(
7842 benchmark_pattern_for_target("all-benchmarks"),
7843 Some("all_benchmarks")
7844 );
7845 assert_eq!(
7846 benchmark_pattern_for_target("onnx-performance"),
7847 Some("onnx_benchmarks")
7848 );
7849 }
7850
7851 #[test]
7852 fn test_single_benchmark_results_render_metrics_and_errors() {
7853 let lines = benchmark_result_lines_from_json(&serde_json::json!({
7854 "name": "vllm",
7855 "success": false,
7856 "execution_time_ms": 248842,
7857 "results": {
7858 "mode": "degraded",
7859 "reason": "model timed out",
7860 "throughput_tokens_per_sec": 0.0,
7861 "visible_devices": "0,1"
7862 },
7863 "errors": ["model timed out after 240 seconds"]
7864 }));
7865 let joined = lines.join("\n");
7866
7867 assert!(joined.contains("MODE: degraded"));
7868 assert!(joined.contains("REASON: model timed out"));
7869 assert!(joined.contains("THROUGHPUT TOKENS PER SEC: 0"));
7870 assert!(joined.contains("ERROR: model timed out after 240 seconds"));
7871 }
7872
7873 #[test]
7874 fn test_persist_onnx_install_status_writes_clean_status_artifact() {
7875 let _global_env = crate::test_support::lock_env();
7876 let _guard = ENV_LOCK.lock().unwrap();
7877 let old_home = std::env::var("HOME").ok();
7878 let old_tmpdir = std::env::var("TMPDIR").ok();
7879 let old_log_dir = std::env::var("MLSTACK_LOG_DIR").ok();
7880 let root = std::env::temp_dir().join(format!(
7881 "rusty-stack-persist-onnx-status-{}",
7882 std::process::id()
7883 ));
7884 let _ = std::fs::remove_dir_all(&root);
7885 let logs = root.join("logs");
7886 let tmp = root.join("tmp");
7887 std::fs::create_dir_all(&logs).unwrap();
7888 std::fs::create_dir_all(&tmp).unwrap();
7889 std::env::set_var("HOME", &root);
7890 std::env::set_var("TMPDIR", &tmp);
7891 std::env::set_var("MLSTACK_LOG_DIR", &logs);
7892
7893 persist_onnx_install_status(
7894 "1.27.1",
7895 &["MIGraphXExecutionProvider", "CPUExecutionProvider"],
7896 );
7897
7898 let written = std::fs::read_dir(&logs)
7899 .unwrap()
7900 .find_map(|entry| {
7901 let path = entry.ok()?.path();
7902 (path
7903 .file_name()?
7904 .to_string_lossy()
7905 .starts_with("onnx_benchmarks_"))
7906 .then_some(path)
7907 })
7908 .expect("onnx status log should be written");
7909 let json: serde_json::Value =
7910 serde_json::from_str(&std::fs::read_to_string(written).unwrap()).unwrap();
7911
7912 if let Some(home) = old_home {
7913 std::env::set_var("HOME", home);
7914 } else {
7915 std::env::remove_var("HOME");
7916 }
7917 if let Some(tmpdir) = old_tmpdir {
7918 std::env::set_var("TMPDIR", tmpdir);
7919 } else {
7920 std::env::remove_var("TMPDIR");
7921 }
7922 if let Some(log_dir) = old_log_dir {
7923 std::env::set_var("MLSTACK_LOG_DIR", log_dir);
7924 } else {
7925 std::env::remove_var("MLSTACK_LOG_DIR");
7926 }
7927 let _ = std::fs::remove_dir_all(&root);
7928
7929 assert_eq!(json["success"], true);
7930 assert_eq!(json["errors"].as_array().unwrap().len(), 0);
7931 assert_eq!(json["results"]["provider"], "MIGraphXExecutionProvider");
7932 assert_eq!(json["results"]["benchmark_status"], "not_run");
7933 }
7934
7935 #[test]
7936 fn test_persist_full_suite_keeps_nested_results() {
7937 let _global_env = crate::test_support::lock_env();
7938 let _guard = ENV_LOCK.lock().unwrap();
7939 let old_home = std::env::var("HOME").ok();
7940 let old_tmpdir = std::env::var("TMPDIR").ok();
7941 let old_log_dir = std::env::var("MLSTACK_LOG_DIR").ok();
7942 let root = std::env::temp_dir().join(format!(
7943 "rusty-stack-persist-full-suite-{}",
7944 std::process::id()
7945 ));
7946 let _ = std::fs::remove_dir_all(&root);
7947 let logs = root.join("logs");
7948 let tmp = root.join("tmp");
7949 std::fs::create_dir_all(&logs).unwrap();
7950 std::fs::create_dir_all(&tmp).unwrap();
7951 std::env::set_var("HOME", &root);
7952 std::env::set_var("TMPDIR", &tmp);
7953 std::env::set_var("MLSTACK_LOG_DIR", &logs);
7954
7955 let output = crate::benchmark_runners::BenchmarkOutput {
7956 name: "all".into(),
7957 success: true,
7958 execution_time_ms: 1,
7959 results: serde_json::json!({
7960 "memory_bandwidth": {
7961 "success": true,
7962 "execution_time_ms": 1,
7963 "metrics": {"hbm_peak_gb_s": 123.0}
7964 }
7965 }),
7966 errors: Vec::new(),
7967 };
7968 persist_benchmark_result("all", &output);
7969
7970 let written = std::fs::read_dir(&logs)
7971 .unwrap()
7972 .find_map(|entry| {
7973 let path = entry.ok()?.path();
7974 (path
7975 .file_name()?
7976 .to_string_lossy()
7977 .starts_with("all_benchmarks_"))
7978 .then_some(path)
7979 })
7980 .expect("all benchmark log should be written");
7981 let json: serde_json::Value =
7982 serde_json::from_str(&std::fs::read_to_string(written).unwrap()).unwrap();
7983
7984 if let Some(home) = old_home {
7985 std::env::set_var("HOME", home);
7986 } else {
7987 std::env::remove_var("HOME");
7988 }
7989 if let Some(tmpdir) = old_tmpdir {
7990 std::env::set_var("TMPDIR", tmpdir);
7991 } else {
7992 std::env::remove_var("TMPDIR");
7993 }
7994 if let Some(log_dir) = old_log_dir {
7995 std::env::set_var("MLSTACK_LOG_DIR", log_dir);
7996 } else {
7997 std::env::remove_var("MLSTACK_LOG_DIR");
7998 }
7999 let _ = std::fs::remove_dir_all(&root);
8000
8001 assert_eq!(
8002 json["results"]["memory_bandwidth"]["metrics"]["hbm_peak_gb_s"],
8003 123.0
8004 );
8005 }
8006
8007 #[test]
8008 fn test_format_user_friendly_error_not_found() {
8009 let msg = format_user_friendly_error("pytorch", "No such file or directory: python3");
8010 assert!(msg.contains("not found"), "Should mention file not found");
8011 assert!(
8012 msg.contains("ROCm") || msg.contains("Python"),
8013 "Should mention ROCm or Python dependency, got: {}",
8014 msg
8015 );
8016 }
8017
8018 #[test]
8019 fn test_format_user_friendly_error_network() {
8020 let msg = format_user_friendly_error(
8022 "onnxruntime",
8023 "network error: Could not resolve host: github.com",
8024 );
8025 assert!(
8026 msg.contains("Network error"),
8027 "Should mention network issue, got: {}",
8028 msg
8029 );
8030 assert!(
8031 msg.contains("internet connection"),
8032 "Should suggest checking internet, got: {}",
8033 msg
8034 );
8035 }
8036
8037 #[test]
8038 fn test_format_user_friendly_error_out_of_memory() {
8039 let msg = format_user_friendly_error("onnxruntime", "out of memory during build");
8041 assert!(
8042 msg.contains("Out of memory"),
8043 "Should mention memory issue, got: {}",
8044 msg
8045 );
8046 assert!(
8047 msg.contains("swap space"),
8048 "Should suggest increasing swap, got: {}",
8049 msg
8050 );
8051 }
8052
8053 #[test]
8054 fn test_format_user_friendly_error_permanent_env_rocminfo() {
8055 let msg = format_user_friendly_error("permanent-env", "rocminfo command not found");
8056 assert!(
8057 msg.contains("rocminfo"),
8058 "Should mention rocminfo specifically"
8059 );
8060 assert!(
8061 msg.contains("ROCm is properly installed"),
8062 "Should suggest ROCm installation"
8063 );
8064 }
8065
8066 #[test]
8067 fn test_format_user_friendly_error_pytorch_specific() {
8068 let msg = format_user_friendly_error("pytorch", "pip install failed: build error");
8070 assert!(msg.contains("PyTorch"), "Should mention PyTorch by name");
8071 assert!(
8072 msg.contains("ROCm is installed first"),
8073 "Should mention ROCm dependency"
8074 );
8075 assert!(
8076 msg.contains("Python 3.10-3.13"),
8077 "Should mention Python version, got: {}",
8078 msg
8079 );
8080 }
8081
8082 #[test]
8083 fn test_format_user_friendly_error_triton_dependency() {
8084 let msg = format_user_friendly_error("triton", "git clone failed");
8085 assert!(
8086 msg.contains("PyTorch and ROCm"),
8087 "Should mention required dependencies"
8088 );
8089 }
8090
8091 #[test]
8092 fn test_format_user_friendly_error_megatron_dependency() {
8093 let msg = format_user_friendly_error("megatron", "pip install failed");
8094 assert!(
8095 msg.contains("PyTorch and MPI4Py"),
8096 "Should mention Megatron's specific dependencies"
8097 );
8098 }
8099
8100 #[test]
8101 fn test_format_user_friendly_error_deepspeed_dependency() {
8102 let msg = format_user_friendly_error("deepspeed", "build failed");
8103 assert!(
8104 msg.contains("PyTorch"),
8105 "Should mention DeepSpeed's dependency on PyTorch"
8106 );
8107 }
8108
8109 #[test]
8110 fn test_format_user_friendly_error_exit_code_passthrough() {
8111 let msg = format_user_friendly_error("rocm", "ROCm failed: General error (exit code 1)");
8112 assert!(
8114 msg.contains("exit code 1"),
8115 "Should preserve exit code context"
8116 );
8117 }
8118
8119 #[test]
8120 fn test_format_user_friendly_error_unknown_component() {
8121 let msg = format_user_friendly_error("some-unknown-component", "something went wrong");
8122 assert!(
8123 msg.contains("Installation failed"),
8124 "Should have generic failure message"
8125 );
8126 assert!(
8127 msg.contains("something went wrong"),
8128 "Should include original error"
8129 );
8130 }
8131
8132 #[test]
8133 fn test_format_user_friendly_error_rocm_not_found() {
8134 let msg = format_user_friendly_error("rocm", "package not found: amdgpu-dkms");
8135 assert!(msg.contains("ROCm"), "Should mention ROCm specifically");
8136 assert!(msg.contains("repositories"), "Should mention repositories");
8137 }
8138
8139 #[test]
8140 fn test_execute_native_command_nonexistent_program() {
8141 let (tx, rx) = std::sync::mpsc::channel();
8144 let cmd = NativeCommand::Shell {
8145 program: "/nonexistent/program/that/does/not/exist".to_string(),
8146 args: vec![],
8147 env: vec![],
8148 working_dir: None,
8149 };
8150 let result = execute_native_command(&cmd, None, &tx, "test-component");
8151 assert!(result.is_err(), "Should fail for non-existent program");
8152 let err_msg = result.unwrap_err().to_string();
8153 assert!(!err_msg.is_empty(), "Error message should not be empty");
8154
8155 drop(tx);
8157 let events: Vec<_> = rx.try_iter().collect();
8158 assert!(
8159 !events.is_empty(),
8160 "Should have sent at least one event before failing"
8161 );
8162 }
8163
8164 #[test]
8165 fn test_execute_native_command_captures_exit_code() {
8166 let (tx, rx) = std::sync::mpsc::channel();
8168 let cmd = NativeCommand::Shell {
8169 program: "bash".to_string(),
8170 args: vec!["-c".to_string(), "echo test_output; exit 42".to_string()],
8171 env: vec![],
8172 working_dir: None,
8173 };
8174 let result = execute_native_command(&cmd, None, &tx, "test-component");
8175 assert!(
8176 result.is_err(),
8177 "Should fail for non-zero exit code, got: {:?}",
8178 result
8179 );
8180
8181 let err_msg = result.unwrap_err().to_string();
8182 assert!(
8183 err_msg.contains("42"),
8184 "Error should include exit code 42, got: {}",
8185 err_msg
8186 );
8187
8188 drop(tx);
8190 let events: Vec<_> = rx.try_iter().collect();
8191 let log_messages: Vec<&str> = events
8192 .iter()
8193 .filter_map(|e| match e {
8194 InstallerEvent::Log(msg, _) => Some(msg.as_str()),
8195 _ => None,
8196 })
8197 .collect();
8198 assert!(
8199 log_messages.iter().any(|m| m.contains("test_output")),
8200 "Should have captured stdout output before failure"
8201 );
8202 assert!(
8203 log_messages
8204 .iter()
8205 .any(|m| m.contains("[ERROR]") || m.contains("exit code")),
8206 "Should have logged error with exit code"
8207 );
8208 }
8209
8210 #[test]
8211 fn test_execute_native_command_success_sends_events() {
8212 let (tx, rx) = std::sync::mpsc::channel();
8214 let cmd = NativeCommand::Shell {
8215 program: "bash".to_string(),
8216 args: vec![
8217 "-c".to_string(),
8218 "echo 'hello world'; echo 'second line'".to_string(),
8219 ],
8220 env: vec![],
8221 working_dir: None,
8222 };
8223 let result = execute_native_command(&cmd, None, &tx, "test-component");
8224 assert!(result.is_ok(), "Should succeed for zero exit code");
8225
8226 drop(tx);
8227 let events: Vec<_> = rx.try_iter().collect();
8228 let log_messages: Vec<&str> = events
8229 .iter()
8230 .filter_map(|e| match e {
8231 InstallerEvent::Log(msg, _) => Some(msg.as_str()),
8232 _ => None,
8233 })
8234 .collect();
8235 assert!(
8236 log_messages.iter().any(|m| m.contains("hello world")),
8237 "Should have captured first line of output"
8238 );
8239 assert!(
8240 log_messages.iter().any(|m| m.contains("second line")),
8241 "Should have captured second line of output"
8242 );
8243 }
8244
8245 #[test]
8250 fn test_no_sudo_wrap_when_password_is_none() {
8251 let (tx, rx) = std::sync::mpsc::channel();
8255 let cmd = NativeCommand::Shell {
8256 program: "echo".to_string(),
8257 args: vec!["hello".to_string()],
8258 env: vec![],
8259 working_dir: None,
8260 };
8261 let result = execute_native_command(&cmd, None, &tx, "test-component");
8262 assert!(result.is_ok(), "Should succeed without sudo: {:?}", result);
8263
8264 drop(tx);
8265 let events: Vec<_> = rx.try_iter().collect();
8266 let log_messages: Vec<String> = events
8267 .iter()
8268 .filter_map(|e| match e {
8269 InstallerEvent::Log(msg, _) => Some(msg.clone()),
8270 _ => None,
8271 })
8272 .collect();
8273
8274 let cmd_log = log_messages.iter().find(|m| m.contains("[native] $"));
8276 assert!(cmd_log.is_some(), "Should have logged the command");
8277 let cmd_log = cmd_log.unwrap();
8278 assert!(
8279 !cmd_log.contains("sudo"),
8280 "Command should NOT be wrapped with sudo when sudo_pw is None, got: {}",
8281 cmd_log
8282 );
8283 assert!(
8284 cmd_log.contains("echo hello"),
8285 "Command should be 'echo hello', got: {}",
8286 cmd_log
8287 );
8288 }
8289
8290 #[test]
8291 fn test_sudo_wrap_when_password_is_some() {
8292 let (tx, rx) = std::sync::mpsc::channel();
8296 let cmd = NativeCommand::Shell {
8297 program: "echo".to_string(),
8298 args: vec!["hello".to_string()],
8299 env: vec![],
8300 working_dir: None,
8301 };
8302 let _result = execute_native_command(&cmd, Some("dummy_pw"), &tx, "test-component");
8304 drop(tx);
8307 let events: Vec<_> = rx.try_iter().collect();
8308 let log_messages: Vec<String> = events
8309 .iter()
8310 .filter_map(|e| match e {
8311 InstallerEvent::Log(msg, _) => Some(msg.clone()),
8312 _ => None,
8313 })
8314 .collect();
8315
8316 let has_sudo_log = log_messages.iter().any(|m| m.contains("Running with sudo"));
8318 assert!(
8319 has_sudo_log,
8320 "Should have logged 'Running with sudo' when sudo_pw is Some"
8321 );
8322 }
8323
8324 #[test]
8325 fn test_git_clone_without_sudo_works() {
8326 let (tx, rx) = std::sync::mpsc::channel();
8331 let cmd = NativeCommand::Shell {
8332 program: "git".to_string(),
8333 args: vec!["--version".to_string()],
8334 env: vec![],
8335 working_dir: None,
8336 };
8337 let result = execute_native_command(&cmd, None, &tx, "textgen");
8338 assert!(
8339 result.is_ok(),
8340 "git --version should succeed without sudo wrapping: {:?}",
8341 result
8342 );
8343
8344 drop(tx);
8345 let events: Vec<_> = rx.try_iter().collect();
8346 let log_messages: Vec<String> = events
8347 .iter()
8348 .filter_map(|e| match e {
8349 InstallerEvent::Log(msg, _) => Some(msg.clone()),
8350 _ => None,
8351 })
8352 .collect();
8353
8354 assert!(
8356 log_messages.iter().any(|m| m.contains("git version")),
8357 "Should have captured 'git version' output, got: {:?}",
8358 log_messages
8359 );
8360
8361 let cmd_log = log_messages.iter().find(|m| m.contains("[native] $"));
8363 assert!(cmd_log.is_some(), "Should have logged the command");
8364 assert!(
8365 !cmd_log.unwrap().contains("sudo"),
8366 "git command should NOT be wrapped with sudo"
8367 );
8368 }
8369
8370 #[test]
8371 fn test_textgen_git_clone_args_not_empty() {
8372 use crate::installers::components::textgen::{TextgenConfig, TextgenInstaller};
8375 let inst = TextgenInstaller::new(TextgenConfig {
8376 install_dir: "/tmp/test-textgen".to_string(),
8377 python_bin: "python3".to_string(),
8378 use_uv: false,
8379 break_system_packages: true,
8380 dry_run: false,
8381 });
8382 let cmd = inst.build_git_clone_command();
8383
8384 for (i, arg) in cmd.args.iter().enumerate() {
8386 assert!(
8387 !arg.is_empty(),
8388 "Argument {} should not be empty in git clone command",
8389 i
8390 );
8391 }
8392
8393 assert_eq!(cmd.program, "git");
8395 assert_eq!(
8396 cmd.args.len(),
8397 3,
8398 "git clone should have 3 args: clone, URL, dir"
8399 );
8400 assert_eq!(cmd.args[0], "clone");
8401 assert_eq!(
8402 cmd.args[1],
8403 "https://github.com/oobabooga/text-generation-webui.git"
8404 );
8405 assert_eq!(cmd.args[2], "/tmp/test-textgen");
8406
8407 assert!(
8409 !cmd.args.contains(&"--".to_string()),
8410 "git clone args should not contain '--' separator (from sudo wrapping)"
8411 );
8412 }
8413
8414 #[test]
8419 fn test_verification_result_labels() {
8420 assert_eq!(VerificationResult::Verified.label(), "Verified");
8421 assert_eq!(VerificationResult::Failed.label(), "Failed");
8422 assert_eq!(VerificationResult::Missing.label(), "Missing");
8423 }
8424}