#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShellCommand {
pub program: String,
pub args: Vec<String>,
pub env: Vec<(String, String)>,
}
impl ShellCommand {
pub fn to_command_string(&self) -> String {
let env_prefix = self
.env
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join(" ");
let cmd = if self.args.is_empty() {
self.program.clone()
} else {
format!("{} {}", self.program, self.args.join(" "))
};
if env_prefix.is_empty() {
cmd
} else {
format!("{env_prefix} {cmd}")
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RepairStepResult {
pub name: String,
pub success: bool,
pub error_message: Option<String>,
}
#[derive(Debug, Clone)]
pub struct RepairResult {
pub steps: Vec<RepairStepResult>,
}
impl RepairResult {
pub fn new() -> Self {
Self { steps: Vec::new() }
}
pub fn add_step(&mut self, name: &str, success: bool, error_message: Option<String>) {
self.steps.push(RepairStepResult {
name: name.to_string(),
success,
error_message,
});
}
pub fn successful_count(&self) -> usize {
self.steps.iter().filter(|s| s.success).count()
}
pub fn failed_count(&self) -> usize {
self.steps.iter().filter(|s| !s.success).count()
}
pub fn all_succeeded(&self) -> bool {
self.steps.iter().all(|s| s.success)
}
pub fn failed_steps(&self) -> Vec<&str> {
self.steps
.iter()
.filter(|s| !s.success)
.map(|s| s.name.as_str())
.collect()
}
pub fn format_summary(&self) -> String {
let mut lines = Vec::new();
lines.push("ML Stack Repair Summary".to_string());
lines.push("=======================".to_string());
for step in &self.steps {
let status = if step.success { "✓" } else { "✗" };
let msg = match &step.error_message {
Some(m) => format!(" ({})", m),
None => String::new(),
};
lines.push(format!("{} {}{}", status, step.name, msg));
}
lines.push(String::new());
lines.push(format!(
"Total: {} succeeded, {} failed",
self.successful_count(),
self.failed_count()
));
lines.join("\n")
}
}
impl Default for RepairResult {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RepairStep {
Rocm,
AmdgpuDrivers,
PyTorch,
MlStackCore,
Aiter,
MigraphxPython,
DeepSpeed,
EnvironmentVariables,
}
impl RepairStep {
pub fn all_in_order() -> Vec<RepairStep> {
vec![
RepairStep::Rocm,
RepairStep::AmdgpuDrivers,
RepairStep::PyTorch,
RepairStep::MlStackCore,
RepairStep::Aiter,
RepairStep::MigraphxPython,
RepairStep::DeepSpeed,
RepairStep::EnvironmentVariables,
]
}
pub fn name(&self) -> &'static str {
match self {
RepairStep::Rocm => "ROCm Installation",
RepairStep::AmdgpuDrivers => "AMDGPU Drivers",
RepairStep::PyTorch => "PyTorch Installation",
RepairStep::MlStackCore => "ML Stack Core",
RepairStep::Aiter => "AITER Installation",
RepairStep::MigraphxPython => "MIGraphX Python",
RepairStep::DeepSpeed => "DeepSpeed Installation",
RepairStep::EnvironmentVariables => "Environment Variables",
}
}
pub fn native_component_id(&self) -> &'static str {
match self {
RepairStep::Rocm => "rocm",
RepairStep::AmdgpuDrivers => "amdgpu-drivers",
RepairStep::PyTorch => "pytorch",
RepairStep::MlStackCore => "ml-stack-core",
RepairStep::Aiter => "aiter",
RepairStep::MigraphxPython => "migraphx-python",
RepairStep::DeepSpeed => "deepspeed",
RepairStep::EnvironmentVariables => "permanent-env",
}
}
}
#[derive(Debug, Clone)]
pub struct RepairConfig {
pub python_bin: String,
pub dry_run: bool,
pub non_interactive: bool,
}
impl Default for RepairConfig {
fn default() -> Self {
Self {
python_bin: "python3".to_string(),
dry_run: false,
non_interactive: false,
}
}
}
pub struct RepairInstaller {
config: RepairConfig,
}
impl RepairInstaller {
pub fn new(config: RepairConfig) -> Self {
Self { config }
}
pub fn with_defaults() -> Self {
Self::new(RepairConfig::default())
}
pub fn repair_sequence() -> Vec<RepairStep> {
RepairStep::all_in_order()
}
pub fn native_component_ids() -> Vec<&'static str> {
Self::repair_sequence()
.iter()
.map(|s| s.native_component_id())
.collect()
}
pub fn run_repair_sequence<F>(&self, mut step_runner: F) -> RepairResult
where
F: FnMut(RepairStep) -> Result<(), String>,
{
let mut result = RepairResult::new();
for step in Self::repair_sequence() {
if self.config.dry_run {
result.add_step(step.name(), true, Some("dry-run: skipped".to_string()));
continue;
}
match step_runner(step) {
Ok(()) => result.add_step(step.name(), true, None),
Err(msg) => result.add_step(step.name(), false, Some(msg)),
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_repair_sequence_order_matches_original_script() {
let sequence = RepairInstaller::repair_sequence();
assert_eq!(sequence.len(), 8);
assert_eq!(sequence[0], RepairStep::Rocm);
assert_eq!(sequence[1], RepairStep::AmdgpuDrivers);
assert_eq!(sequence[2], RepairStep::PyTorch);
assert_eq!(sequence[3], RepairStep::MlStackCore);
assert_eq!(sequence[4], RepairStep::Aiter);
assert_eq!(sequence[5], RepairStep::MigraphxPython);
assert_eq!(sequence[6], RepairStep::DeepSpeed);
assert_eq!(sequence[7], RepairStep::EnvironmentVariables);
}
#[test]
fn test_repair_step_names() {
assert_eq!(RepairStep::Rocm.name(), "ROCm Installation");
assert_eq!(RepairStep::AmdgpuDrivers.name(), "AMDGPU Drivers");
assert_eq!(RepairStep::PyTorch.name(), "PyTorch Installation");
assert_eq!(RepairStep::MlStackCore.name(), "ML Stack Core");
assert_eq!(RepairStep::Aiter.name(), "AITER Installation");
assert_eq!(RepairStep::MigraphxPython.name(), "MIGraphX Python");
assert_eq!(RepairStep::DeepSpeed.name(), "DeepSpeed Installation");
assert_eq!(
RepairStep::EnvironmentVariables.name(),
"Environment Variables"
);
}
#[test]
fn test_repair_step_native_component_ids() {
assert_eq!(RepairStep::Rocm.native_component_id(), "rocm");
assert_eq!(
RepairStep::AmdgpuDrivers.native_component_id(),
"amdgpu-drivers"
);
assert_eq!(RepairStep::PyTorch.native_component_id(), "pytorch");
assert_eq!(
RepairStep::MlStackCore.native_component_id(),
"ml-stack-core"
);
assert_eq!(RepairStep::Aiter.native_component_id(), "aiter");
assert_eq!(
RepairStep::MigraphxPython.native_component_id(),
"migraphx-python"
);
assert_eq!(RepairStep::DeepSpeed.native_component_id(), "deepspeed");
assert_eq!(
RepairStep::EnvironmentVariables.native_component_id(),
"permanent-env"
);
}
#[test]
fn test_native_component_ids_match_repair_sequence() {
let ids = RepairInstaller::native_component_ids();
let steps = RepairInstaller::repair_sequence();
assert_eq!(ids.len(), steps.len());
for (step, id) in steps.iter().zip(ids.iter()) {
assert_eq!(step.native_component_id(), *id);
}
}
#[test]
fn test_repair_uses_native_dispatch_not_scripts() {
for step in RepairInstaller::repair_sequence() {
let id = step.native_component_id();
assert!(
crate::installers::components::is_native_component(id),
"Repair step '{}' maps to component '{}' which must be native",
step.name(),
id
);
}
}
#[test]
fn test_repair_propagates_failures() {
let repair = RepairInstaller::with_defaults();
let result = repair.run_repair_sequence(|step| match step {
RepairStep::PyTorch => Err("PyTorch installation failed".to_string()),
RepairStep::DeepSpeed => Err("DeepSpeed build error".to_string()),
_ => Ok(()),
});
assert_eq!(result.failed_count(), 2);
assert_eq!(result.successful_count(), 6);
assert!(!result.all_succeeded());
let failed = result.failed_steps();
assert!(failed.contains(&"PyTorch Installation"));
assert!(failed.contains(&"DeepSpeed Installation"));
}
#[test]
fn test_repair_all_succeed() {
let repair = RepairInstaller::with_defaults();
let result = repair.run_repair_sequence(|_| Ok(()));
assert!(result.all_succeeded());
assert_eq!(result.successful_count(), 8);
assert_eq!(result.failed_count(), 0);
}
#[test]
fn test_repair_summary_format() {
let repair = RepairInstaller::with_defaults();
let result = repair.run_repair_sequence(|step| match step {
RepairStep::PyTorch => Err("timeout".to_string()),
_ => Ok(()),
});
let summary = result.format_summary();
assert!(summary.contains("ML Stack Repair Summary"));
assert!(summary.contains("7 succeeded, 1 failed"));
assert!(summary.contains("✗ PyTorch Installation (timeout)"));
assert!(summary.contains("✓ ROCm Installation"));
}
#[test]
fn test_repair_continues_after_failure() {
let repair = RepairInstaller::with_defaults();
let mut visited = Vec::new();
let _result = repair.run_repair_sequence(|step| {
visited.push(step);
if step == RepairStep::Rocm {
Err("ROCm failed".to_string())
} else {
Ok(())
}
});
assert_eq!(visited.len(), 8);
assert_eq!(visited[0], RepairStep::Rocm);
assert_eq!(visited[7], RepairStep::EnvironmentVariables);
}
#[test]
fn test_repair_result_default() {
let result = RepairResult::default();
assert_eq!(result.steps.len(), 0);
assert!(result.all_succeeded());
}
}