mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-01 20:31:12 +00:00
Use structured wheel tags everywhere (#10542)
## Summary This PR extends the thinking in #10525 to platform tags, and then uses the structured tag enums everywhere, rather than passing around strings. I think this is a big improvement! It means we're no longer doing ad hoc tag parsing all over the place.
This commit is contained in:
parent
2ffa31946d
commit
5c91217488
33 changed files with 1624 additions and 487 deletions
|
|
@ -265,18 +265,6 @@ impl Lock {
|
|||
.retain(|wheel| requires_python.matches_wheel_tag(&wheel.filename));
|
||||
|
||||
// Filter by platform tags.
|
||||
|
||||
// See https://github.com/pypi/warehouse/blob/ccff64920db7965078cf1fdb50f028e640328887/warehouse/forklift/legacy.py#L100-L169
|
||||
// for a list of relevant platforms.
|
||||
let linux_tags = [
|
||||
"manylinux1_",
|
||||
"manylinux2010_",
|
||||
"manylinux2014_",
|
||||
"musllinux_",
|
||||
"manylinux_",
|
||||
];
|
||||
let windows_tags = ["win32", "win_arm64", "win_amd64", "win_ia64"];
|
||||
|
||||
locked_dist.wheels.retain(|wheel| {
|
||||
// Naively, we'd check whether `platform_system == 'Linux'` is disjoint, or
|
||||
// `os_name == 'posix'` is disjoint, or `sys_platform == 'linux'` is disjoint (each on its
|
||||
|
|
@ -285,21 +273,23 @@ impl Lock {
|
|||
// a single disjointness check with the intersection is sufficient, so we have one
|
||||
// constant per platform.
|
||||
let platform_tags = &wheel.filename.platform_tag;
|
||||
if platform_tags.iter().all(|tag| {
|
||||
linux_tags.into_iter().any(|linux_tag| {
|
||||
// These two linux tags are allowed by warehouse.
|
||||
tag.starts_with(linux_tag) || tag == "linux_armv6l" || tag == "linux_armv7l"
|
||||
})
|
||||
}) {
|
||||
if platform_tags
|
||||
.iter()
|
||||
.all(uv_platform_tags::PlatformTag::is_linux_compatible)
|
||||
{
|
||||
!graph.graph[node_index].marker().is_disjoint(*LINUX_MARKERS)
|
||||
} else if platform_tags
|
||||
.iter()
|
||||
.all(|tag| windows_tags.contains(&&**tag))
|
||||
.all(uv_platform_tags::PlatformTag::is_windows_compatible)
|
||||
{
|
||||
// TODO(charlie): This omits `win_ia64`, which is accepted by Warehouse.
|
||||
!graph.graph[node_index]
|
||||
.marker()
|
||||
.is_disjoint(*WINDOWS_MARKERS)
|
||||
} else if platform_tags.iter().all(|tag| tag.starts_with("macosx_")) {
|
||||
} else if platform_tags
|
||||
.iter()
|
||||
.all(uv_platform_tags::PlatformTag::is_macos_compatible)
|
||||
{
|
||||
!graph.graph[node_index].marker().is_disjoint(*MAC_MARKERS)
|
||||
} else {
|
||||
true
|
||||
|
|
|
|||
|
|
@ -68,13 +68,16 @@ Ok(
|
|||
version: "4.3.0",
|
||||
build_tag: None,
|
||||
python_tag: [
|
||||
"py3",
|
||||
Python {
|
||||
major: 3,
|
||||
minor: None,
|
||||
},
|
||||
],
|
||||
abi_tag: [
|
||||
"none",
|
||||
None,
|
||||
],
|
||||
platform_tag: [
|
||||
"any",
|
||||
Any,
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -75,13 +75,16 @@ Ok(
|
|||
version: "4.3.0",
|
||||
build_tag: None,
|
||||
python_tag: [
|
||||
"py3",
|
||||
Python {
|
||||
major: 3,
|
||||
minor: None,
|
||||
},
|
||||
],
|
||||
abi_tag: [
|
||||
"none",
|
||||
None,
|
||||
],
|
||||
platform_tag: [
|
||||
"any",
|
||||
Any,
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -71,13 +71,16 @@ Ok(
|
|||
version: "4.3.0",
|
||||
build_tag: None,
|
||||
python_tag: [
|
||||
"py3",
|
||||
Python {
|
||||
major: 3,
|
||||
minor: None,
|
||||
},
|
||||
],
|
||||
abi_tag: [
|
||||
"none",
|
||||
None,
|
||||
],
|
||||
platform_tag: [
|
||||
"any",
|
||||
Any,
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,26 +1,13 @@
|
|||
use std::cmp::Ordering;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::ops::Bound;
|
||||
|
||||
use indexmap::IndexSet;
|
||||
use itertools::Itertools;
|
||||
use owo_colors::OwoColorize;
|
||||
use pubgrub::{DerivationTree, Derived, External, Map, Range, ReportFormatter, Term};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::ops::Bound;
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::{PubGrubPackage, PubGrubPackageInner, PubGrubPython};
|
||||
use crate::candidate_selector::CandidateSelector;
|
||||
use crate::error::ErrorTree;
|
||||
use crate::fork_indexes::ForkIndexes;
|
||||
use crate::fork_urls::ForkUrls;
|
||||
use crate::prerelease::AllowPrerelease;
|
||||
use crate::python_requirement::{PythonRequirement, PythonRequirementSource};
|
||||
use crate::resolver::{
|
||||
MetadataUnavailable, UnavailablePackage, UnavailableReason, UnavailableVersion,
|
||||
};
|
||||
use crate::{
|
||||
Flexibility, InMemoryIndex, Options, RequiresPython, ResolverEnvironment, VersionsResponse,
|
||||
};
|
||||
use uv_configuration::{IndexStrategy, NoBinary, NoBuild};
|
||||
use uv_distribution_types::{
|
||||
IncompatibleDist, IncompatibleSource, IncompatibleWheel, Index, IndexCapabilities,
|
||||
|
|
@ -28,7 +15,21 @@ use uv_distribution_types::{
|
|||
};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep440::{Version, VersionSpecifiers};
|
||||
use uv_platform_tags::{AbiTag, IncompatibleTag, LanguageTag, Tags};
|
||||
use uv_platform_tags::{AbiTag, IncompatibleTag, LanguageTag, PlatformTag, Tags};
|
||||
|
||||
use crate::candidate_selector::CandidateSelector;
|
||||
use crate::error::ErrorTree;
|
||||
use crate::fork_indexes::ForkIndexes;
|
||||
use crate::fork_urls::ForkUrls;
|
||||
use crate::prerelease::AllowPrerelease;
|
||||
use crate::pubgrub::{PubGrubPackage, PubGrubPackageInner, PubGrubPython};
|
||||
use crate::python_requirement::{PythonRequirement, PythonRequirementSource};
|
||||
use crate::resolver::{
|
||||
MetadataUnavailable, UnavailablePackage, UnavailableReason, UnavailableVersion,
|
||||
};
|
||||
use crate::{
|
||||
Flexibility, InMemoryIndex, Options, RequiresPython, ResolverEnvironment, VersionsResponse,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PubGrubReportFormatter<'a> {
|
||||
|
|
@ -755,11 +756,7 @@ impl PubGrubReportFormatter<'_> {
|
|||
IncompatibleTag::Invalid => None,
|
||||
IncompatibleTag::Python => {
|
||||
// Return all available language tags.
|
||||
let tags = prioritized
|
||||
.python_tags()
|
||||
.into_iter()
|
||||
.filter_map(|tag| LanguageTag::from_str(tag).ok())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let tags = prioritized.python_tags();
|
||||
if tags.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -774,7 +771,6 @@ impl PubGrubReportFormatter<'_> {
|
|||
let tags = prioritized
|
||||
.abi_tags()
|
||||
.into_iter()
|
||||
.filter_map(|tag| AbiTag::from_str(tag).ok())
|
||||
// Ignore `none`, which is universally compatible.
|
||||
//
|
||||
// As an example, `none` can appear here if we're solving for Python 3.13, and
|
||||
|
|
@ -809,7 +805,7 @@ impl PubGrubReportFormatter<'_> {
|
|||
let tags = prioritized
|
||||
.platform_tags(self.tags?)
|
||||
.into_iter()
|
||||
.map(ToString::to_string)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
if tags.is_empty() {
|
||||
None
|
||||
|
|
@ -1146,7 +1142,7 @@ pub(crate) enum PubGrubHint {
|
|||
// excluded from `PartialEq` and `Hash`
|
||||
version: Version,
|
||||
// excluded from `PartialEq` and `Hash`
|
||||
tags: Vec<String>,
|
||||
tags: Vec<PlatformTag>,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use pubgrub::Range;
|
|||
use uv_distribution_filename::WheelFilename;
|
||||
use uv_pep440::{release_specifiers_to_ranges, Version, VersionSpecifier, VersionSpecifiers};
|
||||
use uv_pep508::{MarkerExpression, MarkerTree, MarkerValueVersion};
|
||||
use uv_platform_tags::AbiTag;
|
||||
use uv_platform_tags::{AbiTag, LanguageTag};
|
||||
|
||||
/// The `Requires-Python` requirement specifier.
|
||||
///
|
||||
|
|
@ -381,53 +381,73 @@ impl RequiresPython {
|
|||
/// sensitivity, we return `true` if the tags are unknown.
|
||||
pub fn matches_wheel_tag(&self, wheel: &WheelFilename) -> bool {
|
||||
wheel.abi_tag.iter().any(|abi_tag| {
|
||||
if abi_tag == "abi3" {
|
||||
if *abi_tag == AbiTag::Abi3 {
|
||||
// Universal tags are allowed.
|
||||
true
|
||||
} else if abi_tag == "none" {
|
||||
} else if *abi_tag == AbiTag::None {
|
||||
wheel.python_tag.iter().any(|python_tag| {
|
||||
// Remove `py2-none-any` and `py27-none-any` and analogous `cp` and `pp` tags.
|
||||
if python_tag.starts_with("py2")
|
||||
|| python_tag.starts_with("cp2")
|
||||
|| python_tag.starts_with("pp2")
|
||||
{
|
||||
if matches!(
|
||||
python_tag,
|
||||
LanguageTag::Python { major: 2, .. }
|
||||
| LanguageTag::CPython {
|
||||
python_version: (2, ..)
|
||||
}
|
||||
| LanguageTag::PyPy {
|
||||
python_version: (2, ..)
|
||||
}
|
||||
| LanguageTag::GraalPy {
|
||||
python_version: (2, ..)
|
||||
}
|
||||
| LanguageTag::Pyston {
|
||||
python_version: (2, ..)
|
||||
}
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove (e.g.) `py312-none-any` if the specifier is `==3.10.*`. However,
|
||||
// `py37-none-any` would be fine, since the `3.7` represents a lower bound.
|
||||
if let Some(minor) = python_tag.strip_prefix("py3") {
|
||||
let Ok(minor) = minor.parse::<u64>() else {
|
||||
return true;
|
||||
};
|
||||
|
||||
if let LanguageTag::Python {
|
||||
major: 3,
|
||||
minor: Some(minor),
|
||||
} = python_tag
|
||||
{
|
||||
// Ex) If the wheel bound is `3.12`, then it doesn't match `<=3.10.`.
|
||||
let wheel_bound = UpperBound(Bound::Included(Version::new([3, minor])));
|
||||
let wheel_bound =
|
||||
UpperBound(Bound::Included(Version::new([3, u64::from(*minor)])));
|
||||
if wheel_bound > self.range.upper().major_minor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
// Remove (e.g.) `cp36-none-any` or `cp312-none-any` if the specifier is
|
||||
// `==3.10.*`, since these tags require an exact match.
|
||||
if let Some(minor) = python_tag
|
||||
.strip_prefix("cp3")
|
||||
.or_else(|| python_tag.strip_prefix("pp3"))
|
||||
if let LanguageTag::CPython {
|
||||
python_version: (3, minor),
|
||||
}
|
||||
| LanguageTag::PyPy {
|
||||
python_version: (3, minor),
|
||||
}
|
||||
| LanguageTag::GraalPy {
|
||||
python_version: (3, minor),
|
||||
}
|
||||
| LanguageTag::Pyston {
|
||||
python_version: (3, minor),
|
||||
} = python_tag
|
||||
{
|
||||
let Ok(minor) = minor.parse::<u64>() else {
|
||||
return true;
|
||||
};
|
||||
|
||||
// Ex) If the wheel bound is `3.6`, then it doesn't match `>=3.10`.
|
||||
let wheel_bound = LowerBound(Bound::Included(Version::new([3, minor])));
|
||||
let wheel_bound =
|
||||
LowerBound(Bound::Included(Version::new([3, u64::from(*minor)])));
|
||||
if wheel_bound < self.range.lower().major_minor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ex) If the wheel bound is `3.12`, then it doesn't match `<=3.10.`.
|
||||
let wheel_bound = UpperBound(Bound::Included(Version::new([3, minor])));
|
||||
let wheel_bound =
|
||||
UpperBound(Bound::Included(Version::new([3, u64::from(*minor)])));
|
||||
if wheel_bound > self.range.upper().major_minor() {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -438,50 +458,49 @@ impl RequiresPython {
|
|||
// Unknown tags are allowed.
|
||||
true
|
||||
})
|
||||
} else if abi_tag.starts_with("cp2") || abi_tag.starts_with("pypy2") {
|
||||
} else if matches!(
|
||||
abi_tag,
|
||||
AbiTag::CPython {
|
||||
python_version: (2, ..),
|
||||
..
|
||||
} | AbiTag::PyPy {
|
||||
python_version: (2, ..),
|
||||
..
|
||||
} | AbiTag::GraalPy {
|
||||
python_version: (2, ..),
|
||||
..
|
||||
} | AbiTag::Pyston {
|
||||
python_version: (2, ..),
|
||||
..
|
||||
}
|
||||
) {
|
||||
// Python 2 is never allowed.
|
||||
false
|
||||
} else if let Some(minor_no_dot_abi) = abi_tag.strip_prefix("cp3") {
|
||||
// Remove ABI tags, both old (dmu) and future (t, and all other letters).
|
||||
let minor_not_dot = minor_no_dot_abi.trim_matches(char::is_alphabetic);
|
||||
let Ok(minor) = minor_not_dot.parse::<u64>() else {
|
||||
// Unknown version pattern are allowed.
|
||||
return true;
|
||||
};
|
||||
|
||||
} else if let AbiTag::CPython {
|
||||
python_version: (3, minor),
|
||||
..
|
||||
}
|
||||
| AbiTag::PyPy {
|
||||
python_version: (3, minor),
|
||||
..
|
||||
}
|
||||
| AbiTag::GraalPy {
|
||||
python_version: (3, minor),
|
||||
..
|
||||
}
|
||||
| AbiTag::Pyston {
|
||||
python_version: (3, minor),
|
||||
..
|
||||
} = abi_tag
|
||||
{
|
||||
// Ex) If the wheel bound is `3.6`, then it doesn't match `>=3.10`.
|
||||
let wheel_bound = LowerBound(Bound::Included(Version::new([3, minor])));
|
||||
let wheel_bound = LowerBound(Bound::Included(Version::new([3, u64::from(*minor)])));
|
||||
if wheel_bound < self.range.lower().major_minor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ex) If the wheel bound is `3.12`, then it doesn't match `<=3.10.`.
|
||||
let wheel_bound = UpperBound(Bound::Included(Version::new([3, minor])));
|
||||
if wheel_bound > self.range.upper().major_minor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
} else if let Some(minor_no_dot_abi) = abi_tag.strip_prefix("pypy3") {
|
||||
// Given `pypy39_pp73`, we just removed `pypy3`, now we remove `_pp73` ...
|
||||
let Some((minor_not_dot, _)) = minor_no_dot_abi.split_once('_') else {
|
||||
// Unknown version pattern are allowed.
|
||||
return true;
|
||||
};
|
||||
// ... and get `9`.
|
||||
let Ok(minor) = minor_not_dot.parse::<u64>() else {
|
||||
// Unknown version pattern are allowed.
|
||||
return true;
|
||||
};
|
||||
|
||||
// Ex) If the wheel bound is `3.6`, then it doesn't match `>=3.10`.
|
||||
let wheel_bound = LowerBound(Bound::Included(Version::new([3, minor])));
|
||||
if wheel_bound < self.range.lower().major_minor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ex) If the wheel bound is `3.12`, then it doesn't match `<=3.10.`.
|
||||
let wheel_bound = UpperBound(Bound::Included(Version::new([3, minor])));
|
||||
let wheel_bound = UpperBound(Bound::Included(Version::new([3, u64::from(*minor)])));
|
||||
if wheel_bound > self.range.upper().major_minor() {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue