1use crate::benchmark_logs;
2use crate::state::{Category, Component};
3use std::collections::HashSet;
4use std::env;
5use std::fs;
6use std::path::{Path, PathBuf};
7use std::process::{Command, Stdio};
8
9#[derive(Debug, Clone)]
10pub struct VerificationCommand {
11 pub label: String,
12 pub target_id: String,
13 pub program: String,
14 pub args: Vec<String>,
15 pub modules: Vec<String>,
16}
17
18pub fn python_interpreters() -> Vec<String> {
19 let mut candidates = Vec::new();
20 if let Ok(value) = env::var("MLSTACK_PYTHON_BIN") {
21 push_python_candidate(&mut candidates, value);
22 }
23 if let Ok(value) = env::var("AITER_VENV_PYTHON") {
24 push_python_candidate(&mut candidates, value);
25 }
26 if let Ok(value) = env::var("UV_PYTHON") {
27 push_python_candidate(&mut candidates, value);
28 }
29 for key in [
30 "WANDB_VENV_PYTHON",
31 "MEGATRON_VENV_PYTHON",
32 "MPI4PY_VENV_PYTHON",
33 "FLASH_ATTENTION_VENV_PYTHON",
34 "PYTORCH_VENV_PYTHON",
35 "DEEPSPEED_VENV_PYTHON",
36 "VLLM_VENV_PYTHON",
37 ] {
38 if let Ok(value) = env::var(key) {
39 push_python_candidate(&mut candidates, value);
40 }
41 }
42 let home = resolve_component_user_home();
43 let venv = Path::new(&home)
44 .join("rocm_venv")
45 .join("bin")
46 .join("python");
47 if venv.exists() {
48 candidates.push(venv.to_string_lossy().to_string());
49 }
50
51 let conda_path = Path::new(&home)
53 .join("anaconda3")
54 .join("bin")
55 .join("python");
56 if conda_path.exists() {
57 candidates.push(conda_path.to_string_lossy().to_string());
58 }
59 let miniconda_path = Path::new(&home)
60 .join("miniconda3")
61 .join("bin")
62 .join("python");
63 if miniconda_path.exists() {
64 candidates.push(miniconda_path.to_string_lossy().to_string());
65 }
66
67 if Path::new("/usr/local/bin/python3").exists() {
68 candidates.push("/usr/local/bin/python3".to_string());
69 }
70 if command_exists("python3.13") {
71 candidates.push("python3.13".to_string());
72 }
73 if command_exists("python3.12") {
74 candidates.push("python3.12".to_string());
75 }
76 if command_exists("python3") {
77 candidates.push("python3".to_string());
78 }
79 if command_exists("python") {
80 candidates.push("python".to_string());
81 }
82
83 let mut roots = vec![PathBuf::from(&home)];
84 if let Ok(cwd) = env::current_dir() {
85 roots.push(cwd.clone());
86 roots.push(cwd.join("rusty-stack"));
87 }
88 for root in roots {
89 push_component_venv_candidates(&mut candidates, &root);
90 }
91
92 dedupe_python_candidates_in_order(candidates)
93}
94
95fn dedupe_python_candidates_in_order(candidates: Vec<String>) -> Vec<String> {
96 let mut seen = HashSet::new();
97 let mut ordered = Vec::new();
98 for candidate in candidates {
99 if seen.insert(candidate.clone()) {
100 ordered.push(candidate);
101 }
102 }
103 ordered
104}
105
106fn push_component_venv_candidates(candidates: &mut Vec<String>, root: &Path) {
107 for rel in [
108 "wandb_venv/bin/python",
109 "megatron_rocm_venv/bin/python",
110 "Megatron-LM/megatron_rocm_venv/bin/python",
111 "mpi4py_venv/bin/python",
112 "flash_attention_venv/bin/python",
113 "vllm_venv/bin/python",
114 ".mlstack/venvs/aiter/bin/python",
115 ".mlstack/venvs/vllm/bin/python",
116 "pytorch_rocm_venv/bin/python",
117 ] {
118 let candidate = root.join(rel);
119 if candidate.exists() {
120 candidates.push(candidate.to_string_lossy().to_string());
121 }
122 }
123}
124
125fn push_python_candidate(candidates: &mut Vec<String>, value: String) {
126 let trimmed = value.trim();
127 if trimmed.is_empty() {
128 return;
129 }
130 let candidate = trimmed.to_string();
131 if Path::new(&candidate).exists() || command_exists(&candidate) {
132 candidates.push(candidate);
133 }
134}
135
136pub fn is_component_installed(component: &Component, python_candidates: &[String]) -> bool {
137 if component.category == Category::Maintenance {
138 return false;
139 }
140 is_component_installed_by_id(&component.id, python_candidates)
141}
142
143pub fn is_component_installed_by_id(component_id: &str, python_candidates: &[String]) -> bool {
144 let home = resolve_component_user_home();
145 let has_benchmark_logs = has_benchmark_log_dirs();
146 match component_id {
147 "rocm" => {
150 let version_exists = path_exists("/opt/rocm/.info/version")
152 || path_exists("/opt/rocm/version")
153 || path_exists("/usr/lib/rocm/.info/version");
154
155 let rocminfo_path = crate::installers::common::utils::resolve_rocminfo_path();
157 let rocminfo_functional = if let Ok(output) = std::process::Command::new(&rocminfo_path)
158 .arg("--version")
159 .output()
160 {
161 output.status.success()
162 } else {
163 false
164 };
165
166 version_exists && rocminfo_functional
169 }
170 "pytorch" => python_any(python_candidates, &["torch"]),
171 "triton" => python_any(python_candidates, &["triton"]),
172 "mpi4py" => python_any(python_candidates, &["mpi4py"]),
173 "deepspeed" => python_any(python_candidates, &["deepspeed"]),
174 "ml-stack-core" => {
175 python_any(python_candidates, &["stans_ml_stack"]) || repo_has_ml_stack_core()
176 }
177 "flash-attn" => python_any(python_candidates, &["flash_attn", "flash_attn_2"]),
182 "flash-attn-triton" => {
190 python_any(python_candidates, &["flash_attn", "flash_attn_2"])
191 && backend_marker_is("triton")
192 }
193 "flash-attn-ck" => {
194 python_any(python_candidates, &["flash_attn", "flash_attn_2"])
195 && backend_marker_is("ck")
196 }
197 "megatron" => {
198 path_exists(home_path(&home, &["megatron", "Megatron-LM"]))
199 || python_any(python_candidates, &["megatron"])
200 }
201 "vllm" => {
202 python_candidates
203 .iter()
204 .any(|python| python_exec(python, vllm_runtime_check_snippet()))
205 || path_exists(home_path(&home, &["vllm_build"]))
206 || path_exists(home_path(&home, &["vllm_py313"]))
207 }
208 "aiter" => python_any(python_candidates, &["aiter"]),
209 "vllm-studio" => {
210 command_exists("vllm-studio") || path_exists(home_path(&home, &["vllm-studio"]))
211 }
212 "onnx" => {
213 python_candidates
214 .iter()
215 .any(|python| python_exec(python, onnxruntime_present_snippet()))
216 || path_exists(home_path(&home, &["onnxruntime_build"]))
217 }
218 "bitsandbytes" => {
219 python_any(python_candidates, &["bitsandbytes"])
220 || path_exists(home_path(&home, &["ml_stack", "bitsandbytes"]))
221 }
222 "rocm-smi" => {
224 let rocm_smi_paths = [
225 "/opt/rocm/bin/rocm-smi",
226 "/usr/bin/rocm-smi",
227 "/usr/local/bin/rocm-smi",
228 ];
229 for path in &rocm_smi_paths {
230 if path_exists(path) {
231 if let Ok(output) = std::process::Command::new(path).arg("--showuse").output() {
233 if output.status.success() {
234 return true;
235 }
236 }
237 }
238 }
239 if command_exists("rocm-smi") {
241 if let Ok(output) = std::process::Command::new("rocm-smi")
242 .arg("--showuse")
243 .output()
244 {
245 return output.status.success();
246 }
247 }
248 false
249 }
250 "migraphx" => {
251 python_any(python_candidates, &["migraphx"])
257 || crate::installers::components::migraphx_multi::migraphx_driver_functional()
258 }
259 "pytorch-profiler" => python_any(python_candidates, &["torch"]),
260 "wandb" => python_any(python_candidates, &["wandb"]),
261 "fastvideo" => python_candidates.iter().any(|python| {
262 python_exec(
263 python,
264 crate::installers::components::fastvideo::fastvideo_verification_snippet(),
265 )
266 }),
267 "llama-cpp" => {
268 crate::installers::components::llama_cpp::is_llama_cli_functional(&home)
270 }
271 "permanent-env" => env_file_has_permanent(&home_path(&home, &[".mlstack_env"])),
272 "basic-env" => path_exists(home_path(&home, &[".mlstack_env"])),
273 "enhanced-env" => env_file_has_enhanced(&home_path(&home, &[".mlstack_env"])),
274 "vllm-performance" => has_benchmark_logs,
275 "deepspeed-performance" => has_benchmark_logs,
276 "megatron-performance" => has_benchmark_logs,
277 "all-benchmarks" => has_benchmark_logs,
278 "comfyui" => {
279 let default_path = home_path(&home, &["ComfyUI"]);
284 path_exists(default_path.join(".git")) || path_exists(default_path.join("main.py"))
285 }
286 "textgen" => {
287 let default_path = home_path(&home, &["text-generation-webui"]);
290 path_exists(default_path.join(".git")) || path_exists(default_path.join("server.py"))
291 }
292 _ => false,
293 }
294}
295
296pub fn verification_commands(
297 component_id: &str,
298 python_candidates: &[String],
299) -> Vec<VerificationCommand> {
300 match component_id {
301 "verify-basic" => basic_verification_commands(python_candidates),
302 "verify-enhanced" => enhanced_verification_commands(python_candidates),
303 "verify-build" => build_verification_commands(python_candidates),
304 _ => Vec::new(),
305 }
306}
307
308pub fn component_verification_commands(
309 component_id: &str,
310 python_candidates: &[String],
311) -> Vec<VerificationCommand> {
312 match component_id {
313 "rocm" => vec![shell_command("ROCm info", "rocm", "rocminfo", &[])],
314 "pytorch" => vec![python_command(
315 "PyTorch",
316 "pytorch",
317 &["torch"],
318 python_candidates,
319 pytorch_diagnostic_snippet(),
320 )],
321 "triton" => vec![python_command(
322 "Triton",
323 "triton",
324 &["triton"],
325 python_candidates,
326 "import triton,sys; import triton.language as tl; import importlib; ok=False\nfor _m in ('triton.runtime.driver','triton.runtime.backends.amd','triton.backends.amd'):\n try:\n importlib.import_module(_m); ok=True; break\n except Exception:\n pass\nprint(f'Triton {triton.__version__} backend_loaded={ok}'); sys.exit(0 if ok else 1)",
327 )],
328 "mpi4py" => vec![python_command(
329 "MPI4Py",
330 "mpi4py",
331 &["mpi4py"],
332 python_candidates,
333 "from mpi4py import MPI; import sys\ntry:\n rank=MPI.COMM_WORLD.Get_rank(); ok=rank>=0\nexcept Exception:\n rank=-1; ok=False\nprint(f'MPI4Py rank={rank} ok={ok}'); sys.exit(0 if ok else 1)",
334 )],
335 "deepspeed" => vec![python_command(
336 "DeepSpeed",
337 "deepspeed",
338 &["deepspeed"],
339 python_candidates,
340 "import deepspeed,sys; ok=False\ntry:\n from deepspeed.ops.adam import DeepSpeedCPUAdam; ok=True\nexcept Exception:\n pass\nprint(f'DeepSpeed {deepspeed.__version__} ops_loaded={ok}'); sys.exit(0 if ok else 1)",
341 )],
342 "ml-stack-core" => vec![python_command(
343 "ML Stack Core",
344 "ml-stack-core",
345 &["stans_ml_stack"],
346 python_candidates,
347 "import stans_ml_stack,sys; import importlib; ok=True\nfor _s in ('core','utils','cli','installers'):\n try:\n importlib.import_module(f'stans_ml_stack.{_s}')\n except Exception:\n ok=False; break\nver=getattr(stans_ml_stack,'__version__','ok')\nprint(f'ML Stack Core {ver} submodule_loaded={ok}'); sys.exit(0 if ok else 1)",
348 )],
349 "flash-attn" => vec![flash_attention_verification_command(
351 "Flash Attention",
352 "flash-attn",
353 None,
354 python_candidates,
355 )],
356 "flash-attn-triton" => vec![flash_attention_verification_command(
359 "Flash Attention (Triton)",
360 "flash-attn-triton",
361 Some("triton"),
362 python_candidates,
363 )],
364 "flash-attn-ck" => vec![flash_attention_verification_command(
365 "Flash Attention (CK)",
366 "flash-attn-ck",
367 Some("ck"),
368 python_candidates,
369 )],
370 "megatron" => vec![python_command(
371 "Megatron-LM",
372 "megatron",
373 &["megatron"],
374 python_candidates,
375 "import megatron,sys; ok=False\ntry:\n from megatron.core import tensor_parallel; ok=True\nexcept Exception:\n pass\nprint(f'Megatron-LM functional={ok}'); sys.exit(0 if ok else 1)",
376 )],
377 "vllm" => vec![python_command(
378 "vLLM",
379 "vllm",
380 &["vllm"],
381 python_candidates,
382 vllm_runtime_check_snippet(),
383 )],
384 "aiter" => vec![python_command(
385 "AITER",
386 "aiter",
387 &["aiter"],
388 python_candidates,
389 "import aiter,sys; ok=False\ntry:\n from aiter import op as _op; ok=True\nexcept Exception:\n pass\nmods=[x for x in dir(aiter) if not x.startswith('_')]\nver=getattr(aiter,'__version__','?')\nprint(f'AITER {ver} op_loaded={ok} exports={len(mods)}'); sys.exit(0 if ok else 1)",
390 )],
391 "vllm-studio" => vec![shell_command(
392 "vLLM Studio",
393 "vllm-studio",
394 "bun",
395 &["--version"],
396 )],
397 "onnx" => vec![python_command(
398 "ONNX Runtime",
399 "onnx",
400 &["onnxruntime"],
401 python_candidates,
402 "import onnxruntime as ort; import sys; print('Version:', ort.__version__); providers=ort.get_available_providers(); print('Providers:', providers); gpu=[p for p in providers if p in ('ROCMExecutionProvider','MIGraphXExecutionProvider','CUDAExecutionProvider')]; sys.exit(0 if gpu else 1)",
403 )],
404 "bitsandbytes" => vec![python_command(
405 "BITSANDBYTES",
406 "bitsandbytes",
407 &["bitsandbytes"],
408 python_candidates,
409 "import bitsandbytes as bnb; import pathlib; import sys; print('Version:', getattr(bnb, '__version__', 'unknown')); path=pathlib.Path(bnb.__file__).parent; libs=list(path.glob('libbitsandbytes_rocm*.so')); print('ROCm Libs:', libs); sys.exit(0 if libs else 1)",
410 )],
411 "rocm-smi" => vec![shell_command(
412 "ROCm SMI",
413 "rocm-smi",
414 "rocm-smi",
415 &["--showproductname"],
416 )],
417 "migraphx" => vec![python_command(
418 "MIGraphX",
419 "migraphx",
420 &["migraphx"],
421 python_candidates,
422 migraphx_verify_snippet(),
423 )],
424 "pytorch-profiler" => vec![python_command(
425 "PyTorch Profiler",
426 "pytorch-profiler",
427 &["torch"],
428 python_candidates,
429 "from torch.profiler import profile; print(profile)",
430 )],
431 "wandb" => vec![python_command(
432 "Weights & Biases",
433 "wandb",
434 &["wandb"],
435 python_candidates,
436 "import wandb,sys; ok=False\ntry:\n _r=wandb.init(mode='disabled'); ok=_r is not None; _r.finish()\nexcept Exception:\n pass\nprint(f'wandb {wandb.__version__} init_run_ok={ok}'); sys.exit(0 if ok else 1)",
437 )],
438 "fastvideo" => vec![python_command(
439 "FastVideo",
440 "fastvideo",
441 &["fastvideo"],
442 python_candidates,
443 crate::installers::components::fastvideo::fastvideo_verification_snippet(),
444 )],
445 "llama-cpp" => vec![shell_command(
446 "llama.cpp",
447 "llama-cpp",
448 "llama-cli",
449 &["--help"],
450 )],
451 "basic-env" => vec![shell_command(
452 "Environment file",
453 "basic-env",
454 "bash",
455 &[
456 "-c",
457 "test -f \"$HOME/.mlstack_env\" && echo OK || exit 1",
458 ],
459 )],
460 "enhanced-env" => vec![shell_command(
461 "Enhanced env",
462 "enhanced-env",
463 "bash",
464 &[
465 "-c",
466 "grep -q \"Enhanced ML Stack Environment Setup Script\" \"$HOME/.mlstack_env\" && echo OK || exit 1",
467 ],
468 )],
469 "permanent-env" => vec![shell_command(
470 "Permanent env",
471 "permanent-env",
472 "bash",
473 &[
474 "-c",
475 "grep -qiE \"Permanent ROCm Environment|Permanent ROCm Env|MLSTACK_PYTHON_BIN\" \"$HOME/.mlstack_env\" && echo OK || exit 1",
476 ],
477 )],
478 "mlperf-inference" => vec![benchmark_log_check_command(
480 "MLPerf benchmark logs",
481 "mlperf-inference",
482 "mlperf_inference",
483 "Benchmark logs found",
484 "No benchmark logs yet",
485 )],
486 "rocm-benchmarks" => vec![benchmark_log_check_command(
487 "ROCm benchmark logs",
488 "rocm-benchmarks",
489 "rocm_benchmarks",
490 "Benchmark logs found",
491 "No benchmark logs yet",
492 )],
493 "gpu-memory-bandwidth" => vec![benchmark_log_check_command(
494 "Memory bandwidth logs",
495 "gpu-memory-bandwidth",
496 "gpu_memory_bandwidth",
497 "Benchmark logs found",
498 "No benchmark logs yet",
499 )],
500 "rocm-smi-bench" => vec![benchmark_log_check_command(
501 "ROCm SMI logs",
502 "rocm-smi-bench",
503 "rocm_smi_benchmarks",
504 "Benchmark logs found",
505 "No benchmark logs yet",
506 )],
507 "pytorch-performance" => vec![benchmark_log_check_command(
508 "PyTorch benchmark logs",
509 "pytorch-performance",
510 "pytorch_performance",
511 "Benchmark logs found",
512 "No benchmark logs yet",
513 )],
514 "vllm-performance" => vec![benchmark_log_check_command(
515 "vLLM benchmark logs",
516 "vllm-performance",
517 "vllm_benchmarks",
518 "Benchmark logs found",
519 "No benchmark logs yet",
520 )],
521 "deepspeed-performance" => vec![benchmark_log_check_command(
522 "DeepSpeed benchmark logs",
523 "deepspeed-performance",
524 "deepspeed_benchmarks",
525 "Benchmark logs found",
526 "No benchmark logs yet",
527 )],
528 "megatron-performance" => vec![benchmark_log_check_command(
529 "Megatron benchmark logs",
530 "megatron-performance",
531 "megatron_benchmarks",
532 "Benchmark logs found",
533 "No benchmark logs yet",
534 )],
535 "all-benchmarks" => vec![benchmark_log_check_command(
536 "Full suite logs",
537 "all-benchmarks",
538 "full_benchmarks",
539 "Suite logs found",
540 "No suite logs yet",
541 )],
542 "comfyui" => vec![{
543 let py = python_candidates
547 .first()
548 .cloned()
549 .or_else(|| env::var("MLSTACK_PYTHON_BIN").ok().filter(|s| !s.is_empty()))
550 .or_else(|| env::var("UV_PYTHON").ok().filter(|s| !s.is_empty()))
551 .unwrap_or_else(|| "python3".to_string());
552 shell_command(
553 "ComfyUI",
554 "comfyui",
555 "bash",
556 &[
557 "-c",
558 &format!(
559 "cd \"$HOME/ComfyUI\" 2>/dev/null && {py} -c 'import folder_paths' 2>/dev/null && echo 'comfyui functional' || exit 1"
560 ),
561 ],
562 )
563 }],
564 "textgen" => vec![{
565 let py = python_candidates
567 .first()
568 .cloned()
569 .or_else(|| env::var("MLSTACK_PYTHON_BIN").ok().filter(|s| !s.is_empty()))
570 .or_else(|| env::var("UV_PYTHON").ok().filter(|s| !s.is_empty()))
571 .unwrap_or_else(|| "python3".to_string());
572 shell_command(
573 "text-generation-webui",
574 "textgen",
575 "bash",
576 &[
577 "-c",
578 &format!(
579 "cd \"$HOME/text-generation-webui\" 2>/dev/null && {py} -c 'import server' 2>/dev/null && echo 'textgen functional' || exit 1"
580 ),
581 ],
582 )
583 }],
584 _ => Vec::new(),
585 }
586}
587
588pub fn modules_available(modules: &[String], python_candidates: &[String]) -> bool {
589 if modules.is_empty() {
590 return true;
591 }
592 let module_refs: Vec<&str> = modules.iter().map(|s| s.as_str()).collect();
593 python_any(python_candidates, &module_refs)
594}
595
596fn basic_verification_commands(python_candidates: &[String]) -> Vec<VerificationCommand> {
597 vec![
598 shell_command("ROCm info", "rocm", "rocminfo", &[]),
599 python_command(
600 "PyTorch",
601 "pytorch",
602 &["torch"],
603 python_candidates,
604 pytorch_diagnostic_snippet(),
605 ),
606 python_command(
607 "Triton",
608 "triton",
609 &["triton"],
610 python_candidates,
611 "import triton; import triton._C; print(f'Triton {triton.__version__} ok')",
612 ),
613 python_command(
614 "MPI4Py",
615 "mpi4py",
616 &["mpi4py"],
617 python_candidates,
618 "from mpi4py import MPI; import sys\ntry:\n rank=MPI.COMM_WORLD.Get_rank(); ok=rank>=0\nexcept Exception:\n rank=-1; ok=False\nprint(f'MPI4Py rank={rank} ok={ok}'); sys.exit(0 if ok else 1)",
619 ),
620 python_command(
621 "DeepSpeed",
622 "deepspeed",
623 &["deepspeed", "einops"],
624 python_candidates,
625 "import deepspeed; import einops; print(deepspeed.__version__)",
626 ),
627 ]
628}
629
630fn enhanced_verification_commands(python_candidates: &[String]) -> Vec<VerificationCommand> {
631 let mut steps = basic_verification_commands(python_candidates);
632 steps.extend(vec![
633 flash_attention_verification_command(
634 "Flash Attention",
635 "flash-attn",
636 None,
637 python_candidates,
638 ),
639 python_command(
640 "vLLM",
641 "vllm",
642 &["vllm"],
643 python_candidates,
644 vllm_runtime_check_snippet(),
645 ),
646 python_command(
647 "AITER",
648 "aiter",
649 &["aiter"],
650 python_candidates,
651 "import aiter; print(getattr(aiter, '__version__', 'ok'))",
652 ),
653 python_command(
654 "ONNX Runtime",
655 "onnx",
656 &["onnxruntime"],
657 python_candidates,
658 "import onnxruntime as ort; import pathlib; import os; import sys; print('Version:', ort.__version__); base=pathlib.Path(ort.__file__).parent; libs=list(base.rglob('libonnxruntime_providers_rocm.so')); [os.environ.update({'ORT_ROCM_EP_PROVIDER_PATH': str(l)}) for l in libs[:1]]; providers=ort.get_available_providers(); print('Providers:', providers); sys.exit(0 if 'ROCMExecutionProvider' in providers else 1)",
659 ),
660 python_command(
661 "BITSANDBYTES",
662 "bitsandbytes",
663 &["bitsandbytes"],
664 python_candidates,
665 "import bitsandbytes as bnb; import pathlib; import sys; print('Version:', getattr(bnb, '__version__', 'unknown')); path=pathlib.Path(bnb.__file__).parent; libs=list(path.glob('libbitsandbytes_rocm*.so')); print('ROCm Libs:', libs); sys.exit(0 if libs else 1)",
666 ),
667 shell_command("ROCm SMI", "rocm-smi", "rocm-smi", &["--showproductname"]),
668 python_command(
669 "MIGraphX",
670 "migraphx",
671 &["migraphx"],
672 python_candidates,
673 migraphx_verify_snippet(),
674 ),
675 python_command(
676 "PyTorch Profiler",
677 "pytorch-profiler",
678 &["torch"],
679 python_candidates,
680 "from torch.profiler import profile; print(profile)",
681 ),
682 python_command(
683 "Weights & Biases",
684 "wandb",
685 &["wandb"],
686 python_candidates,
687 "import wandb,sys; ok=False\ntry:\n _r=wandb.init(mode='disabled'); ok=_r is not None; _r.finish()\nexcept Exception:\n pass\nprint(f'wandb {wandb.__version__} init_run_ok={ok}'); sys.exit(0 if ok else 1)",
688 ),
689 python_command(
690 "FastVideo",
691 "fastvideo",
692 &["fastvideo"],
693 python_candidates,
694 crate::installers::components::fastvideo::fastvideo_verification_snippet(),
695 ),
696 shell_command("vLLM Studio", "vllm-studio", "bun", &["--version"]),
697 ]);
698 steps
699}
700
701fn build_verification_commands(python_candidates: &[String]) -> Vec<VerificationCommand> {
702 let mut steps = basic_verification_commands(python_candidates);
703 steps.extend(vec![
704 python_command(
705 "ONNX Runtime",
706 "onnx",
707 &["onnxruntime"],
708 python_candidates,
709 "import onnxruntime as ort; import pathlib; import os; import sys; print('Version:', ort.__version__); base=pathlib.Path(ort.__file__).parent; libs=list(base.rglob('libonnxruntime_providers_rocm.so')); [os.environ.update({'ORT_ROCM_EP_PROVIDER_PATH': str(l)}) for l in libs[:1]]; providers=ort.get_available_providers(); print('Providers:', providers); sys.exit(0 if 'ROCMExecutionProvider' in providers else 1)",
710 ),
711 flash_attention_verification_command(
712 "Flash Attention (build)",
713 "flash-attn",
714 None,
715 python_candidates,
716 ),
717 python_command(
718 "MIGraphX",
719 "migraphx",
720 &["migraphx"],
721 python_candidates,
722 migraphx_verify_snippet(),
723 ),
724 ]);
725 steps
726}
727
728fn python_command(
729 label: &str,
730 target_id: &str,
731 modules: &[&str],
732 python_candidates: &[String],
733 code: &str,
734) -> VerificationCommand {
735 let program = select_python_for_modules(modules, python_candidates)
736 .or_else(|| python_candidates.first().cloned())
737 .unwrap_or_else(|| "python3".to_string());
738
739 VerificationCommand {
740 label: label.to_string(),
741 target_id: target_id.to_string(),
742 program,
743 args: vec!["-c".to_string(), code.to_string()],
744 modules: modules.iter().map(|m| m.to_string()).collect(),
745 }
746}
747
748fn pytorch_diagnostic_snippet() -> &'static str {
763 r#"import torch, sys, os, ctypes.util
764
765# Tier 1: Basic torch import (already verified by module check)
766print(f'torch version: {torch.__version__}')
767
768# Tier 2: Check if this is a ROCm build
769hip_version = getattr(torch.version, 'hip', None)
770cuda_version = getattr(torch.version, 'cuda', None)
771print(f'hip: {hip_version}')
772print(f'cuda_build: {cuda_version}')
773
774# Tier 3: Runtime HIP/CUDA availability
775try:
776 hip_available = torch.cuda.is_available()
777 # 'cuda' here is AVAILABILITY (CUDA-via-HIP on ROCm), NOT torch.version.cuda
778 # (which is None on a ROCm build — that's the CUDA *toolkit* version torch was
779 # built against, not whether CUDA-style code can run). is_available()==True
780 # means CUDA works through HIP, which is the user-facing answer.
781 print(f'cuda: {hip_available}')
782 if hip_available:
783 device_count = torch.cuda.device_count()
784 print(f'device_count: {device_count}')
785 if device_count > 0:
786 print(f'device_name: {torch.cuda.get_device_name(0)}')
787except Exception as e:
788 hip_available = False
789 print(f'torch.cuda.is_available() error: {e}')
790
791# Tier 4: Diagnostics if HIP not available
792if not hip_available:
793 print('--- ROCm HIP Diagnostics ---')
794
795 # Check LD_LIBRARY_PATH
796 ld_path = os.environ.get('LD_LIBRARY_PATH', '')
797 print(f'LD_LIBRARY_PATH: {ld_path if ld_path else "(not set)"}')
798
799 # Check ROCm_PATH
800 rocm_path = os.environ.get('ROCM_PATH', '')
801 print(f'ROCM_PATH: {rocm_path if rocm_path else "(not set)"}')
802
803 # Check if ROCm libraries exist
804 rocm_lib_dirs = ['/opt/rocm/lib', '/opt/rocm/hip/lib']
805 for d in rocm_lib_dirs:
806 exists = os.path.isdir(d)
807 print(f'{d}: {"exists" if exists else "not found"}')
808
809 # Check if libamdhip64.so can be found
810 hip_lib = ctypes.util.find_library('amdhip64')
811 print(f'libamdhip64: {hip_lib if hip_lib else "not found"}')
812
813 # Check Python executable path
814 print(f'python: {sys.executable}')
815
816 # Provide actionable guidance
817 if hip_version is None and cuda_version is None:
818 print('HINT: torch.version.hip and torch.version.cuda are both None.')
819 print(' This torch build may not have GPU support.')
820 print(' Reinstall with ROCm index: pip install torch --index-url https://download.pytorch.org/whl/rocm7.2/')
821 elif hip_version is not None and not hip_available:
822 print('HINT: torch.version.hip is set but HIP runtime not available.')
823 print(' Possible causes:')
824 print(' 1. LD_LIBRARY_PATH does not include /opt/rocm/lib')
825 print(' 2. Running in a container/environment without GPU access')
826 print(' 3. ROCm drivers not loaded (check with: rocminfo)')
827 print(' Fix: source ~/.mlstack_env or set LD_LIBRARY_PATH=/opt/rocm/lib')
828
829# Stage 4: exit code reflects FUNCTIONAL HIP availability on this ROCm stack.
830# A CPU-only/CUDA torch build, or a ROCm build whose HIP runtime is
831# unreachable, must FAIL verification — not silently pass. The diagnostics
832# above guide the fix; downstream gating belongs in the orchestrator layer,
833# not a lying exit code.
834ok = (hip_version is not None) and hip_available
835sys.exit(0 if ok else 1)"#
836}
837
838fn vllm_runtime_check_snippet() -> &'static str {
839 r#"import importlib
840import vllm
841import cachetools
842import cbor2
843import gguf
844import pybase64
845import ijson
846import mistral_common
847import openai_harmony
848
849loaded = []
850errs = []
851for name in ("vllm._C", "vllm._rocm_C"):
852 try:
853 importlib.import_module(name)
854 loaded.append(name)
855 except Exception as exc:
856 errs.append(f"{name}: {exc}")
857
858if not loaded:
859 raise SystemExit("vLLM native extension load failure: " + " | ".join(errs))
860
861print(vllm.__version__)"#
862}
863
864fn shell_command(
865 label: &str,
866 target_id: &str,
867 program: &str,
868 args: &[&str],
869) -> VerificationCommand {
870 VerificationCommand {
871 label: label.to_string(),
872 target_id: target_id.to_string(),
873 program: program.to_string(),
874 args: args.iter().map(|s| s.to_string()).collect(),
875 modules: Vec::new(),
876 }
877}
878
879fn has_benchmark_log_dirs() -> bool {
880 benchmark_logs::benchmark_log_directories()
881 .iter()
882 .any(|dir| {
883 fs::read_dir(dir)
884 .ok()
885 .map(|entries| {
886 entries
887 .filter_map(|entry| entry.ok())
888 .any(|entry| entry.path().is_file())
889 })
890 .unwrap_or(false)
891 })
892}
893
894fn benchmark_log_check_command(
895 label: &str,
896 target_id: &str,
897 pattern: &str,
898 success_message: &str,
899 failure_message: &str,
900) -> VerificationCommand {
901 let script = benchmark_log_check_script(pattern, success_message, failure_message);
902 shell_command(label, target_id, "bash", &["-c", &script])
903}
904
905fn benchmark_log_check_script(
906 pattern: &str,
907 success_message: &str,
908 failure_message: &str,
909) -> String {
910 format!(
911 "dirs=(\"${{MLSTACK_LOG_DIR:-}}\" \"$HOME/.rusty-stack/logs\" \"${{TMPDIR:-/tmp}}/rusty-stack/logs\")\n\
912 found=0\n\
913 for dir in \"${{dirs[@]}}\"; do\n\
914 if [ -n \"$dir\" ] && [ -d \"$dir\" ]; then\n\
915 if ls \"$dir\" | grep -q \"{}\"; then\n\
916 echo \"{}\"\n\
917 found=1\n\
918 break\n\
919 fi\n\
920 fi\n\
921 done\n\
922 if [ \"$found\" -ne 1 ]; then\n\
923 echo \"{}\"\n\
924 fi",
925 pattern, success_message, failure_message
926 )
927}
928
929fn python_any(python_candidates: &[String], modules: &[&str]) -> bool {
930 python_candidates.iter().any(|python| {
931 modules
932 .iter()
933 .any(|module| python_has_module(python, module))
934 })
935}
936
937fn python_has_module(python: &str, module: &str) -> bool {
938 python_exec(
939 python,
940 &format!(
941 "import importlib.util, sys; sys.exit(0 if importlib.util.find_spec('{module}') else 1)"
942 ),
943 )
944}
945
946fn onnxruntime_present_snippet() -> &'static str {
958 "import sys, os, importlib.util; \
959 sys.path = [p for p in sys.path if p and os.path.abspath(p.rstrip('/')) != os.path.abspath(os.getcwd())]; \
960 s = importlib.util.find_spec('onnxruntime'); \
961 sys.exit(0 if (s is not None and s.origin is not None) else 1)"
962}
963
964fn python_exec(python: &str, code: &str) -> bool {
965 let mut cmd = Command::new(python);
966 cmd.arg("-c")
967 .arg(code)
968 .stdout(Stdio::null())
969 .stderr(Stdio::null());
970
971 if let Ok(existing) = env::var("PYTHONPATH") {
972 let mut path = existing;
973 let extra_paths = python_search_paths();
974 if !extra_paths.is_empty() {
975 if !path.is_empty() {
976 path.push(':');
977 }
978 path.push_str(&extra_paths.join(":"));
979 }
980 cmd.env("PYTHONPATH", path);
981 } else {
982 let extra_paths = python_search_paths();
983 if !extra_paths.is_empty() {
984 cmd.env("PYTHONPATH", extra_paths.join(":"));
985 }
986 }
987
988 let home = resolve_component_user_home();
989 let ld_path = component_status_ld_library_path(&home);
990 if !ld_path.is_empty() {
991 cmd.env("LD_LIBRARY_PATH", ld_path);
992 }
993
994 cmd.status().map(|status| status.success()).unwrap_or(false)
995}
996
997fn push_unique_path(paths: &mut Vec<String>, value: &str) {
998 let value = value.trim();
999 if value.is_empty() {
1000 return;
1001 }
1002 if paths.iter().any(|existing| existing == value) {
1003 return;
1004 }
1005 paths.push(value.to_string());
1006}
1007
1008fn component_user_home_from_passwd(user_name: &str) -> Option<String> {
1009 let target = user_name.trim();
1010 if target.is_empty() {
1011 return None;
1012 }
1013 let passwd = fs::read_to_string("/etc/passwd").ok()?;
1014 passwd.lines().find_map(|line| {
1015 if line.trim().is_empty() || line.starts_with('#') {
1016 return None;
1017 }
1018 let fields: Vec<&str> = line.split(':').collect();
1019 if fields.len() < 6 || fields[0] != target {
1020 return None;
1021 }
1022 let home = fields[5].trim();
1023 if home.is_empty() {
1024 None
1025 } else {
1026 Some(home.to_string())
1027 }
1028 })
1029}
1030
1031fn component_passwd_homes_with_mlstack() -> Vec<String> {
1032 let mut homes = Vec::new();
1033 let Ok(passwd) = fs::read_to_string("/etc/passwd") else {
1034 return homes;
1035 };
1036
1037 for line in passwd.lines() {
1038 if line.trim().is_empty() || line.starts_with('#') {
1039 continue;
1040 }
1041 let fields: Vec<&str> = line.split(':').collect();
1042 if fields.len() < 6 {
1043 continue;
1044 }
1045 let home = fields[5].trim();
1046 if home.is_empty() {
1047 continue;
1048 }
1049 if Path::new(home).join(".mlstack").is_dir()
1050 || Path::new(home).join(".mlstack_env").is_file()
1051 {
1052 push_unique_path(&mut homes, home);
1053 }
1054 }
1055
1056 homes
1057}
1058
1059fn component_candidate_homes(preferred_home: &str) -> Vec<String> {
1060 let mut homes = Vec::new();
1061 push_unique_path(&mut homes, preferred_home);
1062
1063 if let Ok(value) = env::var("MLSTACK_USER_HOME") {
1064 push_unique_path(&mut homes, &value);
1065 }
1066 if let Ok(value) = env::var("HOME") {
1067 push_unique_path(&mut homes, &value);
1068 }
1069
1070 for key in ["SUDO_USER", "USER", "LOGNAME"] {
1071 if let Ok(user_name) = env::var(key) {
1072 if let Some(home) = component_user_home_from_passwd(&user_name) {
1073 push_unique_path(&mut homes, &home);
1074 }
1075 }
1076 }
1077
1078 for home in component_passwd_homes_with_mlstack() {
1079 push_unique_path(&mut homes, &home);
1080 }
1081
1082 homes
1083}
1084
1085fn resolve_component_user_home() -> String {
1086 if let Ok(home) = env::var("MLSTACK_USER_HOME") {
1087 return home;
1088 }
1089
1090 let fallback = env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
1091 let candidates = component_candidate_homes(&fallback);
1092
1093 for home in &candidates {
1094 if Path::new(home).join(".mlstack_env").is_file() {
1095 return home.clone();
1096 }
1097 }
1098 for home in &candidates {
1099 if Path::new(home).join(".mlstack").is_dir() {
1100 return home.clone();
1101 }
1102 }
1103
1104 candidates.into_iter().next().unwrap_or(fallback)
1105}
1106
1107fn component_status_mpi_compat_dirs(home: &str) -> Vec<String> {
1108 let mut dirs = Vec::new();
1109 for home in component_candidate_homes(home) {
1110 let mlstack_dir = Path::new(&home).join(".mlstack");
1111 let primary = mlstack_dir.join("libmpi-compat");
1112 if primary.is_dir() {
1113 dirs.push(primary.to_string_lossy().to_string());
1114 }
1115
1116 if let Ok(entries) = fs::read_dir(&mlstack_dir) {
1117 for entry in entries.flatten() {
1118 let path = entry.path();
1119 if !path.is_dir() {
1120 continue;
1121 }
1122 let Some(name) = path.file_name().and_then(|value| value.to_str()) else {
1123 continue;
1124 };
1125 if name.starts_with("libmpi-compat-user-") {
1126 dirs.push(path.to_string_lossy().to_string());
1127 }
1128 }
1129 }
1130 }
1131
1132 dirs.sort();
1133 dirs.dedup();
1134 dirs
1135}
1136
1137fn component_status_ld_library_path(home: &str) -> String {
1138 let mut paths = Vec::new();
1139
1140 for dir in component_status_mpi_compat_dirs(home) {
1141 push_unique_path(&mut paths, &dir);
1142 }
1143
1144 for rocm_path in ["/opt/rocm/lib", "/opt/rocm/hip/lib", "/opt/rocm/opencl/lib"] {
1145 if Path::new(rocm_path).exists() {
1146 push_unique_path(&mut paths, rocm_path);
1147 }
1148 }
1149
1150 if let Ok(existing) = env::var("LD_LIBRARY_PATH") {
1151 for part in existing.split(':') {
1152 push_unique_path(&mut paths, part);
1153 }
1154 }
1155
1156 paths.join(":")
1157}
1158
1159fn command_exists(command: &str) -> bool {
1160 Command::new("bash")
1161 .arg("-c")
1162 .arg(format!("command -v {}", command))
1163 .stdout(Stdio::null())
1164 .stderr(Stdio::null())
1165 .status()
1166 .map(|status| status.success())
1167 .unwrap_or(false)
1168}
1169
1170pub fn python_search_paths() -> Vec<String> {
1171 let home = resolve_component_user_home();
1172 let mut paths = vec![
1173 format!("{}/pytorch", home),
1174 format!("{}/.local/lib/python3.13/site-packages", home),
1175 format!("{}/.local/lib/python3.12/site-packages", home),
1176 format!("{}/rocm_venv/lib/python3.13/site-packages", home),
1177 format!("{}/rocm_venv/lib/python3.12/site-packages", home),
1178 format!("{}/megatron/Megatron-LM", home),
1179 format!("{}/ml_stack/bitsandbytes/bitsandbytes", home),
1180 ];
1181
1182 let rocm_lib = Path::new("/opt/rocm/lib");
1184 if rocm_lib.exists() {
1185 paths.push(rocm_lib.to_string_lossy().to_string());
1186 if let Ok(entries) = fs::read_dir(rocm_lib) {
1187 for entry in entries.flatten() {
1188 let path = entry.path();
1189 if path.is_dir() {
1190 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
1191 if name.starts_with("python") {
1192 let sp = path.join("site-packages");
1193 if sp.exists() {
1194 paths.push(sp.to_string_lossy().to_string());
1195 }
1196 }
1197 }
1198 }
1199 }
1200 }
1201 }
1202
1203 paths
1204}
1205
1206fn select_python_for_modules(modules: &[&str], python_candidates: &[String]) -> Option<String> {
1207 for key in ["MLSTACK_PYTHON_BIN", "UV_PYTHON"] {
1208 if let Ok(value) = env::var(key) {
1209 let value = value.trim();
1210 if !value.is_empty() {
1211 if modules.is_empty() {
1212 return Some(value.to_string());
1213 }
1214 for module in modules {
1215 if python_has_module(value, module) {
1216 return Some(value.to_string());
1217 }
1218 }
1219 }
1220 }
1221 }
1222
1223 for python in python_candidates {
1224 for module in modules {
1225 if python_has_module(python, module) {
1226 return Some(python.clone());
1227 }
1228 }
1229 }
1230 None
1231}
1232
1233fn path_exists<P: AsRef<Path>>(path: P) -> bool {
1234 path.as_ref().exists()
1235}
1236
1237fn home_path(home: &str, parts: &[&str]) -> PathBuf {
1238 let mut path = PathBuf::from(home);
1239 for part in parts {
1240 path.push(part);
1241 }
1242 path
1243}
1244
1245fn backend_marker_is(name: &str) -> bool {
1251 let home = resolve_component_user_home();
1252 let marker = home_path(&home, &[".mlstack", "flash-attention", ".backend"]);
1253 std::fs::read_to_string(&marker)
1254 .ok()
1255 .map(|s| s.trim() == name)
1256 .unwrap_or(false)
1257}
1258
1259fn migraphx_verify_snippet() -> &'static str {
1276 r#"
1277import os, sys, subprocess
1278# (1) Drop CWD / '' entries so a repo-local migraphx stub can't shadow a real install.
1279sys.path = [p for p in sys.path if p and os.path.abspath(p.rstrip('/')) != os.path.abspath(os.getcwd())]
1280ok = False
1281ver = 'ok'
1282via = 'none'
1283try:
1284 import migraphx
1285 if hasattr(migraphx, 'parse_onnx'):
1286 ok = True
1287 via = 'python'
1288 ver = getattr(migraphx, '__version__', 'ok')
1289 else:
1290 ver = getattr(migraphx, '__version__', 'stub')
1291except ImportError:
1292 ver = 'absent'
1293# (2) Fall through to the C++ driver whenever python bindings are missing OR
1294# incomplete (no parse_onnx). On Arch/CachyOS the driver is the install target.
1295if not ok:
1296 r = subprocess.run(['/opt/rocm/bin/migraphx-driver', '--version'], capture_output=True)
1297 if r.returncode == 0:
1298 ok = True
1299 via = 'driver'
1300 ver = 'driver'
1301print(f'MIGraphX {ver} parse_onnx={ok} via={via}')
1302sys.exit(0 if ok else 1)
1303"#
1304}
1305
1306fn flash_attention_verification_command(
1315 label: &str,
1316 target_id: &str,
1317 expected_backend: Option<&str>,
1318 python_candidates: &[String],
1319) -> VerificationCommand {
1320 let marker_block = match expected_backend {
1321 Some(name) => format!(
1322 "home=pathlib.Path(os.environ.get('HOME',''))\nmarker=home/'.mlstack'/'flash-attention'/'.backend'\nif not marker.exists() or marker.read_text().strip()!='{name}':\n print(f'backend marker mismatch (expected {name})');sys.exit(1)\n"
1323 ),
1324 None => String::new(),
1325 };
1326 let code = format!(
1327 "import importlib,sys,os,pathlib\nm=importlib.import_module('flash_attn')\nver=getattr(m,'__version__','unknown')\nhas_func=hasattr(m,'flash_attn_func') or hasattr(m,'flash_attn_qkvpacked_func')\n{marker_block}print(f'flash_attn {{ver}} func={{has_func}}')"
1328 );
1329 python_command(label, target_id, &["flash_attn"], python_candidates, &code)
1330}
1331
1332fn env_file_has_enhanced(path: &Path) -> bool {
1333 if let Ok(contents) = fs::read_to_string(path) {
1334 return contents.contains("Enhanced ML Stack Environment Setup Script");
1335 }
1336 false
1337}
1338
1339fn env_file_has_permanent(path: &Path) -> bool {
1340 if let Ok(contents) = fs::read_to_string(path) {
1341 let has_marker = contents.contains("Permanent ROCm Environment")
1342 || contents.contains("Permanent ROCm Env")
1343 || (contents.contains("ML Stack Environment File")
1344 && contents.contains("MLSTACK_PYTHON_BIN"));
1345 if !has_marker {
1346 return false;
1347 }
1348 let valid_arch_prefixes = ["gfx9", "gfx10", "gfx11", "gfx12"];
1351 let mut has_gpu_arch_line = false;
1352 for line in contents.lines() {
1353 let trimmed = line.trim();
1354 let assign = trimmed.strip_prefix("export ").unwrap_or(trimmed);
1361 if assign.starts_with("GPU_ARCH=") {
1362 has_gpu_arch_line = true;
1363 if let Some(rest) = assign.split("GPU_ARCH=").nth(1) {
1364 let arch = rest.split_whitespace().next().unwrap_or(rest);
1365 if arch == "gfx000" {
1366 return false;
1367 }
1368 if !valid_arch_prefixes
1370 .iter()
1371 .any(|prefix| arch.starts_with(prefix))
1372 {
1373 return false;
1374 }
1375 }
1376 }
1377 }
1378 has_gpu_arch_line
1380 } else {
1381 false
1382 }
1383}
1384
1385fn repo_has_ml_stack_core() -> bool {
1386 if let Ok(root) = env::var("MLSTACK_REPO_ROOT") {
1387 return path_exists(Path::new(&root).join("stans_ml_stack"));
1388 }
1389 false
1390}
1391
1392#[cfg(test)]
1397mod tests {
1398 use super::*;
1399
1400 #[test]
1401 fn test_fastvideo_verification_uses_shared_compiled_kernel_smoke_test() {
1402 let candidates = vec!["python3".to_string()];
1403 let commands = component_verification_commands("fastvideo", &candidates);
1404 assert_eq!(commands.len(), 1);
1405
1406 let command = &commands[0];
1407 assert_eq!(command.label, "FastVideo");
1408 assert_eq!(command.target_id, "fastvideo");
1409 assert_eq!(command.modules, vec!["fastvideo".to_string()]);
1410 assert_eq!(
1411 command.args,
1412 vec![
1413 "-c".to_string(),
1414 crate::installers::components::fastvideo::fastvideo_verification_snippet()
1415 .to_string(),
1416 ]
1417 );
1418
1419 let snippet = &command.args[1];
1420 for anchor in [
1421 "from fastvideo_kernel import int8_quant",
1422 "range(int(discovered.stdout.strip()))",
1423 "int8_quant(source)",
1424 "torch.cuda.synchronize()",
1425 "raise SystemExit",
1426 ] {
1427 assert!(
1428 snippet.contains(anchor),
1429 "missing verification anchor: {anchor}"
1430 );
1431 }
1432
1433 let enhanced = enhanced_verification_commands(&candidates);
1434 let enhanced_fastvideo: Vec<_> = enhanced
1435 .iter()
1436 .filter(|candidate| candidate.target_id == "fastvideo")
1437 .collect();
1438 assert_eq!(enhanced_fastvideo.len(), 1);
1439 assert_eq!(
1440 enhanced_fastvideo[0].args[1],
1441 crate::installers::components::fastvideo::fastvideo_verification_snippet()
1442 );
1443 assert_eq!(enhanced_fastvideo[0].modules, vec!["fastvideo".to_string()]);
1444 }
1445
1446 #[test]
1447 fn test_fastvideo_installation_status_requires_compiled_kernel_smoke_test() {
1448 use std::os::unix::fs::PermissionsExt;
1449 use std::time::{SystemTime, UNIX_EPOCH};
1450
1451 let unique = SystemTime::now()
1452 .duration_since(UNIX_EPOCH)
1453 .expect("system clock must be after Unix epoch")
1454 .as_nanos();
1455 let temp_dir = env::temp_dir().join(format!(
1456 "rusty-stack-fastvideo-status-{}-{unique}",
1457 std::process::id()
1458 ));
1459 fs::create_dir_all(&temp_dir).expect("create temporary FastVideo status test directory");
1460
1461 let import_only = temp_dir.join("import-only-python");
1462 fs::write(
1463 &import_only,
1464 "#!/bin/sh\ncase \"$2\" in\n *\"find_spec\"*\"fastvideo\"*) exit 0 ;;\n *) exit 1 ;;\nesac\n",
1465 )
1466 .expect("write import-only fake Python");
1467 let mut permissions = fs::metadata(&import_only)
1468 .expect("stat import-only fake Python")
1469 .permissions();
1470 permissions.set_mode(0o755);
1471 fs::set_permissions(&import_only, permissions)
1472 .expect("make import-only fake Python executable");
1473
1474 assert!(
1475 !is_component_installed_by_id(
1476 "fastvideo",
1477 &[import_only.to_string_lossy().into_owned()]
1478 ),
1479 "an import spec alone must not report FastVideo installed"
1480 );
1481
1482 let functional = temp_dir.join("functional-python");
1483 fs::write(
1484 &functional,
1485 "#!/bin/sh\ncase \"$2\" in\n *\"import fastvideo\"*\"from fastvideo_kernel import int8_quant\"*\"int8_quant(source)\"*) exit 0 ;;\n *) exit 1 ;;\nesac\n",
1486 )
1487 .expect("write functional fake Python");
1488 let mut permissions = fs::metadata(&functional)
1489 .expect("stat functional fake Python")
1490 .permissions();
1491 permissions.set_mode(0o755);
1492 fs::set_permissions(&functional, permissions)
1493 .expect("make functional fake Python executable");
1494
1495 assert!(
1496 is_component_installed_by_id("fastvideo", &[functional.to_string_lossy().into_owned()]),
1497 "the shared compiled-kernel smoke must report FastVideo installed"
1498 );
1499
1500 fs::remove_dir_all(&temp_dir).expect("remove temporary FastVideo status test directory");
1501 }
1502
1503 #[test]
1504 fn test_pytorch_diagnostic_snippet_exits_strict() {
1505 let snippet = pytorch_diagnostic_snippet();
1509 assert!(
1510 snippet.contains("(hip_version is not None) and hip_available"),
1511 "Should gate the exit on functional HIP availability"
1512 );
1513 assert!(
1514 snippet.contains("sys.exit(0 if ok else 1)"),
1515 "Should exit non-zero when HIP is unavailable (fail loud)"
1516 );
1517 assert!(
1518 !snippet.contains("\nsys.exit(0)\n"),
1519 "Should NOT have an unconditional soft-pass sys.exit(0)"
1520 );
1521 }
1522
1523 #[test]
1524 fn test_pytorch_diagnostic_snippet_checks_hip() {
1525 let snippet = pytorch_diagnostic_snippet();
1527 assert!(
1528 snippet.contains("torch.version"),
1529 "Should check torch.version for HIP info"
1530 );
1531 assert!(
1532 snippet.contains("torch.cuda.is_available()"),
1533 "Should check torch.cuda.is_available() for runtime HIP"
1534 );
1535 }
1536
1537 #[test]
1538 fn test_pytorch_diagnostic_snippet_provides_hints() {
1539 let snippet = pytorch_diagnostic_snippet();
1541 assert!(
1542 snippet.contains("HINT:"),
1543 "Should provide HINT messages for troubleshooting"
1544 );
1545 assert!(
1546 snippet.contains("LD_LIBRARY_PATH"),
1547 "Should mention LD_LIBRARY_PATH as a possible cause"
1548 );
1549 assert!(
1550 snippet.contains("/opt/rocm/lib"),
1551 "Should mention ROCm lib path"
1552 );
1553 }
1554
1555 #[test]
1556 fn test_pytorch_diagnostic_snippet_checks_rocm_libs() {
1557 let snippet = pytorch_diagnostic_snippet();
1559 assert!(
1560 snippet.contains("libamdhip64"),
1561 "Should check for libamdhip64 (core HIP library)"
1562 );
1563 assert!(
1564 snippet.contains("/opt/rocm/lib"),
1565 "Should check /opt/rocm/lib directory"
1566 );
1567 }
1568
1569 #[test]
1570 fn test_pytorch_component_verification_uses_diagnostic() {
1571 let candidates = vec!["python3".to_string()];
1574 let cmds = component_verification_commands("pytorch", &candidates);
1575 assert_eq!(
1576 cmds.len(),
1577 1,
1578 "Should have exactly one verification command"
1579 );
1580 let cmd = &cmds[0];
1581 assert_eq!(cmd.label, "PyTorch");
1582 assert_eq!(cmd.target_id, "pytorch");
1583 let empty = String::new();
1586 let code = cmd.args.get(1).unwrap_or(&empty);
1587 assert!(
1588 code.contains("(hip_version is not None) and hip_available"),
1589 "Should gate the exit on functional HIP availability (Stage 4)"
1590 );
1591 assert!(
1592 code.contains("sys.exit(0 if ok else 1)"),
1593 "Should exit non-zero when HIP is unavailable (Stage 4: fail loud)"
1594 );
1595 }
1596
1597 #[test]
1598 fn test_pytorch_basic_verification_uses_diagnostic() {
1599 let candidates = vec!["python3".to_string()];
1601 let cmds = basic_verification_commands(&candidates);
1602 let pytorch_cmd = cmds.iter().find(|c| c.target_id == "pytorch");
1603 assert!(
1604 pytorch_cmd.is_some(),
1605 "Should have a PyTorch verification command"
1606 );
1607 let cmd = pytorch_cmd.unwrap();
1608 let empty = String::new();
1609 let code = cmd.args.get(1).unwrap_or(&empty);
1610 assert!(
1611 code.contains("(hip_version is not None) and hip_available"),
1612 "Basic verification should gate exit on functional HIP (Stage 4)"
1613 );
1614 assert!(
1615 code.contains("sys.exit(0 if ok else 1)"),
1616 "Basic verification should fail loud when HIP unavailable (Stage 4)"
1617 );
1618 }
1619
1620 #[test]
1621 fn test_pytorch_installation_check_only_imports() {
1622 let candidates = vec!["python3".to_string()];
1627 let _ = is_component_installed_by_id("pytorch", &candidates);
1630 }
1631
1632 #[test]
1633 fn test_verification_command_structure() {
1634 let candidates = vec!["python3".to_string()];
1636 let cmds = component_verification_commands("pytorch", &candidates);
1637 assert_eq!(cmds.len(), 1);
1638 let cmd = &cmds[0];
1639 assert!(!cmd.label.is_empty());
1640 assert!(!cmd.target_id.is_empty());
1641 assert!(!cmd.program.is_empty());
1642 assert_eq!(cmd.args.len(), 2, "Should have -c and the code");
1643 assert_eq!(cmd.args[0], "-c");
1644 assert!(!cmd.args[1].is_empty());
1645 assert!(cmd.modules.contains(&"torch".to_string()));
1646 }
1647
1648 #[test]
1649 fn test_migraphx_snippet_excludes_cwd_and_falls_back_to_driver() {
1650 let s = migraphx_verify_snippet();
1656
1657 assert!(
1659 s.contains("getcwd()"),
1660 "snippet must drop CWD from sys.path; got: {s}"
1661 );
1662 assert!(
1663 s.contains("sys.path = [p for p in sys.path"),
1664 "snippet must filter sys.path; got: {s}"
1665 );
1666
1667 assert!(
1670 s.contains("if not ok:"),
1671 "snippet must fall back to the driver whenever python bindings are incomplete"
1672 );
1673 assert!(s.contains("migraphx-driver"));
1674 assert!(s.contains("--version"));
1675 assert!(s.contains("hasattr(migraphx, 'parse_onnx')"));
1677 assert!(s.contains("sys.exit(0 if ok else 1)"));
1679 }
1680
1681 #[test]
1682 fn test_onnx_present_snippet_strips_cwd_and_requires_real_package() {
1683 let s = onnxruntime_present_snippet();
1689 assert!(
1690 s.contains("getcwd()"),
1691 "snippet must drop CWD from sys.path; got: {s}"
1692 );
1693 assert!(
1694 s.contains("sys.path = [p for p in sys.path"),
1695 "snippet must filter sys.path; got: {s}"
1696 );
1697 assert!(
1698 s.contains("origin is not None"),
1699 "snippet must require a real package (non-None origin), not a namespace shadow; got: {s}"
1700 );
1701 }
1702}
1703
1704#[cfg(test)]
1705mod component_status_tests;