From 3ee2b1073827d54f9f6d458f02fdb19998c5cc46 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 10 Dec 2024 13:13:22 -0500 Subject: [PATCH] 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. --- Cargo.lock | 1 + crates/uv-tool/src/lib.rs | 2 + crates/uv-virtualenv/src/virtualenv.rs | 2 + crates/uv/Cargo.toml | 3 ++ crates/uv/src/commands/tool/common.rs | 69 +++++++++++++----------- crates/uv/src/commands/tool/uninstall.rs | 12 +++++ 6 files changed, 57 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b861d7023..a9607eab2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4432,6 +4432,7 @@ dependencies = [ "reqwest", "rkyv", "rustc-hash", + "self-replace", "serde", "serde_json", "similar", diff --git a/crates/uv-tool/src/lib.rs b/crates/uv-tool/src/lib.rs index 24be27dc2..94e67a6cc 100644 --- a/crates/uv-tool/src/lib.rs +++ b/crates/uv-tool/src/lib.rs @@ -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(()) diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index d56f45765..36792f962 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -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)?; diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index bd8ba98bb..705a29554 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -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" } diff --git a/crates/uv/src/commands/tool/common.rs b/crates/uv/src/commands/tool/common.rs index 3dcadb7bb..495e7467a 100644 --- a/crates/uv/src/commands/tool/common.rs +++ b/crates/uv/src/commands/tool/common.rs @@ -125,47 +125,52 @@ pub(crate) fn install_executables( return Ok(ExitStatus::Failure); } - // Check if they exist, before installing - let mut existing_entry_points = target_entry_points - .iter() - .filter(|(_, _, target_path)| target_path.exists()) - .peekable(); + // 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(); + if existing_entry_points.peek().is_some() { + // Clean up the environment we just created + installed_tools.remove_environment(name)?; - // 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)?; + let existing_entry_points = existing_entry_points + // SAFETY: We know the target has a filename because we just constructed it above + .map(|(_, _, target)| target.file_name().unwrap().to_string_lossy()) + .collect::>(); + let (s, exists) = if existing_entry_points.len() == 1 { + ("", "exists") + } else { + ("s", "exist") + }; + bail!( + "Executable{s} already {exists}: {} (use `--force` to overwrite)", + existing_entry_points + .iter() + .map(|name| name.bold()) + .join(", ") + ) } - } else if existing_entry_points.peek().is_some() { - // Clean up the environment we just created - installed_tools.remove_environment(name)?; - - let existing_entry_points = existing_entry_points - // SAFETY: We know the target has a filename because we just constructed it above - .map(|(_, _, target)| target.file_name().unwrap().to_string_lossy()) - .collect::>(); - let (s, exists) = if existing_entry_points.len() == 1 { - ("", "exists") - } else { - ("s", "exist") - }; - bail!( - "Executable{s} already {exists}: {} (use `--force` to overwrite)", - existing_entry_points - .iter() - .map(|name| name.bold()) - .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)] - fs_err::copy(source_path, target_path).context("Failed to install entrypoint")?; + 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 { diff --git a/crates/uv/src/commands/tool/uninstall.rs b/crates/uv/src/commands/tool/uninstall.rs index ba419898f..1fad2a0b7 100644 --- a/crates/uv/src/commands/tool/uninstall.rs +++ b/crates/uv/src/commands/tool/uninstall.rs @@ -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 => {