- Core transaction API with add_package, resolve, commit, rollback - Version tracking for upgrades/downgrades - Simple package info with FFI conversion functions - Comprehensive error handling - Basic test suite - Clean, minimal implementation (~326 lines total)
184 lines
5.9 KiB
Rust
184 lines
5.9 KiB
Rust
//! Simple APT transaction implementation
|
|
//!
|
|
//! Provides a DNF-like transaction interface for APT operations.
|
|
|
|
use std::process::Command;
|
|
use anyhow::{Result, anyhow};
|
|
use std::collections::HashMap;
|
|
|
|
/// Simple APT transaction that mimics DNF's imperative model
|
|
pub struct AptTransaction {
|
|
packages: Vec<String>,
|
|
before_versions: HashMap<String, String>, // package -> version before
|
|
after_versions: HashMap<String, String>, // package -> version after
|
|
}
|
|
|
|
impl AptTransaction {
|
|
/// Create a new transaction
|
|
pub fn new() -> Result<Self> {
|
|
Ok(Self {
|
|
packages: Vec::new(),
|
|
before_versions: HashMap::new(),
|
|
after_versions: HashMap::new(),
|
|
})
|
|
}
|
|
|
|
/// Add a package to the transaction
|
|
pub fn add_package(&mut self, name: &str) -> Result<()> {
|
|
// Verify package exists
|
|
let output = Command::new("apt")
|
|
.args(&["show", name])
|
|
.output()?;
|
|
|
|
if !output.status.success() {
|
|
return Err(anyhow!("Package not found: {}", name));
|
|
}
|
|
|
|
self.packages.push(name.to_string());
|
|
Ok(())
|
|
}
|
|
|
|
/// Resolve dependencies (APT handles this automatically)
|
|
pub fn resolve(&self) -> Result<()> {
|
|
// APT handles dependency resolution automatically
|
|
// Just validate all packages are available
|
|
for package in &self.packages {
|
|
let output = Command::new("apt")
|
|
.args(&["show", package])
|
|
.output()?;
|
|
|
|
if !output.status.success() {
|
|
return Err(anyhow!("Package unavailable: {}", package));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Commit the transaction
|
|
pub fn commit(&mut self) -> Result<()> {
|
|
if self.packages.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
// Get current versions before installation
|
|
self.before_versions = self.get_package_versions()?;
|
|
|
|
// Run apt install with all packages
|
|
let output = Command::new("apt")
|
|
.args(&["install", "-y"])
|
|
.args(&self.packages)
|
|
.output()?;
|
|
|
|
if !output.status.success() {
|
|
let error = String::from_utf8_lossy(&output.stderr);
|
|
return Err(anyhow!("APT installation failed: {}", error));
|
|
}
|
|
|
|
// Get versions after installation
|
|
self.after_versions = self.get_package_versions()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Rollback the transaction by restoring previous versions
|
|
pub fn rollback(&self) -> Result<()> {
|
|
if self.before_versions.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
// Build list of packages to restore to previous versions
|
|
let mut packages_to_restore = Vec::new();
|
|
|
|
for (package, before_version) in &self.before_versions {
|
|
if let Some(after_version) = self.after_versions.get(package) {
|
|
// Only rollback if version changed
|
|
if before_version != after_version {
|
|
packages_to_restore.push(format!("{}={}", package, before_version));
|
|
}
|
|
} else {
|
|
// Package was newly installed, remove it
|
|
packages_to_restore.push(format!("{}", package));
|
|
}
|
|
}
|
|
|
|
if packages_to_restore.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
// Restore previous versions or remove newly installed packages
|
|
let output = Command::new("apt")
|
|
.args(&["install", "-y"])
|
|
.args(&packages_to_restore)
|
|
.output()?;
|
|
|
|
if !output.status.success() {
|
|
let error = String::from_utf8_lossy(&output.stderr);
|
|
return Err(anyhow!("APT rollback failed: {}", error));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get current package versions
|
|
fn get_package_versions(&self) -> Result<HashMap<String, String>> {
|
|
let output = Command::new("dpkg")
|
|
.args(&["-l"])
|
|
.output()?;
|
|
|
|
if !output.status.success() {
|
|
return Err(anyhow!("Failed to get package versions"));
|
|
}
|
|
|
|
let mut versions = HashMap::new();
|
|
let output_str = String::from_utf8_lossy(&output.stdout);
|
|
|
|
for line in output_str.lines() {
|
|
if line.starts_with("ii") {
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
if parts.len() >= 3 {
|
|
let package_name = parts[1];
|
|
let version = parts[2];
|
|
versions.insert(package_name.to_string(), version.to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(versions)
|
|
}
|
|
|
|
/// Get list of packages in transaction
|
|
pub fn packages(&self) -> &[String] {
|
|
&self.packages
|
|
}
|
|
|
|
/// Check if transaction is empty
|
|
pub fn is_empty(&self) -> bool {
|
|
self.packages.is_empty()
|
|
}
|
|
|
|
/// Get list of packages that were changed by this transaction
|
|
pub fn changed_packages(&self) -> Vec<String> {
|
|
let mut changed = Vec::new();
|
|
|
|
for (package, before_version) in &self.before_versions {
|
|
if let Some(after_version) = self.after_versions.get(package) {
|
|
if before_version != after_version {
|
|
changed.push(format!("{}: {} -> {}", package, before_version, after_version));
|
|
}
|
|
} else {
|
|
changed.push(format!("{}: removed", package));
|
|
}
|
|
}
|
|
|
|
// Add newly installed packages
|
|
for package in &self.packages {
|
|
if !self.before_versions.contains_key(package) {
|
|
if let Some(after_version) = self.after_versions.get(package) {
|
|
changed.push(format!("{}: installed {}", package, after_version));
|
|
}
|
|
}
|
|
}
|
|
|
|
changed
|
|
}
|
|
}
|