Replace executables with broken symlinks during uv python install (#9706)

I somehow got in a state where we'd fail to install with

```
error: Failed to install cpython-3.13.0-macos-aarch64-none
  Caused by: Executable already exists at `/Users/zb/.local/bin/python3` but is not managed by uv; use `--force` to replace it
error: Failed to install cpython-3.13.0-macos-aarch64-none
  Caused by: Executable already exists at `/Users/zb/.local/bin/python` but is not managed by uv; use `--force` to replace it
```

but `python` / `python3` _were_ managed by uv, they just were linked to
an installation that was deleted.

This updates the logic to replace broken executables that are broken
symlinks. We apply this to broken links regardless of whether or not we
think the target is managed by uv.
This commit is contained in:
Zanie Blue 2024-12-10 16:39:23 -06:00 committed by GitHub
parent 57a7f04f9a
commit 589416183f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 75 additions and 20 deletions

View file

@ -358,32 +358,50 @@ pub(crate) async fn install(
target.simplified_display()
);
// Check if the existing link is valid
let valid_link = target
.read_link()
.and_then(|target| target.try_exists())
.inspect_err(|err| debug!("Failed to inspect executable with error: {err}"))
.unwrap_or(true);
// Figure out what installation it references, if any
let existing = find_matching_bin_link(
installations
.iter()
.copied()
.chain(existing_installations.iter()),
&target,
);
let existing = valid_link
.then(|| {
find_matching_bin_link(
installations
.iter()
.copied()
.chain(existing_installations.iter()),
&target,
)
})
.flatten();
match existing {
None => {
// There's an existing executable we don't manage, require `--force`
if !force {
errors.push((
installation.key(),
anyhow::anyhow!(
"Executable already exists at `{}` but is not managed by uv; use `--force` to replace it",
to.simplified_display()
),
));
continue;
if valid_link {
if !force {
errors.push((
installation.key(),
anyhow::anyhow!(
"Executable already exists at `{}` but is not managed by uv; use `--force` to replace it",
to.simplified_display()
),
));
continue;
}
debug!(
"Replacing existing executable at `{}` due to `--force`",
target.simplified_display()
);
} else {
debug!(
"Replacing broken symlink at `{}`",
target.simplified_display()
);
}
debug!(
"Replacing existing executable at `{}` due to `--force`",
target.simplified_display()
);
}
Some(existing) if existing == *installation => {
// The existing link points to the same installation, so we're done unless

View file

@ -839,3 +839,40 @@ fn python_install_unknown() {
error: `./foo` is not a valid Python download request; see `uv python help` for supported formats and `uv python list --only-downloads` for available versions
"###);
}
#[cfg(unix)]
#[test]
fn python_install_preview_broken_link() {
use assert_fs::prelude::PathCreateDir;
use fs_err::os::unix::fs::symlink;
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix();
let bin_python = context.temp_dir.child("bin").child("python3.13");
// Create a broken symlink
context.temp_dir.child("bin").create_dir_all().unwrap();
symlink(context.temp_dir.join("does-not-exist"), &bin_python).unwrap();
// Install
uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.13"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed Python 3.13.1 in [TIME]
+ cpython-3.13.1-[PLATFORM] (python3.13)
"###);
// We should replace the broken symlink
insta::with_settings!({
filters => context.filters(),
}, {
insta::assert_snapshot!(
read_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.13.1-[PLATFORM]/bin/python3.13"
);
});
}