From ac3a08508461f3dabe1d762fc91978058c8230ff Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 8 Jul 2024 11:32:09 -0500 Subject: [PATCH] Respect `requires-python` when prefetching (#4900) ## Summary This is fallout from https://github.com/astral-sh/uv/pull/4705. We need to respect `requires-python` in the prefetch code to avoid building unsupported distributions. Closes https://github.com/astral-sh/uv/issues/4898. --- .../src/resolver/batch_prefetch.rs | 44 +++++++++++++++-- crates/uv-resolver/src/resolver/mod.rs | 48 +++++++++++++++++-- crates/uv/tests/pip_compile.rs | 29 +++++++++++ 3 files changed, 113 insertions(+), 8 deletions(-) diff --git a/crates/uv-resolver/src/resolver/batch_prefetch.rs b/crates/uv-resolver/src/resolver/batch_prefetch.rs index ce22c17f1cd5..24de36a7c504 100644 --- a/crates/uv-resolver/src/resolver/batch_prefetch.rs +++ b/crates/uv-resolver/src/resolver/batch_prefetch.rs @@ -6,13 +6,13 @@ use rustc_hash::FxHashMap; use tokio::sync::mpsc::Sender; use tracing::{debug, trace}; -use distribution_types::DistributionMetadata; +use distribution_types::{CompatibleDist, DistributionMetadata}; use pep440_rs::Version; -use crate::candidate_selector::{CandidateDist, CandidateSelector}; +use crate::candidate_selector::CandidateSelector; use crate::pubgrub::{PubGrubPackage, PubGrubPackageInner}; use crate::resolver::Request; -use crate::{InMemoryIndex, ResolveError, VersionsResponse}; +use crate::{InMemoryIndex, PythonRequirement, ResolveError, VersionsResponse}; enum BatchPrefetchStrategy { /// Go through the next versions assuming the existing selection and its constraints @@ -49,6 +49,7 @@ impl BatchPrefetcher { next: &PubGrubPackage, version: &Version, current_range: &Range, + python_requirement: &PythonRequirement, request_sink: &Sender, index: &InMemoryIndex, selector: &CandidateSelector, @@ -128,7 +129,7 @@ impl BatchPrefetcher { } }; - let CandidateDist::Compatible(dist) = candidate.dist() else { + let Some(dist) = candidate.compatible() else { continue; }; @@ -136,6 +137,41 @@ impl BatchPrefetcher { if !dist.prefetchable() { continue; } + + // Avoid prefetching for distributions that don't satisfy the Python requirement. + match dist { + CompatibleDist::InstalledDist(_) => {} + CompatibleDist::SourceDist { sdist, .. } + | CompatibleDist::IncompatibleWheel { sdist, .. } => { + // Source distributions must meet both the _target_ Python version and the + // _installed_ Python version (to build successfully). + if let Some(requires_python) = sdist.file.requires_python.as_ref() { + if let Some(target) = python_requirement.target() { + if !target.is_compatible_with(requires_python) { + continue; + } + } + if !requires_python.contains(python_requirement.installed()) { + continue; + } + } + } + CompatibleDist::CompatibleWheel { wheel, .. } => { + // Wheels must meet the _target_ Python version. + if let Some(requires_python) = wheel.file.requires_python.as_ref() { + if let Some(target) = python_requirement.target() { + if !target.is_compatible_with(requires_python) { + continue; + } + } else { + if !requires_python.contains(python_requirement.installed()) { + continue; + } + } + } + } + }; + let dist = dist.for_resolution(); // Emit a request to fetch the metadata for this version. diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index d51d5d93632c..e08ed1e39dce 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -364,6 +364,7 @@ impl ResolverState ResolverState ResolverState( packages: impl Iterator)>, urls: &Urls, + python_requirement: &PythonRequirement, request_sink: &Sender, ) -> Result<(), ResolveError> { // Iterate over the potential packages, and fetch file metadata for any of them. These @@ -740,7 +743,11 @@ impl ResolverState ResolverState { + Request::Prefetch(package_name, range, python_requirement) => { // Wait for the package metadata to become available. let versions_response = self .index @@ -1720,6 +1727,39 @@ impl ResolverState {} + CompatibleDist::SourceDist { sdist, .. } + | CompatibleDist::IncompatibleWheel { sdist, .. } => { + // Source distributions must meet both the _target_ Python version and the + // _installed_ Python version (to build successfully). + if let Some(requires_python) = sdist.file.requires_python.as_ref() { + if let Some(target) = python_requirement.target() { + if !target.is_compatible_with(requires_python) { + return Ok(None); + } + } + if !requires_python.contains(python_requirement.installed()) { + return Ok(None); + } + } + } + CompatibleDist::CompatibleWheel { wheel, .. } => { + // Wheels must meet the _target_ Python version. + if let Some(requires_python) = wheel.file.requires_python.as_ref() { + if let Some(target) = python_requirement.target() { + if !target.is_compatible_with(requires_python) { + return Ok(None); + } + } else { + if !requires_python.contains(python_requirement.installed()) { + return Ok(None); + } + } + } + } + }; + // Emit a request to fetch the metadata for this version. if self.index.distributions().register(candidate.version_id()) { let dist = dist.for_resolution().to_owned(); @@ -2213,7 +2253,7 @@ pub(crate) enum Request { /// A request to fetch the metadata from an already-installed distribution. Installed(InstalledDist), /// A request to pre-fetch the metadata for a package and the best-guess distribution. - Prefetch(PackageName, Range), + Prefetch(PackageName, Range, PythonRequirement), } impl<'a> From> for Request { @@ -2265,7 +2305,7 @@ impl Display for Request { Self::Installed(dist) => { write!(f, "Installed metadata {dist}") } - Self::Prefetch(package_name, range) => { + Self::Prefetch(package_name, range, _) => { write!(f, "Prefetch {package_name} {range}") } } diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 49f731488bf4..c9db3e355657 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -4544,6 +4544,35 @@ coverage = ["example[test]", "extras>=0.0.1,<=0.0.2"] Ok(()) } +/// Respect `requires-python` when prefetching. +/// +/// `voluptuous==0.15.1` requires Python 3.9 or later, so we should resolve to an earlier version +/// and avoiding building 0.15.1 at all. +#[test] +fn requires_python_prefetch() -> Result<()> { + let context = TestContext::new("3.8"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("voluptuous<=0.15.1")?; + + uv_snapshot!(context + .pip_compile() + .env_remove("UV_EXCLUDE_NEWER") + .arg("requirements.in"), @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 + voluptuous==0.14.2 + # via -r requirements.in + + ----- stderr ----- + Resolved 1 package in [TIME] + "###); + + Ok(()) +} + /// Use an existing resolution for `black==23.10.1`, with stale versions of `click` and `pathspec`. /// Nothing should change. #[test]