Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/dependency-installer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" ] }
72 changes: 70 additions & 2 deletions packages/dependency-installer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -39,7 +107,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
```

### Using Individual Detectors
#### Using Individual Detectors

```rust
use torrust_dependency_installer::{ToolDetector, OpenTofuDetector};
Expand Down
182 changes: 182 additions & 0 deletions packages/dependency-installer/src/bin/dependency-installer.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
},

/// 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<dyn std::error::Error>> {
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<String>,
) -> Result<(), Box<dyn std::error::Error>> {
match tool {
Some(tool_name) => check_specific_tool(manager, &tool_name),
None => check_all_tools(manager),
}
}

fn check_all_tools(manager: &DependencyManager) -> Result<(), Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<Dependency, String> {
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}"
))
}
}
}
Loading