Skip to main content

rusty_stack/installers/components/
mod.rs

1//! Native Rust installer modules for major components.
2//!
3//! Each module ports the corresponding shell installer script, constructing
4//! the correct system/pip commands matching the original script behavior.
5//! All modules use `installers::common::` for shared operations (package
6//! manager, distro detection, ROCm env, etc.).
7//!
8//! # Dispatch Architecture (VAL-INSTALL-031, VAL-INSTALL-032, VAL-INSTALL-038)
9//!
10//! `is_native_component(id)` returns `true` for all 35 native components.
11//! `get_dependencies(id)` returns the declared dependency IDs for a component.
12//! `topological_sort(ids)` returns components in dependency order.
13//!
14//! # Validation Assertions
15//!
16//! - **VAL-INSTALL-001**: ROCm installer correct package commands
17//! - **VAL-INSTALL-002**: ROCm channel selection correct version pins
18//! - **VAL-INSTALL-003**: PyTorch installer correct wheel selection
19//! - **VAL-INSTALL-004**: Triton installer correct pip command
20//! - **VAL-INSTALL-005**: MPI4Py installer detects system MPI
21//! - **VAL-INSTALL-006**: DeepSpeed installer correct pip command
22//! - **VAL-INSTALL-007**: ML Stack Core installer correct pip command
23//! - **VAL-INSTALL-008**: Flash Attention correct cmake command
24//! - **VAL-INSTALL-009**: Flash Attention correct make command
25//! - **VAL-INSTALL-010**: ONNX Runtime correct cmake command
26//! - **VAL-INSTALL-011**: ONNX Runtime pip install from build output
27//! - **VAL-INSTALL-012**: Megatron correct git clone
28//! - **VAL-INSTALL-013**: Megatron correct pip install post-clone
29//! - **VAL-INSTALL-015**: vLLM correct git clone and pip install
30//! - **VAL-INSTALL-016**: AITER correct git clone and pip install
31//! - **VAL-INSTALL-017**: bitsandbytes correct pip command
32//! - **VAL-INSTALL-018**: ROCm SMI correct package command
33//! - **VAL-INSTALL-019**: MIGraphX correct pip command
34//! - **VAL-INSTALL-020**: PyTorch Profiler correct pip command
35//! - **VAL-INSTALL-021**: W&B correct pip command
36//! - **VAL-INSTALL-022**: vLLM Studio correct git clone
37//! - **VAL-INSTALL-023**: ComfyUI correct git clone and pip install
38//! - **VAL-INSTALL-024**: text-generation-webui correct git clone
39//! - **VAL-INSTALL-025**: App installers correct target directory
40//! - **VAL-INSTALL-026**: Repair invokes correct sub-installers in order
41//! - **VAL-INSTALL-027**: Repair propagates individual failures
42//! - **VAL-INSTALL-028**: Permanent env creates correct venv and symlinks
43//! - **VAL-INSTALL-029**: AMD GPU drivers correct package command
44//! - **VAL-INSTALL-030**: MIGraphX Python correct pip command
45//! - **VAL-INSTALL-031**: installer.rs no bash subprocess for ported components
46//! - **VAL-INSTALL-032**: installer.rs dispatches to correct Rust module per ID
47//! - **VAL-INSTALL-038**: state.rs supports native module routing
48//! - **VAL-INSTALL-039**: All 35 components reference correct Rust modules
49//! - **VAL-INSTALL-040**: Non-installer components unchanged
50//! - **VAL-INSTALL-041**: Megatron declares dependency on PyTorch and MPI4Py
51//! - **VAL-INSTALL-042**: vLLM declares dependency on PyTorch
52//! - **VAL-INSTALL-043**: DeepSpeed declares dependency on PyTorch
53//! - **VAL-INSTALL-044**: Flash Attention declares dependency on PyTorch and ROCm
54//! - **VAL-INSTALL-045**: ONNX Runtime declares dependency on ROCm
55//! - **VAL-INSTALL-046**: AITER declares dependency on PyTorch and ROCm
56//! - **VAL-INSTALL-047**: ComfyUI declares dependency on PyTorch
57//! - **VAL-INSTALL-048**: Dependency graph is acyclic
58//! - **VAL-INSTALL-049**: installer.rs respects dependency ordering
59//! - **VAL-INSTALL-051**: All installer modules have unit tests
60//! - **VAL-INSTALL-052**: installer.rs integration tests exist
61
62pub mod aiter;
63pub mod amdgpu_drivers;
64pub mod bitsandbytes_multi;
65pub mod comfyui;
66pub mod deepspeed;
67pub mod fastvideo;
68pub mod flash_attention_ck;
69pub mod llama_cpp;
70pub mod megatron;
71pub mod migraphx_multi;
72pub mod migraphx_python;
73pub mod ml_stack;
74pub mod mpi4py;
75pub mod onnxruntime;
76pub mod permanent_env;
77pub mod pytorch;
78pub mod pytorch_profiler;
79pub mod repair;
80pub mod rocm;
81pub mod rocm_smi;
82pub mod textgen;
83pub mod triton;
84pub mod vllm_multi;
85pub mod vllm_studio;
86pub mod wandb;
87
88// Re-export key types for convenience
89pub use aiter::{AiterConfig, AiterInstaller};
90pub use amdgpu_drivers::{AmdgpuConfig, AmdgpuInstaller};
91pub use bitsandbytes_multi::{BitsAndBytesConfig, BitsAndBytesInstaller};
92pub use comfyui::{ComfyuiConfig, ComfyuiInstaller};
93pub use deepspeed::{DeepSpeedConfig, DeepSpeedInstaller};
94pub use flash_attention_ck::{FlashAttentionConfig, FlashAttentionInstaller, GpuArch};
95pub use llama_cpp::{LlamaCppConfig, LlamaCppInstaller};
96pub use megatron::{MegatronConfig, MegatronInstaller};
97pub use migraphx_multi::{MigraphxConfig, MigraphxInstaller, MigraphxSupport};
98pub use migraphx_python::{
99    InstallMethod as MigraphxPythonInstallMethod, MigraphxPythonConfig, MigraphxPythonInstaller,
100};
101pub use ml_stack::{MlStackConfig, MlStackInstaller};
102pub use mpi4py::{Mpi4PyConfig, Mpi4PyInstaller, MpiImplementation};
103pub use onnxruntime::{HipArchs, OnnxRuntimeConfig, OnnxRuntimeInstaller};
104pub use permanent_env::{PermanentEnvConfig, PermanentEnvInstaller};
105pub use pytorch::{PyTorchConfig, PyTorchInstaller, TorchChannel};
106pub use pytorch_profiler::{PytorchProfilerConfig, PytorchProfilerInstaller};
107pub use repair::{RepairConfig, RepairInstaller, RepairResult, RepairStep};
108pub use rocm::{RocmChannel, RocmConfig, RocmInstallType, RocmInstaller};
109pub use rocm_smi::{RocmSmiConfig, RocmSmiInstaller};
110pub use textgen::{TextgenConfig, TextgenInstaller};
111pub use triton::{TritonBranch, TritonConfig, TritonInstaller};
112pub use vllm_multi::{VllmConfig, VllmInstaller};
113pub use vllm_studio::{VllmStudioConfig, VllmStudioInstaller};
114pub use wandb::{WandbConfig, WandbInstaller};
115
116// ===========================================================================
117// Installer Dispatch (VAL-INSTALL-031, VAL-INSTALL-032, VAL-INSTALL-039)
118// ===========================================================================
119
120/// The set of all 35 component IDs that have been ported to native Rust.
121///
122/// These are the installer components that should NOT spawn bash subprocesses.
123/// Verification and performance components are now routed through native Rust modules.
124///
125/// Note: most of these appear in `state::default_components()`. A few utility
126/// components (`amdgpu-drivers`, `migraphx-python`, `enhanced-env`) are
127/// chain-referenced instead of shown as top-level selectable TUI components.
128pub const NATIVE_COMPONENT_IDS: &[&str] = &[
129    "permanent-env",
130    "rocm",
131    "pytorch",
132    "triton",
133    "mpi4py",
134    "deepspeed",
135    "ml-stack-core",
136    "flash-attn",
137    "repair-stack",
138    "megatron",
139    "vllm",
140    "aiter",
141    "vllm-studio",
142    "comfyui",
143    "textgen",
144    "onnx",
145    "bitsandbytes",
146    "rocm-smi",
147    "migraphx",
148    "pytorch-profiler",
149    "wandb",
150    // Chain-referenced / utility scripts (ported but not top-level TUI components)
151    "amdgpu-drivers",
152    "migraphx-python",
153    "enhanced-env",
154    // Benchmark components (dispatched via benchmark_runners module)
155    "mlperf-inference",
156    "rocm-benchmarks",
157    "gpu-memory-bandwidth",
158    "rocm-smi-bench",
159    "pytorch-performance",
160    "vllm-performance",
161    "deepspeed-performance",
162    "megatron-performance",
163    "all-benchmarks",
164    // FastVideo component
165    "fastvideo",
166    // llama.cpp component (HIP/ROCm CMake source build)
167    "llama-cpp",
168];
169
170/// Returns `true` if the given component ID has been ported to native Rust
171/// and should be dispatched through the Rust installer path instead of
172/// spawning a bash subprocess.
173///
174/// # Validation Assertions
175///
176/// - **VAL-INSTALL-031**: installer.rs no bash subprocess for ported components
177/// - **VAL-INSTALL-039**: All 35 components reference correct Rust modules
178pub fn is_native_component(component_id: &str) -> bool {
179    NATIVE_COMPONENT_IDS.contains(&component_id)
180}
181
182/// Dependency declarations for each native installer component.
183///
184/// Maps component IDs to the list of component IDs they depend on.
185/// Dependencies must be installed before dependents.
186///
187/// # Validation Assertions
188///
189/// - **VAL-INSTALL-041**: Megatron depends on PyTorch and MPI4Py
190/// - **VAL-INSTALL-042**: vLLM depends on PyTorch
191/// - **VAL-INSTALL-043**: DeepSpeed depends on PyTorch
192/// - **VAL-INSTALL-044**: Flash Attention depends on PyTorch and ROCm
193/// - **VAL-INSTALL-045**: ONNX Runtime depends on ROCm
194/// - **VAL-INSTALL-046**: AITER depends on PyTorch and ROCm
195/// - **VAL-INSTALL-047**: ComfyUI depends on PyTorch
196/// - **VAL-CROSS-001**: PyTorch for ROCm depends on ROCm
197pub fn get_dependencies(component_id: &str) -> &'static [&'static str] {
198    match component_id {
199        "megatron" => &["pytorch", "mpi4py"],
200        "vllm" => &["pytorch"],
201        "aiter" => &["pytorch", "rocm"],
202        "flash-attn" => &["pytorch", "rocm"],
203        "onnx" => &["rocm"],
204        "deepspeed" => &["pytorch"],
205        "comfyui" => &["pytorch"],
206        "pytorch" => &["rocm"], // PyTorch for ROCm requires ROCm installed
207        "pytorch-profiler" => &["pytorch"],
208        "llama-cpp" => &["rocm"], // llama.cpp HIP build requires ROCm
209        // All other native components have no cross-component dependencies
210        _ => &[],
211    }
212}
213
214/// Sort component IDs in topological order so dependencies are installed
215/// before dependents.
216///
217/// Uses Kahn's algorithm (BFS-based topological sort).
218///
219/// # Validation Assertions
220///
221/// - **VAL-INSTALL-048**: Dependency graph is acyclic
222/// - **VAL-INSTALL-049**: installer.rs respects dependency ordering
223///
224/// # Returns
225///
226/// `Ok(sorted_ids)` on success, `Err(cycle_description)` if a cycle is detected.
227pub fn topological_sort(component_ids: &[String]) -> Result<Vec<String>, String> {
228    use std::collections::{HashMap, HashSet, VecDeque};
229
230    let id_set: HashSet<&str> = component_ids.iter().map(|s| s.as_str()).collect();
231
232    // Build adjacency list: dep -> list of dependents
233    let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
234    let mut in_degree: HashMap<&str, usize> = HashMap::new();
235
236    for id in component_ids.iter().map(|s| s.as_str()) {
237        in_degree.entry(id).or_insert(0);
238        adj.entry(id).or_default();
239    }
240
241    for id in component_ids.iter().map(|s| s.as_str()) {
242        for dep in get_dependencies(id) {
243            if id_set.contains(dep) {
244                // dep must come before id
245                adj.entry(dep).or_default().push(id);
246                *in_degree.entry(id).or_insert(0) += 1;
247            }
248        }
249    }
250
251    let mut queue: VecDeque<&str> = VecDeque::new();
252    for (&id, &degree) in &in_degree {
253        if degree == 0 {
254            queue.push_back(id);
255        }
256    }
257
258    let mut sorted = Vec::with_capacity(component_ids.len());
259    while let Some(id) = queue.pop_front() {
260        sorted.push(id.to_string());
261        if let Some(dependents) = adj.get(id) {
262            for &dependent in dependents {
263                if let Some(degree) = in_degree.get_mut(dependent) {
264                    *degree -= 1;
265                    if *degree == 0 {
266                        queue.push_back(dependent);
267                    }
268                }
269            }
270        }
271    }
272
273    if sorted.len() != component_ids.len() {
274        let remaining: Vec<&str> = in_degree
275            .iter()
276            .filter(|(_, &d)| d > 0)
277            .map(|(&id, _)| id)
278            .collect();
279        return Err(format!(
280            "Dependency cycle detected among: {}",
281            remaining.join(", ")
282        ));
283    }
284
285    Ok(sorted)
286}
287
288#[cfg(test)]
289mod dispatch_tests {
290    use super::*;
291
292    #[test]
293    fn test_all_24_native_components_listed() {
294        // 24 installer + 9 benchmark + 1 fastvideo + 1 llama-cpp = 35
295        assert_eq!(NATIVE_COMPONENT_IDS.len(), 35);
296    }
297
298    #[test]
299    fn test_is_native_component_true_for_ported() {
300        for id in NATIVE_COMPONENT_IDS {
301            assert!(is_native_component(id), "Expected '{}' to be native", id);
302        }
303    }
304
305    #[test]
306    fn test_is_native_component_false_for_verification() {
307        assert!(!is_native_component("verify-basic"));
308        assert!(!is_native_component("verify-enhanced"));
309        assert!(!is_native_component("verify-build"));
310    }
311
312    #[test]
313    fn test_is_native_component_true_for_performance() {
314        assert!(is_native_component("mlperf-inference"));
315        assert!(is_native_component("rocm-benchmarks"));
316        assert!(is_native_component("all-benchmarks"));
317        assert!(is_native_component("fastvideo"));
318    }
319
320    #[test]
321    fn test_is_native_component_false_for_unknown() {
322        assert!(!is_native_component("nonexistent"));
323        assert!(!is_native_component(""));
324    }
325
326    #[test]
327    fn test_megatron_dependencies() {
328        let deps = get_dependencies("megatron");
329        assert!(deps.contains(&"pytorch"));
330        assert!(deps.contains(&"mpi4py"));
331    }
332
333    #[test]
334    fn test_vllm_dependencies() {
335        let deps = get_dependencies("vllm");
336        assert!(deps.contains(&"pytorch"));
337    }
338
339    #[test]
340    fn test_deepspeed_dependencies() {
341        let deps = get_dependencies("deepspeed");
342        assert!(deps.contains(&"pytorch"));
343    }
344
345    #[test]
346    fn test_flash_attention_dependencies() {
347        let deps = get_dependencies("flash-attn");
348        assert!(deps.contains(&"pytorch"));
349        assert!(deps.contains(&"rocm"));
350    }
351
352    #[test]
353    fn test_onnx_dependencies() {
354        let deps = get_dependencies("onnx");
355        assert!(deps.contains(&"rocm"));
356    }
357
358    #[test]
359    fn test_aiter_dependencies() {
360        let deps = get_dependencies("aiter");
361        assert!(deps.contains(&"pytorch"));
362        assert!(deps.contains(&"rocm"));
363    }
364
365    #[test]
366    fn test_comfyui_dependencies() {
367        let deps = get_dependencies("comfyui");
368        assert!(deps.contains(&"pytorch"));
369    }
370
371    #[test]
372    fn test_llama_cpp_dependencies() {
373        let deps = get_dependencies("llama-cpp");
374        assert!(deps.contains(&"rocm"), "llama-cpp must depend on ROCm");
375    }
376
377    #[test]
378    fn test_no_deps_components() {
379        for id in &[
380            "rocm",
381            "triton",
382            "mpi4py",
383            "ml-stack-core",
384            "permanent-env",
385            "rocm-smi",
386            "bitsandbytes",
387            "wandb",
388            "migraphx",
389            "vllm-studio",
390            "textgen",
391            "repair-stack",
392            "amdgpu-drivers",
393            "migraphx-python",
394        ] {
395            assert!(
396                get_dependencies(id).is_empty(),
397                "Expected no dependencies for '{}', got {:?}",
398                id,
399                get_dependencies(id)
400            );
401        }
402    }
403
404    #[test]
405    fn test_pytorch_depends_on_rocm() {
406        let deps = get_dependencies("pytorch");
407        assert!(
408            deps.contains(&"rocm"),
409            "PyTorch for ROCm must depend on ROCm"
410        );
411    }
412
413    #[test]
414    fn test_topological_sort_empty() {
415        let result = topological_sort(&[]);
416        assert!(result.is_ok());
417        assert!(result.unwrap().is_empty());
418    }
419
420    #[test]
421    fn test_topological_sort_no_deps() {
422        let ids = vec![
423            "rocm".to_string(),
424            "pytorch".to_string(),
425            "triton".to_string(),
426        ];
427        let result = topological_sort(&ids);
428        assert!(result.is_ok());
429        assert_eq!(result.unwrap().len(), 3);
430    }
431
432    #[test]
433    fn test_topological_sort_respects_dependencies() {
434        let ids = vec![
435            "megatron".to_string(),
436            "pytorch".to_string(),
437            "mpi4py".to_string(),
438        ];
439        let result = topological_sort(&ids).unwrap();
440        let pytorch_pos = result.iter().position(|s| s == "pytorch").unwrap();
441        let mpi4py_pos = result.iter().position(|s| s == "mpi4py").unwrap();
442        let megatron_pos = result.iter().position(|s| s == "megatron").unwrap();
443        assert!(
444            pytorch_pos < megatron_pos,
445            "pytorch must come before megatron"
446        );
447        assert!(
448            mpi4py_pos < megatron_pos,
449            "mpi4py must come before megatron"
450        );
451    }
452
453    #[test]
454    fn test_topological_sort_complex_graph() {
455        let ids = vec![
456            "megatron".to_string(),
457            "flash-attn".to_string(),
458            "deepspeed".to_string(),
459            "vllm".to_string(),
460            "aiter".to_string(),
461            "onnx".to_string(),
462            "comfyui".to_string(),
463            "pytorch".to_string(),
464            "rocm".to_string(),
465            "mpi4py".to_string(),
466            "triton".to_string(),
467        ];
468        let result = topological_sort(&ids).unwrap();
469        assert_eq!(result.len(), 11);
470
471        // Verify all dependency constraints
472        let pos = |id: &str| result.iter().position(|s| s == id).unwrap();
473        assert!(pos("rocm") < pos("flash-attn"));
474        assert!(pos("rocm") < pos("onnx"));
475        assert!(pos("rocm") < pos("aiter"));
476        assert!(pos("pytorch") < pos("megatron"));
477        assert!(pos("mpi4py") < pos("megatron"));
478        assert!(pos("pytorch") < pos("vllm"));
479        assert!(pos("pytorch") < pos("deepspeed"));
480        assert!(pos("pytorch") < pos("flash-attn"));
481        assert!(pos("pytorch") < pos("aiter"));
482        assert!(pos("pytorch") < pos("comfyui"));
483    }
484
485    #[test]
486    fn test_topological_sort_filters_unrelated_deps() {
487        // Only pytorch and vllm in the set; vllm depends on pytorch
488        let ids = vec!["vllm".to_string(), "pytorch".to_string()];
489        let result = topological_sort(&ids).unwrap();
490        assert_eq!(result.len(), 2);
491        assert_eq!(result[0], "pytorch");
492        assert_eq!(result[1], "vllm");
493    }
494
495    #[test]
496    fn test_dependency_graph_is_acyclic() {
497        // Verify all native components can be sorted together
498        let all_ids: Vec<String> = NATIVE_COMPONENT_IDS.iter().map(|s| s.to_string()).collect();
499        let result = topological_sort(&all_ids);
500        assert!(
501            result.is_ok(),
502            "Dependency graph must be acyclic: {:?}",
503            result
504        );
505        let sorted = result.unwrap();
506        assert_eq!(sorted.len(), NATIVE_COMPONENT_IDS.len());
507    }
508
509    #[test]
510    fn test_native_ids_match_state_component_ids() {
511        // Ensure all NATIVE_COMPONENT_IDS actually correspond to components
512        // defined in state.rs default_components()
513        for id in NATIVE_COMPONENT_IDS {
514            // Just verify they're non-empty strings
515            assert!(!id.is_empty(), "Component ID must not be empty");
516            assert!(
517                id.chars().all(|c| c == '-' || c.is_ascii_alphanumeric()),
518                "Component ID '{}' must be alphanumeric with hyphens",
519                id
520            );
521        }
522    }
523}