Skip to content

Commit 13b6560

Browse files
committed
Fix Python executable installation when multiple patch versions are requested
1 parent 75949f3 commit 13b6560

File tree

2 files changed

+64
-7
lines changed

2 files changed

+64
-7
lines changed

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

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -300,9 +300,11 @@ pub(crate) async fn install(
300300
None
301301
};
302302

303+
let installations: Vec<_> = downloaded.iter().chain(satisfied.iter().copied()).collect();
304+
303305
// Ensure that the installations are _complete_ for both downloaded installations and existing
304306
// installations that match the request
305-
for installation in downloaded.iter().chain(satisfied.iter().copied()) {
307+
for installation in installations.iter() {
306308
installation.ensure_externally_managed()?;
307309
installation.ensure_canonical_executables()?;
308310

@@ -353,7 +355,13 @@ pub(crate) async fn install(
353355
);
354356

355357
// Figure out what installation it references, if any
356-
let existing = find_matching_bin_link(&existing_installations, &target);
358+
let existing = find_matching_bin_link(
359+
installations
360+
.iter()
361+
.copied()
362+
.chain(existing_installations.iter()),
363+
&target,
364+
);
357365

358366
match existing {
359367
None => {
@@ -373,7 +381,7 @@ pub(crate) async fn install(
373381
target.simplified_display()
374382
);
375383
}
376-
Some(existing) if existing == installation => {
384+
Some(existing) if existing == *installation => {
377385
// The existing link points to the same installation, so we're done unless
378386
// they requested we reinstall
379387
if !(reinstall || force) {
@@ -429,6 +437,17 @@ pub(crate) async fn install(
429437

430438
// Replace the existing link
431439
fs_err::remove_file(&to)?;
440+
441+
if let Some(existing) = existing {
442+
// Ensure we do not report installation of this executable for an existing
443+
// key if we undo it
444+
changelog
445+
.installed_executables
446+
.entry(existing.key().clone())
447+
.or_default()
448+
.remove(&target);
449+
}
450+
432451
installation.create_bin_link(&target)?;
433452
debug!(
434453
"Updated executable at `{}` to {}",
@@ -562,6 +581,10 @@ pub(crate) fn format_executables(
562581
return String::new();
563582
};
564583

584+
if installed.is_empty() {
585+
return String::new();
586+
}
587+
565588
let names = installed
566589
.iter()
567590
.filter_map(|path| path.file_name())
@@ -612,7 +635,7 @@ fn warn_if_not_on_path(bin: &Path) {
612635
/// Like [`ManagedPythonInstallation::is_bin_link`], but this method will only resolve the
613636
/// given path one time.
614637
fn find_matching_bin_link<'a>(
615-
installations: &'a [ManagedPythonInstallation],
638+
mut installations: impl Iterator<Item = &'a ManagedPythonInstallation>,
616639
path: &Path,
617640
) -> Option<&'a ManagedPythonInstallation> {
618641
let target = if cfg!(unix) {
@@ -630,7 +653,5 @@ fn find_matching_bin_link<'a>(
630653
unreachable!("Only Windows and Unix are supported")
631654
};
632655

633-
installations
634-
.iter()
635-
.find(|installation| installation.executable() == target)
656+
installations.find(|installation| installation.executable() == target)
636657
}

crates/uv/tests/it/python_install.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,42 @@ fn python_install_preview() {
227227

228228
// The executable should be removed
229229
bin_python.assert(predicate::path::missing());
230+
231+
// Install multiple patch versions
232+
uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.12.7").arg("3.12.6"), @r###"
233+
success: true
234+
exit_code: 0
235+
----- stdout -----
236+
237+
----- stderr -----
238+
Installed 2 versions in [TIME]
239+
+ cpython-3.12.6-[PLATFORM]
240+
+ cpython-3.12.7-[PLATFORM] (python3.12)
241+
"###);
242+
243+
let bin_python = context
244+
.temp_dir
245+
.child("bin")
246+
.child(format!("python3.12{}", std::env::consts::EXE_SUFFIX));
247+
248+
// The link should be for the newer patch version
249+
if cfg!(unix) {
250+
insta::with_settings!({
251+
filters => context.filters(),
252+
}, {
253+
insta::assert_snapshot!(
254+
read_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.7-[PLATFORM]/bin/python3.12"
255+
);
256+
});
257+
} else {
258+
insta::with_settings!({
259+
filters => context.filters(),
260+
}, {
261+
insta::assert_snapshot!(
262+
read_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.7-[PLATFORM]/python"
263+
);
264+
});
265+
}
230266
}
231267

232268
#[test]

0 commit comments

Comments
 (0)