# 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, /// 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, /// Use system root SYSROOT (default: /) #[arg(long)] sysroot: Option, /// 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> { // 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> { // 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> { // 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, Box> { 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> { let apt_manager = AptManager::new().await?; for package in packages { let dependents = apt_manager.get_package_dependents(package).await?; let installed_dependents: Vec = 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> { // 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> { // 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> { // 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> { 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 = 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, Box> { 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, Box> { // This would typically read from /var/lib/dpkg/info/.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 = 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, options: HashMap) -> Result> { 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> { // 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> { 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> { 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> { // 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 for Box { 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