winbrew_engines\windows\exe/
remove.rs

1use anyhow::{Context, Result, bail};
2use std::path::Path;
3use std::process::Command;
4use tracing::warn;
5
6use crate::core::fs::cleanup_path;
7use crate::models::install::installed::InstalledPackage;
8
9use super::NATIVE_EXE_SUCCESS_EXIT_CODES;
10use super::switches::split_switches;
11use super::validation::{validate_install_dir, validate_package_name};
12
13/// Remove a native executable package.
14///
15/// The backend prefers the recorded uninstall command from
16/// `EngineMetadata::NativeExe` when one is available. If the uninstall command
17/// fails or is missing, the module falls back to direct directory cleanup so the
18/// install tree is still removed.
19pub(crate) fn remove(package: &InstalledPackage) -> Result<()> {
20    validate_package_name(&package.name)?;
21    validate_install_dir(Path::new(&package.install_dir))?;
22
23    let uninstall_command = package
24        .engine_metadata
25        .as_ref()
26        .and_then(|metadata| metadata.native_exe_uninstall_command());
27
28    if let Some(command) = uninstall_command {
29        if let Err(err) = run_uninstall_command(command, &package.name) {
30            warn!(
31                package = package.name.as_str(),
32                error = %err,
33                "native executable uninstall command failed; falling back to directory cleanup"
34            );
35        }
36    } else {
37        warn!(
38            package = package.name.as_str(),
39            install_dir = %package.install_dir,
40            "native executable uninstall metadata was not available; falling back to directory cleanup"
41        );
42    }
43
44    cleanup_path(Path::new(&package.install_dir))
45        .with_context(|| format!("failed to remove {}", package.install_dir))?;
46
47    Ok(())
48}
49
50fn run_uninstall_command(command: &str, package_name: &str) -> Result<()> {
51    let mut command_parts = split_switches(command)?;
52
53    if command_parts.is_empty() {
54        bail!("native executable uninstall command is empty for '{package_name}'");
55    }
56
57    let program = command_parts.remove(0);
58    let status = Command::new(program)
59        .args(command_parts)
60        .status()
61        .with_context(|| {
62            format!("failed to launch native executable uninstaller for {package_name}")
63        })?;
64
65    let exit_code = status.code().ok_or_else(|| {
66        anyhow::anyhow!("native executable uninstaller terminated without an exit code")
67    })?;
68
69    if !NATIVE_EXE_SUCCESS_EXIT_CODES.contains(&exit_code) {
70        bail!(
71            "native executable uninstaller for {} failed with exit code {}",
72            package_name,
73            exit_code
74        );
75    }
76
77    Ok(())
78}