Skip to content

Commit

Permalink
Prefer compatible to incompatible distributions when packages exist o…
Browse files Browse the repository at this point in the history
…n multiple indexes (#8961)

## Summary

At time of writing, `markupsafe==3.0.2` exists on the PyTorch index, but
there's
only a single wheel:


`MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl`

Meanwhile, there are a large number of wheels on PyPI for the same
version. If the
user is on Python 3.12, and we return the incompatible PyTorch wheel
without
considering the PyPI wheels, PubGrub will mark 3.0.2 as an incompatible
version,
even though there are compatible wheels on PyPI.

Closes #8922.
  • Loading branch information
charliermarsh authored and konstin committed Nov 10, 2024
1 parent 0b15684 commit d4cfb0b
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 2 deletions.
56 changes: 54 additions & 2 deletions crates/uv-resolver/src/candidate_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,17 +374,33 @@ impl CandidateSelector {
}

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

// If we have an incompatible candidate, and we've progressed past it, return it.
if incompatible
.as_ref()
.is_some_and(|incompatible| version != incompatible.version)
{
trace!(
"Returning incompatible candidate for package {package_name} with range {range} after {steps} steps",
);
return incompatible;
}

let candidate = {
if version.any_prerelease() && !allow_prerelease {
continue;
Expand All @@ -395,7 +411,7 @@ impl CandidateSelector {
let Some(dist) = maybe_dist.prioritized_dist() else {
continue;
};
trace!("found candidate for package {package_name:?} with range {range:?} after {steps} steps: {version:?} version");
trace!("Found candidate for package {package_name} with range {range} after {steps} steps: {version} version");
Candidate::new(package_name, version, dist, VersionChoiceKind::Compatible)
};

Expand All @@ -415,8 +431,44 @@ impl CandidateSelector {
continue;
}

// If the candidate isn't compatible, we store it as incompatible and continue
// searching. Typically, we want to return incompatible candidates so that PubGrub can
// track them (then continue searching, with additional constraints). However, we may
// see multiple entries for the same version (e.g., if the same version exists on
// multiple indexes and `--index-strategy unsafe-best-match` is enabled), and it's
// possible that one of them is compatible while the other is not.
//
// See, e.g., <https://github.com/astral-sh/uv/issues/8922>. At time of writing,
// markupsafe==3.0.2 exists on the PyTorch index, but there's only a single wheel:
//
// MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
//
// Meanwhile, there are a large number of wheels on PyPI for the same version. If the
// user is on Python 3.12, and we return the incompatible PyTorch wheel without
// considering the PyPI wheels, PubGrub will mark 3.0.2 as an incompatible version,
// even though there are compatible wheels on PyPI. Thus, we need to ensure that we
// return the first _compatible_ candidate across all indexes, if such a candidate
// exists.
if matches!(candidate.dist(), CandidateDist::Incompatible(_)) {
if incompatible.is_none() {
incompatible = Some(candidate);
}
continue;
}

trace!(
"Returning candidate for package {package_name} with range {range} after {steps} steps",
);
return Some(candidate);
}

if incompatible.is_some() {
trace!(
"Returning incompatible candidate for package {package_name} with range {range} after {steps} steps",
);
return incompatible;
}

trace!("Exhausted all candidates for package {package_name} with range {range} after {steps} steps");
None
}
Expand Down
37 changes: 37 additions & 0 deletions crates/uv/tests/it/pip_compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13190,3 +13190,40 @@ fn lowest_fork() -> Result<()> {

Ok(())
}

/// See: <https://github.com/astral-sh/uv/issues/8922>
#[test]
fn same_version_multi_index_incompatibility() -> Result<()> {
let context = TestContext::new("3.10");
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("cffi==1.15.1")?;

// `cffi` is present on Test PyPI, but only as a single wheel: `cffi-1.15.1-cp311-cp311-win_arm64.whl`.
// If we don't check PyPI for the same version, we'll fail.
uv_snapshot!(context
.pip_compile()
.arg("requirements.in")
.arg("--extra-index-url")
.arg("https://test.pypi.org/simple")
.arg("--index-strategy")
.arg("unsafe-best-match")
.arg("--python-platform")
.arg("linux")
.arg("--python-version")
.arg("3.10"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] requirements.in --index-strategy unsafe-best-match --python-platform linux --python-version 3.10
cffi==1.15.1
# via -r requirements.in
pycparser==2.21
# via cffi
----- stderr -----
Resolved 2 packages in [TIME]
"###);

Ok(())
}

0 comments on commit d4cfb0b

Please sign in to comment.