Validate URL wheel tags against `Requires-Python` and required environments (#16824)

## Summary

Closes #16818.
This commit is contained in:
Charlie Marsh 2025-11-25 20:05:58 -05:00 committed by GitHub
parent 17c1061676
commit bfdee80f6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 125 additions and 4 deletions

View File

@ -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 {

View File

@ -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<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
| PubGrubPackageInner::Group { name, .. }
| PubGrubPackageInner::Package { name, .. } => {
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<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
/// that version if it is in range and `None` otherwise.
fn choose_version_url(
&self,
id: Id<PubGrubPackage>,
name: &PackageName,
range: &Range<Version>,
url: &VerbatimParsedUrl,
env: &ResolverEnvironment,
python_requirement: &PythonRequirement,
pubgrub: &State<UvDependencyProvider>,
) -> Result<Option<ResolverVersion>, ResolveError> {
debug!(
"Searching for a compatible version of {name} @ {} ({range})",
@ -1178,8 +1181,53 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
return Ok(None);
}
// The version is incompatible due to its Python requirement.
// If the URL points to a pre-built wheel, and the wheel's supported Python versions don't
// match our `Requires-Python`, mark it as incompatible.
let dist = Dist::from_url(name.clone(), url.clone())?;
if let Dist::Built(dist) = dist {
let filename = match &dist {
BuiltDist::Registry(dist) => &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<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
}
}
// If this is a wheel, and the implied Python version doesn't overlap, raise an error.
Ok(Some(ResolverVersion::Unforked(version.clone())))
}

View File

@ -32220,3 +32220,64 @@ fn no_warning_without_and_with_lower_bound() -> 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(())
}