From 0db38844d976d61c44bb00a38aaa0f8c9497675d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 7 Nov 2024 21:57:38 -0500 Subject: [PATCH] Enable uv to replace and delete itself on Windows (#8914) ## Summary On Windows, we can't delete the currently-running executable -- at least, not trivially. But the [`self_replace`](https://docs.rs/self-replace/latest/self_replace/) crate can help us here. Closes https://github.com/astral-sh/uv/issues/1368. Closes https://github.com/astral-sh/uv/issues/4980. ## Test Plan On my Windows machine: - `maturin build` - `python -m venv .venv` - `.venv/Scripts/activate` - `pip install /path/to/uv.whl` - `uv pip install /path/to/uv.whl` - `uv pip uninstall uv` --- Cargo.lock | 2 ++ Cargo.toml | 1 + crates/uv-install-wheel/Cargo.toml | 4 ++++ crates/uv-install-wheel/src/uninstall.rs | 29 ++++++++++++++++++++++++ 4 files changed, 36 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 55802dedd101..846c8605d01a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4796,7 +4796,9 @@ dependencies = [ "reflink-copy", "regex", "rustc-hash", + "same-file", "schemars", + "self-replace", "serde", "serde_json", "sha2", diff --git a/Cargo.toml b/Cargo.toml index 37e8b974f9b6..de26817fef4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -144,6 +144,7 @@ rustix = { version = "0.38.37", default-features = false, features = ["fs", "std same-file = { version = "1.0.6" } schemars = { version = "0.8.21", features = ["url"] } seahash = { version = "4.1.0" } +self-replace = { version = "1.5.0" } serde = { version = "1.0.210", features = ["derive"] } serde-untagged = { version = "0.1.6" } serde_json = { version = "1.0.128" } diff --git a/crates/uv-install-wheel/Cargo.toml b/crates/uv-install-wheel/Cargo.toml index d7dcd8fd1db7..9ce308160727 100644 --- a/crates/uv-install-wheel/Cargo.toml +++ b/crates/uv-install-wheel/Cargo.toml @@ -52,6 +52,10 @@ tracing = { workspace = true } walkdir = { workspace = true } zip = { workspace = true } +[target.'cfg(target_os = "windows")'.dependencies] +same-file = { workspace = true } +self-replace = { workspace = true } + [dev-dependencies] anyhow = { version = "1.0.89" } assert_fs = { version = "1.1.2" } diff --git a/crates/uv-install-wheel/src/uninstall.rs b/crates/uv-install-wheel/src/uninstall.rs index 15bbdc3f625b..b824bceb9b10 100644 --- a/crates/uv-install-wheel/src/uninstall.rs +++ b/crates/uv-install-wheel/src/uninstall.rs @@ -33,10 +33,39 @@ pub fn uninstall_wheel(dist_info: &Path) -> Result { let mut file_count = 0usize; let mut dir_count = 0usize; + #[cfg(windows)] + let itself = std::env::current_exe().ok(); + // Uninstall the files, keeping track of any directories that are left empty. let mut visited = BTreeSet::new(); for entry in &record { let path = site_packages.join(&entry.path); + + // On Windows, deleting the current executable is a special case. + #[cfg(windows)] + if let Some(itself) = itself.as_ref() { + if itself + .file_name() + .is_some_and(|itself| path.file_name().is_some_and(|path| itself == path)) + { + if same_file::is_same_file(itself, &path).unwrap_or(false) { + tracing::debug!("Detected self-delete of executable: {}", path.display()); + match self_replace::self_delete_outside_path(site_packages) { + Ok(()) => { + trace!("Removed file: {}", path.display()); + file_count += 1; + if let Some(parent) = path.parent() { + visited.insert(normalize_path(parent)); + } + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => return Err(err.into()), + } + continue; + } + } + } + match fs::remove_file(&path) { Ok(()) => { trace!("Removed file: {}", path.display());