Skip to content

Commit

Permalink
Enable uv tool uninstall uv on Windows (#8963)
Browse files Browse the repository at this point in the history
## Summary

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

Closes #6400.
  • Loading branch information
charliermarsh authored Dec 10, 2024
1 parent 389a26e commit 3ee2b10
Show file tree
Hide file tree
Showing 6 changed files with 57 additions and 32 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/uv-tool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
Expand Down
2 changes: 2 additions & 0 deletions crates/uv-virtualenv/src/virtualenv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
69 changes: 37 additions & 32 deletions crates/uv/src/commands/tool/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<_>>();
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::<Vec<_>>();
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 {
Expand Down
12 changes: 12 additions & 0 deletions crates/uv/src/commands/tool/uninstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,13 +180,25 @@ 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 {
debug!(
"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 => {
Expand Down

0 comments on commit 3ee2b10

Please sign in to comment.