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", "reqwest",
"rkyv", "rkyv",
"rustc-hash", "rustc-hash",
"self-replace",
"serde", "serde",
"serde_json", "serde_json",
"similar", "similar",

View File

@ -184,6 +184,8 @@ impl InstalledTools {
environment_path.user_display() 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)?; fs_err::remove_dir_all(environment_path)?;
Ok(()) Ok(())

View File

@ -86,6 +86,8 @@ pub(crate) fn create(
if allow_existing { if allow_existing {
debug!("Allowing existing directory"); debug!("Allowing existing directory");
} else if location.join("pyvenv.cfg").is_file() { } 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"); debug!("Removing existing directory");
fs::remove_dir_all(location)?; fs::remove_dir_all(location)?;
fs::create_dir_all(location)?; fs::create_dir_all(location)?;

View File

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

View File

@ -125,20 +125,13 @@ pub(crate) fn install_executables(
return Ok(ExitStatus::Failure); 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 let mut existing_entry_points = target_entry_points
.iter() .iter()
.filter(|(_, _, target_path)| target_path.exists()) .filter(|(_, _, target_path)| target_path.exists())
.peekable(); .peekable();
if existing_entry_points.peek().is_some() {
// 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() {
// Clean up the environment we just created // Clean up the environment we just created
installed_tools.remove_environment(name)?; installed_tools.remove_environment(name)?;
@ -159,14 +152,26 @@ pub(crate) fn install_executables(
.join(", ") .join(", ")
) )
} }
}
#[cfg(windows)]
let itself = std::env::current_exe().ok();
for (name, source_path, target_path) in &target_entry_points { for (name, source_path, target_path) in &target_entry_points {
debug!("Installing executable: `{name}`"); debug!("Installing executable: `{name}`");
#[cfg(unix)] #[cfg(unix)]
replace_symlink(source_path, target_path).context("Failed to install executable")?; replace_symlink(source_path, target_path).context("Failed to install executable")?;
#[cfg(windows)] #[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")?; fs_err::copy(source_path, target_path).context("Failed to install entrypoint")?;
} }
}
let s = if target_entry_points.len() == 1 { let s = if target_entry_points.len() == 1 {
"" ""

View File

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