diff --git a/packages/dependency-installer/Cargo.toml b/packages/dependency-installer/Cargo.toml index 96f5312..b78b4f8 100644 --- a/packages/dependency-installer/Cargo.toml +++ b/packages/dependency-installer/Cargo.toml @@ -9,7 +9,12 @@ license = "MIT" name = "torrust_dependency_installer" path = "src/lib.rs" +[[bin]] +name = "dependency-installer" +path = "src/bin/dependency-installer.rs" + [dependencies] +clap = { version = "4.0", features = [ "derive" ] } thiserror = "1.0" tracing = "0.1" tracing-subscriber = { version = "0.3", features = [ "env-filter" ] } diff --git a/packages/dependency-installer/README.md b/packages/dependency-installer/README.md index dbc02d6..4ca90fa 100644 --- a/packages/dependency-installer/README.md +++ b/packages/dependency-installer/README.md @@ -20,7 +20,75 @@ This package can detect the following development dependencies: ## Usage -### Checking Dependencies +### CLI Binary + +The package provides a `dependency-installer` binary for command-line usage: + +```bash +# Check all dependencies +dependency-installer check + +# Check specific tool +dependency-installer check --tool opentofu + +# List all tools with status +dependency-installer list + +# Enable verbose logging +dependency-installer check --verbose + +# Get help +dependency-installer --help +dependency-installer check --help +``` + +#### Exit Codes + +- **0**: Success (all checks passed) +- **1**: Missing dependencies +- **2**: Invalid arguments +- **3**: Internal error + +#### Examples + +```bash +# Check all dependencies +$ dependency-installer check +Checking dependencies... + +✓ cargo-machete: installed +✗ OpenTofu: not installed +✗ Ansible: not installed +✓ LXD: installed + +Missing 2 out of 4 required dependencies + +# Check specific tool (with aliases) +$ dependency-installer check --tool tofu +✗ OpenTofu: not installed + +# List all tools +$ dependency-installer list +Available tools: + +- cargo-machete (installed) +- OpenTofu (not installed) +- Ansible (not installed) +- LXD (installed) +``` + +#### Tool Aliases + +The CLI accepts multiple aliases for tools: + +- `cargo-machete` or `machete` +- `opentofu` or `tofu` +- `ansible` +- `lxd` + +### Library Usage + +#### Checking Dependencies ```rust use torrust_dependency_installer::DependencyManager; @@ -39,7 +107,7 @@ fn main() -> Result<(), Box> { } ``` -### Using Individual Detectors +#### Using Individual Detectors ```rust use torrust_dependency_installer::{ToolDetector, OpenTofuDetector}; diff --git a/packages/dependency-installer/src/bin/dependency-installer.rs b/packages/dependency-installer/src/bin/dependency-installer.rs new file mode 100644 index 0000000..e952933 --- /dev/null +++ b/packages/dependency-installer/src/bin/dependency-installer.rs @@ -0,0 +1,182 @@ +//! CLI binary for managing development dependencies +//! +//! This binary provides commands to check and list development dependencies +//! required for E2E tests in the Torrust Tracker Deployer project. +//! +//! # Exit Codes +//! +//! - 0: Success (all checks passed) +//! - 1: Missing dependencies +//! - 2: Invalid arguments +//! - 3: Internal error + +use std::process; + +use clap::{Parser, Subcommand}; +use torrust_dependency_installer::{Dependency, DependencyManager}; +use tracing::{error, info}; + +/// Manage development dependencies for E2E tests +#[derive(Parser)] +#[command(name = "dependency-installer")] +#[command(version)] +#[command(about = "Manage development dependencies for E2E tests", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, + + /// Enable verbose output + #[arg(short, long, global = true)] + verbose: bool, +} + +#[derive(Subcommand)] +enum Commands { + /// Check if dependencies are installed + Check { + /// Specific tool to check (if omitted, checks all) + #[arg(short, long)] + tool: Option, + }, + + /// List all available tools and their status + List, +} + +fn main() { + let exit_code = match run() { + Ok(()) => 0, + Err(e) => { + eprintln!("Error: {e}"); + + // Determine exit code based on error type + let error_msg = e.to_string(); + if error_msg.contains("not installed") || error_msg.contains("Missing") { + 1 // Missing dependency + } else if error_msg.contains("Unknown tool") || error_msg.contains("invalid") { + 2 // Invalid argument + } else { + 3 // Internal error + } + } + }; + + process::exit(exit_code); +} + +fn run() -> Result<(), Box> { + let cli = Cli::parse(); + + // Initialize tracing based on verbose flag + // Must set environment variable before calling init_tracing() + if cli.verbose { + std::env::set_var("RUST_LOG", "debug"); + } + torrust_dependency_installer::init_tracing(); + + let manager = DependencyManager::new(); + + match cli.command { + Commands::Check { tool } => handle_check(&manager, tool), + Commands::List => handle_list(&manager), + } +} + +fn handle_check( + manager: &DependencyManager, + tool: Option, +) -> Result<(), Box> { + match tool { + Some(tool_name) => check_specific_tool(manager, &tool_name), + None => check_all_tools(manager), + } +} + +fn check_all_tools(manager: &DependencyManager) -> Result<(), Box> { + info!("Checking all dependencies"); + println!("Checking dependencies...\n"); + + let results = manager.check_all()?; + let mut missing_count = 0; + + for result in &results { + if result.installed { + println!("✓ {}: installed", result.tool); + } else { + println!("✗ {}: not installed", result.tool); + missing_count += 1; + } + } + + println!(); + if missing_count > 0 { + let msg = format!( + "Missing {missing_count} out of {} required dependencies", + results.len() + ); + error!("{}", msg); + eprintln!("{msg}"); + Err(msg.into()) + } else { + info!("All dependencies are installed"); + println!("All dependencies are installed"); + Ok(()) + } +} + +fn check_specific_tool( + manager: &DependencyManager, + tool_name: &str, +) -> Result<(), Box> { + info!(tool = tool_name, "Checking specific tool"); + + // Parse tool name to Dependency enum + let dep = parse_tool_name(tool_name)?; + let detector = manager.get_detector(dep); + + let installed = detector.is_installed()?; + + if installed { + info!(tool = detector.name(), "Tool is installed"); + println!("✓ {}: installed", detector.name()); + Ok(()) + } else { + let msg = format!("{}: not installed", detector.name()); + error!(tool = detector.name(), "Tool is not installed"); + eprintln!("✗ {msg}"); + Err(msg.into()) + } +} + +fn handle_list(manager: &DependencyManager) -> Result<(), Box> { + info!("Listing all available tools"); + println!("Available tools:\n"); + + let results = manager.check_all()?; + for result in results { + let status = if result.installed { + "installed" + } else { + "not installed" + }; + println!("- {} ({status})", result.tool); + } + + Ok(()) +} + +fn parse_tool_name(name: &str) -> Result { + match name.to_lowercase().as_str() { + "cargo-machete" | "machete" => Ok(Dependency::CargoMachete), + "opentofu" | "tofu" => Ok(Dependency::OpenTofu), + "ansible" => Ok(Dependency::Ansible), + "lxd" => Ok(Dependency::Lxd), + _ => { + // List of available tools - should be kept in sync with the match arms above + const AVAILABLE_TOOLS: &str = "cargo-machete, opentofu, ansible, lxd"; + Err(format!( + "Unknown tool: {name}. Available: {AVAILABLE_TOOLS}" + )) + } + } +}