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

20 KiB

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)

#[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)

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)

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)

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)

#[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)

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)

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:

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

#[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

#[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:

#[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:

[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