Skip to content

Commit c25f074

Browse files
committed
Prefer compatible to incompatible distributions when packages exist on multiple indexes
1 parent b5a3d09 commit c25f074

File tree

1 file changed

+52
-2
lines changed

1 file changed

+52
-2
lines changed

crates/uv-resolver/src/candidate_selector.rs

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,17 +374,33 @@ impl CandidateSelector {
374374
}
375375

376376
/// Select the first-matching [`Candidate`] from a set of candidate versions and files,
377-
/// preferring wheels over source distributions.
377+
/// preferring wheels to source distributions.
378+
///
379+
/// The returned [`Candidate`] _may not_ be compatible with the current platform; in such
380+
/// cases, the resolver is responsible for tracking the incompatibility and re-running the
381+
/// selection process with additional constraints.
378382
fn select_candidate<'a>(
379383
versions: impl Iterator<Item = (&'a Version, VersionMapDistHandle<'a>)>,
380384
package_name: &'a PackageName,
381385
range: &Range<Version>,
382386
allow_prerelease: bool,
383387
) -> Option<Candidate<'a>> {
384388
let mut steps = 0usize;
389+
let mut incompatible: Option<Candidate> = None;
385390
for (version, maybe_dist) in versions {
386391
steps += 1;
387392

393+
// If we have an incompatible candidate, and we've progressed past it, return it.
394+
if incompatible
395+
.as_ref()
396+
.is_some_and(|incompatible| version != incompatible.version)
397+
{
398+
trace!(
399+
"Returning incompatible candidate for package {package_name} with range {range} after {steps} steps",
400+
);
401+
return incompatible;
402+
}
403+
388404
let candidate = {
389405
if version.any_prerelease() && !allow_prerelease {
390406
continue;
@@ -395,7 +411,7 @@ impl CandidateSelector {
395411
let Some(dist) = maybe_dist.prioritized_dist() else {
396412
continue;
397413
};
398-
trace!("found candidate for package {package_name:?} with range {range:?} after {steps} steps: {version:?} version");
414+
trace!("Found candidate for package {package_name} with range {range} after {steps} steps: {version} version");
399415
Candidate::new(package_name, version, dist, VersionChoiceKind::Compatible)
400416
};
401417

@@ -415,8 +431,42 @@ impl CandidateSelector {
415431
continue;
416432
}
417433

434+
// If the candidate isn't compatible, we store it as incompatible and continue
435+
// searching. Typically, we want to return incompatible candidates so that PubGrub can
436+
// track them (then continue searching, with additional constraints). However, we may
437+
// see multiple entries for the same version (e.g., if the same version exists on
438+
// multiple indexes and `--index-strategy unsafe-best-match` is enabled), and it's
439+
// possible that one of them is compatible while the other is not.
440+
//
441+
// See, e.g., <https://github.com/astral-sh/uv/issues/8922>. At time of writing,
442+
// markupsafe==3.0.2 exists on the PyTorch index, but there's only a single wheel:
443+
//
444+
// MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
445+
//
446+
// Meanwhile, there are a large number of wheels on PyPI for the same version. If the
447+
// user is on Python 3.12, and we return the incompatible PyTorch wheel without
448+
// considering the PyPI wheels, PubGrub will mark 3.0.2 as an incompatible version,
449+
// even though there are compatible wheels on PyPI.
450+
if matches!(candidate.dist(), CandidateDist::Incompatible(_)) {
451+
if incompatible.is_none() {
452+
incompatible = Some(candidate);
453+
}
454+
continue;
455+
}
456+
457+
trace!(
458+
"Returning candidate for package {package_name} with range {range} after {steps} steps",
459+
);
418460
return Some(candidate);
419461
}
462+
463+
if incompatible.is_some() {
464+
trace!(
465+
"Returning incompatible candidate for package {package_name} with range {range} after {steps} steps",
466+
);
467+
return incompatible;
468+
}
469+
420470
trace!("Exhausted all candidates for package {package_name} with range {range} after {steps} steps");
421471
None
422472
}

0 commit comments

Comments
 (0)