From 5c91217488fbf5fef291bd467b1be2e7c6e7bb8c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 13 Jan 2025 20:39:39 -0500 Subject: [PATCH] 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. --- Cargo.lock | 15 +- .../uv-bench/benches/distribution_filename.rs | 24 +- crates/uv-build-backend/Cargo.toml | 1 + crates/uv-build-backend/src/wheel.rs | 74 +- crates/uv-cache/src/lib.rs | 2 +- crates/uv-distribution-filename/Cargo.toml | 3 +- crates/uv-distribution-filename/src/lib.rs | 2 +- ..._filename__wheel__tests__ok_build_tag.snap | 11 +- ...ename__wheel__tests__ok_multiple_tags.snap | 33 +- ...ilename__wheel__tests__ok_single_tags.snap | 11 +- crates/uv-distribution-filename/src/wheel.rs | 109 ++- crates/uv-distribution-types/src/lib.rs | 4 +- .../src/prioritized_distribution.rs | 186 +--- crates/uv-platform-tags/Cargo.toml | 2 + crates/uv-platform-tags/src/abi_tag.rs | 33 +- crates/uv-platform-tags/src/language_tag.rs | 15 +- crates/uv-platform-tags/src/lib.rs | 8 +- crates/uv-platform-tags/src/platform.rs | 88 +- crates/uv-platform-tags/src/platform_tag.rs | 884 ++++++++++++++++++ crates/uv-platform-tags/src/tags.rs | 347 ++++--- crates/uv-publish/src/lib.rs | 2 +- crates/uv-python/src/platform.rs | 4 + crates/uv-resolver/src/lock/mod.rs | 30 +- ...r__lock__tests__hash_optional_missing.snap | 9 +- ...r__lock__tests__hash_optional_present.snap | 9 +- ...r__lock__tests__hash_required_present.snap | 9 +- crates/uv-resolver/src/pubgrub/report.rs | 48 +- crates/uv-resolver/src/requires_python.rs | 137 +-- crates/uv/tests/it/cache_clean.rs | 4 +- crates/uv/tests/it/pip_compile.rs | 2 +- crates/uv/tests/it/pip_install_scenarios.rs | 2 + crates/uv/tests/it/pip_sync.rs | 2 +- .../it__ecosystem__warehouse-lock-file.snap | 1 - 33 files changed, 1624 insertions(+), 487 deletions(-) create mode 100644 crates/uv-platform-tags/src/platform_tag.rs diff --git a/Cargo.lock b/Cargo.lock index 7e3d61132..06590ce77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/crates/uv-bench/benches/distribution_filename.rs b/crates/uv-bench/benches/distribution_filename.rs index 943b577b7..7d1669621 100644 --- a/crates/uv-bench/benches/distribution_filename.rs +++ b/crates/uv-bench/benches/distribution_filename.rs @@ -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) { - 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) { /// implementation did an exhaustive search over each of them for each tag in /// the wheel filename. fn benchmark_wheelname_tag_compatibility(c: &mut Criterion) { - 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); diff --git a/crates/uv-build-backend/Cargo.toml b/crates/uv-build-backend/Cargo.toml index 2c8a6c76b..23240561c 100644 --- a/crates/uv-build-backend/Cargo.toml +++ b/crates/uv-build-backend/Cargo.toml @@ -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 } diff --git a/crates/uv-build-backend/src/wheel.rs b/crates/uv-build-backend/src/wheel.rs index fd1908a80..38fa4bfea 100644 --- a/crates/uv-build-backend/src/wheel.rs +++ b/crates/uv-build-backend/src/wheel.rs @@ -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" diff --git a/crates/uv-cache/src/lib.rs b/crates/uv-cache/src/lib.rs index c82189d7a..049d09653 100644 --- a/crates/uv-cache/src/lib.rs +++ b/crates/uv-cache/src/lib.rs @@ -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", diff --git a/crates/uv-distribution-filename/Cargo.toml b/crates/uv-distribution-filename/Cargo.toml index b8484b97e..188a988df 100644 --- a/crates/uv-distribution-filename/Cargo.toml +++ b/crates/uv-distribution-filename/Cargo.toml @@ -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 } diff --git a/crates/uv-distribution-filename/src/lib.rs b/crates/uv-distribution-filename/src/lib.rs index f16d2c7c3..c3b00d2a9 100644 --- a/crates/uv-distribution-filename/src/lib.rs +++ b/crates/uv-distribution-filename/src/lib.rs @@ -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; diff --git a/crates/uv-distribution-filename/src/snapshots/uv_distribution_filename__wheel__tests__ok_build_tag.snap b/crates/uv-distribution-filename/src/snapshots/uv_distribution_filename__wheel__tests__ok_build_tag.snap index 1e8c68ebc..2014265e0 100644 --- a/crates/uv-distribution-filename/src/snapshots/uv_distribution_filename__wheel__tests__ok_build_tag.snap +++ b/crates/uv-distribution-filename/src/snapshots/uv_distribution_filename__wheel__tests__ok_build_tag.snap @@ -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, ], }, ) diff --git a/crates/uv-distribution-filename/src/snapshots/uv_distribution_filename__wheel__tests__ok_multiple_tags.snap b/crates/uv-distribution-filename/src/snapshots/uv_distribution_filename__wheel__tests__ok_multiple_tags.snap index fcf8d5aa3..64c6a3948 100644 --- a/crates/uv-distribution-filename/src/snapshots/uv_distribution_filename__wheel__tests__ok_multiple_tags.snap +++ b/crates/uv-distribution-filename/src/snapshots/uv_distribution_filename__wheel__tests__ok_multiple_tags.snap @@ -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, + }, ], }, ) diff --git a/crates/uv-distribution-filename/src/snapshots/uv_distribution_filename__wheel__tests__ok_single_tags.snap b/crates/uv-distribution-filename/src/snapshots/uv_distribution_filename__wheel__tests__ok_single_tags.snap index a649b96df..1f6fcf5e1 100644 --- a/crates/uv-distribution-filename/src/snapshots/uv_distribution_filename__wheel__tests__ok_single_tags.snap +++ b/crates/uv-distribution-filename/src/snapshots/uv_distribution_filename__wheel__tests__ok_single_tags.snap @@ -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, ], }, ) diff --git a/crates/uv-distribution-filename/src/wheel.rs b/crates/uv-distribution-filename/src/wheel.rs index 397f4eba4..8c87b136c 100644 --- a/crates/uv-distribution-filename/src/wheel.rs +++ b/crates/uv-distribution-filename/src/wheel.rs @@ -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 = smallvec::SmallVec<[T; 1]>; + #[derive( Debug, Clone, @@ -28,9 +37,9 @@ pub struct WheelFilename { pub name: PackageName, pub version: Version, pub build_tag: Option, - pub python_tag: Vec, - pub abi_tag: Vec, - pub platform_tag: Vec, + pub python_tag: TagSet, + pub abi_tag: TagSet, + pub platform_tag: TagSet, } 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::>() + .join("."), + self.abi_tag + .iter() + .map(ToString::to_string) + .collect::>() + .join("."), + self.platform_tag + .iter() + .map(ToString::to_string) + .collect::>() + .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::>() + .map_err(|err| WheelFilenameError::InvalidLanguageTag(filename.to_string(), err))?, + abi_tag: abi_tag + .split('.') + .map(AbiTag::from_str) + .collect::>() + .map_err(|err| WheelFilenameError::InvalidAbiTag(filename.to_string(), err))?, + platform_tag: platform_tag + .split('.') + .map(PlatformTag::from_str) + .collect::>() + .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" )); } diff --git a/crates/uv-distribution-types/src/lib.rs b/crates/uv-distribution-types/src/lib.rs index 81e9d53a6..1cb88d6f4 100644 --- a/crates/uv-distribution-types/src/lib.rs +++ b/crates/uv-distribution-types/src/lib.rs @@ -1343,8 +1343,8 @@ mod test { /// Ensure that we don't accidentally grow the `Dist` sizes. #[test] fn dist_size() { - assert!(size_of::() <= 288, "{}", size_of::()); - assert!(size_of::() <= 288, "{}", size_of::()); + assert!(size_of::() <= 312, "{}", size_of::()); + assert!(size_of::() <= 312, "{}", size_of::()); assert!( size_of::() <= 264, "{}", diff --git a/crates/uv-distribution-types/src/prioritized_distribution.rs b/crates/uv-distribution-types/src/prioritized_distribution.rs index d3235b0b4..612ae44db 100644 --- a/crates/uv-distribution-types/src/prioritized_distribution.rs +++ b/crates/uv-distribution-types/src/prioritized_distribution.rs @@ -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 { 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 { 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::().ok()) - .is_none() - { - debug!("Failed to parse macOS major version from tag: {tag}"); - continue; - }; - if parts - .next() - .and_then(|part| part.parse::().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::().ok()) - .is_none() - { - debug!("Failed to parse musllinux major version from tag: {tag}"); - continue; - }; - if parts - .next() - .and_then(|part| part.parse::().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::().ok()) - .is_none() - { - debug!("Failed to parse manylinux major version from tag: {tag}"); - continue; - }; - if parts - .next() - .and_then(|part| part.parse::().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 diff --git a/crates/uv-platform-tags/Cargo.toml b/crates/uv-platform-tags/Cargo.toml index add9e4856..c39069767 100644 --- a/crates/uv-platform-tags/Cargo.toml +++ b/crates/uv-platform-tags/Cargo.toml @@ -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 } diff --git a/crates/uv-platform-tags/src/abi_tag.rs b/crates/uv-platform-tags/src/abi_tag.rs index 2d897fcb4..44f76f17f 100644 --- a/crates/uv-platform-tags/src/abi_tag.rs +++ b/crates/uv-platform-tags/src/abi_tag.rs @@ -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() }) diff --git a/crates/uv-platform-tags/src/language_tag.rs b/crates/uv-platform-tags/src/language_tag.rs index d764a74ad..2776a09f1 100644 --- a/crates/uv-platform-tags/src/language_tag.rs +++ b/crates/uv-platform-tags/src/language_tag.rs @@ -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, diff --git a/crates/uv-platform-tags/src/lib.rs b/crates/uv-platform-tags/src/lib.rs index 0c73985ad..1fe09c405 100644 --- a/crates/uv-platform-tags/src/lib.rs +++ b/crates/uv-platform-tags/src/lib.rs @@ -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; diff --git a/crates/uv-platform-tags/src/platform.rs b/crates/uv-platform-tags/src/platform.rs index b82335036..4450561c2 100644 --- a/crates/uv-platform-tags/src/platform.rs +++ b/crates/uv-platform-tags/src/platform.rs @@ -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 { + 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 { + [ + 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() + } } diff --git a/crates/uv-platform-tags/src/platform_tag.rs b/crates/uv-platform-tags/src/platform_tag.rs new file mode 100644 index 000000000..8d6b8322b --- /dev/null +++ b/crates/uv-platform-tags/src/platform_tag.rs @@ -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 { + // 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())) + ); + } +} diff --git a/crates/uv-platform-tags/src/tags.rs b/crates/uv-platform-tags/src/tags.rs index 3f41222c4..62d2aba35 100644 --- a/crates/uv-platform-tags/src/tags.rs +++ b/crates/uv-platform-tags/src/tags.rs @@ -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>>>, + map: Arc>>>, /// 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 { + 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 { + 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, PlatformError> { +fn compatible_tags(platform: &Platform) -> Result, PlatformError> { let os = platform.os(); let arch = platform.arch(); @@ -445,30 +440,37 @@ fn compatible_tags(platform: &Platform) -> Result, 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 // 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. // - 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, 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, 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, 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, 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, PlatformError> { Ok(platform_tags) } -/// Determine the appropriate binary formats for a macOS version. -/// Source: -fn get_mac_binary_formats(arch: Arch) -> Vec { - 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 { + 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: + 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::>(); assert_debug_snapshot!( tags, @r###" @@ -666,6 +798,7 @@ mod tests { Arch::X86_64, )) .unwrap(); + let tags = tags.iter().map(ToString::to_string).collect::>(); assert_debug_snapshot!( tags, @r###" @@ -826,6 +959,7 @@ mod tests { Arch::X86_64, )) .unwrap(); + let tags = tags.iter().map(ToString::to_string).collect::>(); assert_debug_snapshot!( tags, @r###" @@ -944,6 +1078,7 @@ mod tests { Arch::X86_64, )) .unwrap(); + let tags = tags.iter().map(ToString::to_string).collect::>(); assert_debug_snapshot!( tags, @r###" diff --git a/crates/uv-publish/src/lib.rs b/crates/uv-publish/src/lib.rs index eac5fedc9..7a3a64b72 100644 --- a/crates/uv-publish/src/lib.rs +++ b/crates/uv-publish/src/lib.rs @@ -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())); } diff --git a/crates/uv-python/src/platform.rs b/crates/uv-python/src/platform.rs index d34634754..ec6abfeaa 100644 --- a/crates/uv-python/src/platform.rs +++ b/crates/uv-python/src/platform.rs @@ -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, diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 283889b91..267640d70 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -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 diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap index 1e8349c40..81566c0c7 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap @@ -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, ], }, }, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap index ce380043e..57f836c16 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap @@ -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, ], }, }, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap index d8fd783ed..68fbd77b7 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap @@ -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, ], }, }, diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index 04406a091..8bdf2980a 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -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::>(); + 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::>(); 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, + tags: Vec, }, } diff --git a/crates/uv-resolver/src/requires_python.rs b/crates/uv-resolver/src/requires_python.rs index fb9710d0c..288a72c0f 100644 --- a/crates/uv-resolver/src/requires_python.rs +++ b/crates/uv-resolver/src/requires_python.rs @@ -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::() 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::() 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::() 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::() 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; } diff --git a/crates/uv/tests/it/cache_clean.rs b/crates/uv/tests/it/cache_clean.rs index 10a902f6e..38a08c79a 100644 --- a/crates/uv/tests/it/cache_clean.rs +++ b/crates/uv/tests/it/cache_clean.rs @@ -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"); diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 1bb1c58b8..62619a39f 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -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(()) diff --git a/crates/uv/tests/it/pip_install_scenarios.rs b/crates/uv/tests/it/pip_install_scenarios.rs index f1f13ce82..9dd647c7d 100644 --- a/crates/uv/tests/it/pip_install_scenarios.rs +++ b/crates/uv/tests/it/pip_install_scenarios.rs @@ -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( diff --git a/crates/uv/tests/it/pip_sync.rs b/crates/uv/tests/it/pip_sync.rs index 690c0fdd9..e9ab95686 100644 --- a/crates/uv/tests/it/pip_sync.rs +++ b/crates/uv/tests/it/pip_sync.rs @@ -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"); diff --git a/crates/uv/tests/it/snapshots/it__ecosystem__warehouse-lock-file.snap b/crates/uv/tests/it/snapshots/it__ecosystem__warehouse-lock-file.snap index 250e49b73..431a746d0 100644 --- a/crates/uv/tests/it/snapshots/it__ecosystem__warehouse-lock-file.snap +++ b/crates/uv/tests/it/snapshots/it__ecosystem__warehouse-lock-file.snap @@ -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]]