use super::{InstalledCache, PackageError, PackageManager}; use std::collections::HashSet; use std::io::Write; use std::process::{Command, Stdio}; /// APT package manager (Debian/Ubuntu). pub struct Apt { dry_run: bool, use_sudo: bool, installed_cache: InstalledCache, } impl Apt { pub fn new() -> Self { Self { dry_run: false, use_sudo: true, installed_cache: InstalledCache::default(), } } /// Loads all installed package names in one `dpkg-query` call. fn load_installed() -> Result, PackageError> { let output = Command::new("dpkg-query") .args(["-W", "-f", "${Package}\n"]) .output()?; let mut set = HashSet::new(); if output.status.success() { for line in String::from_utf8_lossy(&output.stdout).lines() { let name = line.trim(); if !name.is_empty() { set.insert(name.to_lowercase()); } } } Ok(set) } pub fn dry_run(mut self, dry_run: bool) -> Self { self.dry_run = dry_run; self } pub fn use_sudo(mut self, use_sudo: bool) -> Self { self.use_sudo = use_sudo; self } #[tracing::instrument(skip(self))] fn run_apt(&self, args: &[&str]) -> Result<(), PackageError> { if self.dry_run { let prefix = if self.use_sudo { "sudo " } else { "" }; println!("[dry-run] {}apt {}", prefix, args.join(" ")); return Ok(()); } let output = if self.use_sudo { Command::new("sudo").arg("apt").args(args).output()? } else { Command::new("apt").args(args).output()? }; if !output.status.success() { return Err(PackageError::InstallFailed { package: args.join(" "), message: String::from_utf8_lossy(&output.stderr).to_string(), }); } Ok(()) } #[tracing::instrument(skip(self, password))] fn run_apt_with_password(&self, args: &[&str], password: &str) -> Result<(), PackageError> { if self.dry_run { println!("[dry-run] sudo apt {}", args.join(" ")); return Ok(()); } let mut child = Command::new("sudo") .arg("-S") .arg("apt") .args(args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn()?; if let Some(mut stdin) = child.stdin.take() { writeln!(stdin, "{}", password).ok(); } let output = child.wait_with_output()?; if !output.status.success() { return Err(PackageError::InstallFailed { package: args.join(" "), message: String::from_utf8_lossy(&output.stderr).to_string(), }); } Ok(()) } } impl PackageManager for Apt { fn name(&self) -> &'static str { "apt" } fn is_available(&self) -> bool { std::path::Path::new("/usr/bin/apt").exists() } fn needs_sudo(&self) -> bool { self.use_sudo } fn install(&self, packages: &[String]) -> Result<(), PackageError> { if packages.is_empty() { return Ok(()); } let mut args = vec!["install", "-y"]; for pkg in packages { args.push(pkg); } let result = self.run_apt(&args); self.installed_cache.invalidate(); result } fn install_with_sudo(&self, packages: &[String], password: &str) -> Result<(), PackageError> { if packages.is_empty() { return Ok(()); } let mut args = vec!["install", "-y"]; for pkg in packages { args.push(pkg); } let result = self.run_apt_with_password(&args, password); self.installed_cache.invalidate(); result } fn uninstall(&self, packages: &[String]) -> Result<(), PackageError> { if packages.is_empty() { return Ok(()); } let mut args = vec!["remove", "-y"]; for pkg in packages { args.push(pkg); } let result = self.run_apt(&args); self.installed_cache.invalidate(); result } fn is_installed(&self, package: &str) -> Result { self.installed_cache.contains(package, Self::load_installed) } fn update(&self) -> Result<(), PackageError> { self.run_apt(&["update"]) } fn upgrade(&self) -> Result<(), PackageError> { self.run_apt(&["upgrade", "-y"]) } } impl Default for Apt { fn default() -> Self { Self::new() } }