Skip to content

Commit 0db3884

Browse files
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 #1368. Closes #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`
1 parent 9cd51c8 commit 0db3884

File tree

4 files changed

+36
-0
lines changed

4 files changed

+36
-0
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ rustix = { version = "0.38.37", default-features = false, features = ["fs", "std
144144
same-file = { version = "1.0.6" }
145145
schemars = { version = "0.8.21", features = ["url"] }
146146
seahash = { version = "4.1.0" }
147+
self-replace = { version = "1.5.0" }
147148
serde = { version = "1.0.210", features = ["derive"] }
148149
serde-untagged = { version = "0.1.6" }
149150
serde_json = { version = "1.0.128" }

crates/uv-install-wheel/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ tracing = { workspace = true }
5252
walkdir = { workspace = true }
5353
zip = { workspace = true }
5454

55+
[target.'cfg(target_os = "windows")'.dependencies]
56+
same-file = { workspace = true }
57+
self-replace = { workspace = true }
58+
5559
[dev-dependencies]
5660
anyhow = { version = "1.0.89" }
5761
assert_fs = { version = "1.1.2" }

crates/uv-install-wheel/src/uninstall.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,39 @@ pub fn uninstall_wheel(dist_info: &Path) -> Result<Uninstall, Error> {
3333
let mut file_count = 0usize;
3434
let mut dir_count = 0usize;
3535

36+
#[cfg(windows)]
37+
let itself = std::env::current_exe().ok();
38+
3639
// Uninstall the files, keeping track of any directories that are left empty.
3740
let mut visited = BTreeSet::new();
3841
for entry in &record {
3942
let path = site_packages.join(&entry.path);
43+
44+
// On Windows, deleting the current executable is a special case.
45+
#[cfg(windows)]
46+
if let Some(itself) = itself.as_ref() {
47+
if itself
48+
.file_name()
49+
.is_some_and(|itself| path.file_name().is_some_and(|path| itself == path))
50+
{
51+
if same_file::is_same_file(itself, &path).unwrap_or(false) {
52+
tracing::debug!("Detected self-delete of executable: {}", path.display());
53+
match self_replace::self_delete_outside_path(site_packages) {
54+
Ok(()) => {
55+
trace!("Removed file: {}", path.display());
56+
file_count += 1;
57+
if let Some(parent) = path.parent() {
58+
visited.insert(normalize_path(parent));
59+
}
60+
}
61+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
62+
Err(err) => return Err(err.into()),
63+
}
64+
continue;
65+
}
66+
}
67+
}
68+
4069
match fs::remove_file(&path) {
4170
Ok(()) => {
4271
trace!("Removed file: {}", path.display());

0 commit comments

Comments
 (0)