Enable `uv tool uninstall uv` on Windows (#8963)

## Summary

Extending self-delete and self-replace functionality to uv itself on
Windows.

Closes https://github.com/astral-sh/uv/issues/6400.
This commit is contained in:
Charlie Marsh 2024-12-10 13:13:22 -05:00 committed by GitHub
parent 389a26ef9e
commit 3ee2b10738
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 57 additions and 32 deletions

1
Cargo.lock generated
View File

@ -4432,6 +4432,7 @@ dependencies = [
"reqwest",
"rkyv",
"rustc-hash",
"self-replace",
"serde",
"serde_json",
"similar",

View File

@ -184,6 +184,8 @@ impl InstalledTools {
environment_path.user_display()
);
// TODO(charlie): On Windows, if the current executable is in the directory,
// we need to use `safe_delete`.
fs_err::remove_dir_all(environment_path)?;
Ok(())

View File

@ -86,6 +86,8 @@ pub(crate) fn create(
if allow_existing {
debug!("Allowing existing directory");
} else if location.join("pyvenv.cfg").is_file() {
// TODO(charlie): On Windows, if the current executable is in the directory,
// we need to use `safe_delete`.
debug!("Removing existing directory");
fs::remove_dir_all(location)?;
fs::create_dir_all(location)?;

View File

@ -99,6 +99,9 @@ walkdir = { workspace = true }
which = { workspace = true }
zip = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
self-replace = { workspace = true }
[dev-dependencies]
assert_cmd = { version = "2.0.16" }
assert_fs = { version = "1.1.2" }

View File

@ -125,20 +125,13 @@ pub(crate) fn install_executables(
return Ok(ExitStatus::Failure);
}
// Check if they exist, before installing
// Error if we're overwriting an existing entrypoint, unless the user passed `--force`.
if !force {
let mut existing_entry_points = target_entry_points
.iter()
.filter(|(_, _, target_path)| target_path.exists())
.peekable();
// Ignore any existing entrypoints if the user passed `--force`, or the existing recept was
// broken.
if force {
for (name, _, target) in existing_entry_points {
debug!("Removing existing executable: `{name}`");
fs_err::remove_file(target)?;
}
} else if existing_entry_points.peek().is_some() {
if existing_entry_points.peek().is_some() {
// Clean up the environment we just created
installed_tools.remove_environment(name)?;
@ -159,14 +152,26 @@ pub(crate) fn install_executables(
.join(", ")
)
}
}
#[cfg(windows)]
let itself = std::env::current_exe().ok();
for (name, source_path, target_path) in &target_entry_points {
debug!("Installing executable: `{name}`");
#[cfg(unix)]
replace_symlink(source_path, target_path).context("Failed to install executable")?;
#[cfg(windows)]
if itself.as_ref().is_some_and(|itself| {
std::path::absolute(target_path).is_ok_and(|target| *itself == target)
}) {
self_replace::self_replace(source_path).context("Failed to install entrypoint")?;
} else {
fs_err::copy(source_path, target_path).context("Failed to install entrypoint")?;
}
}
let s = if target_entry_points.len() == 1 {
""

View File

@ -180,6 +180,9 @@ async fn uninstall_tool(
// Remove the tool itself.
tools.remove_environment(name)?;
#[cfg(windows)]
let itself = std::env::current_exe().ok();
// Remove the tool's entrypoints.
let entrypoints = receipt.entrypoints();
for entrypoint in entrypoints {
@ -187,6 +190,15 @@ async fn uninstall_tool(
"Removing executable: {}",
entrypoint.install_path.user_display()
);
#[cfg(windows)]
if itself.as_ref().is_some_and(|itself| {
std::path::absolute(&entrypoint.install_path).is_ok_and(|target| *itself == target)
}) {
self_replace::self_delete()?;
continue;
}
match fs_err::tokio::remove_file(&entrypoint.install_path).await {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {