use anyhow::{anyhow, bail, Context, Result};
use std::fs;
use std::io::{IsTerminal, Write};
use std::path::{Path, PathBuf};
#[cfg(windows)]
use std::process::{Command, Stdio};
struct Layout {
self_path: PathBuf,
targets: Vec<PathBuf>,
#[cfg_attr(not(windows), allow(dead_code))]
install_dir: PathBuf,
config_dir: Option<PathBuf>,
}
pub async fn run(yes: bool, purge: bool) -> Result<()> {
let layout = resolve_install_layout()?;
print_plan(&layout, purge);
if !yes {
if !std::io::stdin().is_terminal() {
bail!("Refusing to uninstall non-interactively. Pass --yes to proceed.");
}
if !prompt_confirm("Proceed with uninstall? [y/N]: ")? {
println!("Uninstall cancelled.");
return Ok(());
}
}
for target in &layout.targets {
if target == &layout.self_path {
continue;
}
remove_path(target);
}
#[cfg(unix)]
remove_self_unix(&layout.self_path)?;
#[cfg(windows)]
schedule_self_delete_windows(&layout.self_path)?;
#[cfg(windows)]
cleanup_windows_user_path(&layout.install_dir);
if purge {
if let Some(cfg) = &layout.config_dir {
if cfg.exists() {
fs::remove_dir_all(cfg)
.with_context(|| format!("remove config dir {}", cfg.display()))?;
println!("Removed {}", cfg.display());
}
}
}
print_summary(&layout, purge);
Ok(())
}
fn resolve_install_layout() -> Result<Layout> {
let raw = std::env::current_exe().context("locate running binary via current_exe()")?;
let self_path = raw.canonicalize().unwrap_or(raw);
let install_dir = self_path
.parent()
.ok_or_else(|| anyhow!("install dir has no parent: {}", self_path.display()))?
.to_path_buf();
let names: &[&str] = if cfg!(windows) {
&["zilliz.exe", "zz.exe"]
} else {
&["zilliz", "zz"]
};
let mut search_dirs: Vec<PathBuf> = vec![install_dir.clone()];
if let Some(home) = dirs::home_dir() {
for sub in [".zilliz/bin", ".local/bin", ".cargo/bin"] {
let d = home.join(sub);
if !search_dirs.contains(&d) {
search_dirs.push(d);
}
}
}
let mut targets: Vec<PathBuf> = Vec::new();
for dir in &search_dirs {
for name in names {
let p = dir.join(name);
if (p.exists() || fs::symlink_metadata(&p).is_ok()) && !targets.contains(&p) {
targets.push(p);
}
}
}
if !targets.contains(&self_path) {
targets.push(self_path.clone());
}
let config_dir = dirs::home_dir().map(|h| h.join(".zilliz"));
Ok(Layout {
self_path,
targets,
install_dir,
config_dir,
})
}
fn print_plan(layout: &Layout, purge: bool) {
println!("This will remove:");
for target in &layout.targets {
println!(" - {}", target.display());
}
if purge {
if let Some(cfg) = &layout.config_dir {
println!(" - {} (user config: credentials, config)", cfg.display());
}
} else if let Some(cfg) = &layout.config_dir {
println!(
"\nConfig at {} will be preserved (pass --purge to remove it).",
cfg.display()
);
}
println!();
}
fn prompt_confirm(prompt: &str) -> Result<bool> {
print!("{}", prompt);
std::io::stdout().flush().ok();
let mut buf = String::new();
std::io::stdin().read_line(&mut buf).context("read stdin")?;
let trimmed = buf.trim();
Ok(trimmed.eq_ignore_ascii_case("y") || trimmed.eq_ignore_ascii_case("yes"))
}
fn remove_path(p: &Path) {
if !p.exists() && fs::symlink_metadata(p).is_err() {
return;
}
match fs::remove_file(p) {
Ok(()) => println!("Removed {}", p.display()),
Err(e) => eprintln!("Warning: failed to remove {}: {}", p.display(), e),
}
}
#[cfg(unix)]
fn remove_self_unix(binary: &Path) -> Result<()> {
if !binary.exists() && fs::symlink_metadata(binary).is_err() {
return Ok(());
}
fs::remove_file(binary).with_context(|| {
format!(
"remove {} — if your install is system-managed, use your package manager instead",
binary.display()
)
})?;
println!("Removed {}", binary.display());
Ok(())
}
#[cfg(windows)]
fn schedule_self_delete_windows(binary: &Path) -> Result<()> {
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
const DETACHED_PROCESS: u32 = 0x0000_0008;
let script = format!(
"ping -n 3 127.0.0.1 >nul & del /f /q \"{}\"",
binary.display()
);
Command::new("cmd")
.args(["/c", &script])
.creation_flags(CREATE_NO_WINDOW | DETACHED_PROCESS)
.spawn()
.with_context(|| format!("schedule deferred delete of {}", binary.display()))?;
println!(
"Scheduled deletion of {} (will complete after this process exits).",
binary.display()
);
Ok(())
}
#[cfg(windows)]
fn cleanup_windows_user_path(install_dir: &Path) {
let install_str = install_dir.display().to_string();
let ps = format!(
"$p=[Environment]::GetEnvironmentVariable('PATH','User'); \
if ($p) {{ \
$new=($p -split ';' | Where-Object {{ $_ -and ($_ -ne '{0}') }}) -join ';'; \
if ($new -ne $p) {{ [Environment]::SetEnvironmentVariable('PATH', $new, 'User') }} \
}}",
install_str.replace('\'', "''")
);
let _ = Command::new("powershell")
.args(["-NoProfile", "-Command", &ps])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
println!("Cleaned {} from user PATH (if present).", install_str);
}
fn print_summary(layout: &Layout, purge: bool) {
println!();
println!("Uninstalled zilliz.");
if !purge {
if let Some(cfg) = &layout.config_dir {
println!(
"Config preserved at {} (credentials, config).",
cfg.display()
);
#[cfg(unix)]
println!("Remove it manually with: rm -rf {}", cfg.display());
#[cfg(windows)]
println!(
"Remove it manually with: Remove-Item -Recurse -Force {}",
cfg.display()
);
}
}
println!(
"If you installed via `cargo install zilliz`, also run `cargo uninstall zilliz` \
to clear Cargo's registry record."
);
}