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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
members = [
"packages/linting",
"packages/dependency-installer",
]
resolver = "2"

Expand Down
15 changes: 15 additions & 0 deletions packages/dependency-installer/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "torrust-dependency-installer"
version = "0.1.0"
edition = "2021"
description = "Dependency detection and installation utilities for the Torrust Tracker Deployer project"
license = "MIT"

[lib]
name = "torrust_dependency_installer"
path = "src/lib.rs"

[dependencies]
thiserror = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = [ "env-filter" ] }
77 changes: 77 additions & 0 deletions packages/dependency-installer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Torrust Dependency Installer Package

This package provides dependency detection and installation utilities for the Torrust Tracker Deployer project.

## Features

- **Tool Detection**: Check if required development tools are installed
- **Extensible**: Easy to add new tool detectors
- **Logging**: Built-in tracing support for observability
- **Error Handling**: Clear, actionable error messages

## Required Tools

This package can detect the following development dependencies:

- **cargo-machete** - Detects unused Rust dependencies
- **OpenTofu** - Infrastructure provisioning tool
- **Ansible** - Configuration management tool
- **LXD** - VM-based testing infrastructure

## Usage

### Checking Dependencies

```rust
use torrust_dependency_installer::DependencyManager;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = DependencyManager::new();

// Check all dependencies
let results = manager.check_all()?;

for result in results {
println!("{}: {}", result.tool, if result.installed { "✓" } else { "✗" });
}

Ok(())
}
```

### Using Individual Detectors

```rust
use torrust_dependency_installer::{ToolDetector, OpenTofuDetector};

fn main() -> Result<(), Box<dyn std::error::Error>> {
let detector = OpenTofuDetector;

if detector.is_installed()? {
println!("{} is installed", detector.name());
} else {
println!("{} is not installed", detector.name());
}

Ok(())
}
```

## Adding to Your Project

Add to your `Cargo.toml`:

```toml
[dependencies]
torrust-dependency-installer = { path = "path/to/torrust-dependency-installer" }
```

Or if using in a workspace:

```toml
[workspace]
members = ["packages/torrust-dependency-installer"]

[dependencies]
torrust-dependency-installer = { path = "packages/torrust-dependency-installer" }
```
43 changes: 43 additions & 0 deletions packages/dependency-installer/examples/check_dependencies.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//! Example: Check all development dependencies
//!
//! This example demonstrates how to use the dependency installer package
//! to check if all required development dependencies are installed.
//!
//! Run with: `cargo run --example check_dependencies`

use torrust_dependency_installer::{init_tracing, DependencyManager};

fn main() {
// Initialize tracing for structured logging
init_tracing();

println!("Checking development dependencies...\n");

// Create dependency manager
let manager = DependencyManager::new();

// Check all dependencies
match manager.check_all() {
Ok(results) => {
println!("Dependency Status:");
println!("{}", "=".repeat(40));

for result in &results {
let status = if result.installed { "✓" } else { "✗" };
let status_text = if result.installed {
"Installed"
} else {
"Not Installed"
};

println!("{} {:20} {}", status, result.tool, status_text);
}

println!("\n{} dependencies checked", results.len());
}
Err(e) => {
eprintln!("Error checking dependencies: {e}");
std::process::exit(1);
}
}
}
82 changes: 82 additions & 0 deletions packages/dependency-installer/src/command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use std::process::Command;

use crate::errors::CommandError;

/// Check if a command exists in the system PATH
///
/// # Platform Support
///
/// Currently uses the `which` command on Unix-like systems. Windows support
/// would require using `where` command or a different approach.
///
/// # Examples
///
/// ```rust
/// use torrust_dependency_installer::command::command_exists;
///
/// // Check if 'cargo' is installed
/// let exists = command_exists("cargo").unwrap();
/// assert!(exists);
/// ```
///
/// # Errors
///
/// Returns an error if the 'which' command fails to execute
pub fn command_exists(command: &str) -> Result<bool, CommandError> {
// Use 'which' on Unix-like systems to check if command exists
// Note: This is Unix-specific. For Windows support, use 'where' command.
let output =
Command::new("which")
.arg(command)
.output()
.map_err(|e| CommandError::ExecutionFailed {
command: format!("which {command}"),
source: e,
})?;

Ok(output.status.success())
}

/// Execute a command and return its stdout as a string
///
/// # Examples
///
/// ```rust,no_run
/// use torrust_dependency_installer::command::execute_command;
///
/// // Get cargo version
/// let version = execute_command("cargo", &["--version"]).unwrap();
/// println!("Cargo version: {}", version);
/// ```
///
/// # Errors
///
/// Returns an error if the command is not found or fails to execute
pub fn execute_command(command: &str, args: &[&str]) -> Result<String, CommandError> {
let output = Command::new(command).args(args).output().map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
CommandError::CommandNotFound {
command: command.to_string(),
}
} else {
CommandError::ExecutionFailed {
command: format!("{command} {}", args.join(" ")),
source: e,
}
}
})?;

if !output.status.success() {
return Err(CommandError::ExecutionFailed {
command: format!("{command} {}", args.join(" ")),
source: std::io::Error::other(format!("Command exited with status: {}", output.status)),
});
}

String::from_utf8(output.stdout)
.map(|s| s.trim().to_string())
.map_err(|e| CommandError::ExecutionFailed {
command: format!("{command} {}", args.join(" ")),
source: std::io::Error::new(std::io::ErrorKind::InvalidData, e),
})
}
31 changes: 31 additions & 0 deletions packages/dependency-installer/src/detector/ansible.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use tracing::info;

use crate::command::command_exists;
use crate::detector::ToolDetector;
use crate::errors::DetectionError;

/// Detector for `Ansible` tool
pub struct AnsibleDetector;

impl ToolDetector for AnsibleDetector {
fn name(&self) -> &'static str {
"Ansible"
}

fn is_installed(&self) -> Result<bool, DetectionError> {
info!(tool = "ansible", "Checking if Ansible is installed");

let installed = command_exists("ansible").map_err(|e| DetectionError::DetectionFailed {
tool: self.name().to_string(),
source: std::io::Error::other(e.to_string()),
})?;

if installed {
info!(tool = "ansible", "Ansible is installed");
} else {
info!(tool = "ansible", "Ansible is not installed");
}

Ok(installed)
}
}
35 changes: 35 additions & 0 deletions packages/dependency-installer/src/detector/cargo_machete.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use tracing::info;

use crate::command::command_exists;
use crate::detector::ToolDetector;
use crate::errors::DetectionError;

/// Detector for `cargo-machete` tool
pub struct CargoMacheteDetector;

impl ToolDetector for CargoMacheteDetector {
fn name(&self) -> &'static str {
"cargo-machete"
}

fn is_installed(&self) -> Result<bool, DetectionError> {
info!(
tool = "cargo-machete",
"Checking if cargo-machete is installed"
);

let installed =
command_exists("cargo-machete").map_err(|e| DetectionError::DetectionFailed {
tool: self.name().to_string(),
source: std::io::Error::other(e.to_string()),
})?;

if installed {
info!(tool = "cargo-machete", "cargo-machete is installed");
} else {
info!(tool = "cargo-machete", "cargo-machete is not installed");
}

Ok(installed)
}
}
32 changes: 32 additions & 0 deletions packages/dependency-installer/src/detector/lxd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use tracing::info;

use crate::command::command_exists;
use crate::detector::ToolDetector;
use crate::errors::DetectionError;

/// Detector for `LXD` tool
pub struct LxdDetector;

impl ToolDetector for LxdDetector {
fn name(&self) -> &'static str {
"LXD"
}

fn is_installed(&self) -> Result<bool, DetectionError> {
info!(tool = "lxd", "Checking if LXD is installed");

// Check for 'lxc' command (LXD client)
let installed = command_exists("lxc").map_err(|e| DetectionError::DetectionFailed {
tool: self.name().to_string(),
source: std::io::Error::other(e.to_string()),
})?;

if installed {
info!(tool = "lxd", "LXD is installed");
} else {
info!(tool = "lxd", "LXD is not installed");
}

Ok(installed)
}
}
29 changes: 29 additions & 0 deletions packages/dependency-installer/src/detector/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
pub mod ansible;
pub mod cargo_machete;
pub mod lxd;
pub mod opentofu;

use crate::errors::DetectionError;

pub use ansible::AnsibleDetector;
pub use cargo_machete::CargoMacheteDetector;
pub use lxd::LxdDetector;
pub use opentofu::OpenTofuDetector;

/// Trait for detecting if a tool is installed
pub trait ToolDetector {
/// Get the tool name for display purposes
fn name(&self) -> &'static str;

/// Check if the tool is already installed
///
/// # Errors
///
/// Returns an error if the detection process fails
fn is_installed(&self) -> Result<bool, DetectionError>;

/// Get the required version (if applicable)
fn required_version(&self) -> Option<&str> {
None // Default implementation
}
}
31 changes: 31 additions & 0 deletions packages/dependency-installer/src/detector/opentofu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use tracing::info;

use crate::command::command_exists;
use crate::detector::ToolDetector;
use crate::errors::DetectionError;

/// Detector for `OpenTofu` tool
pub struct OpenTofuDetector;

impl ToolDetector for OpenTofuDetector {
fn name(&self) -> &'static str {
"OpenTofu"
}

fn is_installed(&self) -> Result<bool, DetectionError> {
info!(tool = "opentofu", "Checking if OpenTofu is installed");

let installed = command_exists("tofu").map_err(|e| DetectionError::DetectionFailed {
tool: self.name().to_string(),
source: std::io::Error::other(e.to_string()),
})?;

if installed {
info!(tool = "opentofu", "OpenTofu is installed");
} else {
info!(tool = "opentofu", "OpenTofu is not installed");
}

Ok(installed)
}
}
Loading