apt-ostree/.notes/rpm-ostree/how-commands-work/06-uninstall-command.md

621 lines
No EOL
20 KiB
Markdown

# Uninstall Command Implementation Guide
## Overview
The `uninstall` command is an alias for the `remove` command in rpm-ostree, providing an alternative interface for package removal with rollback support.
## Current Implementation Status
- ✅ Basic remove command exists in apt-ostree
- ❌ Uninstall command does not exist
- ❌ Missing proper aliasing
- ❌ Missing uninstall-specific options
## Implementation Requirements
### Phase 1: Command Aliasing Setup
#### Files to Modify:
- `src/main.rs` - Add uninstall command as alias
- `src/system.rs` - Enhance remove method for uninstall
- `src/daemon.rs` - Add uninstall D-Bus method
#### Implementation Steps:
**1.1 Add Uninstall Command Structure (src/main.rs)**
```rust
#[derive(Debug, Parser)]
pub struct UninstallOpts {
/// Packages to uninstall
packages: Vec<String>,
/// Initiate a reboot after operation is complete
#[arg(short = 'r', long)]
reboot: bool,
/// Exit after printing the transaction
#[arg(short = 'n', long)]
dry_run: bool,
/// Operate on provided STATEROOT
#[arg(long)]
stateroot: Option<String>,
/// Use system root SYSROOT (default: /)
#[arg(long)]
sysroot: Option<String>,
/// Force a peer-to-peer connection instead of using the system message bus
#[arg(long)]
peer: bool,
/// Avoid printing most informational messages
#[arg(short = 'q', long)]
quiet: bool,
/// Allow removal of packages that are dependencies of other packages
#[arg(long)]
allow_deps: bool,
/// Remove packages and their dependencies
#[arg(long)]
recursive: bool,
/// Output JSON format
#[arg(long)]
json: bool,
}
```
**1.2 Add Option Validation (src/main.rs)**
```rust
impl UninstallOpts {
pub fn validate(&self) -> Result<(), Box<dyn std::error::Error>> {
// Check for valid stateroot if provided
if let Some(ref stateroot) = self.stateroot {
if !Path::new(stateroot).exists() {
return Err(format!("Stateroot '{}' does not exist", stateroot).into());
}
}
// Check for valid sysroot if provided
if let Some(ref sysroot) = self.sysroot {
if !Path::new(sysroot).exists() {
return Err(format!("Sysroot '{}' does not exist", sysroot).into());
}
}
// Check that packages are specified
if self.packages.is_empty() {
return Err("No packages specified for uninstallation".into());
}
Ok(())
}
pub fn to_remove_opts(&self) -> RemoveOpts {
RemoveOpts {
packages: self.packages.clone(),
reboot: self.reboot,
dry_run: self.dry_run,
stateroot: self.stateroot.clone(),
sysroot: self.sysroot.clone(),
peer: self.peer,
quiet: self.quiet,
allow_deps: self.allow_deps,
recursive: self.recursive,
json: self.json,
}
}
}
```
### Phase 2: Enhanced Remove Logic
#### Files to Modify:
- `src/system.rs` - Enhance remove method for uninstall
- `src/apt.rs` - Add dependency checking for uninstall
#### Implementation Steps:
**2.1 Enhance Remove Logic (src/system.rs)**
```rust
impl AptOstreeSystem {
pub async fn uninstall_packages(&self, opts: &UninstallOpts) -> Result<String, Box<dyn std::error::Error>> {
// Convert to remove operation
let remove_opts = opts.to_remove_opts();
self.remove_packages(&remove_opts).await
}
pub async fn remove_packages(&self, opts: &RemoveOpts) -> Result<String, Box<dyn std::error::Error>> {
// 1. Validate packages exist and are installed
let installed_packages = self.get_installed_packages().await?;
let packages_to_remove = self.validate_packages_for_removal(&opts.packages, &installed_packages, opts).await?;
// 2. Check dependencies if not allowing deps
if !opts.allow_deps {
self.check_removal_dependencies(&packages_to_remove, &installed_packages).await?;
}
// 3. Handle dry-run
if opts.dry_run {
println!("Would uninstall packages: {}", packages_to_remove.join(", "));
return Ok("dry-run-completed".to_string());
}
// 4. Perform removal
let transaction_id = self.perform_package_removal(&packages_to_remove, opts).await?;
Ok(transaction_id)
}
async fn validate_packages_for_removal(&self, packages: &[String], installed: &[String], opts: &RemoveOpts) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let mut valid_packages = Vec::new();
let mut invalid_packages = Vec::new();
for package in packages {
if installed.contains(package) {
valid_packages.push(package.clone());
} else {
invalid_packages.push(package.clone());
}
}
if !invalid_packages.is_empty() {
return Err(format!("Packages not installed: {}", invalid_packages.join(", ")).into());
}
Ok(valid_packages)
}
async fn check_removal_dependencies(&self, packages: &[String], installed: &[String]) -> Result<(), Box<dyn std::error::Error>> {
let apt_manager = AptManager::new().await?;
for package in packages {
let dependents = apt_manager.get_package_dependents(package).await?;
let installed_dependents: Vec<String> = dependents
.into_iter()
.filter(|p| installed.contains(p))
.collect();
if !installed_dependents.is_empty() {
return Err(format!(
"Cannot remove {}: it is required by {}",
package,
installed_dependents.join(", ")
).into());
}
}
Ok(())
}
async fn perform_package_removal(&self, packages: &[String], opts: &RemoveOpts) -> Result<String, Box<dyn std::error::Error>> {
// 1. Create transaction
let transaction_id = self.create_transaction("uninstall").await?;
// 2. Get current deployment
let sysroot = ostree::Sysroot::new_default();
sysroot.load(None)?;
let booted = sysroot.get_booted_deployment()
.ok_or("No booted deployment found")?;
let current_commit = booted.get_csum();
// 3. Create new deployment without packages
let new_commit = self.create_deployment_without_packages(current_commit, packages).await?;
// 4. Update deployment
self.update_deployment(&new_commit).await?;
// 5. Handle reboot if requested
if opts.reboot {
self.schedule_reboot().await?;
}
// 6. Complete transaction
self.complete_transaction(&transaction_id, true).await?;
Ok(transaction_id)
}
async fn create_deployment_without_packages(&self, base_commit: &str, packages: &[String]) -> Result<String, Box<dyn std::error::Error>> {
// 1. Checkout current deployment
let temp_dir = tempfile::tempdir()?;
let repo = ostree::Repo::open_at(libc::AT_FDCWD, "/ostree/repo", None)?;
repo.checkout_tree(ostree::ObjectType::Dir, base_commit, temp_dir.path(), None)?;
// 2. Remove packages from filesystem
for package in packages {
self.remove_package_from_filesystem(temp_dir.path(), package).await?;
}
// 3. Update package database
self.update_package_database_after_removal(temp_dir.path(), packages).await?;
// 4. Create new commit
let new_commit = self.create_commit_from_directory(temp_dir.path(), "Remove packages").await?;
Ok(new_commit)
}
async fn remove_package_from_filesystem(&self, root_path: &Path, package: &str) -> Result<(), Box<dyn std::error::Error>> {
// Get package file list
let apt_manager = AptManager::new().await?;
let files = apt_manager.get_package_files(package).await?;
// Remove files
for file in files {
let file_path = root_path.join(file.strip_prefix("/").unwrap_or(&file));
if file_path.exists() {
if file_path.is_dir() {
tokio::fs::remove_dir_all(&file_path).await?;
} else {
tokio::fs::remove_file(&file_path).await?;
}
}
}
Ok(())
}
async fn update_package_database_after_removal(&self, root_path: &Path, packages: &[String]) -> Result<(), Box<dyn std::error::Error>> {
let status_file = root_path.join("var/lib/dpkg/status");
if !status_file.exists() {
return Ok(());
}
// Read current status file
let content = tokio::fs::read_to_string(&status_file).await?;
let mut paragraphs: Vec<String> = content.split("\n\n").map(|s| s.to_string()).collect();
// Remove packages from status file
paragraphs.retain(|paragraph| {
for package in packages {
if paragraph.starts_with(&format!("Package: {}", package)) {
return false;
}
}
true
});
// Write updated status file
let new_content = paragraphs.join("\n\n");
tokio::fs::write(&status_file, new_content).await?;
Ok(())
}
}
```
**2.2 Add Dependency Checking (src/apt.rs)**
```rust
impl AptManager {
pub async fn get_package_dependents(&self, package: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let cache = Cache::new()?;
let mut dependents = Vec::new();
for pkg in cache.packages() {
if let Some(package_obj) = pkg {
if package_obj.is_installed() {
// Check if this package depends on the target package
if let Some(depends) = package_obj.depends() {
for dep in depends {
if dep.contains(package) {
dependents.push(package_obj.name().to_string());
break;
}
}
}
}
}
}
Ok(dependents)
}
pub async fn get_package_files(&self, package: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
// This would typically read from /var/lib/dpkg/info/<package>.list
let list_file = format!("/var/lib/dpkg/info/{}.list", package);
if Path::new(&list_file).exists() {
let content = tokio::fs::read_to_string(&list_file).await?;
let files: Vec<String> = content.lines().map(|s| s.to_string()).collect();
Ok(files)
} else {
Ok(Vec::new())
}
}
}
```
### Phase 3: D-Bus Integration
#### Files to Modify:
- `src/daemon.rs` - Add uninstall D-Bus method
- `src/client.rs` - Add uninstall client method
#### Implementation Steps:
**3.1 Add Uninstall D-Bus Method (src/daemon.rs)**
```rust
#[dbus_interface(name = "org.aptostree.dev")]
impl AptOstreeDaemon {
/// Uninstall packages
async fn uninstall_packages(&self, packages: Vec<String>, options: HashMap<String, Value>) -> Result<String, Box<dyn std::error::Error>> {
let system = AptOstreeSystem::new().await?;
// Convert options to UninstallOpts
let opts = UninstallOpts {
packages,
reboot: options.get("reboot").and_then(|v| v.as_bool()).unwrap_or(false),
dry_run: options.get("dry-run").and_then(|v| v.as_bool()).unwrap_or(false),
stateroot: options.get("stateroot").and_then(|v| v.as_str()).map(|s| s.to_string()),
sysroot: options.get("sysroot").and_then(|v| v.as_str()).map(|s| s.to_string()),
peer: options.get("peer").and_then(|v| v.as_bool()).unwrap_or(false),
quiet: options.get("quiet").and_then(|v| v.as_bool()).unwrap_or(false),
allow_deps: options.get("allow-deps").and_then(|v| v.as_bool()).unwrap_or(false),
recursive: options.get("recursive").and_then(|v| v.as_bool()).unwrap_or(false),
json: options.get("json").and_then(|v| v.as_bool()).unwrap_or(false),
};
let transaction_id = system.uninstall_packages(&opts).await?;
Ok(transaction_id)
}
}
```
**3.2 Add Uninstall Client Method (src/client.rs)**
```rust
impl AptOstreeClient {
pub async fn uninstall_packages(&self, packages: &[String], opts: &UninstallOpts) -> Result<String, Box<dyn std::error::Error>> {
// Try daemon first
if let Ok(transaction_id) = self.uninstall_packages_via_daemon(packages, opts).await {
return Ok(transaction_id);
}
// Fallback to direct system calls
let system = AptOstreeSystem::new().await?;
system.uninstall_packages(opts).await
}
async fn uninstall_packages_via_daemon(&self, packages: &[String], opts: &UninstallOpts) -> Result<String, Box<dyn std::error::Error>> {
let mut options = HashMap::new();
options.insert("reboot".to_string(), Value::Bool(opts.reboot));
options.insert("dry-run".to_string(), Value::Bool(opts.dry_run));
options.insert("peer".to_string(), Value::Bool(opts.peer));
options.insert("quiet".to_string(), Value::Bool(opts.quiet));
options.insert("allow-deps".to_string(), Value::Bool(opts.allow_deps));
options.insert("recursive".to_string(), Value::Bool(opts.recursive));
options.insert("json".to_string(), Value::Bool(opts.json));
if let Some(ref stateroot) = opts.stateroot {
options.insert("stateroot".to_string(), Value::String(stateroot.clone()));
}
if let Some(ref sysroot) = opts.sysroot {
options.insert("sysroot".to_string(), Value::String(sysroot.clone()));
}
// Call daemon method
let proxy = self.get_dbus_proxy().await?;
let transaction_id: String = proxy.uninstall_packages(packages.to_vec(), options).await?;
Ok(transaction_id)
}
}
```
### Phase 4: Transaction Monitoring
#### Files to Modify:
- `src/client.rs` - Add uninstall transaction monitoring
#### Implementation Steps:
**4.1 Add Uninstall Transaction Monitoring (src/client.rs)**
```rust
impl AptOstreeClient {
pub async fn monitor_uninstall_transaction(&self, transaction_id: &str, opts: &UninstallOpts) -> Result<(), Box<dyn std::error::Error>> {
if opts.dry_run {
// For dry-run, just return success
return Ok(());
}
// Monitor transaction progress
let mut progress = 0;
loop {
let status = self.get_transaction_status(transaction_id).await?;
match status {
TransactionStatus::Running(percent) => {
if percent != progress && !opts.quiet {
progress = percent;
println!("Uninstall progress: {}%", progress);
}
}
TransactionStatus::Completed => {
if !opts.quiet {
println!("Uninstall completed successfully");
}
break;
}
TransactionStatus::Failed(error) => {
return Err(error.into());
}
}
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
Ok(())
}
}
```
## Main Uninstall Command Implementation
### Files to Modify:
- `src/main.rs` - Main uninstall command logic
### Implementation:
```rust
async fn uninstall_command(opts: UninstallOpts) -> Result<(), Box<dyn std::error::Error>> {
// 1. Validate options
opts.validate()?;
// 2. Check permissions
if !opts.dry_run {
check_root_permissions()?;
}
// 3. Perform uninstall
let client = AptOstreeClient::new().await?;
let transaction_id = client.uninstall_packages(&opts.packages, &opts).await?;
// 4. Monitor transaction (if not dry-run)
if !opts.dry_run {
client.monitor_uninstall_transaction(&transaction_id, &opts).await?;
}
Ok(())
}
```
## Testing Strategy
### Unit Tests
```rust
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_uninstall_options_validation() {
let opts = UninstallOpts {
packages: vec!["test-package".to_string()],
reboot: false,
dry_run: false,
stateroot: None,
sysroot: None,
peer: false,
quiet: false,
allow_deps: false,
recursive: false,
json: false,
};
assert!(opts.validate().is_ok());
let opts = UninstallOpts {
packages: vec![],
reboot: false,
dry_run: false,
stateroot: None,
sysroot: None,
peer: false,
quiet: false,
allow_deps: false,
recursive: false,
json: false,
};
assert!(opts.validate().is_err());
}
#[tokio::test]
async fn test_package_validation() {
let system = AptOstreeSystem::new().await.unwrap();
let installed = vec!["package1".to_string(), "package2".to_string()];
let packages = vec!["package1".to_string(), "nonexistent".to_string()];
let result = system.validate_packages_for_removal(&packages, &installed, &RemoveOpts::default()).await;
assert!(result.is_err());
}
}
```
### Integration Tests
```rust
#[tokio::test]
async fn test_uninstall_command_integration() {
let opts = UninstallOpts {
packages: vec!["test-package".to_string()],
reboot: false,
dry_run: true, // Use dry-run for testing
stateroot: None,
sysroot: None,
peer: false,
quiet: false,
allow_deps: false,
recursive: false,
json: false,
};
let result = uninstall_command(opts).await;
assert!(result.is_ok());
}
```
## Error Handling
### Files to Modify:
- `src/error.rs` - Add uninstall-specific errors
### Implementation:
```rust
#[derive(Debug, thiserror::Error)]
pub enum UninstallError {
#[error("Package not installed: {0}")]
PackageNotInstalled(String),
#[error("Cannot remove package {0}: it is required by {1}")]
DependencyConflict(String, String),
#[error("Failed to remove package files: {0}")]
FileRemovalError(String),
#[error("Failed to update package database: {0}")]
DatabaseUpdateError(String),
#[error("No packages specified for uninstallation")]
NoPackagesSpecified,
#[error("Uninstall requires root privileges")]
PermissionError,
}
impl From<UninstallError> for Box<dyn std::error::Error> {
fn from(err: UninstallError) -> Self {
Box::new(err)
}
}
```
## Dependencies to Add
Add to `Cargo.toml`:
```toml
[dependencies]
tempfile = "3.0"
tokio = { version = "1.0", features = ["fs"] }
libc = "0.2"
```
## Implementation Checklist
- [ ] Add CLI structure for uninstall command
- [ ] Implement command aliasing to remove
- [ ] Add package validation for uninstallation
- [ ] Implement dependency checking
- [ ] Add filesystem cleanup logic
- [ ] Add package database updates
- [ ] Add D-Bus integration
- [ ] Add transaction monitoring
- [ ] Add comprehensive error handling
- [ ] Write unit and integration tests
- [ ] Update documentation
## References
- rpm-ostree uninstall/remove implementation patterns
- APT package removal logic
- OSTree deployment management
- DEB package file management