rusty-stack 0.3.0

A comprehensive ML environment installer and platform toolkit optimized for AMD GPUs with ROCm support
Documentation
//! run_rusty_stack.sh equivalent — TUI launcher.
//!
//! Ports `scripts/run_rusty_stack.sh` (70 lines) to native Rust.
//! Builds the Rust binary (if needed) and launches it.
//!
//! # Validation Assertions
//!
//! - **VAL-VBA-013**: run_rusty_stack.sh equivalent — TUI launcher

use crate::installers::common::utils::{print_error, print_step, print_success};
use std::path::PathBuf;
use std::process::Command;

// ===========================================================================
// Types
// ===========================================================================

/// Build mode for the TUI launcher.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BuildMode {
    Debug,
    #[default]
    Release,
}

impl std::fmt::Display for BuildMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            BuildMode::Debug => write!(f, "debug"),
            BuildMode::Release => write!(f, "release"),
        }
    }
}

/// Result of the TUI launcher.
#[derive(Debug, Clone)]
pub struct LaunchResult {
    /// Whether the build and launch preparation was successful.
    pub success: bool,
    /// Path to the built binary.
    pub binary_path: Option<String>,
    /// Any errors encountered.
    pub errors: Vec<String>,
}

impl LaunchResult {
    /// Create a successful result.
    pub fn success(binary_path: String) -> Self {
        Self {
            success: true,
            binary_path: Some(binary_path),
            errors: Vec::new(),
        }
    }

    /// Create a failure result.
    pub fn failure(error: String) -> Self {
        Self {
            success: false,
            binary_path: None,
            errors: vec![error],
        }
    }
}

// ===========================================================================
// Core Logic
// ===========================================================================

/// Build and launch the rusty-stack TUI.
///
/// This is the Rust equivalent of `run_rusty_stack.sh`:
/// 1. Detect project root and rusty-stack directory
/// 2. Build in the specified mode (debug/release)
/// 3. Return the binary path (caller can exec it)
///
/// # Validation
///
/// - **VAL-VBA-013**: TUI launcher builds and executes Rust binary
pub fn launch_tui(build_mode: BuildMode, extra_args: &[String]) -> LaunchResult {
    // Detect project root
    let project_root = detect_project_root();
    let rusty_stack_dir = project_root.join("rusty-stack");

    if !rusty_stack_dir.exists() {
        let msg = format!(
            "Rust project directory not found: {}",
            rusty_stack_dir.display()
        );
        print_error(&msg);
        return LaunchResult::failure(msg);
    }

    print_step(&format!("Build mode: {build_mode}"));

    // Build the binary
    let (exec_path, build_args) = match build_mode {
        BuildMode::Debug => ("target/debug/rusty-stack", vec!["build"]),
        BuildMode::Release => ("target/release/rusty-stack", vec!["build", "--release"]),
    };

    print_step(&format!("Building with cargo {}...", build_mode));

    let output = Command::new("cargo")
        .args(&build_args)
        .current_dir(&rusty_stack_dir)
        .stdout(std::process::Stdio::inherit())
        .stderr(std::process::Stdio::inherit())
        .output();

    match output {
        Ok(output) if output.status.success() => {
            let binary_path = rusty_stack_dir.join(exec_path);
            if !binary_path.exists() {
                let msg = format!("Binary not found after build: {}", binary_path.display());
                print_error(&msg);
                return LaunchResult::failure(msg);
            }
            print_success(&format!("Built successfully: {}", binary_path.display()));
            print_step(&format!("Launching {}", exec_path));

            // Return the binary path and args for the caller to exec
            // (In the original script, `exec` replaces the process — in Rust
            // we return the path so the CLI handler can exec it)
            let _ = extra_args; // passed through to exec
            LaunchResult::success(binary_path.to_string_lossy().to_string())
        }
        Ok(output) => {
            let stderr = String::from_utf8_lossy(&output.stderr);
            let msg = format!("Build failed: {stderr}");
            print_error(&msg);
            LaunchResult::failure(msg)
        }
        Err(e) => {
            let msg = format!("Failed to run cargo: {e}");
            print_error(&msg);
            LaunchResult::failure(msg)
        }
    }
}

// ===========================================================================
// Helpers
// ===========================================================================

/// Detect the project root directory.
///
/// Looks for `rusty-stack/` subdirectory in:
/// 1. Current directory
/// 2. Parent of current directory
/// 3. Parent of parent (for scripts/ directory context)
fn detect_project_root() -> PathBuf {
    let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));

    // Check current dir
    if cwd.join("rusty-stack").exists() {
        return cwd;
    }

    // Check parent
    if let Some(parent) = cwd.parent() {
        if parent.join("rusty-stack").exists() {
            return parent.to_path_buf();
        }

        // Check grandparent (for scripts/ context)
        if let Some(grandparent) = parent.parent() {
            if grandparent.join("rusty-stack").exists() {
                return grandparent.to_path_buf();
            }
        }
    }

    // Fallback — use current directory
    cwd
}

// ===========================================================================
// Tests
// ===========================================================================

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_build_mode_default_is_release() {
        assert_eq!(BuildMode::default(), BuildMode::Release);
    }

    #[test]
    fn test_build_mode_display() {
        assert_eq!(format!("{}", BuildMode::Debug), "debug");
        assert_eq!(format!("{}", BuildMode::Release), "release");
    }

    #[test]
    fn test_launch_result_success() {
        let result = LaunchResult::success("/path/to/rusty-stack".to_string());
        assert!(result.success);
        assert_eq!(result.binary_path, Some("/path/to/rusty-stack".to_string()));
        assert!(result.errors.is_empty());
    }

    #[test]
    fn test_launch_result_failure() {
        let result = LaunchResult::failure("Build error".to_string());
        assert!(!result.success);
        assert!(result.binary_path.is_none());
        assert_eq!(result.errors.len(), 1);
    }

    #[test]
    fn test_detect_project_root_no_panic() {
        // Should not panic even if rusty-stack dir doesn't exist
        let root = detect_project_root();
        // Just verify it returns something
        assert!(!root.as_os_str().is_empty());
    }

    #[test]
    fn test_launch_tui_returns_result() {
        // This will likely fail if rusty-stack dir doesn't exist in CWD parent,
        // but should not panic
        let result = launch_tui(BuildMode::Release, &[]);
        // Just verify it returns a result without panicking
        let _ = result.success;
    }
}