Skip to content

Commit 3ee2b10

Browse files
Enable uv tool uninstall uv on Windows (#8963)
## Summary Extending self-delete and self-replace functionality to uv itself on Windows. Closes #6400.
1 parent 389a26e commit 3ee2b10

File tree

6 files changed

+57
-32
lines changed

6 files changed

+57
-32
lines changed

Cargo.lock

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

crates/uv-tool/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ impl InstalledTools {
184184
environment_path.user_display()
185185
);
186186

187+
// TODO(charlie): On Windows, if the current executable is in the directory,
188+
// we need to use `safe_delete`.
187189
fs_err::remove_dir_all(environment_path)?;
188190

189191
Ok(())

crates/uv-virtualenv/src/virtualenv.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ pub(crate) fn create(
8686
if allow_existing {
8787
debug!("Allowing existing directory");
8888
} else if location.join("pyvenv.cfg").is_file() {
89+
// TODO(charlie): On Windows, if the current executable is in the directory,
90+
// we need to use `safe_delete`.
8991
debug!("Removing existing directory");
9092
fs::remove_dir_all(location)?;
9193
fs::create_dir_all(location)?;

crates/uv/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ walkdir = { workspace = true }
9999
which = { workspace = true }
100100
zip = { workspace = true }
101101

102+
[target.'cfg(target_os = "windows")'.dependencies]
103+
self-replace = { workspace = true }
104+
102105
[dev-dependencies]
103106
assert_cmd = { version = "2.0.16" }
104107
assert_fs = { version = "1.1.2" }

crates/uv/src/commands/tool/common.rs

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -125,47 +125,52 @@ pub(crate) fn install_executables(
125125
return Ok(ExitStatus::Failure);
126126
}
127127

128-
// Check if they exist, before installing
129-
let mut existing_entry_points = target_entry_points
130-
.iter()
131-
.filter(|(_, _, target_path)| target_path.exists())
132-
.peekable();
128+
// Error if we're overwriting an existing entrypoint, unless the user passed `--force`.
129+
if !force {
130+
let mut existing_entry_points = target_entry_points
131+
.iter()
132+
.filter(|(_, _, target_path)| target_path.exists())
133+
.peekable();
134+
if existing_entry_points.peek().is_some() {
135+
// Clean up the environment we just created
136+
installed_tools.remove_environment(name)?;
133137

134-
// Ignore any existing entrypoints if the user passed `--force`, or the existing recept was
135-
// broken.
136-
if force {
137-
for (name, _, target) in existing_entry_points {
138-
debug!("Removing existing executable: `{name}`");
139-
fs_err::remove_file(target)?;
138+
let existing_entry_points = existing_entry_points
139+
// SAFETY: We know the target has a filename because we just constructed it above
140+
.map(|(_, _, target)| target.file_name().unwrap().to_string_lossy())
141+
.collect::<Vec<_>>();
142+
let (s, exists) = if existing_entry_points.len() == 1 {
143+
("", "exists")
144+
} else {
145+
("s", "exist")
146+
};
147+
bail!(
148+
"Executable{s} already {exists}: {} (use `--force` to overwrite)",
149+
existing_entry_points
150+
.iter()
151+
.map(|name| name.bold())
152+
.join(", ")
153+
)
140154
}
141-
} else if existing_entry_points.peek().is_some() {
142-
// Clean up the environment we just created
143-
installed_tools.remove_environment(name)?;
144-
145-
let existing_entry_points = existing_entry_points
146-
// SAFETY: We know the target has a filename because we just constructed it above
147-
.map(|(_, _, target)| target.file_name().unwrap().to_string_lossy())
148-
.collect::<Vec<_>>();
149-
let (s, exists) = if existing_entry_points.len() == 1 {
150-
("", "exists")
151-
} else {
152-
("s", "exist")
153-
};
154-
bail!(
155-
"Executable{s} already {exists}: {} (use `--force` to overwrite)",
156-
existing_entry_points
157-
.iter()
158-
.map(|name| name.bold())
159-
.join(", ")
160-
)
161155
}
162156

157+
#[cfg(windows)]
158+
let itself = std::env::current_exe().ok();
159+
163160
for (name, source_path, target_path) in &target_entry_points {
164161
debug!("Installing executable: `{name}`");
162+
165163
#[cfg(unix)]
166164
replace_symlink(source_path, target_path).context("Failed to install executable")?;
165+
167166
#[cfg(windows)]
168-
fs_err::copy(source_path, target_path).context("Failed to install entrypoint")?;
167+
if itself.as_ref().is_some_and(|itself| {
168+
std::path::absolute(target_path).is_ok_and(|target| *itself == target)
169+
}) {
170+
self_replace::self_replace(source_path).context("Failed to install entrypoint")?;
171+
} else {
172+
fs_err::copy(source_path, target_path).context("Failed to install entrypoint")?;
173+
}
169174
}
170175

171176
let s = if target_entry_points.len() == 1 {

crates/uv/src/commands/tool/uninstall.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,25 @@ async fn uninstall_tool(
180180
// Remove the tool itself.
181181
tools.remove_environment(name)?;
182182

183+
#[cfg(windows)]
184+
let itself = std::env::current_exe().ok();
185+
183186
// Remove the tool's entrypoints.
184187
let entrypoints = receipt.entrypoints();
185188
for entrypoint in entrypoints {
186189
debug!(
187190
"Removing executable: {}",
188191
entrypoint.install_path.user_display()
189192
);
193+
194+
#[cfg(windows)]
195+
if itself.as_ref().is_some_and(|itself| {
196+
std::path::absolute(&entrypoint.install_path).is_ok_and(|target| *itself == target)
197+
}) {
198+
self_replace::self_delete()?;
199+
continue;
200+
}
201+
190202
match fs_err::tokio::remove_file(&entrypoint.install_path).await {
191203
Ok(()) => {}
192204
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {

0 commit comments

Comments
 (0)