From bfdee80f6c6260471aa8663033c9148103d45989 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 25 Nov 2025 20:05:58 -0500 Subject: [PATCH] Validate URL wheel tags against `Requires-Python` and required environments (#16824) ## Summary Closes #16818. --- .../src/supported_environments.rs | 10 +++ crates/uv-resolver/src/resolver/mod.rs | 58 ++++++++++++++++-- crates/uv/tests/it/lock.rs | 61 +++++++++++++++++++ 3 files changed, 125 insertions(+), 4 deletions(-) diff --git a/crates/uv-pypi-types/src/supported_environments.rs b/crates/uv-pypi-types/src/supported_environments.rs index 59cbbf23b..8a1299dac 100644 --- a/crates/uv-pypi-types/src/supported_environments.rs +++ b/crates/uv-pypi-types/src/supported_environments.rs @@ -28,6 +28,16 @@ impl SupportedEnvironments { pub fn iter(&self) -> std::slice::Iter<'_, MarkerTree> { self.0.iter() } + + /// Returns `true` if there are no supported environments. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Returns the number of supported environments. + pub fn len(&self) -> usize { + self.0.len() + } } impl<'a> IntoIterator for &'a SupportedEnvironments { diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index e893a82b0..bb75f5b00 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -26,7 +26,7 @@ use uv_distribution_types::{ BuiltDist, CompatibleDist, DerivationChain, Dist, DistErrorKind, DistributionMetadata, IncompatibleDist, IncompatibleSource, IncompatibleWheel, IndexCapabilities, IndexLocations, IndexMetadata, IndexUrl, InstalledDist, Name, PythonRequirementKind, RemoteSource, Requirement, - ResolvedDist, ResolvedDistRef, SourceDist, VersionOrUrlRef, + ResolvedDist, ResolvedDistRef, SourceDist, VersionOrUrlRef, implied_markers, }; use uv_git::GitResolver; use uv_normalize::{ExtraName, GroupName, PackageName}; @@ -34,7 +34,7 @@ use uv_pep440::{MIN_VERSION, Version, VersionSpecifiers, release_specifiers_to_r use uv_pep508::{ MarkerEnvironment, MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString, }; -use uv_platform_tags::Tags; +use uv_platform_tags::{IncompatibleTag, Tags}; use uv_pypi_types::{ConflictItem, ConflictItemRef, ConflictKindRef, Conflicts, VerbatimParsedUrl}; use uv_torch::TorchStrategy; use uv_types::{BuildContext, HashStrategy, InstalledPackagesProvider}; @@ -1109,7 +1109,7 @@ impl ResolverState { if let Some(url) = package.name().and_then(|name| fork_urls.get(name)) { - self.choose_version_url(name, range, url, python_requirement) + self.choose_version_url(id, name, range, url, env, python_requirement, pubgrub) } else { self.choose_version_registry( package, @@ -1134,10 +1134,13 @@ impl ResolverState, name: &PackageName, range: &Range, url: &VerbatimParsedUrl, + env: &ResolverEnvironment, python_requirement: &PythonRequirement, + pubgrub: &State, ) -> Result, ResolveError> { debug!( "Searching for a compatible version of {name} @ {} ({range})", @@ -1178,8 +1181,53 @@ impl ResolverState &dist.best_wheel().filename, + BuiltDist::DirectUrl(dist) => &dist.filename, + BuiltDist::Path(dist) => &dist.filename, + }; + + // If the wheel does _not_ cover a required platform, it's incompatible. + if env.marker_environment().is_none() && !self.options.required_environments.is_empty() + { + let wheel_marker = implied_markers(filename); + // If the user explicitly marked a platform as required, ensure it has coverage. + for environment_marker in self.options.required_environments.iter().copied() { + // If the platform is part of the current environment... + if env.included_by_marker(environment_marker) + && !find_environments(id, pubgrub).is_disjoint(environment_marker) + { + // ...but the wheel doesn't support it, it's incompatible. + if wheel_marker.is_disjoint(environment_marker) { + return Ok(Some(ResolverVersion::Unavailable( + version.clone(), + UnavailableVersion::IncompatibleDist(IncompatibleDist::Wheel( + IncompatibleWheel::MissingPlatform(environment_marker), + )), + ))); + } + } + } + } + + // If the wheel's Python tag doesn't match the target Python, it's incompatible. + if !python_requirement.target().matches_wheel_tag(filename) { + return Ok(Some(ResolverVersion::Unavailable( + filename.version.clone(), + UnavailableVersion::IncompatibleDist(IncompatibleDist::Wheel( + IncompatibleWheel::Tag(IncompatibleTag::AbiPythonVersion), + )), + ))); + } + } + + // The version is incompatible due to its `Requires-Python` requirement. if let Some(requires_python) = metadata.requires_python.as_ref() { + // TODO(charlie): We only care about this for source distributions. if !python_requirement .installed() .is_contained_by(requires_python) @@ -1207,6 +1255,8 @@ impl ResolverState Result<()> { Ok(()) } + +#[test] +fn lock_unsupported_wheel_url_requires_python() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["numpy @ https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl"] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because only numpy==2.3.5 is available and numpy==2.3.5 has no wheels with a matching Python version tag (e.g., `cp312`), we can conclude that all versions of numpy cannot be used. + And because your project depends on numpy, we can conclude that your project's requirements are unsatisfiable. + "); + + Ok(()) +} + +#[test] +fn lock_unsupported_wheel_url_required_platform() -> Result<()> { + let context = TestContext::new("3.11"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.11" + dependencies = ["numpy @ https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl"] + + [tool.uv] + required-environments = ["sys_platform == 'win32'"] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because only numpy==2.3.5 is available and numpy==2.3.5 has no Windows-compatible wheels, we can conclude that all versions of numpy cannot be used. + And because your project depends on numpy, we can conclude that your project's requirements are unsatisfiable. + "); + + Ok(()) +}