use serde::{Deserialize, Serialize};
use std::error::Error as StdError;
use std::fmt;
use std::process::Command;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum PackageManager {
Apt,
Yum,
Dnf,
Pacman,
Homebrew,
Chocolatey,
Snap,
Flatpak,
}
#[derive(Debug)]
pub enum PackageManagerError {
NotFound(String),
ExecutionFailed(String),
PermissionDenied(String),
NetworkError(String),
Unknown(String),
}
impl fmt::Display for PackageManagerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PackageManagerError::NotFound(msg) => write!(f, "Package manager not found: {}", msg),
PackageManagerError::ExecutionFailed(msg) => write!(f, "Execution failed: {}", msg),
PackageManagerError::PermissionDenied(msg) => write!(f, "Permission denied: {}", msg),
PackageManagerError::NetworkError(msg) => write!(f, "Network error: {}", msg),
PackageManagerError::Unknown(msg) => write!(f, "Unknown error: {}", msg),
}
}
}
impl StdError for PackageManagerError {}
pub trait PackageManagerOps {
fn update_packages(&self) -> Result<String, PackageManagerError>;
fn upgrade_packages(&self) -> Result<String, PackageManagerError>;
fn install_packages(&self, packages: &[&str]) -> Result<String, PackageManagerError>;
fn is_package_installed(&self, package: &str) -> Result<bool, PackageManagerError>;
fn list_updates(&self) -> Result<Vec<String>, PackageManagerError>;
fn name(&self) -> &str;
fn is_available(&self) -> bool;
}
impl PackageManager {
pub fn detect() -> Result<PackageManager, PackageManagerError> {
let managers = [
PackageManager::Apt,
PackageManager::Dnf,
PackageManager::Yum,
PackageManager::Pacman,
PackageManager::Homebrew,
PackageManager::Chocolatey,
];
for manager in &managers {
if manager.is_available() {
return Ok(manager.clone());
}
}
Err(PackageManagerError::NotFound(
"No supported package manager found on this system".to_string(),
))
}
pub fn detect_all() -> Vec<PackageManager> {
let all_managers = [
PackageManager::Apt,
PackageManager::Dnf,
PackageManager::Yum,
PackageManager::Pacman,
PackageManager::Homebrew,
PackageManager::Chocolatey,
PackageManager::Snap,
PackageManager::Flatpak,
];
all_managers
.iter()
.filter(|manager| manager.is_available())
.cloned()
.collect()
}
}
impl PackageManagerOps for PackageManager {
fn update_packages(&self) -> Result<String, PackageManagerError> {
let cmd = match self {
PackageManager::Apt => vec!["sudo", "apt", "update"],
PackageManager::Dnf => vec!["sudo", "dnf", "check-update"],
PackageManager::Yum => vec!["sudo", "yum", "check-update"],
PackageManager::Pacman => vec!["sudo", "pacman", "-Sy"],
PackageManager::Homebrew => vec!["brew", "update"],
PackageManager::Chocolatey => vec!["choco", "upgrade", "all", "--noop"],
PackageManager::Snap => vec!["sudo", "snap", "refresh", "--list"],
PackageManager::Flatpak => vec!["flatpak", "update", "--appstream"],
};
execute_command(&cmd)
}
fn upgrade_packages(&self) -> Result<String, PackageManagerError> {
let cmd = match self {
PackageManager::Apt => vec!["sudo", "apt", "upgrade", "-y"],
PackageManager::Dnf => vec!["sudo", "dnf", "upgrade", "-y"],
PackageManager::Yum => vec!["sudo", "yum", "upgrade", "-y"],
PackageManager::Pacman => vec!["sudo", "pacman", "-Syu", "--noconfirm"],
PackageManager::Homebrew => vec!["brew", "upgrade"],
PackageManager::Chocolatey => vec!["choco", "upgrade", "all", "-y"],
PackageManager::Snap => vec!["sudo", "snap", "refresh"],
PackageManager::Flatpak => vec!["flatpak", "update", "-y"],
};
execute_command(&cmd)
}
fn install_packages(&self, packages: &[&str]) -> Result<String, PackageManagerError> {
if packages.is_empty() {
return Ok("No packages to install".to_string());
}
let mut cmd = match self {
PackageManager::Apt => vec!["sudo", "apt", "install", "-y"],
PackageManager::Dnf => vec!["sudo", "dnf", "install", "-y"],
PackageManager::Yum => vec!["sudo", "yum", "install", "-y"],
PackageManager::Pacman => vec!["sudo", "pacman", "-S", "--noconfirm"],
PackageManager::Homebrew => vec!["brew", "install"],
PackageManager::Chocolatey => vec!["choco", "install", "-y"],
PackageManager::Snap => vec!["sudo", "snap", "install"],
PackageManager::Flatpak => vec!["flatpak", "install", "-y"],
};
cmd.extend(packages);
execute_command(&cmd)
}
fn is_package_installed(&self, package: &str) -> Result<bool, PackageManagerError> {
let cmd = match self {
PackageManager::Apt => vec!["dpkg", "-l", package],
PackageManager::Dnf => vec!["dnf", "list", "installed", package],
PackageManager::Yum => vec!["yum", "list", "installed", package],
PackageManager::Pacman => vec!["pacman", "-Q", package],
PackageManager::Homebrew => vec!["brew", "list", package],
PackageManager::Chocolatey => vec!["choco", "list", "--local-only", package],
PackageManager::Snap => vec!["snap", "list", package],
PackageManager::Flatpak => vec!["flatpak", "list", "--app", package],
};
match execute_command(&cmd) {
Ok(_) => Ok(true),
Err(PackageManagerError::ExecutionFailed(_)) => Ok(false),
Err(e) => Err(e),
}
}
fn list_updates(&self) -> Result<Vec<String>, PackageManagerError> {
let cmd = match self {
PackageManager::Apt => vec!["apt", "list", "--upgradable"],
PackageManager::Dnf => vec!["dnf", "list", "--upgrades"],
PackageManager::Yum => vec!["yum", "list", "updates"],
PackageManager::Pacman => vec!["pacman", "-Qu"],
PackageManager::Homebrew => vec!["brew", "outdated"],
PackageManager::Chocolatey => vec!["choco", "outdated"],
PackageManager::Snap => vec!["snap", "refresh", "--list"],
PackageManager::Flatpak => vec!["flatpak", "remote-ls", "--updates"],
};
let output = execute_command(&cmd)?;
let updates: Vec<String> = output
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| line.trim().to_string())
.collect();
Ok(updates)
}
fn name(&self) -> &str {
match self {
PackageManager::Apt => "apt",
PackageManager::Dnf => "dnf",
PackageManager::Yum => "yum",
PackageManager::Pacman => "pacman",
PackageManager::Homebrew => "brew",
PackageManager::Chocolatey => "choco",
PackageManager::Snap => "snap",
PackageManager::Flatpak => "flatpak",
}
}
fn is_available(&self) -> bool {
let cmd = match self {
PackageManager::Apt => "apt",
PackageManager::Dnf => "dnf",
PackageManager::Yum => "yum",
PackageManager::Pacman => "pacman",
PackageManager::Homebrew => "brew",
PackageManager::Chocolatey => "choco",
PackageManager::Snap => "snap",
PackageManager::Flatpak => "flatpak",
};
Command::new("which")
.arg(cmd)
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
}
fn execute_command(cmd: &[&str]) -> Result<String, PackageManagerError> {
if cmd.is_empty() {
return Err(PackageManagerError::ExecutionFailed(
"Empty command".to_string(),
));
}
let mut command = Command::new(cmd[0]);
if cmd.len() > 1 {
command.args(&cmd[1..]);
}
match command.output() {
Ok(output) => {
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("Permission denied") || stderr.contains("permission denied") {
Err(PackageManagerError::PermissionDenied(stderr.to_string()))
} else if stderr.contains("network") || stderr.contains("Network") {
Err(PackageManagerError::NetworkError(stderr.to_string()))
} else {
Err(PackageManagerError::ExecutionFailed(stderr.to_string()))
}
}
}
Err(e) => Err(PackageManagerError::ExecutionFailed(e.to_string())),
}
}
pub fn get_build_dependencies() -> Vec<&'static str> {
if cfg!(target_os = "linux") {
vec![
"build-essential",
"pkg-config",
"libssl-dev",
"git",
"curl",
"libudev-dev",
]
} else if cfg!(target_os = "macos") {
vec!["pkg-config", "openssl", "git", "curl"]
} else {
vec!["git", "curl"]
}
}
pub fn map_package_name<'a>(package: &'a str, manager: &PackageManager) -> &'a str {
match (package, manager) {
("build-essential", PackageManager::Dnf) => "groupinstall \"Development Tools\"",
("build-essential", PackageManager::Yum) => "groupinstall \"Development Tools\"",
("build-essential", PackageManager::Pacman) => "base-devel",
("libssl-dev", PackageManager::Dnf) => "openssl-devel",
("libssl-dev", PackageManager::Yum) => "openssl-devel",
("libssl-dev", PackageManager::Pacman) => "openssl",
("libssl-dev", PackageManager::Homebrew) => "openssl",
("libudev-dev", PackageManager::Dnf) => "systemd-devel",
("libudev-dev", PackageManager::Yum) => "systemd-devel",
("libudev-dev", PackageManager::Pacman) => "systemd",
_ => package,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_package_manager_detection() {
let detected = PackageManager::detect_all();
assert!(
!detected.is_empty(),
"At least one package manager should be detected"
);
}
#[test]
fn test_package_name_mapping() {
assert_eq!(
map_package_name("libssl-dev", &PackageManager::Dnf),
"openssl-devel"
);
assert_eq!(
map_package_name("libssl-dev", &PackageManager::Homebrew),
"openssl"
);
assert_eq!(
map_package_name("unknown-package", &PackageManager::Apt),
"unknown-package"
);
}
#[test]
fn test_build_dependencies() {
let deps = get_build_dependencies();
assert!(!deps.is_empty(), "Build dependencies should not be empty");
assert!(deps.contains(&"git"), "Should include git");
assert!(deps.contains(&"curl"), "Should include curl");
}
}