//! Package management abstraction for the reconciler.
//!
//! Like [`super::systemd`], the production implementation shells out to the
//! host's package manager. It auto-detects which one is present (zypper, dnf,
//! apt, or pacman) so the same manifest works across distros. Tests inject a
//! fake to assert idempotency without touching the system.
use std::io;
use std::process::Command;
/// Side-effecting package operations, injectable for testing.
pub trait PackageManager {
/// Whether `name` is currently installed.
fn is_installed(&self, name: &str) -> io::Result<bool>;
/// Install `name`.
fn install(&mut self, name: &str) -> io::Result<()>;
/// Remove `name`.
fn remove(&mut self, name: &str) -> io::Result<()>;
/// Human-readable backend name, for logging.
fn backend(&self) -> &str;
}
/// Which package manager the host uses.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Backend {
Zypper,
Dnf,
Apt,
Pacman,
}
impl Backend {
fn as_str(self) -> &'static str {
match self {
Backend::Zypper => "zypper",
Backend::Dnf => "dnf",
Backend::Apt => "apt",
Backend::Pacman => "pacman",
}
}
/// The binary whose presence identifies this backend.
fn probe_bin(self) -> &'static str {
match self {
Backend::Zypper => "zypper",
Backend::Dnf => "dnf",
Backend::Apt => "apt-get",
Backend::Pacman => "pacman",
}
}
}
/// Production [`PackageManager`] backed by the detected system tool. If no known
/// manager is found, package operations error (surfaced per-package, so a host
/// without a supported manager fails only the package resources).
#[derive(Debug, Clone, Copy)]
pub struct SystemPackageManager {
backend: Option<Backend>,
}
impl SystemPackageManager {
/// Detect the host's package manager by probing `PATH` in priority order.
pub fn detect() -> Self {
// zypper before dnf: an openSUSE host may have both, but zypper is
// canonical there; apt/pacman are unambiguous.
let backend = [Backend::Zypper, Backend::Dnf, Backend::Apt, Backend::Pacman]
.into_iter()
.find(|b| binary_in_path(b.probe_bin()));
Self { backend }
}
fn backend_or_err(&self) -> io::Result<Backend> {
self.backend.ok_or_else(|| {
io::Error::new(
io::ErrorKind::Unsupported,
"no supported package manager found (zypper/dnf/apt/pacman)",
)
})
}
/// Run a mutating command, mapping a non-zero exit into an error with stderr.
fn run(&self, program: &str, args: &[&str]) -> io::Result<()> {
let mut cmd = Command::new(program);
cmd.args(args);
// apt is interactive by default; make it quiet + non-interactive.
if program == "apt-get" {
cmd.env("DEBIAN_FRONTEND", "noninteractive");
}
let out = cmd.output()?;
if out.status.success() {
Ok(())
} else {
Err(io::Error::other(format!(
"{program} {} exited with {}: {}",
args.join(" "),
out.status,
String::from_utf8_lossy(&out.stderr).trim()
)))
}
}
}
impl PackageManager for SystemPackageManager {
fn is_installed(&self, name: &str) -> io::Result<bool> {
let backend = self.backend_or_err()?;
// Query commands use exit status to report installed/not-installed, so a
// non-zero exit is "not installed", not an error.
let (program, args): (&str, Vec<&str>) = match backend {
// rpm works for both zypper and dnf and is faster than the manager.
Backend::Zypper | Backend::Dnf => ("rpm", vec!["-q", name]),
Backend::Apt => ("dpkg-query", vec!["-W", "-f=${Status}", name]),
Backend::Pacman => ("pacman", vec!["-Q", name]),
};
let out = Command::new(program).args(&args).output()?;
match backend {
Backend::Apt => {
// dpkg-query prints "install ok installed" for present packages.
Ok(out.status.success()
&& String::from_utf8_lossy(&out.stdout).contains("install ok installed"))
}
_ => Ok(out.status.success()),
}
}
fn install(&mut self, name: &str) -> io::Result<()> {
match self.backend_or_err()? {
Backend::Zypper => self.run("zypper", &["--non-interactive", "install", name]),
Backend::Dnf => self.run("dnf", &["install", "-y", name]),
Backend::Apt => self.run("apt-get", &["install", "-y", name]),
Backend::Pacman => self.run("pacman", &["-S", "--noconfirm", name]),
}
}
fn remove(&mut self, name: &str) -> io::Result<()> {
match self.backend_or_err()? {
Backend::Zypper => self.run("zypper", &["--non-interactive", "remove", name]),
Backend::Dnf => self.run("dnf", &["remove", "-y", name]),
Backend::Apt => self.run("apt-get", &["remove", "-y", name]),
Backend::Pacman => self.run("pacman", &["-R", "--noconfirm", name]),
}
}
fn backend(&self) -> &str {
self.backend.map(Backend::as_str).unwrap_or("none")
}
}
/// Whether `bin` is an executable found on `PATH`.
fn binary_in_path(bin: &str) -> bool {
let Some(path) = std::env::var_os("PATH") else {
return false;
};
std::env::split_paths(&path).any(|dir| {
let candidate = dir.join(bin);
std::fs::metadata(&candidate)
.map(|m| m.is_file())
.unwrap_or(false)
})
}
neiam
/devendra
public · Issues · Pulls · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
5.6 KiB · text
History
d69022d