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`
This commit is contained in:
Charlie Marsh 2024-11-07 21:57:38 -05:00 committed by GitHub
parent 9cd51c8a57
commit 0db38844d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 36 additions and 0 deletions

2
Cargo.lock generated
View File

@ -4796,7 +4796,9 @@ dependencies = [
"reflink-copy", "reflink-copy",
"regex", "regex",
"rustc-hash", "rustc-hash",
"same-file",
"schemars", "schemars",
"self-replace",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",

View File

@ -144,6 +144,7 @@ rustix = { version = "0.38.37", default-features = false, features = ["fs", "std
same-file = { version = "1.0.6" } same-file = { version = "1.0.6" }
schemars = { version = "0.8.21", features = ["url"] } schemars = { version = "0.8.21", features = ["url"] }
seahash = { version = "4.1.0" } seahash = { version = "4.1.0" }
self-replace = { version = "1.5.0" }
serde = { version = "1.0.210", features = ["derive"] } serde = { version = "1.0.210", features = ["derive"] }
serde-untagged = { version = "0.1.6" } serde-untagged = { version = "0.1.6" }
serde_json = { version = "1.0.128" } serde_json = { version = "1.0.128" }

View File

@ -52,6 +52,10 @@ tracing = { workspace = true }
walkdir = { workspace = true } walkdir = { workspace = true }
zip = { workspace = true } zip = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
same-file = { workspace = true }
self-replace = { workspace = true }
[dev-dependencies] [dev-dependencies]
anyhow = { version = "1.0.89" } anyhow = { version = "1.0.89" }
assert_fs = { version = "1.1.2" } assert_fs = { version = "1.1.2" }

View File

@ -33,10 +33,39 @@ pub fn uninstall_wheel(dist_info: &Path) -> Result<Uninstall, Error> {
let mut file_count = 0usize; let mut file_count = 0usize;
let mut dir_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. // Uninstall the files, keeping track of any directories that are left empty.
let mut visited = BTreeSet::new(); let mut visited = BTreeSet::new();
for entry in &record { for entry in &record {
let path = site_packages.join(&entry.path); 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) { match fs::remove_file(&path) {
Ok(()) => { Ok(()) => {
trace!("Removed file: {}", path.display()); trace!("Removed file: {}", path.display());