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:
Charlie Marsh 2025-01-13 20:39:39 -05:00 committed by GitHub
parent 2ffa31946d
commit 5c91217488
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 1624 additions and 487 deletions

15
Cargo.lock generated
View file

@ -1038,7 +1038,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -1912,7 +1912,7 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -2800,7 +2800,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -3140,6 +3140,7 @@ dependencies = [
"rancor",
"rend",
"rkyv_derive",
"smallvec",
"tinyvec",
"uuid",
]
@ -3237,7 +3238,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -3806,7 +3807,7 @@ dependencies = [
"getrandom",
"once_cell",
"rustix",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -4664,6 +4665,7 @@ dependencies = [
"uv-normalize",
"uv-pep440",
"uv-pep508",
"uv-platform-tags",
"uv-pypi-types",
"uv-version",
"uv-warnings",
@ -5002,6 +5004,7 @@ dependencies = [
"insta",
"rkyv",
"serde",
"smallvec",
"thiserror 2.0.11",
"url",
"uv-normalize",
@ -5318,6 +5321,8 @@ name = "uv-platform-tags"
version = "0.0.1"
dependencies = [
"insta",
"memchr",
"rkyv",
"rustc-hash",
"serde",
"thiserror 2.0.11",

View file

@ -1,8 +1,10 @@
use std::str::FromStr;
use uv_bench::criterion::{
criterion_group, criterion_main, measurement::WallTime, BenchmarkId, Criterion, Throughput,
};
use uv_distribution_filename::WheelFilename;
use uv_platform_tags::Tags;
use uv_platform_tags::{AbiTag, LanguageTag, PlatformTag, Tags};
/// A set of platform tags extracted from burntsushi's Archlinux workstation.
/// We could just re-create these via `Tags::from_env`, but those might differ
@ -73,9 +75,15 @@ const INVALID_WHEEL_NAMES: &[(&str, &str)] = &[
/// extra processing. We thus expect construction to become slower, but we
/// write a benchmark to ensure it is still "reasonable."
fn benchmark_build_platform_tags(c: &mut Criterion<WallTime>) {
let tags: Vec<(String, String, String)> = PLATFORM_TAGS
let tags: Vec<(LanguageTag, AbiTag, PlatformTag)> = PLATFORM_TAGS
.iter()
.map(|&(py, abi, plat)| (py.to_string(), abi.to_string(), plat.to_string()))
.map(|&(py, abi, plat)| {
(
LanguageTag::from_str(py).unwrap(),
AbiTag::from_str(abi).unwrap(),
PlatformTag::from_str(plat).unwrap(),
)
})
.collect();
let mut group = c.benchmark_group("build_platform_tags");
@ -132,9 +140,15 @@ fn benchmark_wheelname_parsing_failure(c: &mut Criterion<WallTime>) {
/// implementation did an exhaustive search over each of them for each tag in
/// the wheel filename.
fn benchmark_wheelname_tag_compatibility(c: &mut Criterion<WallTime>) {
let tags: Vec<(String, String, String)> = PLATFORM_TAGS
let tags: Vec<(LanguageTag, AbiTag, PlatformTag)> = PLATFORM_TAGS
.iter()
.map(|&(py, abi, plat)| (py.to_string(), abi.to_string(), plat.to_string()))
.map(|&(py, abi, plat)| {
(
LanguageTag::from_str(py).unwrap(),
AbiTag::from_str(abi).unwrap(),
PlatformTag::from_str(plat).unwrap(),
)
})
.collect();
let tags = Tags::new(tags);

View file

@ -19,6 +19,7 @@ uv-globfilter = { workspace = true }
uv-normalize = { workspace = true }
uv-pep440 = { workspace = true }
uv-pep508 = { workspace = true }
uv-platform-tags = { workspace = true }
uv-pypi-types = { workspace = true }
uv-version = { workspace = true }
uv-warnings = { workspace = true }

View file

@ -1,20 +1,24 @@
use crate::metadata::{BuildBackendSettings, DEFAULT_EXCLUDES};
use crate::{DirectoryWriter, Error, FileList, ListWriter, PyProjectToml};
use std::io::{BufReader, Read, Write};
use std::path::{Path, PathBuf};
use std::{io, mem};
use fs_err::File;
use globset::{GlobSet, GlobSetBuilder};
use itertools::Itertools;
use sha2::{Digest, Sha256};
use std::io::{BufReader, Read, Write};
use std::path::{Path, PathBuf};
use std::{io, mem};
use tracing::{debug, trace};
use uv_distribution_filename::WheelFilename;
use uv_fs::Simplified;
use uv_globfilter::{parse_portable_glob, GlobDirFilter};
use uv_warnings::warn_user_once;
use walkdir::WalkDir;
use zip::{CompressionMethod, ZipWriter};
use uv_distribution_filename::{TagSet, WheelFilename};
use uv_fs::Simplified;
use uv_globfilter::{parse_portable_glob, GlobDirFilter};
use uv_platform_tags::{AbiTag, LanguageTag, PlatformTag};
use uv_warnings::warn_user_once;
use crate::metadata::{BuildBackendSettings, DEFAULT_EXCLUDES};
use crate::{DirectoryWriter, Error, FileList, ListWriter, PyProjectToml};
/// Build a wheel from the source tree and place it in the output directory.
pub fn build_wheel(
source_tree: &Path,
@ -33,9 +37,12 @@ pub fn build_wheel(
name: pyproject_toml.name().clone(),
version: pyproject_toml.version().clone(),
build_tag: None,
python_tag: vec!["py3".to_string()],
abi_tag: vec!["none".to_string()],
platform_tag: vec!["any".to_string()],
python_tag: TagSet::from_slice(&[LanguageTag::Python {
major: 3,
minor: None,
}]),
abi_tag: TagSet::from_buf([AbiTag::None]),
platform_tag: TagSet::from_buf([PlatformTag::Any]),
};
let wheel_path = wheel_dir.join(filename.to_string());
@ -68,9 +75,12 @@ pub fn list_wheel(
name: pyproject_toml.name().clone(),
version: pyproject_toml.version().clone(),
build_tag: None,
python_tag: vec!["py3".to_string()],
abi_tag: vec!["none".to_string()],
platform_tag: vec!["any".to_string()],
python_tag: TagSet::from_slice(&[LanguageTag::Python {
major: 3,
minor: None,
}]),
abi_tag: TagSet::from_buf([AbiTag::None]),
platform_tag: TagSet::from_buf([PlatformTag::Any]),
};
let mut files = FileList::new();
@ -247,9 +257,12 @@ pub fn build_editable(
name: pyproject_toml.name().clone(),
version: pyproject_toml.version().clone(),
build_tag: None,
python_tag: vec!["py3".to_string()],
abi_tag: vec!["none".to_string()],
platform_tag: vec!["any".to_string()],
python_tag: TagSet::from_slice(&[LanguageTag::Python {
major: 3,
minor: None,
}]),
abi_tag: TagSet::from_buf([AbiTag::None]),
platform_tag: TagSet::from_buf([PlatformTag::Any]),
};
let wheel_path = wheel_dir.join(filename.to_string());
@ -299,9 +312,12 @@ pub fn metadata(
name: pyproject_toml.name().clone(),
version: pyproject_toml.version().clone(),
build_tag: None,
python_tag: vec!["py3".to_string()],
abi_tag: vec!["none".to_string()],
platform_tag: vec!["any".to_string()],
python_tag: TagSet::from_slice(&[LanguageTag::Python {
major: 3,
minor: None,
}]),
abi_tag: TagSet::from_buf([AbiTag::None]),
platform_tag: TagSet::from_buf([PlatformTag::Any]),
};
debug!(
@ -744,6 +760,7 @@ mod test {
use uv_fs::Simplified;
use uv_normalize::PackageName;
use uv_pep440::Version;
use uv_platform_tags::{AbiTag, PlatformTag};
use walkdir::WalkDir;
#[test]
@ -752,9 +769,18 @@ mod test {
name: PackageName::from_str("foo").unwrap(),
version: Version::from_str("1.2.3").unwrap(),
build_tag: None,
python_tag: vec!["py2".to_string(), "py3".to_string()],
abi_tag: vec!["none".to_string()],
platform_tag: vec!["any".to_string()],
python_tag: TagSet::from_slice(&[
LanguageTag::Python {
major: 2,
minor: None,
},
LanguageTag::Python {
major: 3,
minor: None,
},
]),
abi_tag: TagSet::from_buf([AbiTag::None]),
platform_tag: TagSet::from_buf([PlatformTag::Any]),
};
assert_snapshot!(wheel_info(&filename, "1.0.0+test"), @r"

View file

@ -787,7 +787,7 @@ impl CacheBucket {
Self::Interpreter => "interpreter-v4",
// Note that when bumping this, you'll also need to bump it
// in crates/uv/tests/cache_clean.rs.
Self::Simple => "simple-v14",
Self::Simple => "simple-v15",
// Note that when bumping this, you'll also need to bump it
// in crates/uv/tests/cache_prune.rs.
Self::Wheels => "wheels-v3",

View file

@ -20,8 +20,9 @@ uv-normalize = { workspace = true }
uv-pep440 = { workspace = true }
uv-platform-tags = { workspace = true }
rkyv = { workspace = true }
rkyv = { workspace = true, features = ["smallvec-1"] }
serde = { workspace = true }
smallvec = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true }

View file

@ -7,7 +7,7 @@ pub use build_tag::{BuildTag, BuildTagError};
pub use egg::{EggInfoFilename, EggInfoFilenameError};
pub use extension::{DistExtension, ExtensionError, SourceDistExtension};
pub use source_dist::{SourceDistFilename, SourceDistFilenameError};
pub use wheel::{WheelFilename, WheelFilenameError};
pub use wheel::{TagSet, WheelFilename, WheelFilenameError};
mod build_tag;
mod egg;

View file

@ -1,6 +1,6 @@
---
source: crates/uv-distribution-filename/src/wheel.rs
expression: "WheelFilename::from_str(\"foo-1.2.3-202206090410-python-abi-platform.whl\")"
expression: "WheelFilename::from_str(\"foo-1.2.3-202206090410-py3-none-any.whl\")"
---
Ok(
WheelFilename {
@ -15,13 +15,16 @@ Ok(
),
),
python_tag: [
"python",
Python {
major: 3,
minor: None,
},
],
abi_tag: [
"abi",
None,
],
platform_tag: [
"platform",
Any,
],
},
)

View file

@ -1,6 +1,6 @@
---
source: crates/uv-distribution-filename/src/wheel.rs
expression: "WheelFilename::from_str(\"foo-1.2.3-ab.cd.ef-gh-ij.kl.mn.op.qr.st.whl\")"
expression: "WheelFilename::from_str(\"foo-1.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\")"
---
Ok(
WheelFilename {
@ -10,20 +10,31 @@ Ok(
version: "1.2.3",
build_tag: None,
python_tag: [
"ab",
"cd",
"ef",
CPython {
python_version: (
3,
11,
),
},
],
abi_tag: [
"gh",
CPython {
gil_disabled: false,
python_version: (
3,
11,
),
},
],
platform_tag: [
"ij",
"kl",
"mn",
"op",
"qr",
"st",
Manylinux {
major: 2,
minor: 17,
arch: X86_64,
},
Manylinux2014 {
arch: X86_64,
},
],
},
)

View file

@ -1,6 +1,6 @@
---
source: crates/uv-distribution-filename/src/wheel.rs
expression: "WheelFilename::from_str(\"foo-1.2.3-foo-bar-baz.whl\")"
expression: "WheelFilename::from_str(\"foo-1.2.3-py3-none-any.whl\")"
---
Ok(
WheelFilename {
@ -10,13 +10,16 @@ Ok(
version: "1.2.3",
build_tag: None,
python_tag: [
"foo",
Python {
major: 3,
minor: None,
},
],
abi_tag: [
"bar",
None,
],
platform_tag: [
"baz",
Any,
],
},
)

View file

@ -7,10 +7,19 @@ use url::Url;
use uv_normalize::{InvalidNameError, PackageName};
use uv_pep440::{Version, VersionParseError};
use uv_platform_tags::{TagCompatibility, Tags};
use uv_platform_tags::{
AbiTag, LanguageTag, ParseAbiTagError, ParseLanguageTagError, ParsePlatformTagError,
PlatformTag, TagCompatibility, Tags,
};
use crate::{BuildTag, BuildTagError};
/// A [`SmallVec`] type for storing tags.
///
/// Wheels tend to include a single language, ABI, and platform tag, so we use a [`SmallVec`] with a
/// capacity of 1 to optimize for this common case.
pub type TagSet<T> = smallvec::SmallVec<[T; 1]>;
#[derive(
Debug,
Clone,
@ -28,9 +37,9 @@ pub struct WheelFilename {
pub name: PackageName,
pub version: Version,
pub build_tag: Option<BuildTag>,
pub python_tag: Vec<String>,
pub abi_tag: Vec<String>,
pub platform_tag: Vec<String>,
pub python_tag: TagSet<LanguageTag>,
pub abi_tag: TagSet<AbiTag>,
pub platform_tag: TagSet<PlatformTag>,
}
impl FromStr for WheelFilename {
@ -87,12 +96,32 @@ impl WheelFilename {
/// Get the tag for this wheel.
fn get_tag(&self) -> String {
format!(
"{}-{}-{}",
self.python_tag.join("."),
self.abi_tag.join("."),
self.platform_tag.join(".")
)
if let ([python_tag], [abi_tag], [platform_tag]) = (
self.python_tag.as_slice(),
self.abi_tag.as_slice(),
self.platform_tag.as_slice(),
) {
format!("{python_tag}-{abi_tag}-{platform_tag}",)
} else {
format!(
"{}-{}-{}",
self.python_tag
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("."),
self.abi_tag
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("."),
self.platform_tag
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("."),
)
}
}
/// Parse a wheel filename from the stem (e.g., `foo-1.2.3-py3-none-any`).
@ -177,11 +206,21 @@ impl WheelFilename {
name,
version,
build_tag,
// TODO(charlie): Consider storing structured tags here. We need to benchmark to
// understand whether it's impactful.
python_tag: python_tag.split('.').map(String::from).collect(),
abi_tag: abi_tag.split('.').map(String::from).collect(),
platform_tag: platform_tag.split('.').map(String::from).collect(),
python_tag: python_tag
.split('.')
.map(LanguageTag::from_str)
.collect::<Result<_, _>>()
.map_err(|err| WheelFilenameError::InvalidLanguageTag(filename.to_string(), err))?,
abi_tag: abi_tag
.split('.')
.map(AbiTag::from_str)
.collect::<Result<_, _>>()
.map_err(|err| WheelFilenameError::InvalidAbiTag(filename.to_string(), err))?,
platform_tag: platform_tag
.split('.')
.map(PlatformTag::from_str)
.collect::<Result<_, _>>()
.map_err(|err| WheelFilenameError::InvalidPlatformTag(filename.to_string(), err))?,
})
}
}
@ -238,6 +277,12 @@ pub enum WheelFilenameError {
InvalidPackageName(String, InvalidNameError),
#[error("The wheel filename \"{0}\" has an invalid build tag: {1}")]
InvalidBuildTag(String, BuildTagError),
#[error("The wheel filename \"{0}\" has an invalid language tag: {1}")]
InvalidLanguageTag(String, ParseLanguageTagError),
#[error("The wheel filename \"{0}\" has an invalid ABI tag: {1}")]
InvalidAbiTag(String, ParseAbiTagError),
#[error("The wheel filename \"{0}\" has an invalid platform tag: {1}")]
InvalidPlatformTag(String, ParsePlatformTagError),
}
#[cfg(test)]
@ -264,63 +309,63 @@ mod tests {
#[test]
fn err_2_part_no_pythontag() {
let err = WheelFilename::from_str("foo-version.whl").unwrap_err();
insta::assert_snapshot!(err, @r###"The wheel filename "foo-version.whl" is invalid: Must have a Python tag"###);
let err = WheelFilename::from_str("foo-1.2.3.whl").unwrap_err();
insta::assert_snapshot!(err, @r###"The wheel filename "foo-1.2.3.whl" is invalid: Must have a Python tag"###);
}
#[test]
fn err_3_part_no_abitag() {
let err = WheelFilename::from_str("foo-version-python.whl").unwrap_err();
insta::assert_snapshot!(err, @r###"The wheel filename "foo-version-python.whl" is invalid: Must have an ABI tag"###);
let err = WheelFilename::from_str("foo-1.2.3-py3.whl").unwrap_err();
insta::assert_snapshot!(err, @r###"The wheel filename "foo-1.2.3-py3.whl" is invalid: Must have an ABI tag"###);
}
#[test]
fn err_4_part_no_platformtag() {
let err = WheelFilename::from_str("foo-version-python-abi.whl").unwrap_err();
insta::assert_snapshot!(err, @r###"The wheel filename "foo-version-python-abi.whl" is invalid: Must have a platform tag"###);
let err = WheelFilename::from_str("foo-1.2.3-py3-none.whl").unwrap_err();
insta::assert_snapshot!(err, @r###"The wheel filename "foo-1.2.3-py3-none.whl" is invalid: Must have a platform tag"###);
}
#[test]
fn err_too_many_parts() {
let err =
WheelFilename::from_str("foo-1.2.3-build-python-abi-platform-oops.whl").unwrap_err();
insta::assert_snapshot!(err, @r###"The wheel filename "foo-1.2.3-build-python-abi-platform-oops.whl" is invalid: Must have 5 or 6 components, but has more"###);
WheelFilename::from_str("foo-1.2.3-202206090410-py3-none-any-whoops.whl").unwrap_err();
insta::assert_snapshot!(err, @r###"The wheel filename "foo-1.2.3-202206090410-py3-none-any-whoops.whl" is invalid: Must have 5 or 6 components, but has more"###);
}
#[test]
fn err_invalid_package_name() {
let err = WheelFilename::from_str("f!oo-1.2.3-python-abi-platform.whl").unwrap_err();
insta::assert_snapshot!(err, @r###"The wheel filename "f!oo-1.2.3-python-abi-platform.whl" has an invalid package name"###);
let err = WheelFilename::from_str("f!oo-1.2.3-py3-none-any.whl").unwrap_err();
insta::assert_snapshot!(err, @r###"The wheel filename "f!oo-1.2.3-py3-none-any.whl" has an invalid package name"###);
}
#[test]
fn err_invalid_version() {
let err = WheelFilename::from_str("foo-x.y.z-python-abi-platform.whl").unwrap_err();
insta::assert_snapshot!(err, @r###"The wheel filename "foo-x.y.z-python-abi-platform.whl" has an invalid version: expected version to start with a number, but no leading ASCII digits were found"###);
let err = WheelFilename::from_str("foo-x.y.z-py3-none-any.whl").unwrap_err();
insta::assert_snapshot!(err, @r###"The wheel filename "foo-x.y.z-py3-none-any.whl" has an invalid version: expected version to start with a number, but no leading ASCII digits were found"###);
}
#[test]
fn err_invalid_build_tag() {
let err = WheelFilename::from_str("foo-1.2.3-tag-python-abi-platform.whl").unwrap_err();
insta::assert_snapshot!(err, @r###"The wheel filename "foo-1.2.3-tag-python-abi-platform.whl" has an invalid build tag: must start with a digit"###);
let err = WheelFilename::from_str("foo-1.2.3-tag-py3-none-any.whl").unwrap_err();
insta::assert_snapshot!(err, @r###"The wheel filename "foo-1.2.3-tag-py3-none-any.whl" has an invalid build tag: must start with a digit"###);
}
#[test]
fn ok_single_tags() {
insta::assert_debug_snapshot!(WheelFilename::from_str("foo-1.2.3-foo-bar-baz.whl"));
insta::assert_debug_snapshot!(WheelFilename::from_str("foo-1.2.3-py3-none-any.whl"));
}
#[test]
fn ok_multiple_tags() {
insta::assert_debug_snapshot!(WheelFilename::from_str(
"foo-1.2.3-ab.cd.ef-gh-ij.kl.mn.op.qr.st.whl"
"foo-1.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
));
}
#[test]
fn ok_build_tag() {
insta::assert_debug_snapshot!(WheelFilename::from_str(
"foo-1.2.3-202206090410-python-abi-platform.whl"
"foo-1.2.3-202206090410-py3-none-any.whl"
));
}

View file

@ -1343,8 +1343,8 @@ mod test {
/// Ensure that we don't accidentally grow the `Dist` sizes.
#[test]
fn dist_size() {
assert!(size_of::<Dist>() <= 288, "{}", size_of::<Dist>());
assert!(size_of::<BuiltDist>() <= 288, "{}", size_of::<BuiltDist>());
assert!(size_of::<Dist>() <= 312, "{}", size_of::<Dist>());
assert!(size_of::<BuiltDist>() <= 312, "{}", size_of::<BuiltDist>());
assert!(
size_of::<SourceDist>() <= 264,
"{}",

View file

@ -8,7 +8,7 @@ use tracing::debug;
use uv_distribution_filename::{BuildTag, WheelFilename};
use uv_pep440::VersionSpecifiers;
use uv_pep508::{MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString};
use uv_platform_tags::{AbiTag, IncompatibleTag, TagPriority, Tags};
use uv_platform_tags::{AbiTag, IncompatibleTag, LanguageTag, PlatformTag, TagPriority, Tags};
use uv_pypi_types::{HashDigest, Yanked};
use crate::{
@ -175,11 +175,11 @@ impl IncompatibleDist {
match self {
Self::Wheel(incompatibility) => match incompatibility {
IncompatibleWheel::Tag(IncompatibleTag::Python) => {
let tag = tags?.python_tag().map(ToString::to_string)?;
let tag = tags?.python_tag().as_ref().map(ToString::to_string)?;
Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
}
IncompatibleWheel::Tag(IncompatibleTag::Abi) => {
let tag = tags?.abi_tag().map(ToString::to_string)?;
let tag = tags?.abi_tag().as_ref().map(ToString::to_string)?;
Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
}
IncompatibleWheel::Tag(IncompatibleTag::AbiPythonVersion) => {
@ -523,32 +523,32 @@ impl PrioritizedDist {
}
/// Returns the set of all Python tags for the distribution.
pub fn python_tags(&self) -> BTreeSet<&str> {
pub fn python_tags(&self) -> BTreeSet<LanguageTag> {
self.0
.wheels
.iter()
.flat_map(|(wheel, _)| wheel.filename.python_tag.iter().map(String::as_str))
.flat_map(|(wheel, _)| wheel.filename.python_tag.iter().copied())
.collect()
}
/// Returns the set of all ABI tags for the distribution.
pub fn abi_tags(&self) -> BTreeSet<&str> {
pub fn abi_tags(&self) -> BTreeSet<AbiTag> {
self.0
.wheels
.iter()
.flat_map(|(wheel, _)| wheel.filename.abi_tag.iter().map(String::as_str))
.flat_map(|(wheel, _)| wheel.filename.abi_tag.iter().copied())
.collect()
}
/// Returns the set of platform tags for the distribution that are ABI-compatible with the given
/// tags.
pub fn platform_tags<'a>(&'a self, tags: &'a Tags) -> BTreeSet<&'a str> {
pub fn platform_tags<'a>(&'a self, tags: &'a Tags) -> BTreeSet<&'a PlatformTag> {
let mut candidates = BTreeSet::new();
for (wheel, _) in &self.0.wheels {
for wheel_py in &wheel.filename.python_tag {
for wheel_abi in &wheel.filename.abi_tag {
if tags.is_compatible_abi(wheel_py.as_str(), wheel_abi.as_str()) {
candidates.extend(wheel.filename.platform_tag.iter().map(String::as_str));
if tags.is_compatible_abi(*wheel_py, *wheel_abi) {
candidates.extend(wheel.filename.platform_tag.iter());
}
}
}
@ -725,13 +725,13 @@ impl IncompatibleWheel {
pub fn implied_markers(filename: &WheelFilename) -> MarkerTree {
let mut marker = MarkerTree::FALSE;
for platform_tag in &filename.platform_tag {
match platform_tag.as_str() {
"any" => {
match platform_tag {
PlatformTag::Any => {
return MarkerTree::TRUE;
}
// Windows
"win32" => {
PlatformTag::Win32 => {
let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::SysPlatform,
operator: MarkerOperator::Equal,
@ -744,7 +744,7 @@ pub fn implied_markers(filename: &WheelFilename) -> MarkerTree {
}));
marker.or(tag_marker);
}
"win_amd64" => {
PlatformTag::WinAmd64 => {
let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::SysPlatform,
operator: MarkerOperator::Equal,
@ -757,7 +757,7 @@ pub fn implied_markers(filename: &WheelFilename) -> MarkerTree {
}));
marker.or(tag_marker);
}
"win_arm64" => {
PlatformTag::WinArm64 => {
let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::SysPlatform,
operator: MarkerOperator::Equal,
@ -772,95 +772,20 @@ pub fn implied_markers(filename: &WheelFilename) -> MarkerTree {
}
// macOS
tag if tag.starts_with("macosx_") => {
PlatformTag::Macos { binary_format, .. } => {
let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::SysPlatform,
operator: MarkerOperator::Equal,
value: arcstr::literal!("darwin"),
});
// Parse the macOS version from the tag.
//
// For example, given `macosx_10_9_x86_64`, infer `10.9`, followed by `x86_64`.
//
// If at any point we fail to parse, we assume the tag is invalid and skip it.
let mut parts = tag.splitn(4, '_');
// Skip the "macosx_" prefix.
if parts.next().is_none_or(|part| part != "macosx") {
debug!("Failed to parse macOS prefix from tag: {tag}");
continue;
}
// Skip the major and minor version numbers.
if parts
.next()
.and_then(|part| part.parse::<u16>().ok())
.is_none()
{
debug!("Failed to parse macOS major version from tag: {tag}");
continue;
};
if parts
.next()
.and_then(|part| part.parse::<u16>().ok())
.is_none()
{
debug!("Failed to parse macOS minor version from tag: {tag}");
continue;
};
// Extract the architecture from the end of the tag.
let Some(arch) = parts.next() else {
debug!("Failed to parse macOS architecture from tag: {tag}");
continue;
};
// Extract the architecture from the end of the tag.
let mut arch_marker = MarkerTree::FALSE;
let supported_architectures = match arch {
"universal" => {
// Allow any of: "x86_64", "i386", "ppc64", "ppc", "intel"
["x86_64", "i386", "ppc64", "ppc", "intel"].iter()
}
"universal2" => {
// Allow any of: "x86_64", "arm64"
["x86_64", "arm64"].iter()
}
"intel" => {
// Allow any of: "x86_64", "i386"
["x86_64", "i386"].iter()
}
"x86_64" => {
// Allow only "x86_64"
["x86_64"].iter()
}
"arm64" => {
// Allow only "arm64"
["arm64"].iter()
}
"ppc64" => {
// Allow only "ppc64"
["ppc64"].iter()
}
"ppc" => {
// Allow only "ppc"
["ppc"].iter()
}
"i386" => {
// Allow only "i386"
["i386"].iter()
}
_ => {
debug!("Unknown macOS architecture in wheel tag: {tag}");
continue;
}
};
for arch in supported_architectures {
for arch in binary_format.platform_machine() {
arch_marker.or(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::PlatformMachine,
operator: MarkerOperator::Equal,
value: ArcStr::from(*arch),
value: ArcStr::from(arch.name()),
}));
}
tag_marker.and(arch_marker);
@ -869,81 +794,28 @@ pub fn implied_markers(filename: &WheelFilename) -> MarkerTree {
}
// Linux
tag => {
PlatformTag::Manylinux { arch, .. }
| PlatformTag::Manylinux1 { arch, .. }
| PlatformTag::Manylinux2010 { arch, .. }
| PlatformTag::Manylinux2014 { arch, .. }
| PlatformTag::Musllinux { arch, .. }
| PlatformTag::Linux { arch } => {
let mut tag_marker = MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::SysPlatform,
operator: MarkerOperator::Equal,
value: arcstr::literal!("linux"),
});
// Parse the architecture from the tag.
let arch = if let Some(arch) = tag.strip_prefix("linux_") {
arch
} else if let Some(arch) = tag.strip_prefix("manylinux1_") {
arch
} else if let Some(arch) = tag.strip_prefix("manylinux2010_") {
arch
} else if let Some(arch) = tag.strip_prefix("manylinux2014_") {
arch
} else if let Some(arch) = tag.strip_prefix("musllinux_") {
// Skip over the version tags (e.g., given `musllinux_1_2`, skip over `1` and `2`).
let mut parts = arch.splitn(3, '_');
if parts
.next()
.and_then(|part| part.parse::<u16>().ok())
.is_none()
{
debug!("Failed to parse musllinux major version from tag: {tag}");
continue;
};
if parts
.next()
.and_then(|part| part.parse::<u16>().ok())
.is_none()
{
debug!("Failed to parse musllinux minor version from tag: {tag}");
continue;
};
let Some(arch) = parts.next() else {
debug!("Failed to parse musllinux architecture from tag: {tag}");
continue;
};
arch
} else if let Some(arch) = tag.strip_prefix("manylinux_") {
// Skip over the version tags (e.g., given `manylinux_2_17`, skip over `2` and `17`).
let mut parts = arch.splitn(3, '_');
if parts
.next()
.and_then(|part| part.parse::<u16>().ok())
.is_none()
{
debug!("Failed to parse manylinux major version from tag: {tag}");
continue;
};
if parts
.next()
.and_then(|part| part.parse::<u16>().ok())
.is_none()
{
debug!("Failed to parse manylinux minor version from tag: {tag}");
continue;
};
let Some(arch) = parts.next() else {
debug!("Failed to parse manylinux architecture from tag: {tag}");
continue;
};
arch
} else {
continue;
};
tag_marker.and(MarkerTree::expression(MarkerExpression::String {
key: MarkerValueString::PlatformMachine,
operator: MarkerOperator::Equal,
value: ArcStr::from(arch),
value: ArcStr::from(arch.name()),
}));
marker.or(tag_marker);
}
tag => {
debug!("Unknown platform tag in wheel tag: {tag}");
}
}
}
marker

View file

@ -16,6 +16,8 @@ doctest = false
workspace = true
[dependencies]
memchr = { workspace = true }
rkyv = { workspace = true}
rustc-hash = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }

View file

@ -5,7 +5,20 @@ use std::str::FromStr;
///
/// This is the second segment in the wheel filename, following the language tag. For example,
/// in `cp39-none-manylinux_2_24_x86_64.whl`, the ABI tag is `none`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[derive(
Debug,
Copy,
Clone,
Eq,
PartialEq,
Ord,
PartialOrd,
Hash,
rkyv::Archive,
rkyv::Deserialize,
rkyv::Serialize,
)]
#[rkyv(derive(Debug))]
pub enum AbiTag {
/// Ex) `none`
None,
@ -65,7 +78,7 @@ impl std::fmt::Display for AbiTag {
} => {
write!(
f,
"graalpy{py_major}{py_minor}_graalpy{impl_major}{impl_minor}_{py_major}{py_minor}native"
"graalpy{py_major}{py_minor}_graalpy{impl_major}{impl_minor}_{py_major}{py_minor}_native"
)
}
Self::Pyston {
@ -203,6 +216,13 @@ impl FromStr for AbiTag {
implementation: "GraalPy",
tag: s.to_string(),
})?;
let version_end = rest
.find('_')
.ok_or_else(|| ParseAbiTagError::InvalidFormat {
implementation: "GraalPy",
tag: s.to_string(),
})?;
let rest = &rest[..version_end];
let (impl_major, impl_minor) = parse_impl_version(rest, "GraalPy", s)?;
Ok(Self::GraalPy {
python_version: (major, minor),
@ -374,8 +394,11 @@ mod tests {
python_version: (3, 10),
implementation_version: (2, 40),
};
assert_eq!(AbiTag::from_str("graalpy310_graalpy240"), Ok(tag));
assert_eq!(tag.to_string(), "graalpy310_graalpy240_310native");
assert_eq!(
AbiTag::from_str("graalpy310_graalpy240_310_native"),
Ok(tag)
);
assert_eq!(tag.to_string(), "graalpy310_graalpy240_310_native");
assert_eq!(
AbiTag::from_str("graalpy310"),
@ -393,7 +416,7 @@ mod tests {
);
assert_eq!(
AbiTag::from_str("graalpy310_graalpyXY"),
Err(ParseAbiTagError::InvalidImplMajorVersion {
Err(ParseAbiTagError::InvalidFormat {
implementation: "GraalPy",
tag: "graalpy310_graalpyXY".to_string()
})

View file

@ -5,7 +5,20 @@ use std::str::FromStr;
///
/// This is the first segment in the wheel filename. For example, in `cp39-none-manylinux_2_24_x86_64.whl`,
/// the language tag is `cp39`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[derive(
Debug,
Copy,
Clone,
Eq,
PartialEq,
Ord,
PartialOrd,
Hash,
rkyv::Archive,
rkyv::Deserialize,
rkyv::Serialize,
)]
#[rkyv(derive(Debug))]
pub enum LanguageTag {
/// Ex) `none`
None,

View file

@ -1,9 +1,11 @@
pub use abi_tag::AbiTag;
pub use language_tag::LanguageTag;
pub use abi_tag::{AbiTag, ParseAbiTagError};
pub use language_tag::{LanguageTag, ParseLanguageTagError};
pub use platform::{Arch, Os, Platform, PlatformError};
pub use tags::{IncompatibleTag, TagCompatibility, TagPriority, Tags, TagsError};
pub use platform_tag::{ParsePlatformTagError, PlatformTag};
pub use tags::{BinaryFormat, IncompatibleTag, TagCompatibility, TagPriority, Tags, TagsError};
mod abi_tag;
mod language_tag;
mod platform;
mod platform_tag;
mod tags;

View file

@ -1,8 +1,8 @@
//! Abstractions for understanding the current platform (operating system and architecture).
use std::str::FromStr;
use std::{fmt, io};
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Error, Debug)]
@ -13,7 +13,7 @@ pub enum PlatformError {
OsVersionDetectionError(String),
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct Platform {
os: Os,
arch: Arch,
@ -37,7 +37,7 @@ impl Platform {
}
/// All supported operating systems.
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(tag = "name", rename_all = "lowercase")]
pub enum Os {
Manylinux { major: u16, minor: u16 },
@ -72,7 +72,22 @@ impl fmt::Display for Os {
}
/// All supported CPU architectures
#[derive(Debug, Clone, Copy, Eq, PartialEq, Deserialize, Serialize)]
#[derive(
Debug,
Copy,
Clone,
Eq,
PartialEq,
Ord,
PartialOrd,
Hash,
rkyv::Archive,
rkyv::Deserialize,
rkyv::Serialize,
serde::Deserialize,
serde::Serialize,
)]
#[rkyv(derive(Debug))]
#[serde(rename_all = "lowercase")]
pub enum Arch {
#[serde(alias = "arm64")]
@ -85,6 +100,8 @@ pub enum Arch {
Powerpc64Le,
#[serde(alias = "ppc64")]
Powerpc64,
#[serde(alias = "ppc")]
Powerpc,
#[serde(alias = "i386", alias = "i686")]
X86,
#[serde(alias = "amd64")]
@ -103,6 +120,7 @@ impl fmt::Display for Arch {
Self::Armv7L => write!(f, "armv7l"),
Self::Powerpc64Le => write!(f, "ppc64le"),
Self::Powerpc64 => write!(f, "ppc64"),
Self::Powerpc => write!(f, "ppc"),
Self::X86 => write!(f, "i686"),
Self::X86_64 => write!(f, "x86_64"),
Self::S390X => write!(f, "s390x"),
@ -112,6 +130,28 @@ impl fmt::Display for Arch {
}
}
impl FromStr for Arch {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"aarch64" => Ok(Self::Aarch64),
"armv5tel" => Ok(Self::Armv5TEL),
"armv6l" => Ok(Self::Armv6L),
"armv7l" => Ok(Self::Armv7L),
"ppc64le" => Ok(Self::Powerpc64Le),
"ppc64" => Ok(Self::Powerpc64),
"ppc" => Ok(Self::Powerpc),
"i686" => Ok(Self::X86),
"x86_64" => Ok(Self::X86_64),
"s390x" => Ok(Self::S390X),
"loongarch64" => Ok(Self::LoongArch64),
"riscv64" => Ok(Self::Riscv64),
_ => Err(format!("Unknown architecture: {s}")),
}
}
}
impl Arch {
/// Returns the oldest possible `manylinux` tag for this architecture, if it supports
/// `manylinux`.
@ -126,7 +166,45 @@ impl Arch {
// manylinux_2_31
Self::Riscv64 => Some(31),
// unsupported
Self::Armv5TEL | Self::Armv6L | Self::LoongArch64 => None,
Self::Powerpc | Self::Armv5TEL | Self::Armv6L | Self::LoongArch64 => None,
}
}
/// Returns the canonical name of the architecture.
pub fn name(&self) -> &'static str {
match self {
Self::Aarch64 => "aarch64",
Self::Armv5TEL => "armv5tel",
Self::Armv6L => "armv6l",
Self::Armv7L => "armv7l",
Self::Powerpc64Le => "ppc64le",
Self::Powerpc64 => "ppc64",
Self::Powerpc => "ppc",
Self::X86 => "i686",
Self::X86_64 => "x86_64",
Self::S390X => "s390x",
Self::LoongArch64 => "loongarch64",
Self::Riscv64 => "riscv64",
}
}
/// Returns an iterator over all supported architectures.
pub fn iter() -> impl Iterator<Item = Self> {
[
Self::Aarch64,
Self::Armv5TEL,
Self::Armv6L,
Self::Armv7L,
Self::Powerpc64Le,
Self::Powerpc64,
Self::Powerpc,
Self::X86,
Self::X86_64,
Self::S390X,
Self::LoongArch64,
Self::Riscv64,
]
.iter()
.copied()
}
}

View file

@ -0,0 +1,884 @@
use std::fmt::Formatter;
use std::str::FromStr;
use crate::{Arch, BinaryFormat};
/// A tag to represent the platform compatibility of a Python distribution.
///
/// This is the third segment in the wheel filename, following the language and ABI tags. For
/// example, in `cp39-none-manylinux_2_24_x86_64.whl`, the platform tag is `manylinux_2_24_x86_64`.
#[derive(
Debug,
Clone,
Eq,
PartialEq,
Ord,
PartialOrd,
Hash,
rkyv::Archive,
rkyv::Deserialize,
rkyv::Serialize,
)]
#[rkyv(derive(Debug))]
pub enum PlatformTag {
/// Ex) `any`
Any,
/// Ex) `manylinux_2_24_x86_64`
Manylinux { major: u16, minor: u16, arch: Arch },
/// Ex) `manylinux1_x86_64`
Manylinux1 { arch: Arch },
/// Ex) `manylinux2010_x86_64`
Manylinux2010 { arch: Arch },
/// Ex) `manylinux2014_x86_64`
Manylinux2014 { arch: Arch },
/// Ex) `linux_x86_64`
Linux { arch: Arch },
/// Ex) `musllinux_1_2_x86_64`
Musllinux { major: u16, minor: u16, arch: Arch },
/// Ex) `macosx_11_0_x86_64`
Macos {
major: u16,
minor: u16,
binary_format: BinaryFormat,
},
/// Ex) `win32`
Win32,
/// Ex) `win_amd64`
WinAmd64,
/// Ex) `win_arm64`
WinArm64,
/// Ex) `android_21_x86_64`
Android { api_level: u16, arch: Arch },
/// Ex) `freebsd_12_x86_64`
FreeBsd { release: String, arch: Arch },
/// Ex) `netbsd_9_x86_64`
NetBsd { release: String, arch: Arch },
/// Ex) `openbsd_6_x86_64`
OpenBsd { release: String, arch: Arch },
/// Ex) `dragonfly_6_x86_64`
Dragonfly { release: String, arch: Arch },
/// Ex) `haiku_1_x86_64`
Haiku { release: String, arch: Arch },
/// Ex) `illumos_5_11_x86_64`
Illumos { release: String, arch: String },
/// Ex) `solaris_11_4_x86_64`
Solaris { release: String, arch: String },
}
impl PlatformTag {
/// Returns `true` if the platform is manylinux-compatible.
pub fn is_manylinux_compatible(&self) -> bool {
matches!(
self,
Self::Manylinux { .. }
| Self::Manylinux1 { .. }
| Self::Manylinux2010 { .. }
| Self::Manylinux2014 { .. }
)
}
/// Returns `true` if the platform is Linux-compatible.
pub fn is_linux_compatible(&self) -> bool {
matches!(
self,
Self::Manylinux { .. }
| Self::Manylinux1 { .. }
| Self::Manylinux2010 { .. }
| Self::Manylinux2014 { .. }
| Self::Musllinux { .. }
| Self::Linux { .. }
)
}
/// Returns `true` if the platform is macOS-compatible.
pub fn is_macos_compatible(&self) -> bool {
matches!(self, Self::Macos { .. })
}
/// Returns `true` if the platform is Windows-compatible.
pub fn is_windows_compatible(&self) -> bool {
matches!(self, Self::Win32 | Self::WinAmd64 | Self::WinArm64)
}
}
impl std::fmt::Display for PlatformTag {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Any => write!(f, "any"),
Self::Manylinux { major, minor, arch } => {
write!(f, "manylinux_{major}_{minor}_{arch}")
}
Self::Manylinux1 { arch } => write!(f, "manylinux1_{arch}"),
Self::Manylinux2010 { arch } => write!(f, "manylinux2010_{arch}"),
Self::Manylinux2014 { arch } => write!(f, "manylinux2014_{arch}"),
Self::Linux { arch } => write!(f, "linux_{arch}"),
Self::Musllinux { major, minor, arch } => {
write!(f, "musllinux_{major}_{minor}_{arch}")
}
Self::Macos {
major,
minor,
binary_format: format,
} => write!(f, "macosx_{major}_{minor}_{format}"),
Self::Win32 => write!(f, "win32"),
Self::WinAmd64 => write!(f, "win_amd64"),
Self::WinArm64 => write!(f, "win_arm64"),
Self::Android { api_level, arch } => write!(f, "android_{api_level}_{arch}"),
Self::FreeBsd { release, arch } => write!(f, "freebsd_{release}_{arch}"),
Self::NetBsd { release, arch } => write!(f, "netbsd_{release}_{arch}"),
Self::OpenBsd { release, arch } => write!(f, "openbsd_{release}_{arch}"),
Self::Dragonfly { release, arch } => write!(f, "dragonfly_{release}_{arch}"),
Self::Haiku { release, arch } => write!(f, "haiku_{release}_{arch}"),
Self::Illumos { release, arch } => write!(f, "illumos_{release}_{arch}"),
Self::Solaris { release, arch } => write!(f, "solaris_{release}_{arch}_64bit"),
}
}
}
impl FromStr for PlatformTag {
type Err = ParsePlatformTagError;
/// Parse a [`PlatformTag`] from a string.
fn from_str(s: &str) -> Result<Self, Self::Err> {
// Match against any static variants.
match s {
"any" => return Ok(Self::Any),
"win32" => return Ok(Self::Win32),
"win_amd64" => return Ok(Self::WinAmd64),
"win_arm64" => return Ok(Self::WinArm64),
_ => {}
}
if let Some(rest) = s.strip_prefix("manylinux_") {
// Ex) manylinux_2_17_x86_64
let first_underscore = memchr::memchr(b'_', rest.as_bytes()).ok_or_else(|| {
ParsePlatformTagError::InvalidFormat {
platform: "manylinux",
tag: s.to_string(),
}
})?;
let second_underscore = memchr::memchr(b'_', &rest.as_bytes()[first_underscore + 1..])
.map(|i| i + first_underscore + 1)
.ok_or_else(|| ParsePlatformTagError::InvalidFormat {
platform: "manylinux",
tag: s.to_string(),
})?;
let major = rest[..first_underscore].parse().map_err(|_| {
ParsePlatformTagError::InvalidMajorVersion {
platform: "manylinux",
tag: s.to_string(),
}
})?;
let minor = rest[first_underscore + 1..second_underscore]
.parse()
.map_err(|_| ParsePlatformTagError::InvalidMinorVersion {
platform: "manylinux",
tag: s.to_string(),
})?;
let arch_str = &rest[second_underscore + 1..];
if arch_str.is_empty() {
return Err(ParsePlatformTagError::InvalidFormat {
platform: "manylinux",
tag: s.to_string(),
});
}
let arch = arch_str
.parse()
.map_err(|_| ParsePlatformTagError::InvalidArch {
platform: "manylinux",
tag: s.to_string(),
})?;
return Ok(Self::Manylinux { major, minor, arch });
}
if let Some(rest) = s.strip_prefix("manylinux1_") {
// Ex) manylinux1_x86_64
let arch = rest
.parse()
.map_err(|_| ParsePlatformTagError::InvalidArch {
platform: "manylinux1",
tag: s.to_string(),
})?;
return Ok(Self::Manylinux1 { arch });
}
if let Some(rest) = s.strip_prefix("manylinux2010_") {
// Ex) manylinux2010_x86_64
let arch = rest
.parse()
.map_err(|_| ParsePlatformTagError::InvalidArch {
platform: "manylinux2010",
tag: s.to_string(),
})?;
return Ok(Self::Manylinux2010 { arch });
}
if let Some(rest) = s.strip_prefix("manylinux2014_") {
// Ex) manylinux2014_x86_64
let arch = rest
.parse()
.map_err(|_| ParsePlatformTagError::InvalidArch {
platform: "manylinux2014",
tag: s.to_string(),
})?;
return Ok(Self::Manylinux2014 { arch });
}
if let Some(rest) = s.strip_prefix("linux_") {
// Ex) linux_x86_64
let arch = rest
.parse()
.map_err(|_| ParsePlatformTagError::InvalidArch {
platform: "linux",
tag: s.to_string(),
})?;
return Ok(Self::Linux { arch });
}
if let Some(rest) = s.strip_prefix("musllinux_") {
// Ex) musllinux_1_1_x86_64
let first_underscore = memchr::memchr(b'_', rest.as_bytes()).ok_or_else(|| {
ParsePlatformTagError::InvalidFormat {
platform: "musllinux",
tag: s.to_string(),
}
})?;
let second_underscore = memchr::memchr(b'_', &rest.as_bytes()[first_underscore + 1..])
.map(|i| i + first_underscore + 1)
.ok_or_else(|| ParsePlatformTagError::InvalidFormat {
platform: "musllinux",
tag: s.to_string(),
})?;
let major = rest[..first_underscore].parse().map_err(|_| {
ParsePlatformTagError::InvalidMajorVersion {
platform: "musllinux",
tag: s.to_string(),
}
})?;
let minor = rest[first_underscore + 1..second_underscore]
.parse()
.map_err(|_| ParsePlatformTagError::InvalidMinorVersion {
platform: "musllinux",
tag: s.to_string(),
})?;
let arch_str = &rest[second_underscore + 1..];
if arch_str.is_empty() {
return Err(ParsePlatformTagError::InvalidFormat {
platform: "musllinux",
tag: s.to_string(),
});
}
let arch = arch_str
.parse()
.map_err(|_| ParsePlatformTagError::InvalidArch {
platform: "musllinux",
tag: s.to_string(),
})?;
return Ok(Self::Musllinux { major, minor, arch });
}
if let Some(rest) = s.strip_prefix("macosx_") {
// Ex) macosx_11_0_arm64
let first_underscore = memchr::memchr(b'_', rest.as_bytes()).ok_or_else(|| {
ParsePlatformTagError::InvalidFormat {
platform: "macosx",
tag: s.to_string(),
}
})?;
let second_underscore = memchr::memchr(b'_', &rest.as_bytes()[first_underscore + 1..])
.map(|i| i + first_underscore + 1)
.ok_or_else(|| ParsePlatformTagError::InvalidFormat {
platform: "macosx",
tag: s.to_string(),
})?;
let major = rest[..first_underscore].parse().map_err(|_| {
ParsePlatformTagError::InvalidMajorVersion {
platform: "macosx",
tag: s.to_string(),
}
})?;
let minor = rest[first_underscore + 1..second_underscore]
.parse()
.map_err(|_| ParsePlatformTagError::InvalidMinorVersion {
platform: "macosx",
tag: s.to_string(),
})?;
let binary_format_str = &rest[second_underscore + 1..];
if binary_format_str.is_empty() {
return Err(ParsePlatformTagError::InvalidFormat {
platform: "macosx",
tag: s.to_string(),
});
}
let binary_format =
binary_format_str
.parse()
.map_err(|_| ParsePlatformTagError::InvalidArch {
platform: "macosx",
tag: s.to_string(),
})?;
return Ok(Self::Macos {
major,
minor,
binary_format,
});
}
if let Some(rest) = s.strip_prefix("android_") {
// Ex) android_21_arm64
let underscore = memchr::memchr(b'_', rest.as_bytes()).ok_or_else(|| {
ParsePlatformTagError::InvalidFormat {
platform: "android",
tag: s.to_string(),
}
})?;
let api_level =
rest[..underscore]
.parse()
.map_err(|_| ParsePlatformTagError::InvalidApiLevel {
platform: "android",
tag: s.to_string(),
})?;
let arch_str = &rest[underscore + 1..];
if arch_str.is_empty() {
return Err(ParsePlatformTagError::InvalidFormat {
platform: "android",
tag: s.to_string(),
});
}
let arch = arch_str
.parse()
.map_err(|_| ParsePlatformTagError::InvalidArch {
platform: "android",
tag: s.to_string(),
})?;
return Ok(Self::Android { api_level, arch });
}
if let Some(rest) = s.strip_prefix("freebsd_") {
// Ex) freebsd_13_x86_64 or freebsd_13_14_x86_64
if rest.is_empty() {
return Err(ParsePlatformTagError::InvalidFormat {
platform: "freebsd",
tag: s.to_string(),
});
}
// Try each known Arch value as a potential suffix
for arch in Arch::iter() {
if let Some(release) = rest.strip_suffix(arch.name()) {
// Remove trailing underscore from release
let release = release.strip_suffix('_').unwrap_or(release).to_string();
if !release.is_empty() {
return Ok(Self::FreeBsd { release, arch });
}
}
}
return Err(ParsePlatformTagError::InvalidArch {
platform: "freebsd",
tag: s.to_string(),
});
}
if let Some(rest) = s.strip_prefix("netbsd_") {
// Ex) netbsd_9_x86_64
if rest.is_empty() {
return Err(ParsePlatformTagError::InvalidFormat {
platform: "netbsd",
tag: s.to_string(),
});
}
// Try each known Arch value as a potential suffix
for arch in Arch::iter() {
if let Some(release) = rest.strip_suffix(arch.name()) {
// Remove trailing underscore from release
let release = release.strip_suffix('_').unwrap_or(release).to_string();
if !release.is_empty() {
return Ok(Self::NetBsd { release, arch });
}
}
}
return Err(ParsePlatformTagError::InvalidArch {
platform: "netbsd",
tag: s.to_string(),
});
}
if let Some(rest) = s.strip_prefix("openbsd_") {
// Ex) openbsd_7_x86_64
if rest.is_empty() {
return Err(ParsePlatformTagError::InvalidFormat {
platform: "openbsd",
tag: s.to_string(),
});
}
// Try each known Arch value as a potential suffix
for arch in Arch::iter() {
if let Some(release) = rest.strip_suffix(arch.name()) {
// Remove trailing underscore from release
let release = release.strip_suffix('_').unwrap_or(release).to_string();
if !release.is_empty() {
return Ok(Self::OpenBsd { release, arch });
}
}
}
return Err(ParsePlatformTagError::InvalidArch {
platform: "openbsd",
tag: s.to_string(),
});
}
if let Some(rest) = s.strip_prefix("dragonfly_") {
// Ex) dragonfly_6_x86_64
if rest.is_empty() {
return Err(ParsePlatformTagError::InvalidFormat {
platform: "dragonfly",
tag: s.to_string(),
});
}
// Try each known Arch value as a potential suffix
for arch in Arch::iter() {
if let Some(release) = rest.strip_suffix(arch.name()) {
// Remove trailing underscore from release
let release = release.strip_suffix('_').unwrap_or(release).to_string();
if !release.is_empty() {
return Ok(Self::Dragonfly { release, arch });
}
}
}
return Err(ParsePlatformTagError::InvalidArch {
platform: "dragonfly",
tag: s.to_string(),
});
}
if let Some(rest) = s.strip_prefix("haiku_") {
// Ex) haiku_1_x86_64
if rest.is_empty() {
return Err(ParsePlatformTagError::InvalidFormat {
platform: "haiku",
tag: s.to_string(),
});
}
// Try each known Arch value as a potential suffix
for arch in Arch::iter() {
if let Some(release) = rest.strip_suffix(arch.name()) {
// Remove trailing underscore from release
let release = release.strip_suffix('_').unwrap_or(release).to_string();
if !release.is_empty() {
return Ok(Self::Haiku { release, arch });
}
}
}
return Err(ParsePlatformTagError::InvalidArch {
platform: "haiku",
tag: s.to_string(),
});
}
if let Some(rest) = s.strip_prefix("illumos_") {
// Ex) illumos_5_11_x86_64
if rest.is_empty() {
return Err(ParsePlatformTagError::InvalidFormat {
platform: "illumos",
tag: s.to_string(),
});
}
// Try each known Arch value as a potential suffix
for arch in Arch::iter() {
if let Some(release) = rest.strip_suffix(arch.name()) {
// Remove trailing underscore from release
let release = release.strip_suffix('_').unwrap_or(release).to_string();
if !release.is_empty() {
return Ok(Self::Illumos {
release,
arch: arch.name().to_string(),
});
}
}
}
return Err(ParsePlatformTagError::InvalidArch {
platform: "illumos",
tag: s.to_string(),
});
}
if let Some(rest) = s.strip_prefix("solaris_") {
// Ex) solaris_11_4_x86_64_64bit
if rest.is_empty() {
return Err(ParsePlatformTagError::InvalidFormat {
platform: "solaris",
tag: s.to_string(),
});
}
// Try each known Arch value as a potential suffix
for arch in Arch::iter() {
if let Some(rest) = rest.strip_suffix("_64bit") {
if let Some(rest) = rest.strip_suffix(&format!("_{}", arch.name())) {
// Remove trailing underscore from release
let release = rest.strip_suffix('_').unwrap_or(rest).to_string();
if !release.is_empty() {
return Ok(Self::Solaris {
release,
arch: arch.name().to_string(),
});
}
}
}
}
return Err(ParsePlatformTagError::InvalidArch {
platform: "solaris",
tag: s.to_string(),
});
}
Err(ParsePlatformTagError::UnknownFormat(s.to_string()))
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ParsePlatformTagError {
#[error("Unknown platform tag format: {0}")]
UnknownFormat(String),
#[error("Invalid format for {platform} platform tag: {tag}")]
InvalidFormat { platform: &'static str, tag: String },
#[error("Invalid major version in {platform} platform tag: {tag}")]
InvalidMajorVersion { platform: &'static str, tag: String },
#[error("Invalid minor version in {platform} platform tag: {tag}")]
InvalidMinorVersion { platform: &'static str, tag: String },
#[error("Invalid architecture in {platform} platform tag: {tag}")]
InvalidArch { platform: &'static str, tag: String },
#[error("Invalid API level in {platform} platform tag: {tag}")]
InvalidApiLevel { platform: &'static str, tag: String },
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use crate::platform_tag::{ParsePlatformTagError, PlatformTag};
use crate::{Arch, BinaryFormat};
#[test]
fn any_platform() {
assert_eq!(PlatformTag::from_str("any"), Ok(PlatformTag::Any));
assert_eq!(PlatformTag::Any.to_string(), "any");
}
#[test]
fn manylinux_platform() {
let tag = PlatformTag::Manylinux {
major: 2,
minor: 24,
arch: Arch::X86_64,
};
assert_eq!(
PlatformTag::from_str("manylinux_2_24_x86_64").as_ref(),
Ok(&tag)
);
assert_eq!(tag.to_string(), "manylinux_2_24_x86_64");
assert_eq!(
PlatformTag::from_str("manylinux_x_24_x86_64"),
Err(ParsePlatformTagError::InvalidMajorVersion {
platform: "manylinux",
tag: "manylinux_x_24_x86_64".to_string()
})
);
assert_eq!(
PlatformTag::from_str("manylinux_2_x_x86_64"),
Err(ParsePlatformTagError::InvalidMinorVersion {
platform: "manylinux",
tag: "manylinux_2_x_x86_64".to_string()
})
);
assert_eq!(
PlatformTag::from_str("manylinux_2_24_invalid"),
Err(ParsePlatformTagError::InvalidArch {
platform: "manylinux",
tag: "manylinux_2_24_invalid".to_string()
})
);
}
#[test]
fn manylinux1_platform() {
let tag = PlatformTag::Manylinux1 { arch: Arch::X86_64 };
assert_eq!(
PlatformTag::from_str("manylinux1_x86_64").as_ref(),
Ok(&tag)
);
assert_eq!(tag.to_string(), "manylinux1_x86_64");
assert_eq!(
PlatformTag::from_str("manylinux1_invalid"),
Err(ParsePlatformTagError::InvalidArch {
platform: "manylinux1",
tag: "manylinux1_invalid".to_string()
})
);
}
#[test]
fn manylinux2010_platform() {
let tag = PlatformTag::Manylinux2010 { arch: Arch::X86_64 };
assert_eq!(
PlatformTag::from_str("manylinux2010_x86_64").as_ref(),
Ok(&tag)
);
assert_eq!(tag.to_string(), "manylinux2010_x86_64");
assert_eq!(
PlatformTag::from_str("manylinux2010_invalid"),
Err(ParsePlatformTagError::InvalidArch {
platform: "manylinux2010",
tag: "manylinux2010_invalid".to_string()
})
);
}
#[test]
fn manylinux2014_platform() {
let tag = PlatformTag::Manylinux2014 { arch: Arch::X86_64 };
assert_eq!(
PlatformTag::from_str("manylinux2014_x86_64").as_ref(),
Ok(&tag)
);
assert_eq!(tag.to_string(), "manylinux2014_x86_64");
assert_eq!(
PlatformTag::from_str("manylinux2014_invalid"),
Err(ParsePlatformTagError::InvalidArch {
platform: "manylinux2014",
tag: "manylinux2014_invalid".to_string()
})
);
}
#[test]
fn linux_platform() {
let tag = PlatformTag::Linux { arch: Arch::X86_64 };
assert_eq!(PlatformTag::from_str("linux_x86_64").as_ref(), Ok(&tag));
assert_eq!(tag.to_string(), "linux_x86_64");
assert_eq!(
PlatformTag::from_str("linux_invalid"),
Err(ParsePlatformTagError::InvalidArch {
platform: "linux",
tag: "linux_invalid".to_string()
})
);
}
#[test]
fn musllinux_platform() {
let tag = PlatformTag::Musllinux {
major: 1,
minor: 2,
arch: Arch::X86_64,
};
assert_eq!(
PlatformTag::from_str("musllinux_1_2_x86_64").as_ref(),
Ok(&tag)
);
assert_eq!(tag.to_string(), "musllinux_1_2_x86_64");
assert_eq!(
PlatformTag::from_str("musllinux_x_2_x86_64"),
Err(ParsePlatformTagError::InvalidMajorVersion {
platform: "musllinux",
tag: "musllinux_x_2_x86_64".to_string()
})
);
assert_eq!(
PlatformTag::from_str("musllinux_1_x_x86_64"),
Err(ParsePlatformTagError::InvalidMinorVersion {
platform: "musllinux",
tag: "musllinux_1_x_x86_64".to_string()
})
);
assert_eq!(
PlatformTag::from_str("musllinux_1_2_invalid"),
Err(ParsePlatformTagError::InvalidArch {
platform: "musllinux",
tag: "musllinux_1_2_invalid".to_string()
})
);
}
#[test]
fn macos_platform() {
let tag = PlatformTag::Macos {
major: 11,
minor: 0,
binary_format: BinaryFormat::Universal2,
};
assert_eq!(
PlatformTag::from_str("macosx_11_0_universal2").as_ref(),
Ok(&tag)
);
assert_eq!(tag.to_string(), "macosx_11_0_universal2");
assert_eq!(
PlatformTag::from_str("macosx_x_0_universal2"),
Err(ParsePlatformTagError::InvalidMajorVersion {
platform: "macosx",
tag: "macosx_x_0_universal2".to_string()
})
);
assert_eq!(
PlatformTag::from_str("macosx_11_x_universal2"),
Err(ParsePlatformTagError::InvalidMinorVersion {
platform: "macosx",
tag: "macosx_11_x_universal2".to_string()
})
);
assert_eq!(
PlatformTag::from_str("macosx_11_0_invalid"),
Err(ParsePlatformTagError::InvalidArch {
platform: "macosx",
tag: "macosx_11_0_invalid".to_string()
})
);
}
#[test]
fn win32_platform() {
assert_eq!(PlatformTag::from_str("win32"), Ok(PlatformTag::Win32));
assert_eq!(PlatformTag::Win32.to_string(), "win32");
}
#[test]
fn win_amd64_platform() {
assert_eq!(
PlatformTag::from_str("win_amd64"),
Ok(PlatformTag::WinAmd64)
);
assert_eq!(PlatformTag::WinAmd64.to_string(), "win_amd64");
}
#[test]
fn win_arm64_platform() {
assert_eq!(
PlatformTag::from_str("win_arm64"),
Ok(PlatformTag::WinArm64)
);
assert_eq!(PlatformTag::WinArm64.to_string(), "win_arm64");
}
#[test]
fn freebsd_platform() {
let tag = PlatformTag::FreeBsd {
release: "13_14".to_string(),
arch: Arch::X86_64,
};
assert_eq!(
PlatformTag::from_str("freebsd_13_14_x86_64").as_ref(),
Ok(&tag)
);
assert_eq!(tag.to_string(), "freebsd_13_14_x86_64");
assert_eq!(
PlatformTag::from_str("freebsd_13_14"),
Err(ParsePlatformTagError::InvalidArch {
platform: "freebsd",
tag: "freebsd_13_14".to_string()
})
);
}
#[test]
fn illumos_platform() {
let tag = PlatformTag::Illumos {
release: "5_11".to_string(),
arch: "x86_64".to_string(),
};
assert_eq!(
PlatformTag::from_str("illumos_5_11_x86_64").as_ref(),
Ok(&tag)
);
assert_eq!(tag.to_string(), "illumos_5_11_x86_64");
assert_eq!(
PlatformTag::from_str("illumos_5_11"),
Err(ParsePlatformTagError::InvalidArch {
platform: "illumos",
tag: "illumos_5_11".to_string()
})
);
}
#[test]
fn solaris_platform() {
let tag = PlatformTag::Solaris {
release: "11_4".to_string(),
arch: "x86_64".to_string(),
};
assert_eq!(
PlatformTag::from_str("solaris_11_4_x86_64_64bit").as_ref(),
Ok(&tag)
);
assert_eq!(tag.to_string(), "solaris_11_4_x86_64_64bit");
assert_eq!(
PlatformTag::from_str("solaris_11_4_x86_64"),
Err(ParsePlatformTagError::InvalidArch {
platform: "solaris",
tag: "solaris_11_4_x86_64".to_string()
})
);
}
#[test]
fn unknown_platform() {
assert_eq!(
PlatformTag::from_str("unknown"),
Err(ParsePlatformTagError::UnknownFormat("unknown".to_string()))
);
assert_eq!(
PlatformTag::from_str(""),
Err(ParsePlatformTagError::UnknownFormat(String::new()))
);
}
}

View file

@ -1,11 +1,12 @@
use std::collections::BTreeSet;
use std::fmt::Formatter;
use std::str::FromStr;
use std::sync::Arc;
use std::{cmp, num::NonZeroU32};
use rustc_hash::FxHashMap;
use crate::{AbiTag, Arch, LanguageTag, Os, Platform, PlatformError};
use crate::{AbiTag, Arch, LanguageTag, Os, Platform, PlatformError, PlatformTag};
#[derive(Debug, thiserror::Error)]
pub enum TagsError {
@ -73,9 +74,9 @@ impl TagCompatibility {
pub struct Tags {
/// `python_tag` |--> `abi_tag` |--> `platform_tag` |--> priority
#[allow(clippy::type_complexity)]
map: Arc<FxHashMap<String, FxHashMap<String, FxHashMap<String, TagPriority>>>>,
map: Arc<FxHashMap<LanguageTag, FxHashMap<AbiTag, FxHashMap<PlatformTag, TagPriority>>>>,
/// The highest-priority tag for the Python version and platform.
best: Option<(String, String, String)>,
best: Option<(LanguageTag, AbiTag, PlatformTag)>,
}
impl Tags {
@ -83,7 +84,7 @@ impl Tags {
///
/// Tags are prioritized based on their position in the given vector. Specifically, tags that
/// appear earlier in the vector are given higher priority than tags that appear later.
pub fn new(tags: Vec<(String, String, String)>) -> Self {
pub fn new(tags: Vec<(LanguageTag, AbiTag, PlatformTag)>) -> Self {
// Store the highest-priority tag for each component.
let best = tags.first().cloned();
@ -120,7 +121,7 @@ impl Tags {
let platform_tags = {
let mut platform_tags = compatible_tags(platform)?;
if matches!(platform.os(), Os::Manylinux { .. }) && !manylinux_compatible {
platform_tags.retain(|tag| !tag.starts_with("manylinux"));
platform_tags.retain(|tag| !tag.is_manylinux_compatible());
}
platform_tags
};
@ -130,10 +131,8 @@ impl Tags {
// 1. This exact c api version
for platform_tag in &platform_tags {
tags.push((
implementation.language_tag(python_version).to_string(),
implementation
.abi_tag(python_version, implementation_version)
.to_string(),
implementation.language_tag(python_version),
implementation.abi_tag(python_version, implementation_version),
platform_tag.clone(),
));
}
@ -145,10 +144,8 @@ impl Tags {
if !gil_disabled {
for platform_tag in &platform_tags {
tags.push((
implementation
.language_tag((python_version.0, minor))
.to_string(),
AbiTag::Abi3.to_string(),
implementation.language_tag((python_version.0, minor)),
AbiTag::Abi3,
platform_tag.clone(),
));
}
@ -157,10 +154,8 @@ impl Tags {
if minor == python_version.1 {
for platform_tag in &platform_tags {
tags.push((
implementation
.language_tag((python_version.0, minor))
.to_string(),
AbiTag::None.to_string(),
implementation.language_tag((python_version.0, minor)),
AbiTag::None,
platform_tag.clone(),
));
}
@ -174,9 +169,8 @@ impl Tags {
LanguageTag::Python {
major: python_version.0,
minor: Some(minor),
}
.to_string(),
AbiTag::None.to_string(),
},
AbiTag::None,
platform_tag.clone(),
));
}
@ -184,8 +178,11 @@ impl Tags {
if minor == python_version.1 {
for platform_tag in &platform_tags {
tags.push((
format!("py{}", python_version.0),
AbiTag::None.to_string(),
LanguageTag::Python {
major: python_version.0,
minor: None,
},
AbiTag::None,
platform_tag.clone(),
));
}
@ -194,9 +191,9 @@ impl Tags {
// 4. no binary
if matches!(implementation, Implementation::CPython { .. }) {
tags.push((
implementation.language_tag(python_version).to_string(),
AbiTag::None.to_string(),
"any".to_string(),
implementation.language_tag(python_version),
AbiTag::None,
PlatformTag::Any,
));
}
for minor in (0..=python_version.1).rev() {
@ -204,10 +201,9 @@ impl Tags {
LanguageTag::Python {
major: python_version.0,
minor: Some(minor),
}
.to_string(),
AbiTag::None.to_string(),
"any".to_string(),
},
AbiTag::None,
PlatformTag::Any,
));
// After the matching version emit `none` tags for the major version i.e. `py3`
if minor == python_version.1 {
@ -215,10 +211,9 @@ impl Tags {
LanguageTag::Python {
major: python_version.0,
minor: None,
}
.to_string(),
AbiTag::None.to_string(),
"any".to_string(),
},
AbiTag::None,
PlatformTag::Any,
));
}
}
@ -232,9 +227,9 @@ impl Tags {
/// tag is found.
pub fn is_compatible(
&self,
wheel_python_tags: &[String],
wheel_abi_tags: &[String],
wheel_platform_tags: &[String],
wheel_python_tags: &[LanguageTag],
wheel_abi_tags: &[AbiTag],
wheel_platform_tags: &[PlatformTag],
) -> bool {
// NOTE: A typical work-load is a context in which the platform tags
// are quite large, but the tags of a wheel are quite small. It is
@ -267,9 +262,9 @@ impl Tags {
/// If incompatible, includes the tag part which was a closest match.
pub fn compatibility(
&self,
wheel_python_tags: &[String],
wheel_abi_tags: &[String],
wheel_platform_tags: &[String],
wheel_python_tags: &[LanguageTag],
wheel_abi_tags: &[AbiTag],
wheel_platform_tags: &[PlatformTag],
) -> TagCompatibility {
let mut max_compatibility = TagCompatibility::Incompatible(IncompatibleTag::Invalid);
@ -301,26 +296,26 @@ impl Tags {
}
/// Return the highest-priority Python tag for the [`Tags`].
pub fn python_tag(&self) -> Option<&str> {
self.best.as_ref().map(|(py, _, _)| py.as_str())
pub fn python_tag(&self) -> Option<LanguageTag> {
self.best.as_ref().map(|(python, _, _)| *python)
}
/// Return the highest-priority ABI tag for the [`Tags`].
pub fn abi_tag(&self) -> Option<&str> {
self.best.as_ref().map(|(_, abi, _)| abi.as_str())
pub fn abi_tag(&self) -> Option<AbiTag> {
self.best.as_ref().map(|(_, abi, _)| *abi)
}
/// Return the highest-priority platform tag for the [`Tags`].
pub fn platform_tag(&self) -> Option<&str> {
self.best.as_ref().map(|(_, _, platform)| platform.as_str())
pub fn platform_tag(&self) -> Option<&PlatformTag> {
self.best.as_ref().map(|(_, _, platform)| platform)
}
/// Returns `true` if the given language and ABI tags are compatible with the current
/// environment.
pub fn is_compatible_abi<'a>(&'a self, python_tag: &'a str, abi_tag: &'a str) -> bool {
pub fn is_compatible_abi(&self, python_tag: LanguageTag, abi_tag: AbiTag) -> bool {
self.map
.get(python_tag)
.map(|abis| abis.contains_key(abi_tag))
.get(&python_tag)
.map(|abis| abis.contains_key(&abi_tag))
.unwrap_or(false)
}
}
@ -436,7 +431,7 @@ impl Implementation {
///
/// We have two cases: Actual platform specific tags (including "merged" tags such as universal2)
/// and "any".
fn compatible_tags(platform: &Platform) -> Result<Vec<String>, PlatformError> {
fn compatible_tags(platform: &Platform) -> Result<Vec<PlatformTag>, PlatformError> {
let os = platform.os();
let arch = platform.arch();
@ -445,30 +440,37 @@ fn compatible_tags(platform: &Platform) -> Result<Vec<String>, PlatformError> {
let mut platform_tags = Vec::new();
if let Some(min_minor) = arch.get_minimum_manylinux_minor() {
for minor in (min_minor..=*minor).rev() {
platform_tags.push(format!("manylinux_{major}_{minor}_{arch}"));
platform_tags.push(PlatformTag::Manylinux {
major: *major,
minor,
arch,
});
// Support legacy manylinux tags with lower priority
// <https://peps.python.org/pep-0600/#legacy-manylinux-tags>
if minor == 12 {
platform_tags.push(format!("manylinux2010_{arch}"));
platform_tags.push(PlatformTag::Manylinux2010 { arch });
}
if minor == 17 {
platform_tags.push(format!("manylinux2014_{arch}"));
platform_tags.push(PlatformTag::Manylinux2014 { arch });
}
if minor == 5 {
platform_tags.push(format!("manylinux1_{arch}"));
platform_tags.push(PlatformTag::Manylinux1 { arch });
}
}
}
// Non-manylinux is given lowest priority.
// <https://github.com/pypa/packaging/blob/fd4f11139d1c884a637be8aa26bb60a31fbc9411/packaging/tags.py#L444>
platform_tags.push(format!("linux_{arch}"));
platform_tags.push(PlatformTag::Linux { arch });
platform_tags
}
(Os::Musllinux { major, minor }, _) => {
let mut platform_tags = vec![format!("linux_{arch}")];
let mut platform_tags = vec![PlatformTag::Linux { arch }];
// musl 1.1 is the lowest supported version in musllinux
platform_tags
.extend((1..=*minor).map(|minor| format!("musllinux_{major}_{minor}_{arch}")));
platform_tags.extend((1..=*minor).map(|minor| PlatformTag::Musllinux {
major: *major,
minor,
arch,
}));
platform_tags
}
(Os::Macos { major, minor }, Arch::X86_64) => {
@ -479,8 +481,12 @@ fn compatible_tags(platform: &Platform) -> Result<Vec<String>, PlatformError> {
// Prior to Mac OS 11, each yearly release of Mac OS bumped the "minor" version
// number. The major version was always 10.
for minor in (4..=*minor).rev() {
for binary_format in get_mac_binary_formats(arch) {
platform_tags.push(format!("macosx_{major}_{minor}_{binary_format}"));
for binary_format in BinaryFormat::from_arch(arch) {
platform_tags.push(PlatformTag::Macos {
major: 10,
minor,
binary_format: *binary_format,
});
}
}
}
@ -488,16 +494,23 @@ fn compatible_tags(platform: &Platform) -> Result<Vec<String>, PlatformError> {
// Starting with Mac OS 11, each yearly release bumps the major version number.
// The minor versions are now the midyear updates.
for major in (11..=*major).rev() {
for binary_format in get_mac_binary_formats(arch) {
platform_tags.push(format!("macosx_{}_{}_{}", major, 0, binary_format));
for binary_format in BinaryFormat::from_arch(arch) {
platform_tags.push(PlatformTag::Macos {
major,
minor: 0,
binary_format: *binary_format,
});
}
}
// The "universal2" binary format can have a macOS version earlier than 11.0
// when the x86_64 part of the binary supports that version of macOS.
for minor in (4..=16).rev() {
for binary_format in get_mac_binary_formats(arch) {
platform_tags
.push(format!("macosx_{}_{}_{}", 10, minor, binary_format));
for binary_format in BinaryFormat::from_arch(arch) {
platform_tags.push(PlatformTag::Macos {
major: 10,
minor,
binary_format: *binary_format,
});
}
}
}
@ -515,36 +528,49 @@ fn compatible_tags(platform: &Platform) -> Result<Vec<String>, PlatformError> {
// Starting with Mac OS 11, each yearly release bumps the major version number.
// The minor versions are now the midyear updates.
for major in (11..=*major).rev() {
for binary_format in get_mac_binary_formats(arch) {
platform_tags.push(format!("macosx_{}_{}_{}", major, 0, binary_format));
for binary_format in BinaryFormat::from_arch(arch) {
platform_tags.push(PlatformTag::Macos {
major,
minor: 0,
binary_format: *binary_format,
});
}
}
// The "universal2" binary format can have a macOS version earlier than 11.0
// when the x86_64 part of the binary supports that version of macOS.
platform_tags.extend(
(4..=16)
.rev()
.map(|minor| format!("macosx_{}_{}_universal2", 10, minor)),
);
platform_tags.extend((4..=16).rev().map(|minor| PlatformTag::Macos {
major: 10,
minor,
binary_format: BinaryFormat::Universal2,
}));
platform_tags
}
(Os::Windows, Arch::X86) => {
vec!["win32".to_string()]
vec![PlatformTag::Win32]
}
(Os::Windows, Arch::X86_64) => {
vec!["win_amd64".to_string()]
vec![PlatformTag::WinAmd64]
}
(Os::Windows, Arch::Aarch64) => vec!["win_arm64".to_string()],
(
Os::FreeBsd { release }
| Os::NetBsd { release }
| Os::OpenBsd { release }
| Os::Dragonfly { release }
| Os::Haiku { release },
_,
) => {
(Os::Windows, Arch::Aarch64) => vec![PlatformTag::WinArm64],
(Os::FreeBsd { release }, _) => {
let release = release.replace(['.', '-'], "_");
vec![format!("{os}_{release}_{arch}")]
vec![PlatformTag::FreeBsd { release, arch }]
}
(Os::NetBsd { release }, _) => {
let release = release.replace(['.', '-'], "_");
vec![PlatformTag::NetBsd { release, arch }]
}
(Os::OpenBsd { release }, _) => {
let release = release.replace(['.', '-'], "_");
vec![PlatformTag::OpenBsd { release, arch }]
}
(Os::Dragonfly { release }, _) => {
let release = release.replace(['.', '-'], "_");
vec![PlatformTag::Dragonfly { release, arch }]
}
(Os::Haiku { release }, _) => {
let release = release.replace(['.', '-'], "_");
vec![PlatformTag::Haiku { release, arch }]
}
(Os::Illumos { release, arch }, _) => {
// See https://github.com/python/cpython/blob/46c8d915715aa2bd4d697482aa051fe974d440e1/Lib/sysconfig.py#L722-L730
@ -556,17 +582,22 @@ fn compatible_tags(platform: &Platform) -> Result<Vec<String>, PlatformError> {
})?;
if major_ver >= 5 {
// SunOS 5 == Solaris 2
let os = "solaris";
let release = format!("{}_{}", major_ver - 3, other);
let arch = format!("{arch}_64bit");
return Ok(vec![format!("{os}_{release}_{arch}")]);
return Ok(vec![PlatformTag::Solaris { release, arch }]);
}
}
vec![format!("{os}_{release}_{arch}")]
vec![PlatformTag::Illumos {
release: release.to_string(),
arch: arch.to_string(),
}]
}
(Os::Android { api_level }, _) => {
vec![format!("{os}_{api_level}_{arch}")]
vec![PlatformTag::Android {
api_level: *api_level,
arch,
}]
}
_ => {
return Err(PlatformError::OsVersionDetectionError(format!(
@ -577,31 +608,131 @@ fn compatible_tags(platform: &Platform) -> Result<Vec<String>, PlatformError> {
Ok(platform_tags)
}
/// Determine the appropriate binary formats for a macOS version.
/// Source: <https://github.com/pypa/packaging/blob/fd4f11139d1c884a637be8aa26bb60a31fbc9411/packaging/tags.py#L314>
fn get_mac_binary_formats(arch: Arch) -> Vec<String> {
let mut formats = vec![match arch {
Arch::Aarch64 => "arm64".to_string(),
_ => arch.to_string(),
}];
#[derive(
Debug,
Copy,
Clone,
Eq,
PartialEq,
Ord,
PartialOrd,
Hash,
rkyv::Archive,
rkyv::Deserialize,
rkyv::Serialize,
)]
#[rkyv(derive(Debug))]
pub enum BinaryFormat {
Arm64,
Fat,
Fat32,
Fat64,
I386,
Intel,
Ppc,
Ppc64,
Universal,
Universal2,
X86_64,
}
if matches!(arch, Arch::X86_64) {
formats.extend([
"intel".to_string(),
"fat64".to_string(),
"fat32".to_string(),
]);
impl std::fmt::Display for BinaryFormat {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
impl FromStr for BinaryFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"arm64" => Ok(Self::Arm64),
"fat" => Ok(Self::Fat),
"fat32" => Ok(Self::Fat32),
"fat64" => Ok(Self::Fat64),
"i386" => Ok(Self::I386),
"intel" => Ok(Self::Intel),
"ppc" => Ok(Self::Ppc),
"ppc64" => Ok(Self::Ppc64),
"universal" => Ok(Self::Universal),
"universal2" => Ok(Self::Universal2),
"x86_64" => Ok(Self::X86_64),
_ => Err(format!("Invalid binary format: {s}")),
}
}
}
impl BinaryFormat {
/// Determine the appropriate binary formats for a macOS version.
///
/// See: <https://github.com/pypa/packaging/blob/fd4f11139d1c884a637be8aa26bb60a31fbc9411/packaging/tags.py#L314>
pub fn from_arch(arch: Arch) -> &'static [Self] {
match arch {
Arch::Aarch64 => &[Self::Arm64, Self::Universal2],
Arch::Powerpc64 => &[Self::Ppc64, Self::Fat64, Self::Universal],
Arch::Powerpc => &[Self::Ppc, Self::Fat32, Self::Fat, Self::Universal],
Arch::X86 => &[
Self::I386,
Self::Intel,
Self::Fat32,
Self::Fat,
Self::Universal,
],
Arch::X86_64 => &[
Self::X86_64,
Self::Intel,
Self::Fat64,
Self::Fat32,
Self::Universal2,
Self::Universal,
],
_ => unreachable!(),
}
}
if matches!(arch, Arch::X86_64 | Arch::Aarch64) {
formats.push("universal2".to_string());
/// Return the supported `platform_machine` tags for the binary format.
///
/// This is roughly the inverse of the above: given a binary format, which `platform_machine`
/// tags are supported?
pub fn platform_machine(&self) -> &'static [BinaryFormat] {
match self {
Self::Arm64 => &[Self::Arm64],
Self::Fat => &[Self::X86_64, Self::Ppc],
Self::Fat32 => &[Self::X86_64, Self::I386, Self::Ppc, Self::Ppc64],
Self::Fat64 => &[Self::X86_64, Self::Ppc64],
Self::I386 => &[Self::I386],
Self::Intel => &[Self::X86_64, Self::I386],
Self::Ppc => &[Self::Ppc],
Self::Ppc64 => &[Self::Ppc64],
Self::Universal => &[
Self::X86_64,
Self::I386,
Self::Ppc64,
Self::Ppc,
Self::Intel,
],
Self::Universal2 => &[Self::X86_64, Self::Arm64],
Self::X86_64 => &[Self::X86_64],
}
}
if matches!(arch, Arch::X86_64) {
formats.push("universal".to_string());
/// Return the canonical name of the binary format.
pub fn name(&self) -> &'static str {
match self {
Self::Arm64 => "arm64",
Self::Fat => "fat",
Self::Fat32 => "fat32",
Self::Fat64 => "fat64",
Self::I386 => "i386",
Self::Intel => "intel",
Self::Ppc => "ppc",
Self::Ppc64 => "ppc64",
Self::Universal => "universal",
Self::Universal2 => "universal2",
Self::X86_64 => "x86_64",
}
}
formats
}
#[cfg(test)]
@ -627,6 +758,7 @@ mod tests {
Arch::X86_64,
))
.unwrap();
let tags = tags.iter().map(ToString::to_string).collect::<Vec<_>>();
assert_debug_snapshot!(
tags,
@r###"
@ -666,6 +798,7 @@ mod tests {
Arch::X86_64,
))
.unwrap();
let tags = tags.iter().map(ToString::to_string).collect::<Vec<_>>();
assert_debug_snapshot!(
tags,
@r###"
@ -826,6 +959,7 @@ mod tests {
Arch::X86_64,
))
.unwrap();
let tags = tags.iter().map(ToString::to_string).collect::<Vec<_>>();
assert_debug_snapshot!(
tags,
@r###"
@ -944,6 +1078,7 @@ mod tests {
Arch::X86_64,
))
.unwrap();
let tags = tags.iter().map(ToString::to_string).collect::<Vec<_>>();
assert_debug_snapshot!(
tags,
@r###"

View file

@ -674,7 +674,7 @@ async fn form_metadata(
];
if let DistFilename::WheelFilename(wheel) = filename {
form_metadata.push(("pyversion", wheel.python_tag.join(".")));
form_metadata.push(("pyversion", wheel.python_tag.iter().join(".")));
} else {
form_metadata.push(("pyversion", "source".to_string()));
}

View file

@ -256,6 +256,10 @@ impl From<&uv_platform_tags::Arch> for Arch {
family: target_lexicon::Architecture::S390x,
variant: None,
},
uv_platform_tags::Arch::Powerpc => Self {
family: target_lexicon::Architecture::Powerpc,
variant: None,
},
uv_platform_tags::Arch::Powerpc64 => Self {
family: target_lexicon::Architecture::Powerpc64,
variant: None,

View file

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

View file

@ -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,
],
},
},

View file

@ -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,
],
},
},

View file

@ -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,
],
},
},

View file

@ -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>,
},
}

View file

@ -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;
}

View file

@ -51,7 +51,7 @@ fn clean_package_pypi() -> Result<()> {
// Assert that the `.rkyv` file is created for `iniconfig`.
let rkyv = context
.cache_dir
.child("simple-v14")
.child("simple-v15")
.child("pypi")
.child("iniconfig.rkyv");
assert!(
@ -123,7 +123,7 @@ fn clean_package_index() -> Result<()> {
// Assert that the `.rkyv` file is created for `iniconfig`.
let rkyv = context
.cache_dir
.child("simple-v14")
.child("simple-v15")
.child("index")
.child("e8208120cae3ba69")
.child("iniconfig.rkyv");

View file

@ -13949,7 +13949,7 @@ fn invalid_platform() -> Result<()> {
hint: Wheels are available for `open3d` (v0.15.2) with the following ABI tags: `cp36m`, `cp37m`, `cp38`, `cp39`
hint: Wheels are available for `open3d` (v0.18.0) on the following platforms: `macosx_11_0_x86_64`, `macosx_13_0_arm64`, `manylinux_2_27_aarch64`, `manylinux_2_27_x86_64`, `win_amd64`
hint: Wheels are available for `open3d` (v0.18.0) on the following platforms: `manylinux_2_27_aarch64`, `manylinux_2_27_x86_64`, `macosx_11_0_x86_64`, `macosx_13_0_arm64`, `win_amd64`
"###);
Ok(())

View file

@ -4090,6 +4090,8 @@ fn no_sdist_no_wheels_with_matching_abi() {
× No solution found when resolving dependencies:
Because only package-a==1.0.0 is available and package-a==1.0.0 has no wheels with a matching Python ABI tag (e.g., `cp38`), we can conclude that all versions of package-a cannot be used.
And because you require package-a, we can conclude that your requirements are unsatisfiable.
hint: Wheels are available for `package-a` (v1.0.0) with the following ABI tag: `graalpy310_graalpy240_310_native`
"###);
assert_not_installed(

View file

@ -2599,7 +2599,7 @@ fn sync_editable_and_local() -> Result<()> {
#[test]
fn incompatible_wheel() -> Result<()> {
let context = TestContext::new("3.12");
let wheel = context.temp_dir.child("foo-1.2.3-not-compatible-wheel.whl");
let wheel = context.temp_dir.child("foo-1.2.3-py3-none-any.whl");
wheel.touch()?;
let requirements_txt = context.temp_dir.child("requirements.txt");

View file

@ -4118,7 +4118,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/24/01/a4034a94a5f1828eb050230e7cf13af3ac23cf763512b6afe008d3def97c/watchdog-4.0.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84", size = 83012 },
{ url = "https://files.pythonhosted.org/packages/8f/5e/c0d7dad506adedd584188578901871fe923abf6c0c5dc9e79d9be5c7c24e/watchdog-4.0.1-py3-none-win32.whl", hash = "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429", size = 82996 },
{ url = "https://files.pythonhosted.org/packages/85/e0/2a9f43008902427b5f074c497705d6ef8f815c85d4bc25fbf83f720a6159/watchdog-4.0.1-py3-none-win_amd64.whl", hash = "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a", size = 83002 },
{ url = "https://files.pythonhosted.org/packages/db/54/23e5845ef68e1817b3792b2a11fb2088d7422814d41af8186d9058c4ff07/watchdog-4.0.1-py3-none-win_ia64.whl", hash = "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d", size = 83002 },
]
[[package]]