From 96e76a8849570b22b48961a90fb04ff456275c30 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 12 Jan 2025 11:25:36 -0500 Subject: [PATCH] Fix up remaining sites --- Cargo.lock | 13 +- .../uv-bench/benches/distribution_filename.rs | 24 +- crates/uv-build-backend/src/wheel.rs | 59 +- 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 | 91 +- crates/uv-distribution-types/src/lib.rs | 4 +- .../src/prioritized_distribution.rs | 323 ++----- crates/uv-platform-tags/Cargo.toml | 1 + crates/uv-platform-tags/src/abi_tag.rs | 18 +- crates/uv-platform-tags/src/lib.rs | 3 +- crates/uv-platform-tags/src/platform.rs | 54 +- crates/uv-platform-tags/src/platform_tag.rs | 884 +++++++++++++++++- crates/uv-platform-tags/src/tags.rs | 569 ++--------- crates/uv-publish/src/lib.rs | 2 +- crates/uv-python/src/platform.rs | 4 + crates/uv-resolver/src/lock/mod.rs | 70 +- ...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/requires_python.rs | 247 ++--- crates/uv/tests/it/cache_clean.rs | 4 +- crates/uv/tests/it/pip_sync.rs | 2 +- .../it__ecosystem__warehouse-lock-file.snap | 1 - 28 files changed, 1478 insertions(+), 984 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6afeb78c553c7..8db71554f5931 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]] @@ -5003,6 +5004,7 @@ dependencies = [ "insta", "rkyv", "serde", + "smallvec", "thiserror 2.0.11", "url", "uv-normalize", @@ -5318,6 +5320,7 @@ name = "uv-platform-tags" version = "0.0.1" dependencies = [ "insta", + "memchr", "rkyv", "rustc-hash", "serde", diff --git a/crates/uv-bench/benches/distribution_filename.rs b/crates/uv-bench/benches/distribution_filename.rs index 943b577b7d65b..7d166962166dd 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/src/wheel.rs b/crates/uv-build-backend/src/wheel.rs index 66272e40bcb8a..38fa4bfea2957 100644 --- a/crates/uv-build-backend/src/wheel.rs +++ b/crates/uv-build-backend/src/wheel.rs @@ -1,20 +1,23 @@ -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 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 walkdir::WalkDir; -use zip::{CompressionMethod, ZipWriter}; + +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( @@ -34,12 +37,12 @@ pub fn build_wheel( name: pyproject_toml.name().clone(), version: pyproject_toml.version().clone(), build_tag: None, - python_tag: vec![LanguageTag::Python { + python_tag: TagSet::from_slice(&[LanguageTag::Python { major: 3, minor: None, - }], - abi_tag: vec![AbiTag::None], - platform_tag: vec![PlatformTag::Any], + }]), + abi_tag: TagSet::from_buf([AbiTag::None]), + platform_tag: TagSet::from_buf([PlatformTag::Any]), }; let wheel_path = wheel_dir.join(filename.to_string()); @@ -72,12 +75,12 @@ pub fn list_wheel( name: pyproject_toml.name().clone(), version: pyproject_toml.version().clone(), build_tag: None, - python_tag: vec![LanguageTag::Python { + python_tag: TagSet::from_slice(&[LanguageTag::Python { major: 3, minor: None, - }], - abi_tag: vec![AbiTag::None], - platform_tag: vec![PlatformTag::Any], + }]), + abi_tag: TagSet::from_buf([AbiTag::None]), + platform_tag: TagSet::from_buf([PlatformTag::Any]), }; let mut files = FileList::new(); @@ -254,12 +257,12 @@ pub fn build_editable( name: pyproject_toml.name().clone(), version: pyproject_toml.version().clone(), build_tag: None, - python_tag: vec![LanguageTag::Python { + python_tag: TagSet::from_slice(&[LanguageTag::Python { major: 3, minor: None, - }], - abi_tag: vec![AbiTag::None], - platform_tag: vec![PlatformTag::Any], + }]), + abi_tag: TagSet::from_buf([AbiTag::None]), + platform_tag: TagSet::from_buf([PlatformTag::Any]), }; let wheel_path = wheel_dir.join(filename.to_string()); @@ -309,12 +312,12 @@ pub fn metadata( name: pyproject_toml.name().clone(), version: pyproject_toml.version().clone(), build_tag: None, - python_tag: vec![LanguageTag::Python { + python_tag: TagSet::from_slice(&[LanguageTag::Python { major: 3, minor: None, - }], - abi_tag: vec![AbiTag::None], - platform_tag: vec![PlatformTag::Any], + }]), + abi_tag: TagSet::from_buf([AbiTag::None]), + platform_tag: TagSet::from_buf([PlatformTag::Any]), }; debug!( @@ -766,7 +769,7 @@ mod test { name: PackageName::from_str("foo").unwrap(), version: Version::from_str("1.2.3").unwrap(), build_tag: None, - python_tag: vec![ + python_tag: TagSet::from_slice(&[ LanguageTag::Python { major: 2, minor: None, @@ -775,9 +778,9 @@ mod test { major: 3, minor: None, }, - ], - abi_tag: vec![AbiTag::None], - platform_tag: vec![PlatformTag::Any], + ]), + 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 c82189d7a42c9..049d096536d28 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 b8484b97edba4..188a988df49fb 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 f16d2c7c39215..c3b00d2a952e1 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 1e8c68ebc0032..2014265e095af 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 fcf8d5aa3512f..64c6a39489ca0 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 a649b96df3afd..1f6fcf5e1d01c 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 158af5362784b..8c87b136c969c 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::{AbiTag, LanguageTag, ParseAbiTagError, ParseLanguageTagError, ParsePlatformTagError, PlatformTag, 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[0], - self.abi_tag[0], - self.platform_tag[0], - ) + 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,8 +206,6 @@ 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(LanguageTag::from_str) @@ -189,7 +216,11 @@ impl WheelFilename { .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))?, + platform_tag: platform_tag + .split('.') + .map(PlatformTag::from_str) + .collect::>() + .map_err(|err| WheelFilenameError::InvalidPlatformTag(filename.to_string(), err))?, }) } } @@ -278,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 81e9d53a697e7..d10e2eb3ed678 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::() <= 304, "{}", size_of::()); + assert!(size_of::() <= 304, "{}", 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 ce793751a87b8..cf13022246976 100644 --- a/crates/uv-distribution-types/src/prioritized_distribution.rs +++ b/crates/uv-distribution-types/src/prioritized_distribution.rs @@ -6,7 +6,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::{IncompatibleTag, TagPriority}; +use uv_platform_tags::{IncompatibleTag, PlatformTag, TagPriority}; use uv_pypi_types::{HashDigest, Yanked}; use crate::{ @@ -651,231 +651,102 @@ impl IncompatibleWheel { /// This is roughly the inverse of platform tag generation: given a tag, we want to infer the /// supported platforms (rather than generating the supported tags from a given platform). pub fn implied_markers(filename: &WheelFilename) -> MarkerTree { - return MarkerTree::TRUE; - // let mut marker = MarkerTree::FALSE; - // for platform_tag in &filename.platform_tag { - // match platform_tag.as_str() { - // "any" => { - // return MarkerTree::TRUE; - // } - // - // // Windows - // "win32" => { - // let mut tag_marker = MarkerTree::expression(MarkerExpression::String { - // key: MarkerValueString::SysPlatform, - // operator: MarkerOperator::Equal, - // value: arcstr::literal!("win32"), - // }); - // tag_marker.and(MarkerTree::expression(MarkerExpression::String { - // key: MarkerValueString::PlatformMachine, - // operator: MarkerOperator::Equal, - // value: arcstr::literal!("x86"), - // })); - // marker.or(tag_marker); - // } - // "win_amd64" => { - // let mut tag_marker = MarkerTree::expression(MarkerExpression::String { - // key: MarkerValueString::SysPlatform, - // operator: MarkerOperator::Equal, - // value: arcstr::literal!("win32"), - // }); - // tag_marker.and(MarkerTree::expression(MarkerExpression::String { - // key: MarkerValueString::PlatformMachine, - // operator: MarkerOperator::Equal, - // value: arcstr::literal!("x86_64"), - // })); - // marker.or(tag_marker); - // } - // "win_arm64" => { - // let mut tag_marker = MarkerTree::expression(MarkerExpression::String { - // key: MarkerValueString::SysPlatform, - // operator: MarkerOperator::Equal, - // value: arcstr::literal!("win32"), - // }); - // tag_marker.and(MarkerTree::expression(MarkerExpression::String { - // key: MarkerValueString::PlatformMachine, - // operator: MarkerOperator::Equal, - // value: arcstr::literal!("arm64"), - // })); - // marker.or(tag_marker); - // } - // - // // macOS - // tag if tag.starts_with("macosx_") => { - // 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 { - // arch_marker.or(MarkerTree::expression(MarkerExpression::String { - // key: MarkerValueString::PlatformMachine, - // operator: MarkerOperator::Equal, - // value: ArcStr::from(*arch), - // })); - // } - // tag_marker.and(arch_marker); - // - // marker.or(tag_marker); - // } - // - // // Linux - // tag => { - // 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), - // })); - // - // marker.or(tag_marker); - // } - // } - // } - // marker + let mut marker = MarkerTree::FALSE; + for platform_tag in &filename.platform_tag { + match platform_tag { + PlatformTag::Any => { + return MarkerTree::TRUE; + } + + // Windows + PlatformTag::Win32 => { + let mut tag_marker = MarkerTree::expression(MarkerExpression::String { + key: MarkerValueString::SysPlatform, + operator: MarkerOperator::Equal, + value: arcstr::literal!("win32"), + }); + tag_marker.and(MarkerTree::expression(MarkerExpression::String { + key: MarkerValueString::PlatformMachine, + operator: MarkerOperator::Equal, + value: arcstr::literal!("x86"), + })); + marker.or(tag_marker); + } + PlatformTag::WinAmd64 => { + let mut tag_marker = MarkerTree::expression(MarkerExpression::String { + key: MarkerValueString::SysPlatform, + operator: MarkerOperator::Equal, + value: arcstr::literal!("win32"), + }); + tag_marker.and(MarkerTree::expression(MarkerExpression::String { + key: MarkerValueString::PlatformMachine, + operator: MarkerOperator::Equal, + value: arcstr::literal!("x86_64"), + })); + marker.or(tag_marker); + } + PlatformTag::WinArm64 => { + let mut tag_marker = MarkerTree::expression(MarkerExpression::String { + key: MarkerValueString::SysPlatform, + operator: MarkerOperator::Equal, + value: arcstr::literal!("win32"), + }); + tag_marker.and(MarkerTree::expression(MarkerExpression::String { + key: MarkerValueString::PlatformMachine, + operator: MarkerOperator::Equal, + value: arcstr::literal!("arm64"), + })); + marker.or(tag_marker); + } + + // macOS + PlatformTag::Macos { binary_format, .. } => { + let mut tag_marker = MarkerTree::expression(MarkerExpression::String { + key: MarkerValueString::SysPlatform, + operator: MarkerOperator::Equal, + value: arcstr::literal!("darwin"), + }); + + // Extract the architecture from the end of the tag. + let mut arch_marker = MarkerTree::FALSE; + for arch in binary_format.platform_machine() { + arch_marker.or(MarkerTree::expression(MarkerExpression::String { + key: MarkerValueString::PlatformMachine, + operator: MarkerOperator::Equal, + value: ArcStr::from(arch.name()), + })); + } + tag_marker.and(arch_marker); + + marker.or(tag_marker); + } + + // Linux + 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"), + }); + tag_marker.and(MarkerTree::expression(MarkerExpression::String { + key: MarkerValueString::PlatformMachine, + operator: MarkerOperator::Equal, + value: ArcStr::from(arch.name()), + })); + marker.or(tag_marker); + } + + tag => { + debug!("Unknown platform tag in wheel tag: {tag}"); + } + } + } + marker } #[cfg(test)] diff --git a/crates/uv-platform-tags/Cargo.toml b/crates/uv-platform-tags/Cargo.toml index 7a38fb655d7d1..c390697671a85 100644 --- a/crates/uv-platform-tags/Cargo.toml +++ b/crates/uv-platform-tags/Cargo.toml @@ -16,6 +16,7 @@ doctest = false workspace = true [dependencies] +memchr = { workspace = true } rkyv = { workspace = true} rustc-hash = { workspace = true } serde = { workspace = true } diff --git a/crates/uv-platform-tags/src/abi_tag.rs b/crates/uv-platform-tags/src/abi_tag.rs index 3d78ed0c7fbb0..44f76f17f9769 100644 --- a/crates/uv-platform-tags/src/abi_tag.rs +++ b/crates/uv-platform-tags/src/abi_tag.rs @@ -78,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 { @@ -216,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), @@ -387,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"), @@ -406,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/lib.rs b/crates/uv-platform-tags/src/lib.rs index 62535d2e3c5db..1fe09c405555f 100644 --- a/crates/uv-platform-tags/src/lib.rs +++ b/crates/uv-platform-tags/src/lib.rs @@ -1,7 +1,8 @@ pub use abi_tag::{AbiTag, ParseAbiTagError}; pub use language_tag::{LanguageTag, ParseLanguageTagError}; pub use platform::{Arch, Os, Platform, PlatformError}; -pub use tags::{IncompatibleTag, PlatformTag, ParsePlatformTagError, 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; diff --git a/crates/uv-platform-tags/src/platform.rs b/crates/uv-platform-tags/src/platform.rs index 5f8508f2f0179..4450561c2fcc9 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::{fmt, io}; use std::str::FromStr; -use serde::{Deserialize, Serialize}; +use std::{fmt, io}; + 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 }, @@ -100,6 +100,8 @@ pub enum Arch { Powerpc64Le, #[serde(alias = "ppc64")] Powerpc64, + #[serde(alias = "ppc")] + Powerpc, #[serde(alias = "i386", alias = "i686")] X86, #[serde(alias = "amd64")] @@ -118,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"), @@ -138,12 +141,13 @@ impl FromStr for Arch { "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)), + _ => Err(format!("Unknown architecture: {s}")), } } } @@ -162,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 index ad5dd455dd408..8d6b8322b0c69 100644 --- a/crates/uv-platform-tags/src/platform_tag.rs +++ b/crates/uv-platform-tags/src/platform_tag.rs @@ -1,10 +1,14 @@ -/// A tag to represent the language and implementation of the Python interpreter. +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 first segment in the wheel filename. For example, in `cp39-none-manylinux_2_24_x86_64.whl`, -/// the language tag is `cp39`. +/// 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, - Copy, Clone, Eq, PartialEq, @@ -17,16 +21,864 @@ )] #[rkyv(derive(Debug))] pub enum PlatformTag { - /// Ex) `none` - None, - /// Ex) `py3`, `py39` - Python { major: u8, minor: Option }, - /// Ex) `cp39` - CPython { python_version: (u8, u8) }, - /// Ex) `pp39` - PyPy { python_version: (u8, u8) }, - /// Ex) `graalpy310` - GraalPy { python_version: (u8, u8) }, - /// Ex) `pt38` - Pyston { python_version: (u8, u8) }, + /// 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 cc3373dcb963f..2889d380ea968 100644 --- a/crates/uv-platform-tags/src/tags.rs +++ b/crates/uv-platform-tags/src/tags.rs @@ -4,10 +4,10 @@ use std::str::FromStr; use std::sync::Arc; use std::{cmp, num::NonZeroU32}; -use thiserror::Error; use rustc_hash::FxHashMap; use crate::abi_tag::AbiTag; +use crate::platform_tag::PlatformTag; use crate::{Arch, LanguageTag, Os, Platform, PlatformError}; #[derive(Debug, thiserror::Error)] @@ -114,7 +114,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.is_manylinux()); + platform_tags.retain(|tag| !tag.is_manylinux_compatible()); } platform_tags }; @@ -395,399 +395,6 @@ impl Implementation { } } -#[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 { - pub fn is_manylinux(&self) -> bool { - matches!(self, Self::Manylinux { .. } | Self::Manylinux1 { .. } | Self::Manylinux2010 { .. } | Self::Manylinux2014 { .. }) - } -} - -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. - #[allow(clippy::cast_possible_truncation)] - fn from_str(s: &str) -> Result { - if s == "any" { - return Ok(Self::Any); - } - - if s == "win32" { - return Ok(Self::Win32); - } - - if s == "win_amd64" { - return Ok(Self::WinAmd64); - } - - if s == "win_arm64" { - return Ok(Self::WinArm64); - } - - if let Some(rest) = s.strip_prefix("manylinux_") { - // Ex) manylinux_2_17_x86_64 - let parts: Vec<&str> = rest.split('_').collect(); - if parts.len() != 3 { - return Err(ParsePlatformTagError::InvalidFormat { - platform: "manylinux", - tag: s.to_string(), - }); - } - let major = parts[0].parse().map_err(|_| ParsePlatformTagError::InvalidMajorVersion { - platform: "manylinux", - tag: s.to_string(), - })?; - let minor = parts[1].parse().map_err(|_| ParsePlatformTagError::InvalidMinorVersion { - platform: "manylinux", - tag: s.to_string(), - })?; - let arch = parts[2].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 parts: Vec<&str> = rest.split('_').collect(); - if parts.len() != 3 { - return Err(ParsePlatformTagError::InvalidFormat { - platform: "musllinux", - tag: s.to_string(), - }); - } - let major = parts[0].parse().map_err(|_| ParsePlatformTagError::InvalidMajorVersion { - platform: "musllinux", - tag: s.to_string(), - })?; - let minor = parts[1].parse().map_err(|_| ParsePlatformTagError::InvalidMinorVersion { - platform: "musllinux", - tag: s.to_string(), - })?; - let arch = parts[2].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 parts: Vec<&str> = rest.split('_').collect(); - if parts.len() != 3 { - return Err(ParsePlatformTagError::InvalidFormat { - platform: "macosx", - tag: s.to_string(), - }); - } - let major = parts[0].parse().map_err(|_| ParsePlatformTagError::InvalidMajorVersion { - platform: "macosx", - tag: s.to_string(), - })?; - let minor = parts[1].parse().map_err(|_| ParsePlatformTagError::InvalidMinorVersion { - platform: "macosx", - tag: s.to_string(), - })?; - let binary_format = parts[2].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 parts: Vec<&str> = rest.split('_').collect(); - if parts.len() != 2 { - return Err(ParsePlatformTagError::InvalidFormat { - platform: "android", - tag: s.to_string(), - }); - } - let api_level = parts[0].parse().map_err(|_| ParsePlatformTagError::InvalidApiLevel { - platform: "android", - tag: s.to_string(), - })?; - let arch = parts[1].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 - let parts: Vec<&str> = rest.split('_').collect(); - if parts.len() != 2 { - return Err(ParsePlatformTagError::InvalidFormat { - platform: "freebsd", - tag: s.to_string(), - }); - } - return Ok(Self::FreeBsd { - release: parts[0].to_string(), - arch: parts[1].parse().map_err(|_| ParsePlatformTagError::InvalidArch { - platform: "freebsd", - tag: s.to_string(), - })?, - }); - } - - if let Some(rest) = s.strip_prefix("netbsd_") { - // Ex) netbsd_9_x86_64 - let parts: Vec<&str> = rest.split('_').collect(); - if parts.len() != 2 { - return Err(ParsePlatformTagError::InvalidFormat { - platform: "netbsd", - tag: s.to_string(), - }); - } - return Ok(Self::NetBsd { - release: parts[0].to_string(), - arch: parts[1].parse().map_err(|_| ParsePlatformTagError::InvalidArch { - platform: "netbsd", - tag: s.to_string(), - })?, - }); - } - - if let Some(rest) = s.strip_prefix("openbsd_") { - // Ex) openbsd_7_x86_64 - let parts: Vec<&str> = rest.split('_').collect(); - if parts.len() != 2 { - return Err(ParsePlatformTagError::InvalidFormat { - platform: "openbsd", - tag: s.to_string(), - }); - } - return Ok(Self::OpenBsd { - release: parts[0].to_string(), - arch: parts[1].parse().map_err(|_| ParsePlatformTagError::InvalidArch { - platform: "openbsd", - tag: s.to_string(), - })?, - }); - } - - if let Some(rest) = s.strip_prefix("dragonfly_") { - // Ex) dragonfly_6_x86_64 - let parts: Vec<&str> = rest.split('_').collect(); - if parts.len() != 2 { - return Err(ParsePlatformTagError::InvalidFormat { - platform: "dragonfly", - tag: s.to_string(), - }); - } - return Ok(Self::Dragonfly { - release: parts[0].to_string(), - arch: parts[1].parse().map_err(|_| ParsePlatformTagError::InvalidArch { - platform: "dragonfly", - tag: s.to_string(), - })?, - }); - } - - if let Some(rest) = s.strip_prefix("haiku_") { - // Ex) haiku_1_x86_64 - let parts: Vec<&str> = rest.split('_').collect(); - if parts.len() != 2 { - return Err(ParsePlatformTagError::InvalidFormat { - platform: "haiku", - tag: s.to_string(), - }); - } - return Ok(Self::Haiku { - release: parts[0].to_string(), - arch: parts[1].parse().map_err(|_| ParsePlatformTagError::InvalidArch { - platform: "haiku", - tag: s.to_string(), - })?, - }); - } - - if let Some(rest) = s.strip_prefix("illumos_") { - // Ex) illumos_5_11_x86_64 - let parts: Vec<&str> = rest.split('_').collect(); - if parts.len() != 3 { - return Err(ParsePlatformTagError::InvalidFormat { - platform: "illumos", - tag: s.to_string(), - }); - } - return Ok(Self::Illumos { - release: format!("{}_{}", parts[0], parts[1]), - arch: parts[2].to_string(), - }); - } - - if let Some(rest) = s.strip_prefix("solaris_") { - // Ex) solaris_11_4_x86_64_64bit - let parts: Vec<&str> = rest.split('_').collect(); - if parts.len() != 4 || parts[3] != "64bit" { - return Err(ParsePlatformTagError::InvalidFormat { - platform: "solaris", - tag: s.to_string(), - }); - } - return Ok(Self::Solaris { - release: format!("{}_{}", parts[0], parts[1]), - arch: parts[2].to_string(), - }); - } - - Err(ParsePlatformTagError::UnknownFormat(s.to_string())) - } -} - -#[derive(Debug, 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 }, -} - - /// Returns the compatible tags for the current [`Platform`] (e.g., `manylinux_2_17`, /// `macosx_11_0_arm64`, or `win_amd64`). /// @@ -828,12 +435,11 @@ fn compatible_tags(platform: &Platform) -> Result, PlatformErro (Os::Musllinux { major, minor }, _) => { 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| PlatformTag::Musllinux { - major: *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) => { @@ -848,7 +454,7 @@ fn compatible_tags(platform: &Platform) -> Result, PlatformErro platform_tags.push(PlatformTag::Macos { major: 10, minor, - binary_format, + binary_format: *binary_format, }); } } @@ -861,7 +467,7 @@ fn compatible_tags(platform: &Platform) -> Result, PlatformErro platform_tags.push(PlatformTag::Macos { major, minor: 0, - binary_format, + binary_format: *binary_format, }); } } @@ -872,7 +478,7 @@ fn compatible_tags(platform: &Platform) -> Result, PlatformErro platform_tags.push(PlatformTag::Macos { major: 10, minor, - binary_format, + binary_format: *binary_format, }); } } @@ -895,21 +501,17 @@ fn compatible_tags(platform: &Platform) -> Result, PlatformErro platform_tags.push(PlatformTag::Macos { major, minor: 0, - binary_format, + 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| PlatformTag::Macos { - major: 10, - minor, - binary_format: BinaryFormat::Universal2, - }), - ); + platform_tags.extend((4..=16).rev().map(|minor| PlatformTag::Macos { + major: 10, + minor, + binary_format: BinaryFormat::Universal2, + })); platform_tags } (Os::Windows, Arch::X86) => { @@ -921,38 +523,23 @@ fn compatible_tags(platform: &Platform) -> Result, PlatformErro (Os::Windows, Arch::Aarch64) => vec![PlatformTag::WinArm64], (Os::FreeBsd { release }, _) => { let release = release.replace(['.', '-'], "_"); - vec![PlatformTag::FreeBsd { - release, - arch, - }] + vec![PlatformTag::FreeBsd { release, arch }] } (Os::NetBsd { release }, _) => { let release = release.replace(['.', '-'], "_"); - vec![PlatformTag::NetBsd { - release, - arch, - }] + vec![PlatformTag::NetBsd { release, arch }] } (Os::OpenBsd { release }, _) => { let release = release.replace(['.', '-'], "_"); - vec![PlatformTag::OpenBsd { - release, - arch, - }] + vec![PlatformTag::OpenBsd { release, arch }] } (Os::Dragonfly { release }, _) => { let release = release.replace(['.', '-'], "_"); - vec![PlatformTag::Dragonfly { - release, - arch, - }] + vec![PlatformTag::Dragonfly { release, arch }] } (Os::Haiku { release }, _) => { let release = release.replace(['.', '-'], "_"); - vec![PlatformTag::Haiku { - release, - arch, - }] + vec![PlatformTag::Haiku { release, arch }] } (Os::Illumos { release, arch }, _) => { // See https://github.com/python/cpython/blob/46c8d915715aa2bd4d697482aa051fe974d440e1/Lib/sysconfig.py#L722-L730 @@ -966,10 +553,7 @@ fn compatible_tags(platform: &Platform) -> Result, PlatformErro // SunOS 5 == Solaris 2 let release = format!("{}_{}", major_ver - 3, other); let arch = format!("{arch}_64bit"); - return Ok(vec![PlatformTag::Solaris { - release, - arch, - }]); + return Ok(vec![PlatformTag::Solaris { release, arch }]); } } @@ -1007,27 +591,23 @@ fn compatible_tags(platform: &Platform) -> Result, PlatformErro rkyv::Serialize, )] #[rkyv(derive(Debug))] -enum BinaryFormat { - Intel, +pub enum BinaryFormat { Arm64, - Fat64, + Fat, Fat32, - Universal2, + Fat64, + I386, + Intel, + Ppc, + Ppc64, Universal, + Universal2, X86_64, } impl std::fmt::Display for BinaryFormat { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::Intel => write!(f, "intel"), - Self::Arm64 => write!(f, "arm64"), - Self::Fat64 => write!(f, "fat64"), - Self::Fat32 => write!(f, "fat32"), - Self::Universal2 => write!(f, "universal2"), - Self::Universal => write!(f, "universal"), - Self::X86_64 => write!(f, "x86_64"), - } + write!(f, "{}", self.name()) } } @@ -1036,12 +616,16 @@ impl FromStr for BinaryFormat { fn from_str(s: &str) -> Result { match s { - "intel" => Ok(Self::Intel), "arm64" => Ok(Self::Arm64), - "fat64" => Ok(Self::Fat64), + "fat" => Ok(Self::Fat), "fat32" => Ok(Self::Fat32), - "universal2" => Ok(Self::Universal2), + "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}")), } @@ -1052,30 +636,71 @@ impl BinaryFormat { /// Determine the appropriate binary formats for a macOS version. /// /// See: - pub fn from_arch(arch: Arch) -> Vec { - let mut formats = vec![match arch { - Arch::Aarch64 => Self::Arm64, - Arch::X86_64 => Self::X86_64, - _ => unreachable!(), - }]; - - if matches!(arch, Arch::X86_64) { - formats.extend([ + 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(Self::Universal2); + /// 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(Self::Universal); + /// 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 } } @@ -1102,6 +727,7 @@ mod tests { Arch::X86_64, )) .unwrap(); + let tags = tags.iter().map(ToString::to_string).collect::>(); assert_debug_snapshot!( tags, @r###" @@ -1141,6 +767,7 @@ mod tests { Arch::X86_64, )) .unwrap(); + let tags = tags.iter().map(ToString::to_string).collect::>(); assert_debug_snapshot!( tags, @r###" @@ -1301,6 +928,7 @@ mod tests { Arch::X86_64, )) .unwrap(); + let tags = tags.iter().map(ToString::to_string).collect::>(); assert_debug_snapshot!( tags, @r###" @@ -1419,6 +1047,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 09fa80d15d415..7a3a64b72c2c2 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[0].to_string())); + 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 d346347543783..ec6abfeaa72d7 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 0662ca837b05e..267640d70fadc 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -265,46 +265,36 @@ 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 - // // own sufficient to exclude linux wheels), but due to - // // `(A ∩ (B ∩ C) = ∅) => ((A ∩ B = ∅) or (A ∩ C = ∅))` - // // 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" - // }) - // }) { - // !graph.graph[node_index].marker().is_disjoint(*LINUX_MARKERS) - // } else if platform_tags - // .iter() - // .all(|tag| windows_tags.contains(&&**tag)) - // { - // !graph.graph[node_index] - // .marker() - // .is_disjoint(*WINDOWS_MARKERS) - // } else if platform_tags.iter().all(|tag| tag.starts_with("macosx_")) { - // !graph.graph[node_index].marker().is_disjoint(*MAC_MARKERS) - // } else { - // true - // } - // }); + 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 + // own sufficient to exclude linux wheels), but due to + // `(A ∩ (B ∩ C) = ∅) => ((A ∩ B = ∅) or (A ∩ C = ∅))` + // 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(uv_platform_tags::PlatformTag::is_linux_compatible) + { + !graph.graph[node_index].marker().is_disjoint(*LINUX_MARKERS) + } else if platform_tags + .iter() + .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(uv_platform_tags::PlatformTag::is_macos_compatible) + { + !graph.graph[node_index].marker().is_disjoint(*MAC_MARKERS) + } else { + true + } + }); } /// Initialize a [`Lock`] from a list of [`Package`] entries. 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 1e8349c40acf2..81566c0c7e24d 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 ce380043e1f39..57f836c16d51f 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 d8fd783ed507c..68fbd77b72d13 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/requires_python.rs b/crates/uv-resolver/src/requires_python.rs index 8cb090c61eb03..2b96072100723 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. /// @@ -363,120 +363,137 @@ impl RequiresPython { /// It is meant to filter out clearly unusable wheels with perfect specificity and acceptable /// sensitivity, we return `true` if the tags are unknown. pub fn matches_wheel_tag(&self, wheel: &WheelFilename) -> bool { - false - - // wheel.abi_tag.iter().any(|abi_tag| { - // if abi_tag == AbiTag::Abi3 { - // // Universal tags are allowed. - // true - // } 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") - // { - // 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; - // }; - // - // // 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; - // } - // - // 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")) - // { - // 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]))); - // 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; - // } - // - // return true; - // } - // - // // Unknown tags are allowed. - // true - // }) - // } else if abi_tag.starts_with("cp2") || abi_tag.starts_with("pypy2") { - // // 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; - // }; - // - // // 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]))); - // 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]))); - // if wheel_bound > self.range.upper().major_minor() { - // return false; - // } - // - // true - // } else { - // // Unknown tags are allowed. - // true - // } - // }) + wheel.abi_tag.iter().any(|abi_tag| { + if *abi_tag == AbiTag::Abi3 { + // Universal tags are allowed. + true + } 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 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 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, 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 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 + { + // Ex) If the wheel bound is `3.6`, then it doesn't match `>=3.10`. + 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, u64::from(*minor)]))); + if wheel_bound > self.range.upper().major_minor() { + return false; + } + + return true; + } + + // Unknown tags are allowed. + true + }) + } 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 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, 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, u64::from(*minor)]))); + if wheel_bound > self.range.upper().major_minor() { + return false; + } + + true + } else { + // Unknown tags are allowed. + true + } + }) } } diff --git a/crates/uv/tests/it/cache_clean.rs b/crates/uv/tests/it/cache_clean.rs index 10a902f6e43a4..38a08c79a2760 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_sync.rs b/crates/uv/tests/it/pip_sync.rs index 690c0fdd9b28f..e9ab956864476 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 250e49b735ba0..431a746d0ecb6 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]]