use crate::{command, ok};
use miette::IntoDiagnostic;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PackageManager {
Apt,
Dnf,
Pacman,
Zypper,
Brew,
}
impl PackageManager {
#[must_use]
pub fn detect() -> Option<Self> {
if std::process::Command::new("apt-get")
.arg("--version")
.output()
.is_ok()
{
Some(PackageManager::Apt)
} else if std::process::Command::new("dnf")
.arg("--version")
.output()
.is_ok()
{
Some(PackageManager::Dnf)
} else if std::process::Command::new("pacman")
.arg("-V")
.output()
.is_ok()
{
Some(PackageManager::Pacman)
} else if std::process::Command::new("zypper")
.arg("--version")
.output()
.is_ok()
{
Some(PackageManager::Zypper)
} else if std::process::Command::new("brew")
.arg("--version")
.output()
.is_ok()
{
Some(PackageManager::Brew)
} else {
None
}
}
#[must_use]
pub fn check_command(&self) -> (&'static str, &'static [&'static str]) {
match self {
PackageManager::Apt => ("dpkg-query", &["-s"]),
PackageManager::Dnf | PackageManager::Zypper => ("rpm", &["-q"]),
PackageManager::Pacman => ("pacman", &["-Q"]),
PackageManager::Brew => ("brew", &["list"]),
}
}
#[must_use]
pub fn install_command(&self) -> (&'static str, &'static [&'static str]) {
match self {
PackageManager::Apt => ("apt", &["install", "-y"]),
PackageManager::Dnf => ("dnf", &["install", "-y"]),
PackageManager::Pacman => ("pacman", &["-S", "--noconfirm"]),
PackageManager::Zypper => ("zypper", &["install", "-y"]),
PackageManager::Brew => ("brew", &["install"]),
}
}
#[must_use]
pub fn requires_sudo(&self) -> bool { !matches!(self, PackageManager::Brew) }
}
pub async fn check_if_package_is_installed(package_name: &str) -> miette::Result<bool> {
let pkg_mgr = PackageManager::detect()
.ok_or_else(|| miette::miette!("No supported package manager found"))?;
let (cmd, base_args) = pkg_mgr.check_command();
let output = command!(
program => cmd,
args => base_args[0], package_name
)
.output()
.await
.into_diagnostic()?;
ok!(output.status.success())
}
pub async fn install_package(package_name: &str) -> miette::Result<()> {
let pkg_mgr = PackageManager::detect()
.ok_or_else(|| miette::miette!("No supported package manager found"))?;
let (cmd, base_args) = pkg_mgr.install_command();
let command_result = if pkg_mgr.requires_sudo() {
let mut args = vec![cmd];
args.extend(base_args.iter().copied());
args.push(package_name);
command!(
program => "sudo",
args => args[0], args[1], args[2], args[3]
)
.output()
.await
.into_diagnostic()?
} else {
let mut args: Vec<&str> = base_args.to_vec();
args.push(package_name);
command!(
program => cmd,
args => args[0], args[1]
)
.output()
.await
.into_diagnostic()?
};
if command_result.status.success() {
ok!()
} else {
miette::bail!(
"Failed to install package '{}' with {}: {:?}",
package_name,
cmd,
String::from_utf8_lossy(&command_result.stderr)
);
}
}
#[must_use]
pub fn get_package_manager() -> Option<PackageManager> { PackageManager::detect() }
#[cfg(test)]
mod tests_package_manager {
use super::*;
use crate::{TTYResult, is_output_interactive};
#[test]
fn test_package_manager_detection() {
let pkg_mgr = PackageManager::detect();
if let Some(pm) = pkg_mgr {
println!("Detected package manager: {pm:?}");
} else {
println!("No package manager detected (this is OK in some environments)");
}
}
#[tokio::test]
async fn test_check_if_package_is_installed() {
if let TTYResult::IsNotInteractive = is_output_interactive() {
return;
}
let package_name = "bash";
let result = check_if_package_is_installed(package_name).await;
if let Ok(is_installed) = result {
assert!(is_installed, "bash should be installed");
}
}
#[tokio::test]
async fn test_install_nonexistent_package() {
if let TTYResult::IsNotInteractive = is_output_interactive() {
return;
}
let package_name = "this_package_definitely_does_not_exist_12345";
let result = install_package(package_name).await;
assert!(result.is_err());
}
}