Skip to content

Commit 5894161

Browse files
authored
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.
1 parent 57a7f04 commit 5894161

File tree

2 files changed

+75
-20
lines changed

2 files changed

+75
-20
lines changed

crates/uv/src/commands/python/install.rs

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -358,32 +358,50 @@ pub(crate) async fn install(
358358
target.simplified_display()
359359
);
360360

361+
// Check if the existing link is valid
362+
let valid_link = target
363+
.read_link()
364+
.and_then(|target| target.try_exists())
365+
.inspect_err(|err| debug!("Failed to inspect executable with error: {err}"))
366+
.unwrap_or(true);
367+
361368
// Figure out what installation it references, if any
362-
let existing = find_matching_bin_link(
363-
installations
364-
.iter()
365-
.copied()
366-
.chain(existing_installations.iter()),
367-
&target,
368-
);
369+
let existing = valid_link
370+
.then(|| {
371+
find_matching_bin_link(
372+
installations
373+
.iter()
374+
.copied()
375+
.chain(existing_installations.iter()),
376+
&target,
377+
)
378+
})
379+
.flatten();
369380

370381
match existing {
371382
None => {
372383
// There's an existing executable we don't manage, require `--force`
373-
if !force {
374-
errors.push((
375-
installation.key(),
376-
anyhow::anyhow!(
377-
"Executable already exists at `{}` but is not managed by uv; use `--force` to replace it",
378-
to.simplified_display()
379-
),
380-
));
381-
continue;
384+
if valid_link {
385+
if !force {
386+
errors.push((
387+
installation.key(),
388+
anyhow::anyhow!(
389+
"Executable already exists at `{}` but is not managed by uv; use `--force` to replace it",
390+
to.simplified_display()
391+
),
392+
));
393+
continue;
394+
}
395+
debug!(
396+
"Replacing existing executable at `{}` due to `--force`",
397+
target.simplified_display()
398+
);
399+
} else {
400+
debug!(
401+
"Replacing broken symlink at `{}`",
402+
target.simplified_display()
403+
);
382404
}
383-
debug!(
384-
"Replacing existing executable at `{}` due to `--force`",
385-
target.simplified_display()
386-
);
387405
}
388406
Some(existing) if existing == *installation => {
389407
// The existing link points to the same installation, so we're done unless

crates/uv/tests/it/python_install.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,3 +839,40 @@ fn python_install_unknown() {
839839
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
840840
"###);
841841
}
842+
843+
#[cfg(unix)]
844+
#[test]
845+
fn python_install_preview_broken_link() {
846+
use assert_fs::prelude::PathCreateDir;
847+
use fs_err::os::unix::fs::symlink;
848+
849+
let context: TestContext = TestContext::new_with_versions(&[])
850+
.with_filtered_python_keys()
851+
.with_filtered_exe_suffix();
852+
853+
let bin_python = context.temp_dir.child("bin").child("python3.13");
854+
855+
// Create a broken symlink
856+
context.temp_dir.child("bin").create_dir_all().unwrap();
857+
symlink(context.temp_dir.join("does-not-exist"), &bin_python).unwrap();
858+
859+
// Install
860+
uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.13"), @r###"
861+
success: true
862+
exit_code: 0
863+
----- stdout -----
864+
865+
----- stderr -----
866+
Installed Python 3.13.1 in [TIME]
867+
+ cpython-3.13.1-[PLATFORM] (python3.13)
868+
"###);
869+
870+
// We should replace the broken symlink
871+
insta::with_settings!({
872+
filters => context.filters(),
873+
}, {
874+
insta::assert_snapshot!(
875+
read_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.13.1-[PLATFORM]/bin/python3.13"
876+
);
877+
});
878+
}

0 commit comments

Comments
 (0)