Initial commit: apt-wrapper - Simple APT transaction wrapper
- 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)
This commit is contained in:
commit
7aaefb9957
10 changed files with 761 additions and 0 deletions
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "apt-wrapper"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["apt-ostree team"]
|
||||||
|
description = "A simple DNF-like API wrapper around APT for apt-ostree"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/apt-ostree/apt-wrapper"
|
||||||
|
keywords = ["apt", "package-management", "debian", "ubuntu"]
|
||||||
|
categories = ["os::linux-apis", "development-tools::build-utils"]
|
||||||
|
|
||||||
|
# This is part of the apt-ostree workspace
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0"
|
||||||
|
thiserror = "1.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3.0"
|
||||||
195
README.md
Normal file
195
README.md
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
# APT Wrapper
|
||||||
|
|
||||||
|
A simple DNF-like API wrapper around APT for porting rpm-ostree to apt-ostree.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This library provides a simple transaction interface that mimics DNF's imperative model, making it easier to adapt rpm-ostree code for Debian/Ubuntu systems.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Simple transaction interface**: `add_package()`, `resolve()`, `commit()`, `rollback()`
|
||||||
|
- **DNF-like API**: Easy to port from rpm-ostree
|
||||||
|
- **Version-based rollback**: Track versions and restore previous states
|
||||||
|
- **Minimal dependencies**: Only `anyhow` and `thiserror`
|
||||||
|
- **~250 lines total**: Focused and maintainable
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use apt_wrapper::{AptTransaction, init};
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Initialize
|
||||||
|
init()?;
|
||||||
|
|
||||||
|
// Create transaction
|
||||||
|
let mut tx = AptTransaction::new()?;
|
||||||
|
|
||||||
|
// Add packages
|
||||||
|
tx.add_package("vim")?;
|
||||||
|
tx.add_package("git")?;
|
||||||
|
|
||||||
|
// Resolve dependencies
|
||||||
|
tx.resolve()?;
|
||||||
|
|
||||||
|
// Commit transaction
|
||||||
|
tx.commit()?;
|
||||||
|
|
||||||
|
// If something goes wrong, rollback
|
||||||
|
// tx.rollback()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### AptTransaction
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct AptTransaction {
|
||||||
|
packages: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AptTransaction {
|
||||||
|
pub fn new() -> Result<Self>; // Create new transaction
|
||||||
|
pub fn add_package(&mut self, name: &str) -> Result<()>; // Add package
|
||||||
|
pub fn resolve(&self) -> Result<()>; // Resolve dependencies
|
||||||
|
pub fn commit(&mut self) -> Result<()>; // Commit transaction
|
||||||
|
pub fn rollback(&self) -> Result<()>; // Rollback transaction
|
||||||
|
pub fn packages(&self) -> &[String]; // Get package list
|
||||||
|
pub fn changed_packages(&self) -> Vec<String>; // Get changed packages
|
||||||
|
pub fn is_empty(&self) -> bool; // Check if empty
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Utility Functions
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn init() -> Result<()>; // Initialize
|
||||||
|
pub fn search_packages(query: &str) -> Result<Vec<String>>; // Search packages
|
||||||
|
pub fn is_package_installed(name: &str) -> Result<bool>; // Check installed
|
||||||
|
pub fn get_package_info(name: &str) -> Result<AptPackage>; // Get package info
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Add to your `Cargo.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
apt-wrapper = "0.1.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Transaction
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use apt_wrapper::AptTransaction;
|
||||||
|
|
||||||
|
let mut tx = AptTransaction::new()?;
|
||||||
|
tx.add_package("vim")?;
|
||||||
|
tx.add_package("git")?;
|
||||||
|
tx.resolve()?;
|
||||||
|
tx.commit()?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search Packages
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use apt_wrapper::search_packages;
|
||||||
|
|
||||||
|
let packages = search_packages("editor")?;
|
||||||
|
for package in packages {
|
||||||
|
println!("Found: {}", package);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Installation
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use apt_wrapper::is_package_installed;
|
||||||
|
|
||||||
|
if is_package_installed("vim")? {
|
||||||
|
println!("vim is installed");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rollback Support
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use apt_wrapper::AptTransaction;
|
||||||
|
|
||||||
|
let mut tx = AptTransaction::new()?;
|
||||||
|
tx.add_package("vim")?;
|
||||||
|
tx.add_package("git")?;
|
||||||
|
tx.resolve()?;
|
||||||
|
|
||||||
|
// Commit the transaction
|
||||||
|
tx.commit()?;
|
||||||
|
|
||||||
|
// If something goes wrong later, rollback
|
||||||
|
tx.rollback()?;
|
||||||
|
|
||||||
|
// Check what was changed
|
||||||
|
println!("Changed packages: {:?}", tx.changed_packages());
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --example simple_usage
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design Philosophy
|
||||||
|
|
||||||
|
This wrapper is designed to be:
|
||||||
|
|
||||||
|
1. **Simple**: Minimal API surface, easy to understand
|
||||||
|
2. **Focused**: Only what's needed for apt-ostree porting
|
||||||
|
3. **DNF-like**: Familiar interface for rpm-ostree developers
|
||||||
|
4. **Minimal**: ~200 lines total, no complex abstractions
|
||||||
|
|
||||||
|
## Differences from DNF
|
||||||
|
|
||||||
|
- **APT is declarative**: Dependencies are resolved automatically
|
||||||
|
- **No complex repo management**: APT uses simple text files
|
||||||
|
- **Simpler error handling**: APT provides clear error messages
|
||||||
|
- **No transaction rollback**: APT doesn't have built-in rollback
|
||||||
|
|
||||||
|
## OSTree Integration
|
||||||
|
|
||||||
|
For atomic operations, use OSTree's native checkpoint/rollback:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 1. Create OSTree checkpoint
|
||||||
|
let checkpoint = ostree_create_checkpoint()?;
|
||||||
|
|
||||||
|
// 2. Run APT transaction
|
||||||
|
let mut tx = AptTransaction::new()?;
|
||||||
|
tx.add_package("vim")?;
|
||||||
|
tx.commit()?;
|
||||||
|
|
||||||
|
// 3. Commit or rollback based on result
|
||||||
|
if success {
|
||||||
|
ostree_commit_changes()?;
|
||||||
|
} else {
|
||||||
|
ostree_rollback_to_checkpoint(checkpoint)?;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - see [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
This is a focused tool for apt-ostree. Contributions should maintain simplicity and focus on the core use case.
|
||||||
45
examples/simple_usage.rs
Normal file
45
examples/simple_usage.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
//! Simple usage example for APT wrapper
|
||||||
|
|
||||||
|
use apt_wrapper::{AptTransaction, init, search_packages};
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("=== Simple APT Wrapper Example ===");
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
init()?;
|
||||||
|
println!("✓ APT wrapper initialized");
|
||||||
|
|
||||||
|
// Search for packages
|
||||||
|
let packages = search_packages("vim")?;
|
||||||
|
println!("Found {} packages matching 'vim'", packages.len());
|
||||||
|
|
||||||
|
// Create transaction
|
||||||
|
let mut tx = AptTransaction::new()?;
|
||||||
|
println!("✓ Created transaction");
|
||||||
|
|
||||||
|
// Add packages
|
||||||
|
tx.add_package("apt")?;
|
||||||
|
tx.add_package("curl")?;
|
||||||
|
println!("✓ Added packages to transaction");
|
||||||
|
|
||||||
|
// Resolve dependencies
|
||||||
|
tx.resolve()?;
|
||||||
|
println!("✓ Dependencies resolved");
|
||||||
|
|
||||||
|
// Show what would be installed
|
||||||
|
println!("Packages in transaction: {:?}", tx.packages());
|
||||||
|
|
||||||
|
// Note: We don't actually commit in the example to avoid installing packages
|
||||||
|
// tx.commit()?;
|
||||||
|
// println!("✓ Transaction committed");
|
||||||
|
|
||||||
|
// If commit failed, you could rollback:
|
||||||
|
// tx.rollback()?;
|
||||||
|
// println!("✓ Transaction rolled back");
|
||||||
|
// println!("Changed packages: {:?}", tx.changed_packages());
|
||||||
|
|
||||||
|
println!("✓ Transaction ready (not committed in example)");
|
||||||
|
|
||||||
|
println!("=== Example completed ===");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
47
src/bridge.rs
Normal file
47
src/bridge.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
use cxx::bridge;
|
||||||
|
|
||||||
|
#[bridge]
|
||||||
|
mod ffi {
|
||||||
|
/// C++ side representation of AptPackage
|
||||||
|
extern "C++" {
|
||||||
|
include!("apt-wrapper/bridge.h");
|
||||||
|
|
||||||
|
type AptPackage;
|
||||||
|
|
||||||
|
/// Get package name
|
||||||
|
fn name(self: &AptPackage) -> &str;
|
||||||
|
|
||||||
|
/// Get package version
|
||||||
|
fn version(self: &AptPackage) -> &str;
|
||||||
|
|
||||||
|
/// Get package description
|
||||||
|
fn description(self: &AptPackage) -> &str;
|
||||||
|
|
||||||
|
/// Check if package is installed
|
||||||
|
fn is_installed(self: &AptPackage) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rust side AptPackage
|
||||||
|
extern "Rust" {
|
||||||
|
type AptPackage;
|
||||||
|
|
||||||
|
/// Create AptPackage from name, version, description, and installed status
|
||||||
|
fn new_ffi(name: String, version: String, description: String, installed: bool) -> AptPackage;
|
||||||
|
|
||||||
|
/// Get package name
|
||||||
|
fn name_ffi(self: &AptPackage) -> &str;
|
||||||
|
|
||||||
|
/// Get package version
|
||||||
|
fn version_ffi(self: &AptPackage) -> &str;
|
||||||
|
|
||||||
|
/// Get package description
|
||||||
|
fn description_ffi(self: &AptPackage) -> &str;
|
||||||
|
|
||||||
|
/// Check if package is installed
|
||||||
|
fn is_installed_ffi(self: &AptPackage) -> bool;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use ffi::AptPackage as FFIAptPackage;
|
||||||
|
|
||||||
|
|
||||||
25
src/error.rs
Normal file
25
src/error.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
//! Simple error types for APT wrapper
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// APT wrapper error types
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum AptError {
|
||||||
|
#[error("Package not found: {0}")]
|
||||||
|
PackageNotFound(String),
|
||||||
|
|
||||||
|
#[error("APT command failed: {0}")]
|
||||||
|
AptCommandFailed(String),
|
||||||
|
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("UTF-8 error: {0}")]
|
||||||
|
Utf8(#[from] std::string::FromUtf8Error),
|
||||||
|
|
||||||
|
#[error("Generic error: {0}")]
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result type alias
|
||||||
|
pub type AptResult<T> = Result<T, AptError>;
|
||||||
71
src/lib.rs
Normal file
71
src/lib.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
//! Simple APT wrapper for apt-ostree
|
||||||
|
//!
|
||||||
|
//! Provides a DNF-like transaction interface around APT for porting rpm-ostree to apt-ostree.
|
||||||
|
|
||||||
|
use std::process::Command;
|
||||||
|
use anyhow::{Result, anyhow};
|
||||||
|
|
||||||
|
pub mod transaction;
|
||||||
|
pub mod package;
|
||||||
|
pub mod error;
|
||||||
|
|
||||||
|
pub use transaction::AptTransaction;
|
||||||
|
pub use package::AptPackage;
|
||||||
|
pub use error::{AptError, AptResult};
|
||||||
|
|
||||||
|
/// Initialize the APT wrapper system
|
||||||
|
pub fn init() -> Result<()> {
|
||||||
|
// Simple initialization - just verify APT is available
|
||||||
|
let output = Command::new("apt")
|
||||||
|
.arg("--version")
|
||||||
|
.output()
|
||||||
|
.map_err(|_| anyhow!("APT not found in PATH"))?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(anyhow!("APT not working properly"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple package search using APT
|
||||||
|
pub fn search_packages(query: &str) -> Result<Vec<String>> {
|
||||||
|
let output = Command::new("apt")
|
||||||
|
.args(&["search", "--names-only", query])
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(anyhow!("Package search failed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let packages: Vec<String> = String::from_utf8(output.stdout)?
|
||||||
|
.lines()
|
||||||
|
.filter(|line| !line.is_empty() && !line.starts_with("Sorting"))
|
||||||
|
.map(|line| line.split('/').next().unwrap_or(line).to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(packages)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a package is installed
|
||||||
|
pub fn is_package_installed(name: &str) -> Result<bool> {
|
||||||
|
let output = Command::new("dpkg")
|
||||||
|
.args(&["-l", name])
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
Ok(output.status.success())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get package information
|
||||||
|
pub fn get_package_info(name: &str) -> Result<AptPackage> {
|
||||||
|
let output = Command::new("apt")
|
||||||
|
.args(&["show", name])
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(anyhow!("Package not found: {}", name));
|
||||||
|
}
|
||||||
|
|
||||||
|
let info = String::from_utf8(output.stdout)?;
|
||||||
|
AptPackage::from_apt_show(&info)
|
||||||
|
}
|
||||||
69
src/package.rs
Normal file
69
src/package.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
//! Simple package information structure
|
||||||
|
|
||||||
|
use anyhow::{Result, anyhow};
|
||||||
|
|
||||||
|
/// Simple package information
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AptPackage {
|
||||||
|
pub name: String,
|
||||||
|
pub version: String,
|
||||||
|
pub description: String,
|
||||||
|
pub installed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AptPackage {
|
||||||
|
/// Create from apt show output
|
||||||
|
pub fn from_apt_show(output: &str) -> Result<Self> {
|
||||||
|
let mut name = String::new();
|
||||||
|
let mut version = String::new();
|
||||||
|
let mut description = String::new();
|
||||||
|
|
||||||
|
for line in output.lines() {
|
||||||
|
if line.starts_with("Package: ") {
|
||||||
|
name = line.strip_prefix("Package: ").unwrap_or("").to_string();
|
||||||
|
} else if line.starts_with("Version: ") {
|
||||||
|
version = line.strip_prefix("Version: ").unwrap_or("").to_string();
|
||||||
|
} else if line.starts_with("Description: ") {
|
||||||
|
description = line.strip_prefix("Description: ").unwrap_or("").to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if name.is_empty() {
|
||||||
|
return Err(anyhow!("Invalid package information"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
description,
|
||||||
|
installed: false, // Will be set separately
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if package is installed
|
||||||
|
pub fn is_installed(&self) -> bool {
|
||||||
|
self.installed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert to C-compatible struct for FFI
|
||||||
|
/// This provides the data needed for cxx::bridge without complex trait implementations
|
||||||
|
pub fn to_ffi_data(&self) -> (String, String, String, bool) {
|
||||||
|
(
|
||||||
|
self.name.clone(),
|
||||||
|
self.version.clone(),
|
||||||
|
self.description.clone(),
|
||||||
|
self.installed,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create from C-compatible data
|
||||||
|
pub fn from_ffi_data(name: String, version: String, description: String, installed: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
description,
|
||||||
|
installed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
184
src/transaction.rs
Normal file
184
src/transaction.rs
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
//! 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
|
||||||
|
}
|
||||||
|
}
|
||||||
56
tests/basic.rs
Normal file
56
tests/basic.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
//! Basic tests for APT wrapper
|
||||||
|
|
||||||
|
use apt_wrapper::{AptTransaction, init, search_packages};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transaction_creation() {
|
||||||
|
let tx = AptTransaction::new().unwrap();
|
||||||
|
assert!(tx.is_empty());
|
||||||
|
assert_eq!(tx.packages().len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transaction_add_package() {
|
||||||
|
let mut tx = AptTransaction::new().unwrap();
|
||||||
|
|
||||||
|
// Try to add a common package (should exist)
|
||||||
|
let result = tx.add_package("apt");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(tx.packages().len(), 1);
|
||||||
|
assert_eq!(tx.packages()[0], "apt");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transaction_add_nonexistent_package() {
|
||||||
|
let mut tx = AptTransaction::new().unwrap();
|
||||||
|
|
||||||
|
// Try to add a package that definitely doesn't exist
|
||||||
|
let result = tx.add_package("this-package-definitely-does-not-exist-12345");
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(tx.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_init() {
|
||||||
|
// This will fail if APT is not available
|
||||||
|
let result = init();
|
||||||
|
// We can't assert success since APT might not be available in test environment
|
||||||
|
// Just ensure it doesn't panic
|
||||||
|
let _ = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transaction_rollback_tracking() {
|
||||||
|
let mut tx = AptTransaction::new().unwrap();
|
||||||
|
|
||||||
|
// Add a package
|
||||||
|
let result = tx.add_package("apt");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
// Check that changed_packages is initially empty
|
||||||
|
assert!(tx.changed_packages().is_empty());
|
||||||
|
|
||||||
|
// Note: We don't actually commit in tests to avoid installing packages
|
||||||
|
// In real usage, after commit(), changed_packages would contain
|
||||||
|
// the packages that were changed (installed/upgraded)
|
||||||
|
}
|
||||||
50
tests/unit/test_basic.rs
Normal file
50
tests/unit/test_basic.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
//! Unit tests for basic functionality
|
||||||
|
|
||||||
|
use apt_wrapper::{AptTransaction, AptPackage, AptRepository, PackageDatabase};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transaction_creation() {
|
||||||
|
let transaction = AptTransaction::new().unwrap();
|
||||||
|
assert!(transaction.is_empty());
|
||||||
|
assert_eq!(transaction.packages().len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transaction_add_package() {
|
||||||
|
let mut transaction = AptTransaction::new().unwrap();
|
||||||
|
transaction.add_package("vim").unwrap();
|
||||||
|
assert!(!transaction.is_empty());
|
||||||
|
assert_eq!(transaction.packages().len(), 1);
|
||||||
|
assert_eq!(transaction.packages()[0], "vim");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transaction_resolve() {
|
||||||
|
let mut transaction = AptTransaction::new().unwrap();
|
||||||
|
transaction.add_package("vim").unwrap();
|
||||||
|
transaction.resolve().unwrap();
|
||||||
|
// Should not panic
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_package_creation() {
|
||||||
|
let package = AptPackage::new("vim".to_string(), "2:8.2.2434-3+deb11u1".to_string());
|
||||||
|
assert_eq!(package.name(), "vim");
|
||||||
|
assert_eq!(package.version(), "2:8.2.2434-3+deb11u1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_repository_creation() {
|
||||||
|
let repo = AptRepository::new(
|
||||||
|
"debian".to_string(),
|
||||||
|
"http://deb.debian.org/debian".to_string(),
|
||||||
|
);
|
||||||
|
assert_eq!(repo.name(), "debian");
|
||||||
|
assert_eq!(repo.url(), "http://deb.debian.org/debian");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_package_database_creation() {
|
||||||
|
let db = PackageDatabase::new().unwrap();
|
||||||
|
assert!(!db.is_stale());
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue