621 lines
No EOL
20 KiB
Markdown
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 |