xbp 10.15.4

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
//! Package installation module for xbp
//!
//! This module provides functionality to install various packages using
//! predefined shell scripts or native cross-platform logic. Supports
//! Linux (apt/bash), macOS (Homebrew), and Windows (winget).

use crate::utils::command_exists;
use colored::Colorize;
use std::path::PathBuf;
use std::process::Output;
use tokio::process::Command;
use tracing::{debug, info};

/// Installs a package by name using the corresponding installation script
///
/// # Arguments
///
/// * `package_name` - The name of the package to install (e.g., "grafana")
/// * `debug` - Whether to enable debug output during installation
///
/// # Returns
///
/// * `Ok(())` if the installation was successful
/// * `Err(String)` if the package is not supported or installation failed
///
/// # Examples
///
/// ```no_run
/// use tokio::runtime::Runtime;
/// use tracing::{info, error};
/// use xbp::commands::install::install_package;
///
/// let runtime = Runtime::new().unwrap();
/// runtime.block_on(async {
///     let result = install_package("grafana", true).await;
///     match result {
///         Ok(()) => info!("Installation successful"),
///         Err(e) => error!("Installation failed: {}", e),
///     }
/// });
/// ```
pub async fn install_package(package_name: &str, debug: bool) -> Result<(), String> {
    if package_name.is_empty() || package_name == "--help" || package_name == "help" {
        list_available_packages();
        return Ok(());
    }

    match package_name.to_lowercase().replace('_', "-").as_str() {
        "azure-cli" | "azure_cli" => install_azure_cli(debug).await,
        "grafana" => execute_install_script("grafana", debug).await,
        "scylladb" => execute_install_script("scylladb", debug).await,
        "nginx_full" => execute_install_script("nginx_full", debug).await,
        "opencv-rust" => execute_install_script("opencv-rust", debug).await,
        "elixir_erlang" => execute_install_script("elixir_erlang", debug).await,
        "docker" => execute_install_script("docker", debug).await,
        "triggerdotdev" => execute_install_script("triggerdotdev", debug).await,
        "iotop" => execute_install_script("iotop", debug).await,
        _ => {
            info!("{} '{}'", "Unknown package:".red(), package_name);
            info!("");
            list_available_packages();
            Err(format!("Package '{}' is not supported", package_name))
        }
    }
}

fn list_available_packages() {
    info!("{}", "Available packages:".bright_blue().bold());
    info!("");
    info!(
        "  {} - Azure CLI (cross-platform: winget/Linux script)",
        "azure-cli".cyan()
    );
    info!(
        "  {} - Monitoring and observability platform",
        "grafana".cyan()
    );
    info!("  {} - High-performance NoSQL database", "scylladb".cyan());
    info!(
        "  {} - Full Nginx installation with modules",
        "nginx_full".cyan()
    );
    info!("  {} - OpenCV bindings for Rust", "opencv-rust".cyan());
    info!("  {} - Container platform", "docker".cyan());
    info!("  {} - Elixir and Erlang runtime", "elixir_erlang".cyan());
    info!("  {} - Background job framework", "triggerdotdev".cyan());
    info!("  {} - I/O monitoring tool", "iotop".cyan());
    info!("");
    info!(
        "{} {}",
        "Usage:".bright_blue(),
        "xbp install <package>".yellow()
    );
}

/// Install Azure CLI natively: winget on Windows, curl|bash on Linux/macOS.
async fn install_azure_cli(_debug: bool) -> Result<(), String> {
    if command_exists("az") {
        info!("{}", "Azure CLI is already installed.".green());
        return Ok(());
    }

    #[cfg(target_os = "windows")]
    {
        if !command_exists("winget") {
            return Err(
                "winget is required. Install it from the Microsoft Store or use Windows 10/11."
                    .to_string(),
            );
        }
        info!("Installing Azure CLI via winget...");
        let output = Command::new("winget")
            .args([
                "install",
                "--exact",
                "--id",
                "Microsoft.AzureCLI",
                "--accept-package-agreements",
                "--accept-source-agreements",
            ])
            .output()
            .await
            .map_err(|e| format!("winget failed: {}", e))?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            let stdout = String::from_utf8_lossy(&output.stdout);
            return Err(format!("Azure CLI install failed: {}{}", stdout, stderr));
        }
        info!(
            "{}",
            "Azure CLI installed. Restart your terminal to use 'az'.".green()
        );
        Ok(())
    }

    #[cfg(target_os = "macos")]
    {
        if !command_exists("brew") {
            return Err("Homebrew is required. Install from https://brew.sh/".to_string());
        }
        info!("Installing Azure CLI via Homebrew...");
        let output = Command::new("brew")
            .args(["install", "azure-cli"])
            .output()
            .await
            .map_err(|e| format!("brew install failed: {}", e))?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(format!("Azure CLI install failed: {}", stderr));
        }
        info!("{}", "Azure CLI installed successfully.".green());
        Ok(())
    }

    #[cfg(target_os = "linux")]
    {
        info!("Installing Azure CLI via official script...");
        let script = "curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash";
        let output = Command::new("sh")
            .args(["-c", script])
            .output()
            .await
            .map_err(|e| format!("Azure CLI install failed: {}", e))?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(format!("Azure CLI install failed: {}", stderr));
        }
        info!("{}", "Azure CLI installed successfully.".green());
        Ok(())
    }

    #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
    {
        Err("Azure CLI install is supported on Windows, Linux, and macOS only.".to_string())
    }
}

/// Executes the installation script for a specific package
///
/// This function locates the installation script in the package_install_scripts
/// directory, makes it executable, and runs it. It provides detailed debug
/// output when debug mode is enabled.
///
/// # Arguments
///
/// * `package_name` - The name of the package (used to find the script file)
/// * `debug` - Whether to enable debug output during script execution
///
/// # Returns
///
/// * `Ok(())` if the script executed successfully
/// * `Err(String)` if the script was not found, couldn't be made executable, or failed to run
///
/// # Script Location
///
/// Scripts are expected to be located at `./package_install_scripts/{package_name}.sh`
async fn execute_install_script(package_name: &str, debug: bool) -> Result<(), String> {
    let script_path: String = format!("./package_install_scripts/{}.sh", package_name);
    let script_pathbuf: PathBuf = PathBuf::from(&script_path);

    if debug {
        debug!("Looking for install script at: {}", script_path);
    }

    if !script_pathbuf.exists() {
        return Err(format!("Install script not found: {}", script_path));
    }

    if debug {
        debug!("Making script executable: {}", script_path);
    }

    // Make the script executable
    let chmod_output: Output = Command::new("chmod")
        .arg("+x")
        .arg(&script_path)
        .output()
        .await
        .map_err(|e| format!("Failed to execute chmod command: {}", e))?;

    if !chmod_output.status.success() {
        return Err(format!(
            "Failed to make script executable: {}",
            String::from_utf8_lossy(&chmod_output.stderr)
        ));
    }

    if debug {
        debug!("Executing install script: {}", script_path);
    }

    let script_output: Output = Command::new("bash")
        .arg(&script_path)
        .output()
        .await
        .map_err(|e| format!("Failed to execute install script: {}", e))?;

    if debug {
        debug!("Script output: {:?}", script_output);
    }

    if !script_output.status.success() {
        return Err(format!(
            "Install script failed: {}",
            String::from_utf8_lossy(&script_output.stderr)
        ));
    }

    let stdout = String::from_utf8_lossy(&script_output.stdout);
    if !stdout.trim().is_empty() {
        info!("{}", stdout);
    }

    info!("{} installation completed successfully!", package_name);

    Ok(())
}