421 lines
13 KiB
Rust
421 lines
13 KiB
Rust
use std::process::Command;
|
|
use crate::process::CommandResult;
|
|
|
|
/// Error type for package management operations
|
|
#[derive(Debug)]
|
|
pub enum PackageError {
|
|
/// Command failed with error message
|
|
CommandFailed(String),
|
|
/// Command execution failed with IO error
|
|
CommandExecutionFailed(std::io::Error),
|
|
/// Unsupported platform
|
|
UnsupportedPlatform(String),
|
|
/// Other error
|
|
Other(String),
|
|
}
|
|
|
|
impl std::fmt::Display for PackageError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
PackageError::CommandFailed(msg) => write!(f, "Command failed: {}", msg),
|
|
PackageError::CommandExecutionFailed(e) => write!(f, "Command execution failed: {}", e),
|
|
PackageError::UnsupportedPlatform(msg) => write!(f, "Unsupported platform: {}", msg),
|
|
PackageError::Other(msg) => write!(f, "Error: {}", msg),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for PackageError {}
|
|
|
|
/// Platform enum for detecting the current operating system
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub enum Platform {
|
|
/// Ubuntu Linux
|
|
Ubuntu,
|
|
/// macOS
|
|
MacOS,
|
|
/// Unknown platform
|
|
Unknown,
|
|
}
|
|
|
|
impl Platform {
|
|
/// Detect the current platform
|
|
pub fn detect() -> Self {
|
|
// Check for macOS
|
|
if std::path::Path::new("/usr/bin/sw_vers").exists() {
|
|
return Platform::MacOS;
|
|
}
|
|
|
|
// Check for Ubuntu
|
|
if std::path::Path::new("/etc/lsb-release").exists() {
|
|
// Read the file to confirm it's Ubuntu
|
|
if let Ok(content) = std::fs::read_to_string("/etc/lsb-release") {
|
|
if content.contains("Ubuntu") {
|
|
return Platform::Ubuntu;
|
|
}
|
|
}
|
|
}
|
|
|
|
Platform::Unknown
|
|
}
|
|
}
|
|
|
|
/// Thread-local storage for debug flag
|
|
thread_local! {
|
|
static DEBUG: std::cell::RefCell<bool> = std::cell::RefCell::new(false);
|
|
}
|
|
|
|
/// Set the debug flag for the current thread
|
|
pub fn set_thread_local_debug(debug: bool) {
|
|
DEBUG.with(|cell| {
|
|
*cell.borrow_mut() = debug;
|
|
});
|
|
}
|
|
|
|
/// Get the debug flag for the current thread
|
|
pub fn thread_local_debug() -> bool {
|
|
DEBUG.with(|cell| {
|
|
*cell.borrow()
|
|
})
|
|
}
|
|
|
|
/// Execute a package management command and return the result
|
|
pub fn execute_package_command(args: &[&str], debug: bool) -> Result<CommandResult, PackageError> {
|
|
// Save the current debug flag
|
|
let previous_debug = thread_local_debug();
|
|
|
|
// Set the thread-local debug flag
|
|
set_thread_local_debug(debug);
|
|
|
|
if debug {
|
|
println!("Executing command: {}", args.join(" "));
|
|
}
|
|
|
|
let output = Command::new(args[0])
|
|
.args(&args[1..])
|
|
.output();
|
|
|
|
// Restore the previous debug flag
|
|
set_thread_local_debug(previous_debug);
|
|
|
|
match output {
|
|
Ok(output) => {
|
|
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
|
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
|
|
let result = CommandResult {
|
|
stdout,
|
|
stderr,
|
|
success: output.status.success(),
|
|
code: output.status.code().unwrap_or(-1),
|
|
};
|
|
|
|
// Always output stdout/stderr when debug is true
|
|
if debug {
|
|
if !result.stdout.is_empty() {
|
|
println!("Command stdout: {}", result.stdout);
|
|
}
|
|
|
|
if !result.stderr.is_empty() {
|
|
println!("Command stderr: {}", result.stderr);
|
|
}
|
|
|
|
if result.success {
|
|
println!("Command succeeded with code {}", result.code);
|
|
} else {
|
|
println!("Command failed with code {}", result.code);
|
|
}
|
|
}
|
|
|
|
if result.success {
|
|
Ok(result)
|
|
} else {
|
|
// If command failed and debug is false, output stderr
|
|
if !debug {
|
|
println!("Command failed with code {}: {}", result.code, result.stderr.trim());
|
|
}
|
|
Err(PackageError::CommandFailed(format!("Command failed with code {}: {}",
|
|
result.code, result.stderr.trim())))
|
|
}
|
|
},
|
|
Err(e) => {
|
|
// Always output error information
|
|
println!("Command execution failed: {}", e);
|
|
Err(PackageError::CommandExecutionFailed(e))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Trait for package managers
|
|
pub trait PackageManager {
|
|
/// Install a package
|
|
fn install(&self, package: &str) -> Result<CommandResult, PackageError>;
|
|
|
|
/// Remove a package
|
|
fn remove(&self, package: &str) -> Result<CommandResult, PackageError>;
|
|
|
|
/// Update package lists
|
|
fn update(&self) -> Result<CommandResult, PackageError>;
|
|
|
|
/// Upgrade installed packages
|
|
fn upgrade(&self) -> Result<CommandResult, PackageError>;
|
|
|
|
/// List installed packages
|
|
fn list_installed(&self) -> Result<Vec<String>, PackageError>;
|
|
|
|
/// Search for packages
|
|
fn search(&self, query: &str) -> Result<Vec<String>, PackageError>;
|
|
|
|
/// Check if a package is installed
|
|
fn is_installed(&self, package: &str) -> Result<bool, PackageError>;
|
|
}
|
|
|
|
/// APT package manager for Ubuntu
|
|
pub struct AptPackageManager {
|
|
debug: bool,
|
|
}
|
|
|
|
impl AptPackageManager {
|
|
/// Create a new APT package manager
|
|
pub fn new(debug: bool) -> Self {
|
|
Self { debug }
|
|
}
|
|
}
|
|
|
|
impl PackageManager for AptPackageManager {
|
|
fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
|
|
// Use -y to make it non-interactive and --quiet to reduce output
|
|
execute_package_command(&["apt-get", "install", "-y", "--quiet", package], self.debug)
|
|
}
|
|
|
|
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
|
|
// Use -y to make it non-interactive and --quiet to reduce output
|
|
execute_package_command(&["apt-get", "remove", "-y", "--quiet", package], self.debug)
|
|
}
|
|
|
|
fn update(&self) -> Result<CommandResult, PackageError> {
|
|
// Use -y to make it non-interactive and --quiet to reduce output
|
|
execute_package_command(&["apt-get", "update", "-y", "--quiet"], self.debug)
|
|
}
|
|
|
|
fn upgrade(&self) -> Result<CommandResult, PackageError> {
|
|
// Use -y to make it non-interactive and --quiet to reduce output
|
|
execute_package_command(&["apt-get", "upgrade", "-y", "--quiet"], self.debug)
|
|
}
|
|
|
|
fn list_installed(&self) -> Result<Vec<String>, PackageError> {
|
|
let result = execute_package_command(&["dpkg", "--get-selections"], self.debug)?;
|
|
let packages = result.stdout
|
|
.lines()
|
|
.filter_map(|line| {
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
if parts.len() >= 2 && parts[1] == "install" {
|
|
Some(parts[0].to_string())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
Ok(packages)
|
|
}
|
|
|
|
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
|
|
let result = execute_package_command(&["apt-cache", "search", query], self.debug)?;
|
|
let packages = result.stdout
|
|
.lines()
|
|
.map(|line| {
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
if !parts.is_empty() {
|
|
parts[0].to_string()
|
|
} else {
|
|
String::new()
|
|
}
|
|
})
|
|
.filter(|s| !s.is_empty())
|
|
.collect();
|
|
Ok(packages)
|
|
}
|
|
|
|
fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
|
|
let result = execute_package_command(&["dpkg", "-s", package], self.debug);
|
|
match result {
|
|
Ok(cmd_result) => Ok(cmd_result.success),
|
|
Err(_) => Ok(false),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Homebrew package manager for macOS
|
|
pub struct BrewPackageManager {
|
|
debug: bool,
|
|
}
|
|
|
|
impl BrewPackageManager {
|
|
/// Create a new Homebrew package manager
|
|
pub fn new(debug: bool) -> Self {
|
|
Self { debug }
|
|
}
|
|
}
|
|
|
|
impl PackageManager for BrewPackageManager {
|
|
fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
|
|
// Use --quiet to reduce output
|
|
execute_package_command(&["brew", "install", "--quiet", package], self.debug)
|
|
}
|
|
|
|
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
|
|
// Use --quiet to reduce output
|
|
execute_package_command(&["brew", "uninstall", "--quiet", package], self.debug)
|
|
}
|
|
|
|
fn update(&self) -> Result<CommandResult, PackageError> {
|
|
// Use --quiet to reduce output
|
|
execute_package_command(&["brew", "update", "--quiet"], self.debug)
|
|
}
|
|
|
|
fn upgrade(&self) -> Result<CommandResult, PackageError> {
|
|
// Use --quiet to reduce output
|
|
execute_package_command(&["brew", "upgrade", "--quiet"], self.debug)
|
|
}
|
|
|
|
fn list_installed(&self) -> Result<Vec<String>, PackageError> {
|
|
let result = execute_package_command(&["brew", "list", "--formula"], self.debug)?;
|
|
let packages = result.stdout
|
|
.lines()
|
|
.map(|line| line.trim().to_string())
|
|
.filter(|s| !s.is_empty())
|
|
.collect();
|
|
Ok(packages)
|
|
}
|
|
|
|
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
|
|
let result = execute_package_command(&["brew", "search", query], self.debug)?;
|
|
let packages = result.stdout
|
|
.lines()
|
|
.map(|line| line.trim().to_string())
|
|
.filter(|s| !s.is_empty())
|
|
.collect();
|
|
Ok(packages)
|
|
}
|
|
|
|
fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
|
|
let result = execute_package_command(&["brew", "list", package], self.debug);
|
|
match result {
|
|
Ok(cmd_result) => Ok(cmd_result.success),
|
|
Err(_) => Ok(false),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// PackHero factory for package management
|
|
pub struct PackHero {
|
|
platform: Platform,
|
|
debug: bool,
|
|
}
|
|
|
|
impl PackHero {
|
|
/// Create a new PackHero instance
|
|
pub fn new() -> Self {
|
|
let platform = Platform::detect();
|
|
Self {
|
|
platform,
|
|
debug: false,
|
|
}
|
|
}
|
|
|
|
/// Set the debug mode
|
|
pub fn set_debug(&mut self, debug: bool) -> &mut Self {
|
|
self.debug = debug;
|
|
self
|
|
}
|
|
|
|
/// Get the debug mode
|
|
pub fn debug(&self) -> bool {
|
|
self.debug
|
|
}
|
|
|
|
/// Get the detected platform
|
|
pub fn platform(&self) -> Platform {
|
|
self.platform
|
|
}
|
|
|
|
/// Get a package manager for the current platform
|
|
fn get_package_manager(&self) -> Result<Box<dyn PackageManager>, PackageError> {
|
|
match self.platform {
|
|
Platform::Ubuntu => Ok(Box::new(AptPackageManager::new(self.debug))),
|
|
Platform::MacOS => Ok(Box::new(BrewPackageManager::new(self.debug))),
|
|
Platform::Unknown => Err(PackageError::UnsupportedPlatform("Unsupported platform".to_string())),
|
|
}
|
|
}
|
|
|
|
/// Install a package
|
|
pub fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
|
|
let pm = self.get_package_manager()?;
|
|
pm.install(package)
|
|
}
|
|
|
|
/// Remove a package
|
|
pub fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
|
|
let pm = self.get_package_manager()?;
|
|
pm.remove(package)
|
|
}
|
|
|
|
/// Update package lists
|
|
pub fn update(&self) -> Result<CommandResult, PackageError> {
|
|
let pm = self.get_package_manager()?;
|
|
pm.update()
|
|
}
|
|
|
|
/// Upgrade installed packages
|
|
pub fn upgrade(&self) -> Result<CommandResult, PackageError> {
|
|
let pm = self.get_package_manager()?;
|
|
pm.upgrade()
|
|
}
|
|
|
|
/// List installed packages
|
|
pub fn list_installed(&self) -> Result<Vec<String>, PackageError> {
|
|
let pm = self.get_package_manager()?;
|
|
pm.list_installed()
|
|
}
|
|
|
|
/// Search for packages
|
|
pub fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
|
|
let pm = self.get_package_manager()?;
|
|
pm.search(query)
|
|
}
|
|
|
|
/// Check if a package is installed
|
|
pub fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
|
|
let pm = self.get_package_manager()?;
|
|
pm.is_installed(package)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_platform_detection() {
|
|
// This test will return different results depending on the platform it's run on
|
|
let platform = Platform::detect();
|
|
println!("Detected platform: {:?}", platform);
|
|
|
|
// Just ensure it doesn't panic
|
|
assert!(true);
|
|
}
|
|
|
|
#[test]
|
|
fn test_debug_flag() {
|
|
// Test setting and getting the debug flag
|
|
set_thread_local_debug(true);
|
|
assert_eq!(thread_local_debug(), true);
|
|
|
|
set_thread_local_debug(false);
|
|
assert_eq!(thread_local_debug(), false);
|
|
}
|
|
|
|
// More tests would be added for each platform-specific implementation
|
|
// These would likely be integration tests that are conditionally compiled
|
|
// based on the platform they're running on
|
|
} |