Skip to main content

rusty_stack/
component_status.rs

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    // Anaconda/Miniconda candidates
52    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::Verification {
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 requires BOTH version file AND functional rocminfo (not just binary existence)
148        // This prevents false positives from partial downloads
149        "rocm" => {
150            // Check for version file in multiple locations
151            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            // Check if rocminfo actually works (not just exists)
156            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            // Require both: version file AND functional rocminfo
167            // This ensures we don't report partial installs as complete
168            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" => {
178            python_any(python_candidates, &["flash_attn", "flash_attn_2"])
179                || path_exists(home_path(&home, &["ml_stack", "flash_attn_amd"]))
180                || path_exists(home_path(&home, &["ml_stack", "flash_attn_amd_direct"]))
181        }
182        "megatron" => {
183            path_exists(home_path(&home, &["megatron", "Megatron-LM"]))
184                || python_any(python_candidates, &["megatron"])
185        }
186        "vllm" => {
187            python_candidates
188                .iter()
189                .any(|python| python_exec(python, vllm_runtime_check_snippet()))
190                || path_exists(home_path(&home, &["vllm_build"]))
191                || path_exists(home_path(&home, &["vllm_py313"]))
192        }
193        "aiter" => python_any(python_candidates, &["aiter"]),
194        "vllm-studio" => {
195            command_exists("vllm-studio") || path_exists(home_path(&home, &["vllm-studio"]))
196        }
197        "onnx" => {
198            python_any(python_candidates, &["onnxruntime"])
199                || path_exists(home_path(&home, &["onnxruntime_build"]))
200        }
201        "bitsandbytes" => {
202            python_any(python_candidates, &["bitsandbytes"])
203                || path_exists(home_path(&home, &["ml_stack", "bitsandbytes"]))
204        }
205        // rocm-smi - check multiple locations and verify it works
206        "rocm-smi" => {
207            let rocm_smi_paths = [
208                "/opt/rocm/bin/rocm-smi",
209                "/usr/bin/rocm-smi",
210                "/usr/local/bin/rocm-smi",
211            ];
212            for path in &rocm_smi_paths {
213                if path_exists(path) {
214                    // Verify it actually works
215                    if let Ok(output) = std::process::Command::new(path).arg("--showuse").output() {
216                        if output.status.success() {
217                            return true;
218                        }
219                    }
220                }
221            }
222            // Also check if rocm-smi is in PATH and works
223            if command_exists("rocm-smi") {
224                if let Ok(output) = std::process::Command::new("rocm-smi")
225                    .arg("--showuse")
226                    .output()
227                {
228                    return output.status.success();
229                }
230            }
231            false
232        }
233        "migraphx" => {
234            python_any(python_candidates, &["migraphx"])
235                || path_exists(home_path(&home, &["migraphx_build"]))
236        }
237        "pytorch-profiler" => python_any(python_candidates, &["torch"]),
238        "wandb" => python_any(python_candidates, &["wandb"]),
239        "fastvideo" => python_any(python_candidates, &["fastvideo"]),
240        "llama-cpp" => {
241            // Use the detection contract from llama_cpp.rs: llama-cli --help must succeed
242            crate::installers::components::llama_cpp::is_llama_cli_functional(&home)
243        }
244        "permanent-env" => env_file_has_permanent(&home_path(&home, &[".mlstack_env"])),
245        "basic-env" => path_exists(home_path(&home, &[".mlstack_env"])),
246        "enhanced-env" => env_file_has_enhanced(&home_path(&home, &[".mlstack_env"])),
247        "vllm-performance" => has_benchmark_logs,
248        "deepspeed-performance" => has_benchmark_logs,
249        "megatron-performance" => has_benchmark_logs,
250        "all-benchmarks" => has_benchmark_logs,
251        "comfyui" => {
252            // Check for ComfyUI installation
253            // Default location: $HOME/ComfyUI
254            // User location: /mnt/e0f7c1a8-b834-4827-b579-0251b006bc1f/ComfyUI/
255            // Check for .git folder and main.py
256            let default_path = home_path(&home, &["ComfyUI"]);
257            path_exists(default_path.join(".git")) || path_exists(default_path.join("main.py"))
258        }
259        "textgen" => {
260            // Check for text-generation-webui installation
261            // Default location: $HOME/text-generation-webui
262            let default_path = home_path(&home, &["text-generation-webui"]);
263            path_exists(default_path.join(".git")) || path_exists(default_path.join("server.py"))
264        }
265        _ => false,
266    }
267}
268
269pub fn verification_commands(
270    component_id: &str,
271    python_candidates: &[String],
272) -> Vec<VerificationCommand> {
273    match component_id {
274        "verify-basic" => basic_verification_commands(python_candidates),
275        "verify-enhanced" => enhanced_verification_commands(python_candidates),
276        "verify-build" => build_verification_commands(python_candidates),
277        _ => Vec::new(),
278    }
279}
280
281pub fn component_verification_commands(
282    component_id: &str,
283    python_candidates: &[String],
284) -> Vec<VerificationCommand> {
285    match component_id {
286        "rocm" => vec![shell_command("ROCm info", "rocm", "rocminfo", &[])],
287        "pytorch" => vec![python_command(
288            "PyTorch",
289            "pytorch",
290            &["torch"],
291            python_candidates,
292            pytorch_diagnostic_snippet(),
293        )],
294        "triton" => vec![python_command(
295            "Triton",
296            "triton",
297            &["triton"],
298            python_candidates,
299            "import triton; import triton.language as tl; print(f'Triton {triton.__version__} — language OK')",
300        )],
301        "mpi4py" => vec![python_command(
302            "MPI4Py",
303            "mpi4py",
304            &["mpi4py"],
305            python_candidates,
306            "import importlib.metadata as m; import mpi4py; print(getattr(mpi4py, '__version__', m.version('mpi4py')))",
307        )],
308        "deepspeed" => vec![python_command(
309            "DeepSpeed",
310            "deepspeed",
311            &["deepspeed"],
312            python_candidates,
313            "import deepspeed; print(deepspeed.__version__)",
314        )],
315        "ml-stack-core" => vec![python_command(
316            "ML Stack Core",
317            "ml-stack-core",
318            &["stans_ml_stack"],
319            python_candidates,
320            "import stans_ml_stack; print(getattr(stans_ml_stack, '__version__', 'ok'))",
321        )],
322        "flash-attn" => vec![python_command(
323            "Flash Attention",
324            "flash-attn",
325            &["flash_attn", "flash_attention_amd", "flash_attn_2"],
326            python_candidates,
327            "import importlib, sys;\nmodules=[('flash_attn','flash_attn.flash_attn_func'),('flash_attention_amd',None),('flash_attn_2',None)];\nfor mod_name, func_path in modules:\n    try:\n        m = importlib.import_module(mod_name);\n        ver = getattr(m, '__version__', 'unknown');\n        if func_path:\n            parts = func_path.split('.');\n            obj = m;\n            [obj := getattr(obj, p) for p in parts[1:]];\n            print(f'{mod_name} {ver} — {func_path} OK');\n        else:\n            print(f'{mod_name} {ver} — imported OK');\n        sys.exit(0);\n    except SystemExit:\n        raise\n    except Exception:\n        continue;\nprint('No flash attention module found'); sys.exit(1)",
328        )],
329        "megatron" => vec![python_command(
330            "Megatron-LM",
331            "megatron",
332            &["megatron"],
333            python_candidates,
334            "import megatron; print('megatron ok')",
335        )],
336        "vllm" => vec![python_command(
337            "vLLM",
338            "vllm",
339            &["vllm"],
340            python_candidates,
341            vllm_runtime_check_snippet(),
342        )],
343        "aiter" => vec![python_command(
344            "AITER",
345            "aiter",
346            &["aiter"],
347            python_candidates,
348            "import aiter; mods=[x for x in dir(aiter) if not x.startswith('_')]; print(f'AITER imported — {len(mods)} exports available')",
349        )],
350        "vllm-studio" => vec![shell_command(
351            "vLLM Studio",
352            "vllm-studio",
353            "bun",
354            &["--version"],
355        )],
356        "onnx" => vec![python_command(
357            "ONNX Runtime",
358            "onnx",
359            &["onnxruntime"],
360            python_candidates,
361            "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)",
362        )],
363        "bitsandbytes" => vec![python_command(
364            "BITSANDBYTES",
365            "bitsandbytes",
366            &["bitsandbytes"],
367            python_candidates,
368            "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)",
369        )],
370        "rocm-smi" => vec![shell_command(
371            "ROCm SMI",
372            "rocm-smi",
373            "rocm-smi",
374            &["--showproductname"],
375        )],
376        "migraphx" => vec![python_command(
377            "MIGraphX",
378            "migraphx",
379            &["migraphx"],
380            python_candidates,
381            "import migraphx; print(getattr(migraphx, '__version__', 'ok'))",
382        )],
383        "pytorch-profiler" => vec![python_command(
384            "PyTorch Profiler",
385            "pytorch-profiler",
386            &["torch"],
387            python_candidates,
388            "from torch.profiler import profile; print(profile)",
389        )],
390        "wandb" => vec![python_command(
391            "Weights & Biases",
392            "wandb",
393            &["wandb"],
394            python_candidates,
395            "import wandb; print(wandb.__version__)",
396        )],
397        "fastvideo" => vec![python_command(
398            "FastVideo",
399            "fastvideo",
400            &["fastvideo"],
401            python_candidates,
402            "import fastvideo; print(fastvideo.__version__)",
403        )],
404        "llama-cpp" => vec![shell_command(
405            "llama.cpp",
406            "llama-cpp",
407            "llama-cli",
408            &["--help"],
409        )],
410        "basic-env" => vec![shell_command(
411            "Environment file",
412            "basic-env",
413            "bash",
414            &[
415                "-c",
416                "test -f \"$HOME/.mlstack_env\" && echo OK || exit 1",
417            ],
418        )],
419        "enhanced-env" => vec![shell_command(
420            "Enhanced env",
421            "enhanced-env",
422            "bash",
423            &[
424                "-c",
425                "grep -q \"Enhanced ML Stack Environment Setup Script\" \"$HOME/.mlstack_env\" && echo OK || exit 1",
426            ],
427        )],
428        "permanent-env" => vec![shell_command(
429            "Permanent env",
430            "permanent-env",
431            "bash",
432            &[
433                "-c",
434                "grep -qiE \"Permanent ROCm Environment|Permanent ROCm Env|MLSTACK_PYTHON_BIN\" \"$HOME/.mlstack_env\" && echo OK || exit 1",
435            ],
436        )],
437        // Benchmark components - verify by checking log files exist
438        "mlperf-inference" => vec![benchmark_log_check_command(
439            "MLPerf benchmark logs",
440            "mlperf-inference",
441            "mlperf_inference",
442            "Benchmark logs found",
443            "No benchmark logs yet",
444        )],
445        "rocm-benchmarks" => vec![benchmark_log_check_command(
446            "ROCm benchmark logs",
447            "rocm-benchmarks",
448            "rocm_benchmarks",
449            "Benchmark logs found",
450            "No benchmark logs yet",
451        )],
452        "gpu-memory-bandwidth" => vec![benchmark_log_check_command(
453            "Memory bandwidth logs",
454            "gpu-memory-bandwidth",
455            "gpu_memory_bandwidth",
456            "Benchmark logs found",
457            "No benchmark logs yet",
458        )],
459        "rocm-smi-bench" => vec![benchmark_log_check_command(
460            "ROCm SMI logs",
461            "rocm-smi-bench",
462            "rocm_smi_benchmarks",
463            "Benchmark logs found",
464            "No benchmark logs yet",
465        )],
466        "pytorch-performance" => vec![benchmark_log_check_command(
467            "PyTorch benchmark logs",
468            "pytorch-performance",
469            "pytorch_performance",
470            "Benchmark logs found",
471            "No benchmark logs yet",
472        )],
473        "vllm-performance" => vec![benchmark_log_check_command(
474            "vLLM benchmark logs",
475            "vllm-performance",
476            "vllm_benchmarks",
477            "Benchmark logs found",
478            "No benchmark logs yet",
479        )],
480        "deepspeed-performance" => vec![benchmark_log_check_command(
481            "DeepSpeed benchmark logs",
482            "deepspeed-performance",
483            "deepspeed_benchmarks",
484            "Benchmark logs found",
485            "No benchmark logs yet",
486        )],
487        "megatron-performance" => vec![benchmark_log_check_command(
488            "Megatron benchmark logs",
489            "megatron-performance",
490            "megatron_benchmarks",
491            "Benchmark logs found",
492            "No benchmark logs yet",
493        )],
494        "all-benchmarks" => vec![benchmark_log_check_command(
495            "Full suite logs",
496            "all-benchmarks",
497            "full_benchmarks",
498            "Suite logs found",
499            "No suite logs yet",
500        )],
501        "comfyui" => vec![shell_command(
502            "ComfyUI",
503            "comfyui",
504            "bash",
505            &["-c", "test -f \"$HOME/ComfyUI/main.py\" && echo 'ComfyUI installed' || exit 1"],
506        )],
507        "textgen" => vec![shell_command(
508            "text-generation-webui",
509            "textgen",
510            "bash",
511            &["-c", "test -f \"$HOME/text-generation-webui/server.py\" && echo 'text-generation-webui installed' || exit 1"],
512        )],
513        _ => Vec::new(),
514    }
515}
516
517pub fn modules_available(modules: &[String], python_candidates: &[String]) -> bool {
518    if modules.is_empty() {
519        return true;
520    }
521    let module_refs: Vec<&str> = modules.iter().map(|s| s.as_str()).collect();
522    python_any(python_candidates, &module_refs)
523}
524
525fn basic_verification_commands(python_candidates: &[String]) -> Vec<VerificationCommand> {
526    vec![
527        shell_command("ROCm info", "rocm", "rocminfo", &[]),
528        python_command(
529            "PyTorch",
530            "pytorch",
531            &["torch"],
532            python_candidates,
533            pytorch_diagnostic_snippet(),
534        ),
535        python_command(
536            "Triton",
537            "triton",
538            &["triton"],
539            python_candidates,
540            "import triton; import triton._C; print(f'Triton {triton.__version__} ok')",
541        ),
542        python_command(
543            "MPI4Py",
544            "mpi4py",
545            &["mpi4py"],
546            python_candidates,
547            "import importlib.metadata as m; import mpi4py; print(getattr(mpi4py, '__version__', m.version('mpi4py')))",
548        ),
549        python_command(
550            "DeepSpeed",
551            "deepspeed",
552            &["deepspeed", "einops"],
553            python_candidates,
554            "import deepspeed; import einops; print(deepspeed.__version__)",
555        ),
556    ]
557}
558
559fn enhanced_verification_commands(python_candidates: &[String]) -> Vec<VerificationCommand> {
560    let mut steps = basic_verification_commands(python_candidates);
561    steps.extend(vec![
562        python_command(
563            "Flash Attention",
564            "flash-attn",
565            &["flash_attn", "flash_attention_amd", "flash_attn_2"],
566            python_candidates,
567            "import importlib, sys;\nmodules=[('flash_attn','flash_attn.flash_attn_func'),('flash_attention_amd',None),('flash_attn_2',None)];\nfor mod_name, func_path in modules:\n    try:\n        m = importlib.import_module(mod_name);\n        ver = getattr(m, '__version__', 'unknown');\n        if func_path:\n            parts = func_path.split('.');\n            obj = m;\n            [obj := getattr(obj, p) for p in parts[1:]];\n            print(f'{mod_name} {ver} — {func_path} OK');\n        else:\n            print(f'{mod_name} {ver} — imported OK');\n        sys.exit(0);\n    except SystemExit:\n        raise\n    except Exception:\n        continue;\nprint('No flash attention module found'); sys.exit(1)",
568        ),
569        python_command(
570            "vLLM",
571            "vllm",
572            &["vllm"],
573            python_candidates,
574            vllm_runtime_check_snippet(),
575        ),
576        python_command(
577            "AITER",
578            "aiter",
579            &["aiter"],
580            python_candidates,
581            "import aiter; print(getattr(aiter, '__version__', 'ok'))",
582        ),
583        python_command(
584            "ONNX Runtime",
585            "onnx",
586            &["onnxruntime"],
587            python_candidates,
588            "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)",
589        ),
590        python_command(
591            "BITSANDBYTES",
592            "bitsandbytes",
593            &["bitsandbytes"],
594            python_candidates,
595            "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)",
596        ),
597        shell_command("ROCm SMI", "rocm-smi", "rocm-smi", &["--showproductname"]),
598        python_command(
599            "MIGraphX",
600            "migraphx",
601            &["migraphx"],
602            python_candidates,
603            "import migraphx; print(getattr(migraphx, '__version__', 'ok'))",
604        ),
605        python_command(
606            "PyTorch Profiler",
607            "pytorch-profiler",
608            &["torch"],
609            python_candidates,
610            "from torch.profiler import profile; print(profile)",
611        ),
612        python_command(
613            "Weights & Biases",
614            "wandb",
615            &["wandb"],
616            python_candidates,
617            "import wandb; print(wandb.__version__)",
618        ),
619        python_command(
620            "FastVideo",
621            "fastvideo",
622            &["fastvideo"],
623            python_candidates,
624            "import fastvideo; print(fastvideo.__version__)",
625        ),
626        shell_command("vLLM Studio", "vllm-studio", "bun", &["--version"]),
627    ]);
628    steps
629}
630
631fn build_verification_commands(python_candidates: &[String]) -> Vec<VerificationCommand> {
632    let mut steps = basic_verification_commands(python_candidates);
633    steps.extend(vec![
634        python_command(
635            "ONNX Runtime",
636            "onnx",
637            &["onnxruntime"],
638            python_candidates,
639            "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)",
640        ),
641        python_command(
642            "Flash Attention (build)",
643            "flash-attn",
644            &["flash_attn", "flash_attention_amd", "flash_attn_2"],
645            python_candidates,
646            "import importlib, sys;\nmodules=[('flash_attn','flash_attn.flash_attn_func'),('flash_attention_amd',None),('flash_attn_2',None)];\nfor mod_name, func_path in modules:\n    try:\n        m = importlib.import_module(mod_name);\n        ver = getattr(m, '__version__', 'unknown');\n        if func_path:\n            parts = func_path.split('.');\n            obj = m;\n            [obj := getattr(obj, p) for p in parts[1:]];\n            print(f'{mod_name} {ver} — {func_path} OK');\n        else:\n            print(f'{mod_name} {ver} — imported OK');\n        sys.exit(0);\n    except SystemExit:\n        raise\n    except Exception:\n        continue;\nprint('No flash attention module found'); sys.exit(1)",
647        ),
648        python_command(
649            "MIGraphX",
650            "migraphx",
651            &["migraphx"],
652            python_candidates,
653            "import migraphx; print(getattr(migraphx, '__version__', 'ok'))",
654        ),
655    ]);
656    steps
657}
658
659fn python_command(
660    label: &str,
661    target_id: &str,
662    modules: &[&str],
663    python_candidates: &[String],
664    code: &str,
665) -> VerificationCommand {
666    let program = select_python_for_modules(modules, python_candidates)
667        .or_else(|| python_candidates.first().cloned())
668        .unwrap_or_else(|| "python3".to_string());
669
670    VerificationCommand {
671        label: label.to_string(),
672        target_id: target_id.to_string(),
673        program,
674        args: vec!["-c".to_string(), code.to_string()],
675        modules: modules.iter().map(|m| m.to_string()).collect(),
676    }
677}
678
679/// PyTorch diagnostic verification snippet.
680///
681/// This performs a multi-tier check:
682/// 1. Can we import torch? (basic install check)
683/// 2. Is torch.version.hip set? (ROCm build check)
684/// 3. Is torch.cuda.is_available()? (runtime HIP check)
685/// 4. Diagnostics for why HIP might not be available
686///
687/// Unlike the old strict check that failed when `torch.version.hip` was None,
688/// this approach succeeds if torch is importable and provides diagnostic
689/// information about HIP availability. This prevents cascading failures
690/// to downstream components (DeepSpeed, Flash Attention, etc.) when PyTorch
691/// is correctly installed but HIP libraries aren't accessible from the
692/// current Python environment (e.g., system Python vs venv, LD_LIBRARY_PATH
693/// issues, or running without GPU access).
694fn pytorch_diagnostic_snippet() -> &'static str {
695    r#"import torch, sys, os, ctypes.util
696
697# Tier 1: Basic torch import (already verified by module check)
698print(f'torch version: {torch.__version__}')
699
700# Tier 2: Check if this is a ROCm build
701hip_version = getattr(torch.version, 'hip', None)
702cuda_version = getattr(torch.version, 'cuda', None)
703print(f'hip: {hip_version}')
704print(f'cuda: {cuda_version}')
705
706# Tier 3: Runtime HIP/CUDA availability
707try:
708    hip_available = torch.cuda.is_available()
709    print(f'torch.cuda.is_available(): {hip_available}')
710    if hip_available:
711        device_count = torch.cuda.device_count()
712        print(f'device_count: {device_count}')
713        if device_count > 0:
714            print(f'device_name: {torch.cuda.get_device_name(0)}')
715except Exception as e:
716    hip_available = False
717    print(f'torch.cuda.is_available() error: {e}')
718
719# Tier 4: Diagnostics if HIP not available
720if not hip_available:
721    print('--- ROCm HIP Diagnostics ---')
722
723    # Check LD_LIBRARY_PATH
724    ld_path = os.environ.get('LD_LIBRARY_PATH', '')
725    print(f'LD_LIBRARY_PATH: {ld_path if ld_path else "(not set)"}')
726
727    # Check ROCm_PATH
728    rocm_path = os.environ.get('ROCM_PATH', '')
729    print(f'ROCM_PATH: {rocm_path if rocm_path else "(not set)"}')
730
731    # Check if ROCm libraries exist
732    rocm_lib_dirs = ['/opt/rocm/lib', '/opt/rocm/hip/lib']
733    for d in rocm_lib_dirs:
734        exists = os.path.isdir(d)
735        print(f'{d}: {"exists" if exists else "not found"}')
736
737    # Check if libamdhip64.so can be found
738    hip_lib = ctypes.util.find_library('amdhip64')
739    print(f'libamdhip64: {hip_lib if hip_lib else "not found"}')
740
741    # Check Python executable path
742    print(f'python: {sys.executable}')
743
744    # Provide actionable guidance
745    if hip_version is None and cuda_version is None:
746        print('HINT: torch.version.hip and torch.version.cuda are both None.')
747        print('  This torch build may not have GPU support.')
748        print('  Reinstall with ROCm index: pip install torch --index-url https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/')
749    elif hip_version is not None and not hip_available:
750        print('HINT: torch.version.hip is set but HIP runtime not available.')
751        print('  Possible causes:')
752        print('  1. LD_LIBRARY_PATH does not include /opt/rocm/lib')
753        print('  2. Running in a container/environment without GPU access')
754        print('  3. ROCm drivers not loaded (check with: rocminfo)')
755        print('  Fix: source ~/.mlstack_env or set LD_LIBRARY_PATH=/opt/rocm/lib')
756
757# Exit successfully if torch is importable (even without HIP)
758# This prevents cascading failures to downstream components.
759# The diagnostic output above tells the user if HIP is available.
760sys.exit(0)"#
761}
762
763fn vllm_runtime_check_snippet() -> &'static str {
764    r#"import importlib
765import vllm
766import cachetools
767import cbor2
768import gguf
769import pybase64
770import ijson
771import mistral_common
772import openai_harmony
773
774loaded = []
775errs = []
776for name in ("vllm._C", "vllm._rocm_C"):
777    try:
778        importlib.import_module(name)
779        loaded.append(name)
780    except Exception as exc:
781        errs.append(f"{name}: {exc}")
782
783if not loaded:
784    raise SystemExit("vLLM native extension load failure: " + " | ".join(errs))
785
786print(vllm.__version__)"#
787}
788
789fn shell_command(
790    label: &str,
791    target_id: &str,
792    program: &str,
793    args: &[&str],
794) -> VerificationCommand {
795    VerificationCommand {
796        label: label.to_string(),
797        target_id: target_id.to_string(),
798        program: program.to_string(),
799        args: args.iter().map(|s| s.to_string()).collect(),
800        modules: Vec::new(),
801    }
802}
803
804fn has_benchmark_log_dirs() -> bool {
805    benchmark_logs::benchmark_log_directories()
806        .iter()
807        .any(|dir| {
808            fs::read_dir(dir)
809                .ok()
810                .map(|entries| {
811                    entries
812                        .filter_map(|entry| entry.ok())
813                        .any(|entry| entry.path().is_file())
814                })
815                .unwrap_or(false)
816        })
817}
818
819fn benchmark_log_check_command(
820    label: &str,
821    target_id: &str,
822    pattern: &str,
823    success_message: &str,
824    failure_message: &str,
825) -> VerificationCommand {
826    let script = benchmark_log_check_script(pattern, success_message, failure_message);
827    shell_command(label, target_id, "bash", &["-c", &script])
828}
829
830fn benchmark_log_check_script(
831    pattern: &str,
832    success_message: &str,
833    failure_message: &str,
834) -> String {
835    format!(
836        "dirs=(\"${{MLSTACK_LOG_DIR:-}}\" \"$HOME/.rusty-stack/logs\" \"${{TMPDIR:-/tmp}}/rusty-stack/logs\")\n\
837         found=0\n\
838         for dir in \"${{dirs[@]}}\"; do\n\
839             if [ -n \"$dir\" ] && [ -d \"$dir\" ]; then\n\
840                 if ls \"$dir\" | grep -q \"{}\"; then\n\
841                     echo \"{}\"\n\
842                     found=1\n\
843                     break\n\
844                 fi\n\
845             fi\n\
846         done\n\
847         if [ \"$found\" -ne 1 ]; then\n\
848             echo \"{}\"\n\
849         fi",
850        pattern, success_message, failure_message
851    )
852}
853
854fn python_any(python_candidates: &[String], modules: &[&str]) -> bool {
855    python_candidates.iter().any(|python| {
856        modules
857            .iter()
858            .any(|module| python_has_module(python, module))
859    })
860}
861
862fn python_has_module(python: &str, module: &str) -> bool {
863    python_exec(
864        python,
865        &format!(
866            "import importlib.util, sys; sys.exit(0 if importlib.util.find_spec('{module}') else 1)"
867        ),
868    )
869}
870
871fn python_exec(python: &str, code: &str) -> bool {
872    let mut cmd = Command::new(python);
873    cmd.arg("-c")
874        .arg(code)
875        .stdout(Stdio::null())
876        .stderr(Stdio::null());
877
878    if let Ok(existing) = env::var("PYTHONPATH") {
879        let mut path = existing;
880        let extra_paths = python_search_paths();
881        if !extra_paths.is_empty() {
882            if !path.is_empty() {
883                path.push(':');
884            }
885            path.push_str(&extra_paths.join(":"));
886        }
887        cmd.env("PYTHONPATH", path);
888    } else {
889        let extra_paths = python_search_paths();
890        if !extra_paths.is_empty() {
891            cmd.env("PYTHONPATH", extra_paths.join(":"));
892        }
893    }
894
895    let home = resolve_component_user_home();
896    let ld_path = component_status_ld_library_path(&home);
897    if !ld_path.is_empty() {
898        cmd.env("LD_LIBRARY_PATH", ld_path);
899    }
900
901    cmd.status().map(|status| status.success()).unwrap_or(false)
902}
903
904fn push_unique_path(paths: &mut Vec<String>, value: &str) {
905    let value = value.trim();
906    if value.is_empty() {
907        return;
908    }
909    if paths.iter().any(|existing| existing == value) {
910        return;
911    }
912    paths.push(value.to_string());
913}
914
915fn component_user_home_from_passwd(user_name: &str) -> Option<String> {
916    let target = user_name.trim();
917    if target.is_empty() {
918        return None;
919    }
920    let passwd = fs::read_to_string("/etc/passwd").ok()?;
921    passwd.lines().find_map(|line| {
922        if line.trim().is_empty() || line.starts_with('#') {
923            return None;
924        }
925        let fields: Vec<&str> = line.split(':').collect();
926        if fields.len() < 6 || fields[0] != target {
927            return None;
928        }
929        let home = fields[5].trim();
930        if home.is_empty() {
931            None
932        } else {
933            Some(home.to_string())
934        }
935    })
936}
937
938fn component_passwd_homes_with_mlstack() -> Vec<String> {
939    let mut homes = Vec::new();
940    let Ok(passwd) = fs::read_to_string("/etc/passwd") else {
941        return homes;
942    };
943
944    for line in passwd.lines() {
945        if line.trim().is_empty() || line.starts_with('#') {
946            continue;
947        }
948        let fields: Vec<&str> = line.split(':').collect();
949        if fields.len() < 6 {
950            continue;
951        }
952        let home = fields[5].trim();
953        if home.is_empty() {
954            continue;
955        }
956        if Path::new(home).join(".mlstack").is_dir()
957            || Path::new(home).join(".mlstack_env").is_file()
958        {
959            push_unique_path(&mut homes, home);
960        }
961    }
962
963    homes
964}
965
966fn component_candidate_homes(preferred_home: &str) -> Vec<String> {
967    let mut homes = Vec::new();
968    push_unique_path(&mut homes, preferred_home);
969
970    if let Ok(value) = env::var("MLSTACK_USER_HOME") {
971        push_unique_path(&mut homes, &value);
972    }
973    if let Ok(value) = env::var("HOME") {
974        push_unique_path(&mut homes, &value);
975    }
976
977    for key in ["SUDO_USER", "USER", "LOGNAME"] {
978        if let Ok(user_name) = env::var(key) {
979            if let Some(home) = component_user_home_from_passwd(&user_name) {
980                push_unique_path(&mut homes, &home);
981            }
982        }
983    }
984
985    for home in component_passwd_homes_with_mlstack() {
986        push_unique_path(&mut homes, &home);
987    }
988
989    homes
990}
991
992fn resolve_component_user_home() -> String {
993    let fallback = env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
994    let candidates = component_candidate_homes(&fallback);
995
996    for home in &candidates {
997        if Path::new(home).join(".mlstack_env").is_file() {
998            return home.clone();
999        }
1000    }
1001    for home in &candidates {
1002        if Path::new(home).join(".mlstack").is_dir() {
1003            return home.clone();
1004        }
1005    }
1006
1007    candidates.into_iter().next().unwrap_or(fallback)
1008}
1009
1010fn component_status_mpi_compat_dirs(home: &str) -> Vec<String> {
1011    let mut dirs = Vec::new();
1012    for home in component_candidate_homes(home) {
1013        let mlstack_dir = Path::new(&home).join(".mlstack");
1014        let primary = mlstack_dir.join("libmpi-compat");
1015        if primary.is_dir() {
1016            dirs.push(primary.to_string_lossy().to_string());
1017        }
1018
1019        if let Ok(entries) = fs::read_dir(&mlstack_dir) {
1020            for entry in entries.flatten() {
1021                let path = entry.path();
1022                if !path.is_dir() {
1023                    continue;
1024                }
1025                let Some(name) = path.file_name().and_then(|value| value.to_str()) else {
1026                    continue;
1027                };
1028                if name.starts_with("libmpi-compat-user-") {
1029                    dirs.push(path.to_string_lossy().to_string());
1030                }
1031            }
1032        }
1033    }
1034
1035    dirs.sort();
1036    dirs.dedup();
1037    dirs
1038}
1039
1040fn component_status_ld_library_path(home: &str) -> String {
1041    let mut paths = Vec::new();
1042
1043    for dir in component_status_mpi_compat_dirs(home) {
1044        push_unique_path(&mut paths, &dir);
1045    }
1046
1047    for rocm_path in ["/opt/rocm/lib", "/opt/rocm/hip/lib", "/opt/rocm/opencl/lib"] {
1048        if Path::new(rocm_path).exists() {
1049            push_unique_path(&mut paths, rocm_path);
1050        }
1051    }
1052
1053    if let Ok(existing) = env::var("LD_LIBRARY_PATH") {
1054        for part in existing.split(':') {
1055            push_unique_path(&mut paths, part);
1056        }
1057    }
1058
1059    paths.join(":")
1060}
1061
1062fn command_exists(command: &str) -> bool {
1063    Command::new("bash")
1064        .arg("-c")
1065        .arg(format!("command -v {}", command))
1066        .stdout(Stdio::null())
1067        .stderr(Stdio::null())
1068        .status()
1069        .map(|status| status.success())
1070        .unwrap_or(false)
1071}
1072
1073pub fn python_search_paths() -> Vec<String> {
1074    let home = resolve_component_user_home();
1075    let mut paths = vec![
1076        format!("{}/pytorch", home),
1077        format!("{}/ml_stack/flash_attn_amd_direct", home),
1078        format!("{}/ml_stack/flash_attn_amd", home),
1079        format!(
1080            "{}/ml_stack/flash_attn_amd/build/lib.linux-x86_64-cpython-313",
1081            home
1082        ),
1083        format!(
1084            "{}/ml_stack/flash_attn_amd/build/lib.linux-x86_64-cpython-312",
1085            home
1086        ),
1087        format!("{}/.local/lib/python3.13/site-packages", home),
1088        format!("{}/.local/lib/python3.12/site-packages", home),
1089        format!("{}/rocm_venv/lib/python3.13/site-packages", home),
1090        format!("{}/rocm_venv/lib/python3.12/site-packages", home),
1091        format!("{}/megatron/Megatron-LM", home),
1092        format!("{}/ml_stack/bitsandbytes/bitsandbytes", home),
1093    ];
1094
1095    // Dynamically add /opt/rocm/lib/python*/site-packages and /opt/rocm/lib
1096    let rocm_lib = Path::new("/opt/rocm/lib");
1097    if rocm_lib.exists() {
1098        paths.push(rocm_lib.to_string_lossy().to_string());
1099        if let Ok(entries) = fs::read_dir(rocm_lib) {
1100            for entry in entries.flatten() {
1101                let path = entry.path();
1102                if path.is_dir() {
1103                    if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
1104                        if name.starts_with("python") {
1105                            let sp = path.join("site-packages");
1106                            if sp.exists() {
1107                                paths.push(sp.to_string_lossy().to_string());
1108                            }
1109                        }
1110                    }
1111                }
1112            }
1113        }
1114    }
1115
1116    paths
1117}
1118
1119fn select_python_for_modules(modules: &[&str], python_candidates: &[String]) -> Option<String> {
1120    for key in ["MLSTACK_PYTHON_BIN", "UV_PYTHON"] {
1121        if let Ok(value) = env::var(key) {
1122            let value = value.trim();
1123            if !value.is_empty() {
1124                if modules.is_empty() {
1125                    return Some(value.to_string());
1126                }
1127                for module in modules {
1128                    if python_has_module(value, module) {
1129                        return Some(value.to_string());
1130                    }
1131                }
1132            }
1133        }
1134    }
1135
1136    for python in python_candidates {
1137        for module in modules {
1138            if python_has_module(python, module) {
1139                return Some(python.clone());
1140            }
1141        }
1142    }
1143    None
1144}
1145
1146fn path_exists<P: AsRef<Path>>(path: P) -> bool {
1147    path.as_ref().exists()
1148}
1149
1150fn home_path(home: &str, parts: &[&str]) -> PathBuf {
1151    let mut path = PathBuf::from(home);
1152    for part in parts {
1153        path.push(part);
1154    }
1155    path
1156}
1157
1158fn env_file_has_enhanced(path: &Path) -> bool {
1159    if let Ok(contents) = fs::read_to_string(path) {
1160        return contents.contains("Enhanced ML Stack Environment Setup Script");
1161    }
1162    false
1163}
1164
1165fn env_file_has_permanent(path: &Path) -> bool {
1166    if let Ok(contents) = fs::read_to_string(path) {
1167        return contents.contains("Permanent ROCm Environment")
1168            || contents.contains("Permanent ROCm Env")
1169            || (contents.contains("ML Stack Environment File")
1170                && contents.contains("MLSTACK_PYTHON_BIN"));
1171    }
1172    false
1173}
1174
1175fn repo_has_ml_stack_core() -> bool {
1176    if let Ok(root) = env::var("MLSTACK_REPO_ROOT") {
1177        return path_exists(Path::new(&root).join("stans_ml_stack"));
1178    }
1179    false
1180}
1181
1182// ===========================================================================
1183// Tests
1184// ===========================================================================
1185
1186#[cfg(test)]
1187mod tests {
1188    use super::*;
1189
1190    #[test]
1191    fn test_pytorch_diagnostic_snippet_exits_zero() {
1192        // The diagnostic snippet should always exit 0 (not fail) even when
1193        // HIP is not available. This prevents cascading failures to downstream
1194        // components like DeepSpeed, Flash Attention, etc.
1195        let snippet = pytorch_diagnostic_snippet();
1196        assert!(
1197            snippet.contains("sys.exit(0)"),
1198            "PyTorch diagnostic snippet should exit 0 (not block downstream components)"
1199        );
1200        assert!(
1201            !snippet.contains("sys.exit(1)"),
1202            "PyTorch diagnostic snippet should NOT exit 1 (was the old strict check)"
1203        );
1204    }
1205
1206    #[test]
1207    fn test_pytorch_diagnostic_snippet_checks_hip() {
1208        // The snippet should check for HIP availability to provide diagnostics
1209        let snippet = pytorch_diagnostic_snippet();
1210        assert!(
1211            snippet.contains("torch.version"),
1212            "Should check torch.version for HIP info"
1213        );
1214        assert!(
1215            snippet.contains("torch.cuda.is_available()"),
1216            "Should check torch.cuda.is_available() for runtime HIP"
1217        );
1218    }
1219
1220    #[test]
1221    fn test_pytorch_diagnostic_snippet_provides_hints() {
1222        // The snippet should provide actionable hints when HIP is not available
1223        let snippet = pytorch_diagnostic_snippet();
1224        assert!(
1225            snippet.contains("HINT:"),
1226            "Should provide HINT messages for troubleshooting"
1227        );
1228        assert!(
1229            snippet.contains("LD_LIBRARY_PATH"),
1230            "Should mention LD_LIBRARY_PATH as a possible cause"
1231        );
1232        assert!(
1233            snippet.contains("/opt/rocm/lib"),
1234            "Should mention ROCm lib path"
1235        );
1236    }
1237
1238    #[test]
1239    fn test_pytorch_diagnostic_snippet_checks_rocm_libs() {
1240        // The snippet should check for ROCm library existence
1241        let snippet = pytorch_diagnostic_snippet();
1242        assert!(
1243            snippet.contains("libamdhip64"),
1244            "Should check for libamdhip64 (core HIP library)"
1245        );
1246        assert!(
1247            snippet.contains("/opt/rocm/lib"),
1248            "Should check /opt/rocm/lib directory"
1249        );
1250    }
1251
1252    #[test]
1253    fn test_pytorch_component_verification_uses_diagnostic() {
1254        // Verify that the pytorch component verification command uses
1255        // the diagnostic snippet (not the old strict check)
1256        let candidates = vec!["python3".to_string()];
1257        let cmds = component_verification_commands("pytorch", &candidates);
1258        assert_eq!(
1259            cmds.len(),
1260            1,
1261            "Should have exactly one verification command"
1262        );
1263        let cmd = &cmds[0];
1264        assert_eq!(cmd.label, "PyTorch");
1265        assert_eq!(cmd.target_id, "pytorch");
1266        // The args should contain the diagnostic snippet, not the old strict check
1267        let empty = String::new();
1268        let code = cmd.args.get(1).unwrap_or(&empty);
1269        assert!(
1270            !code.contains("sys.exit(0 if getattr(torch.version, 'hip', None) else 1)"),
1271            "Should NOT use old strict HIP check that exits 1"
1272        );
1273        assert!(
1274            code.contains("sys.exit(0)"),
1275            "Should use new diagnostic snippet that always exits 0"
1276        );
1277    }
1278
1279    #[test]
1280    fn test_pytorch_basic_verification_uses_diagnostic() {
1281        // Verify that basic verification also uses the diagnostic snippet
1282        let candidates = vec!["python3".to_string()];
1283        let cmds = basic_verification_commands(&candidates);
1284        let pytorch_cmd = cmds.iter().find(|c| c.target_id == "pytorch");
1285        assert!(
1286            pytorch_cmd.is_some(),
1287            "Should have a PyTorch verification command"
1288        );
1289        let cmd = pytorch_cmd.unwrap();
1290        let empty = String::new();
1291        let code = cmd.args.get(1).unwrap_or(&empty);
1292        assert!(
1293            !code.contains("sys.exit(0 if getattr(torch.version, 'hip', None) else 1)"),
1294            "Basic verification should NOT use old strict HIP check"
1295        );
1296        assert!(
1297            code.contains("sys.exit(0)"),
1298            "Basic verification should use new diagnostic snippet"
1299        );
1300    }
1301
1302    #[test]
1303    fn test_pytorch_installation_check_only_imports() {
1304        // The is_component_installed_by_id check for pytorch should only
1305        // check if the torch module is importable (not check HIP).
1306        // This is the correct basic detection — HIP availability is a
1307        // verification concern, not an installation concern.
1308        let candidates = vec!["python3".to_string()];
1309        // We can't assert the result since it depends on the system state,
1310        // but we can verify the function doesn't panic
1311        let _ = is_component_installed_by_id("pytorch", &candidates);
1312    }
1313
1314    #[test]
1315    fn test_verification_command_structure() {
1316        // Verify the VerificationCommand struct for pytorch is well-formed
1317        let candidates = vec!["python3".to_string()];
1318        let cmds = component_verification_commands("pytorch", &candidates);
1319        assert_eq!(cmds.len(), 1);
1320        let cmd = &cmds[0];
1321        assert!(!cmd.label.is_empty());
1322        assert!(!cmd.target_id.is_empty());
1323        assert!(!cmd.program.is_empty());
1324        assert_eq!(cmd.args.len(), 2, "Should have -c and the code");
1325        assert_eq!(cmd.args[0], "-c");
1326        assert!(!cmd.args[1].is_empty());
1327        assert!(cmd.modules.contains(&"torch".to_string()));
1328    }
1329}
1330
1331#[cfg(test)]
1332mod component_status_tests;