doot/crates/doot-core/src/package/apt.rs
Ray Andrew 0eb4d38392
feat(utils): introduce doot-utils crate with XDG directory helpers
- Add new `doot-utils` crate with `xdg` module for consistent cross-platform directory resolution
- Replace `dirs` crate usage with `doot_utils::xdg` functions in cli, core, and lang crates
- Use XDG layout on all platforms (including macOS) for config, data, cache, and state directories
- Add home_dir(), config_home(), data_home(), cache_home(), and state_home() helpers
- Update dependencies to use doot-utils workspace reference
- Remove unused dirs and related crates from Cargo.lock
- Improve error handling in template rendering and package installation
- Add InstalledCache for package managers to reduce process spawning
- Optimize brew package installation with parallel fetching and sequential installing
- Fix path canonicalization in e2e tests for consistent symlink handling
- Add clippy allowance for large parser errors in doot-lang
2026-06-09 23:24:46 -07:00

181 lines
4.7 KiB
Rust

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<HashSet<String>, 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<bool, PackageError> {
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()
}
}