diff --git a/crates/uv-distribution-filename/src/wheel.rs b/crates/uv-distribution-filename/src/wheel.rs index fda2de853d4b..397f4eba451d 100644 --- a/crates/uv-distribution-filename/src/wheel.rs +++ b/crates/uv-distribution-filename/src/wheel.rs @@ -177,6 +177,8 @@ impl WheelFilename { name, version, build_tag, + // TODO(charlie): Consider storing structured tags here. We need to benchmark to + // understand whether it's impactful. python_tag: python_tag.split('.').map(String::from).collect(), abi_tag: abi_tag.split('.').map(String::from).collect(), platform_tag: platform_tag.split('.').map(String::from).collect(), diff --git a/crates/uv-platform-tags/src/abi_tag.rs b/crates/uv-platform-tags/src/abi_tag.rs new file mode 100644 index 000000000000..97fcd37c4f52 --- /dev/null +++ b/crates/uv-platform-tags/src/abi_tag.rs @@ -0,0 +1,445 @@ +use std::fmt::Formatter; +use std::str::FromStr; + +/// A tag to represent the ABI compatibility of a Python distribution. +/// +/// This is the second segment in the wheel filename, following the language tag. For example, +/// in `cp39-none-manylinux_2_24_x86_64.whl`, the ABI tag is `none`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum AbiTag { + /// Ex) `none` + None, + /// Ex) `abi3` + Abi3, + /// Ex) `cp39m`, `cp310t` + CPython { + gil_disabled: bool, + python_version: (u8, u8), + }, + /// Ex) `pypy39_pp73` + PyPy { + python_version: (u8, u8), + implementation_version: (u8, u8), + }, + /// Ex) `graalpy310_graalpy240_310_native` + GraalPy { + python_version: (u8, u8), + implementation_version: (u8, u8), + }, + /// Ex) `pyston38-pyston_23` + Pyston { + python_version: (u8, u8), + implementation_version: (u8, u8), + }, +} + +impl std::fmt::Display for AbiTag { + /// Format an [`AbiTag`] as a string. + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::None => write!(f, "none"), + Self::Abi3 => write!(f, "abi3"), + Self::CPython { + gil_disabled, + python_version: (major, minor), + } => { + if *minor <= 7 { + write!(f, "cp{major}{minor}m") + } else if *gil_disabled { + // https://peps.python.org/pep-0703/#build-configuration-changes + // Python 3.13+ only, but it makes more sense to just rely on the sysconfig var. + write!(f, "cp{major}{minor}t") + } else { + write!(f, "cp{major}{minor}") + } + } + Self::PyPy { + python_version: (py_major, py_minor), + implementation_version: (impl_major, impl_minor), + } => { + write!(f, "pypy{py_major}{py_minor}_pp{impl_major}{impl_minor}") + } + Self::GraalPy { + python_version: (py_major, py_minor), + implementation_version: (impl_major, impl_minor), + } => { + write!( + f, + "graalpy{py_major}{py_minor}_graalpy{impl_major}{impl_minor}_{py_major}{py_minor}native" + ) + } + Self::Pyston { + python_version: (py_major, py_minor), + implementation_version: (impl_major, impl_minor), + } => { + write!( + f, + "pyston{py_major}{py_minor}-pyston_{impl_major}{impl_minor}" + ) + } + } + } +} + +impl FromStr for AbiTag { + type Err = ParseAbiTagError; + + /// Parse an [`AbiTag`] from a string. + #[allow(clippy::cast_possible_truncation)] + fn from_str(s: &str) -> Result { + /// Parse a Python version from a string (e.g., convert `39` into `(3, 9)`). + fn parse_python_version( + version_str: &str, + implementation: &'static str, + full_tag: &str, + ) -> Result<(u8, u8), ParseAbiTagError> { + let major = version_str + .chars() + .next() + .ok_or_else(|| ParseAbiTagError::MissingMajorVersion { + implementation, + tag: full_tag.to_string(), + })? + .to_digit(10) + .ok_or_else(|| ParseAbiTagError::InvalidMajorVersion { + implementation, + tag: full_tag.to_string(), + })? as u8; + let minor = version_str + .get(1..) + .ok_or_else(|| ParseAbiTagError::MissingMinorVersion { + implementation, + tag: full_tag.to_string(), + })? + .parse::() + .map_err(|_| ParseAbiTagError::InvalidMinorVersion { + implementation, + tag: full_tag.to_string(), + })?; + Ok((major, minor)) + } + + /// Parse an implementation version from a string (e.g., convert `37` into `(3, 7)`). + fn parse_impl_version( + version_str: &str, + implementation: &'static str, + full_tag: &str, + ) -> Result<(u8, u8), ParseAbiTagError> { + let major = version_str + .chars() + .next() + .ok_or_else(|| ParseAbiTagError::MissingImplMajorVersion { + implementation, + tag: full_tag.to_string(), + })? + .to_digit(10) + .ok_or_else(|| ParseAbiTagError::InvalidImplMajorVersion { + implementation, + tag: full_tag.to_string(), + })? as u8; + let minor = version_str + .get(1..) + .ok_or_else(|| ParseAbiTagError::MissingImplMinorVersion { + implementation, + tag: full_tag.to_string(), + })? + .parse::() + .map_err(|_| ParseAbiTagError::InvalidImplMinorVersion { + implementation, + tag: full_tag.to_string(), + })?; + Ok((major, minor)) + } + + if s == "none" { + Ok(Self::None) + } else if s == "abi3" { + Ok(Self::Abi3) + } else if let Some(cp) = s.strip_prefix("cp") { + // Ex) `cp39m`, `cp310t` + let version_end = cp.find(|c: char| !c.is_ascii_digit()).unwrap_or(cp.len()); + let version_str = &cp[..version_end]; + let (major, minor) = parse_python_version(version_str, "CPython", s)?; + let gil_disabled = cp.ends_with('t'); + Ok(Self::CPython { + gil_disabled, + python_version: (major, minor), + }) + } else if let Some(rest) = s.strip_prefix("pypy") { + // Ex) `pypy39_pp73` + let version_end = rest + .find('_') + .ok_or_else(|| ParseAbiTagError::InvalidFormat { + implementation: "PyPy", + tag: s.to_string(), + })?; + let version_str = &rest[..version_end]; + let (major, minor) = parse_python_version(version_str, "PyPy", s)?; + let rest = rest[version_end + 1..].strip_prefix("pp").ok_or_else(|| { + ParseAbiTagError::InvalidFormat { + implementation: "PyPy", + tag: s.to_string(), + } + })?; + let (impl_major, impl_minor) = parse_impl_version(rest, "PyPy", s)?; + Ok(Self::PyPy { + python_version: (major, minor), + implementation_version: (impl_major, impl_minor), + }) + } else if let Some(rest) = s.strip_prefix("graalpy") { + // Ex) `graalpy310_graalpy240_310_native` + let version_end = rest + .find('_') + .ok_or_else(|| ParseAbiTagError::InvalidFormat { + implementation: "GraalPy", + tag: s.to_string(), + })?; + let version_str = &rest[..version_end]; + let (major, minor) = parse_python_version(version_str, "GraalPy", s)?; + let rest = rest[version_end + 1..] + .strip_prefix("graalpy") + .ok_or_else(|| ParseAbiTagError::InvalidFormat { + implementation: "GraalPy", + tag: s.to_string(), + })?; + let (impl_major, impl_minor) = parse_impl_version(rest, "GraalPy", s)?; + Ok(Self::GraalPy { + python_version: (major, minor), + implementation_version: (impl_major, impl_minor), + }) + } else if let Some(rest) = s.strip_prefix("pyston") { + // Ex) `pyston38-pyston_23` + let version_end = rest + .find('-') + .ok_or_else(|| ParseAbiTagError::InvalidFormat { + implementation: "Pyston", + tag: s.to_string(), + })?; + let version_str = &rest[..version_end]; + let (major, minor) = parse_python_version(version_str, "Pyston", s)?; + let rest = rest[version_end + 1..] + .strip_prefix("pyston_") + .ok_or_else(|| ParseAbiTagError::InvalidFormat { + implementation: "Pyston", + tag: s.to_string(), + })?; + let (impl_major, impl_minor) = parse_impl_version(rest, "Pyston", s)?; + Ok(Self::Pyston { + python_version: (major, minor), + implementation_version: (impl_major, impl_minor), + }) + } else { + Err(ParseAbiTagError::UnknownFormat(s.to_string())) + } + } +} + +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum ParseAbiTagError { + #[error("Unknown ABI tag format: {0}")] + UnknownFormat(String), + #[error("Missing major version in {implementation} ABI tag: {tag}")] + MissingMajorVersion { + implementation: &'static str, + tag: String, + }, + #[error("Invalid major version in {implementation} ABI tag: {tag}")] + InvalidMajorVersion { + implementation: &'static str, + tag: String, + }, + #[error("Missing minor version in {implementation} ABI tag: {tag}")] + MissingMinorVersion { + implementation: &'static str, + tag: String, + }, + #[error("Invalid minor version in {implementation} ABI tag: {tag}")] + InvalidMinorVersion { + implementation: &'static str, + tag: String, + }, + #[error("Invalid {implementation} ABI tag format: {tag}")] + InvalidFormat { + implementation: &'static str, + tag: String, + }, + #[error("Missing implementation major version in {implementation} ABI tag: {tag}")] + MissingImplMajorVersion { + implementation: &'static str, + tag: String, + }, + #[error("Invalid implementation major version in {implementation} ABI tag: {tag}")] + InvalidImplMajorVersion { + implementation: &'static str, + tag: String, + }, + #[error("Missing implementation minor version in {implementation} ABI tag: {tag}")] + MissingImplMinorVersion { + implementation: &'static str, + tag: String, + }, + #[error("Invalid implementation minor version in {implementation} ABI tag: {tag}")] + InvalidImplMinorVersion { + implementation: &'static str, + tag: String, + }, +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use crate::abi_tag::{AbiTag, ParseAbiTagError}; + + #[test] + fn none_abi() { + assert_eq!(AbiTag::from_str("none"), Ok(AbiTag::None)); + assert_eq!(AbiTag::None.to_string(), "none"); + } + + #[test] + fn abi3() { + assert_eq!(AbiTag::from_str("abi3"), Ok(AbiTag::Abi3)); + assert_eq!(AbiTag::Abi3.to_string(), "abi3"); + } + + #[test] + fn cpython_abi() { + let tag = AbiTag::CPython { + gil_disabled: false, + python_version: (3, 9), + }; + assert_eq!(AbiTag::from_str("cp39"), Ok(tag)); + assert_eq!(tag.to_string(), "cp39"); + + let tag = AbiTag::CPython { + gil_disabled: false, + python_version: (3, 7), + }; + assert_eq!(AbiTag::from_str("cp37m"), Ok(tag)); + assert_eq!(tag.to_string(), "cp37m"); + + let tag = AbiTag::CPython { + gil_disabled: true, + python_version: (3, 13), + }; + assert_eq!(AbiTag::from_str("cp313t"), Ok(tag)); + assert_eq!(tag.to_string(), "cp313t"); + + assert_eq!( + AbiTag::from_str("cpXY"), + Err(ParseAbiTagError::MissingMajorVersion { + implementation: "CPython", + tag: "cpXY".to_string() + }) + ); + } + + #[test] + fn pypy_abi() { + let tag = AbiTag::PyPy { + python_version: (3, 9), + implementation_version: (7, 3), + }; + assert_eq!(AbiTag::from_str("pypy39_pp73"), Ok(tag)); + assert_eq!(tag.to_string(), "pypy39_pp73"); + + assert_eq!( + AbiTag::from_str("pypy39"), + Err(ParseAbiTagError::InvalidFormat { + implementation: "PyPy", + tag: "pypy39".to_string() + }) + ); + assert_eq!( + AbiTag::from_str("pypy39_73"), + Err(ParseAbiTagError::InvalidFormat { + implementation: "PyPy", + tag: "pypy39_73".to_string() + }) + ); + assert_eq!( + AbiTag::from_str("pypy39_ppXY"), + Err(ParseAbiTagError::InvalidImplMajorVersion { + implementation: "PyPy", + tag: "pypy39_ppXY".to_string() + }) + ); + } + + #[test] + fn graalpy_abi() { + let tag = AbiTag::GraalPy { + 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"), + Err(ParseAbiTagError::InvalidFormat { + implementation: "GraalPy", + tag: "graalpy310".to_string() + }) + ); + assert_eq!( + AbiTag::from_str("graalpy310_240"), + Err(ParseAbiTagError::InvalidFormat { + implementation: "GraalPy", + tag: "graalpy310_240".to_string() + }) + ); + assert_eq!( + AbiTag::from_str("graalpy310_graalpyXY"), + Err(ParseAbiTagError::InvalidImplMajorVersion { + implementation: "GraalPy", + tag: "graalpy310_graalpyXY".to_string() + }) + ); + } + + #[test] + fn pyston_abi() { + let tag = AbiTag::Pyston { + python_version: (3, 8), + implementation_version: (2, 3), + }; + assert_eq!(AbiTag::from_str("pyston38-pyston_23"), Ok(tag)); + assert_eq!(tag.to_string(), "pyston38-pyston_23"); + + assert_eq!( + AbiTag::from_str("pyston38"), + Err(ParseAbiTagError::InvalidFormat { + implementation: "Pyston", + tag: "pyston38".to_string() + }) + ); + assert_eq!( + AbiTag::from_str("pyston38_23"), + Err(ParseAbiTagError::InvalidFormat { + implementation: "Pyston", + tag: "pyston38_23".to_string() + }) + ); + assert_eq!( + AbiTag::from_str("pyston38-pyston_XY"), + Err(ParseAbiTagError::InvalidImplMajorVersion { + implementation: "Pyston", + tag: "pyston38-pyston_XY".to_string() + }) + ); + } + + #[test] + fn unknown_abi() { + assert_eq!( + AbiTag::from_str("unknown"), + Err(ParseAbiTagError::UnknownFormat("unknown".to_string())) + ); + assert_eq!( + AbiTag::from_str(""), + Err(ParseAbiTagError::UnknownFormat(String::new())) + ); + } +} diff --git a/crates/uv-platform-tags/src/language_tag.rs b/crates/uv-platform-tags/src/language_tag.rs new file mode 100644 index 000000000000..d764a74ad8b3 --- /dev/null +++ b/crates/uv-platform-tags/src/language_tag.rs @@ -0,0 +1,367 @@ +use std::fmt::Formatter; +use std::str::FromStr; + +/// A tag to represent the language and implementation of the Python interpreter. +/// +/// This is the first segment in the wheel filename. For example, in `cp39-none-manylinux_2_24_x86_64.whl`, +/// the language tag is `cp39`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum LanguageTag { + /// 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) }, +} + +impl std::fmt::Display for LanguageTag { + /// Format a [`LanguageTag`] as a string. + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::None => write!(f, "none"), + Self::Python { major, minor } => { + if let Some(minor) = minor { + write!(f, "py{major}{minor}") + } else { + write!(f, "py{major}") + } + } + Self::CPython { + python_version: (major, minor), + } => { + write!(f, "cp{major}{minor}") + } + Self::PyPy { + python_version: (major, minor), + } => { + write!(f, "pp{major}{minor}") + } + Self::GraalPy { + python_version: (major, minor), + } => { + write!(f, "graalpy{major}{minor}") + } + Self::Pyston { + python_version: (major, minor), + } => { + write!(f, "pt{major}{minor}") + } + } + } +} + +impl FromStr for LanguageTag { + type Err = ParseLanguageTagError; + + /// Parse a [`LanguageTag`] from a string. + #[allow(clippy::cast_possible_truncation)] + fn from_str(s: &str) -> Result { + /// Parse a Python version from a string (e.g., convert `39` into `(3, 9)`). + fn parse_python_version( + version_str: &str, + implementation: &'static str, + full_tag: &str, + ) -> Result<(u8, u8), ParseLanguageTagError> { + let major = version_str + .chars() + .next() + .ok_or_else(|| ParseLanguageTagError::MissingMajorVersion { + implementation, + tag: full_tag.to_string(), + })? + .to_digit(10) + .ok_or_else(|| ParseLanguageTagError::InvalidMajorVersion { + implementation, + tag: full_tag.to_string(), + })? as u8; + let minor = version_str + .get(1..) + .ok_or_else(|| ParseLanguageTagError::MissingMinorVersion { + implementation, + tag: full_tag.to_string(), + })? + .parse::() + .map_err(|_| ParseLanguageTagError::InvalidMinorVersion { + implementation, + tag: full_tag.to_string(), + })?; + Ok((major, minor)) + } + + if s == "none" { + Ok(Self::None) + } else if let Some(py) = s.strip_prefix("py") { + if py.len() == 1 { + // Ex) `py3` + let major = py + .chars() + .next() + .ok_or_else(|| ParseLanguageTagError::MissingMajorVersion { + implementation: "Python", + tag: s.to_string(), + })? + .to_digit(10) + .ok_or_else(|| ParseLanguageTagError::InvalidMajorVersion { + implementation: "Python", + tag: s.to_string(), + })? as u8; + Ok(Self::Python { major, minor: None }) + } else { + // Ex) `py39` + let (major, minor) = parse_python_version(py, "Python", s)?; + Ok(Self::Python { + major, + minor: Some(minor), + }) + } + } else if let Some(cp) = s.strip_prefix("cp") { + // Ex) `cp39` + let (major, minor) = parse_python_version(cp, "CPython", s)?; + Ok(Self::CPython { + python_version: (major, minor), + }) + } else if let Some(pp) = s.strip_prefix("pp") { + // Ex) `pp39` + let (major, minor) = parse_python_version(pp, "PyPy", s)?; + Ok(Self::PyPy { + python_version: (major, minor), + }) + } else if let Some(graalpy) = s.strip_prefix("graalpy") { + // Ex) `graalpy310` + let (major, minor) = parse_python_version(graalpy, "GraalPy", s)?; + Ok(Self::GraalPy { + python_version: (major, minor), + }) + } else if let Some(pt) = s.strip_prefix("pt") { + // Ex) `pt38` + let (major, minor) = parse_python_version(pt, "Pyston", s)?; + Ok(Self::Pyston { + python_version: (major, minor), + }) + } else { + Err(ParseLanguageTagError::UnknownFormat(s.to_string())) + } + } +} + +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum ParseLanguageTagError { + #[error("Unknown language tag format: {0}")] + UnknownFormat(String), + #[error("Missing major version in {implementation} language tag: {tag}")] + MissingMajorVersion { + implementation: &'static str, + tag: String, + }, + #[error("Invalid major version in {implementation} language tag: {tag}")] + InvalidMajorVersion { + implementation: &'static str, + tag: String, + }, + #[error("Missing minor version in {implementation} language tag: {tag}")] + MissingMinorVersion { + implementation: &'static str, + tag: String, + }, + #[error("Invalid minor version in {implementation} language tag: {tag}")] + InvalidMinorVersion { + implementation: &'static str, + tag: String, + }, +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use crate::language_tag::ParseLanguageTagError; + use crate::LanguageTag; + + #[test] + fn none() { + assert_eq!(LanguageTag::from_str("none"), Ok(LanguageTag::None)); + assert_eq!(LanguageTag::None.to_string(), "none"); + } + + #[test] + fn python_language() { + let tag = LanguageTag::Python { + major: 3, + minor: None, + }; + assert_eq!(LanguageTag::from_str("py3"), Ok(tag)); + assert_eq!(tag.to_string(), "py3"); + + let tag = LanguageTag::Python { + major: 3, + minor: Some(9), + }; + assert_eq!(LanguageTag::from_str("py39"), Ok(tag)); + assert_eq!(tag.to_string(), "py39"); + + assert_eq!( + LanguageTag::from_str("py"), + Err(ParseLanguageTagError::MissingMajorVersion { + implementation: "Python", + tag: "py".to_string() + }) + ); + assert_eq!( + LanguageTag::from_str("pyX"), + Err(ParseLanguageTagError::InvalidMajorVersion { + implementation: "Python", + tag: "pyX".to_string() + }) + ); + assert_eq!( + LanguageTag::from_str("py3X"), + Err(ParseLanguageTagError::InvalidMinorVersion { + implementation: "Python", + tag: "py3X".to_string() + }) + ); + } + + #[test] + fn cpython_language() { + let tag = LanguageTag::CPython { + python_version: (3, 9), + }; + assert_eq!(LanguageTag::from_str("cp39"), Ok(tag)); + assert_eq!(tag.to_string(), "cp39"); + + assert_eq!( + LanguageTag::from_str("cp"), + Err(ParseLanguageTagError::MissingMajorVersion { + implementation: "CPython", + tag: "cp".to_string() + }) + ); + assert_eq!( + LanguageTag::from_str("cpX"), + Err(ParseLanguageTagError::InvalidMajorVersion { + implementation: "CPython", + tag: "cpX".to_string() + }) + ); + assert_eq!( + LanguageTag::from_str("cp3X"), + Err(ParseLanguageTagError::InvalidMinorVersion { + implementation: "CPython", + tag: "cp3X".to_string() + }) + ); + } + + #[test] + fn pypy_language() { + let tag = LanguageTag::PyPy { + python_version: (3, 9), + }; + assert_eq!(LanguageTag::from_str("pp39"), Ok(tag)); + assert_eq!(tag.to_string(), "pp39"); + + assert_eq!( + LanguageTag::from_str("pp"), + Err(ParseLanguageTagError::MissingMajorVersion { + implementation: "PyPy", + tag: "pp".to_string() + }) + ); + assert_eq!( + LanguageTag::from_str("ppX"), + Err(ParseLanguageTagError::InvalidMajorVersion { + implementation: "PyPy", + tag: "ppX".to_string() + }) + ); + assert_eq!( + LanguageTag::from_str("pp3X"), + Err(ParseLanguageTagError::InvalidMinorVersion { + implementation: "PyPy", + tag: "pp3X".to_string() + }) + ); + } + + #[test] + fn graalpy_language() { + let tag = LanguageTag::GraalPy { + python_version: (3, 10), + }; + assert_eq!(LanguageTag::from_str("graalpy310"), Ok(tag)); + assert_eq!(tag.to_string(), "graalpy310"); + + assert_eq!( + LanguageTag::from_str("graalpy"), + Err(ParseLanguageTagError::MissingMajorVersion { + implementation: "GraalPy", + tag: "graalpy".to_string() + }) + ); + assert_eq!( + LanguageTag::from_str("graalpyX"), + Err(ParseLanguageTagError::InvalidMajorVersion { + implementation: "GraalPy", + tag: "graalpyX".to_string() + }) + ); + assert_eq!( + LanguageTag::from_str("graalpy3X"), + Err(ParseLanguageTagError::InvalidMinorVersion { + implementation: "GraalPy", + tag: "graalpy3X".to_string() + }) + ); + } + + #[test] + fn pyston_language() { + let tag = LanguageTag::Pyston { + python_version: (3, 8), + }; + assert_eq!(LanguageTag::from_str("pt38"), Ok(tag)); + assert_eq!(tag.to_string(), "pt38"); + + assert_eq!( + LanguageTag::from_str("pt"), + Err(ParseLanguageTagError::MissingMajorVersion { + implementation: "Pyston", + tag: "pt".to_string() + }) + ); + assert_eq!( + LanguageTag::from_str("ptX"), + Err(ParseLanguageTagError::InvalidMajorVersion { + implementation: "Pyston", + tag: "ptX".to_string() + }) + ); + assert_eq!( + LanguageTag::from_str("pt3X"), + Err(ParseLanguageTagError::InvalidMinorVersion { + implementation: "Pyston", + tag: "pt3X".to_string() + }) + ); + } + + #[test] + fn unknown_language() { + assert_eq!( + LanguageTag::from_str("unknown"), + Err(ParseLanguageTagError::UnknownFormat("unknown".to_string())) + ); + assert_eq!( + LanguageTag::from_str(""), + Err(ParseLanguageTagError::UnknownFormat(String::new())) + ); + } +} diff --git a/crates/uv-platform-tags/src/lib.rs b/crates/uv-platform-tags/src/lib.rs index e0c6980377f4..0c73985adfde 100644 --- a/crates/uv-platform-tags/src/lib.rs +++ b/crates/uv-platform-tags/src/lib.rs @@ -1,5 +1,9 @@ +pub use abi_tag::AbiTag; +pub use language_tag::LanguageTag; pub use platform::{Arch, Os, Platform, PlatformError}; pub use tags::{IncompatibleTag, TagCompatibility, TagPriority, Tags, TagsError}; +mod abi_tag; +mod language_tag; mod platform; mod tags; diff --git a/crates/uv-platform-tags/src/tags.rs b/crates/uv-platform-tags/src/tags.rs index 8a15f6f13f09..1fc887b68332 100644 --- a/crates/uv-platform-tags/src/tags.rs +++ b/crates/uv-platform-tags/src/tags.rs @@ -5,7 +5,8 @@ use std::{cmp, num::NonZeroU32}; use rustc_hash::FxHashMap; -use crate::{Arch, Os, Platform, PlatformError}; +use crate::abi_tag::AbiTag; +use crate::{Arch, LanguageTag, Os, Platform, PlatformError}; #[derive(Debug, thiserror::Error)] pub enum TagsError { @@ -82,6 +83,7 @@ impl Tags { /// Tags are prioritized based on their position in the given vector. Specifically, tags that /// appear earlier in the vector are given higher priority than tags that appear later. pub fn new(tags: Vec<(String, String, String)>) -> Self { + // Index the tags by Python version, ABI, and platform. let mut map = FxHashMap::default(); for (index, (py, abi, platform)) in tags.into_iter().rev().enumerate() { map.entry(py) @@ -120,8 +122,10 @@ impl Tags { // 1. This exact c api version for platform_tag in &platform_tags { tags.push(( - implementation.language_tag(python_version), - implementation.abi_tag(python_version, implementation_version), + implementation.language_tag(python_version).to_string(), + implementation + .abi_tag(python_version, implementation_version) + .to_string(), platform_tag.clone(), )); } @@ -133,8 +137,10 @@ impl Tags { if !gil_disabled { for platform_tag in &platform_tags { tags.push(( - implementation.language_tag((python_version.0, minor)), - "abi3".to_string(), + implementation + .language_tag((python_version.0, minor)) + .to_string(), + AbiTag::Abi3.to_string(), platform_tag.clone(), )); } @@ -143,8 +149,10 @@ impl Tags { if minor == python_version.1 { for platform_tag in &platform_tags { tags.push(( - implementation.language_tag((python_version.0, minor)), - "none".to_string(), + implementation + .language_tag((python_version.0, minor)) + .to_string(), + AbiTag::None.to_string(), platform_tag.clone(), )); } @@ -155,8 +163,12 @@ impl Tags { for minor in (0..=python_version.1).rev() { for platform_tag in &platform_tags { tags.push(( - format!("py{}{}", python_version.0, minor), - "none".to_string(), + LanguageTag::Python { + major: python_version.0, + minor: Some(minor), + } + .to_string(), + AbiTag::None.to_string(), platform_tag.clone(), )); } @@ -165,7 +177,7 @@ impl Tags { for platform_tag in &platform_tags { tags.push(( format!("py{}", python_version.0), - "none".to_string(), + AbiTag::None.to_string(), platform_tag.clone(), )); } @@ -174,22 +186,30 @@ impl Tags { // 4. no binary if matches!(implementation, Implementation::CPython { .. }) { tags.push(( - implementation.language_tag(python_version), - "none".to_string(), + implementation.language_tag(python_version).to_string(), + AbiTag::None.to_string(), "any".to_string(), )); } for minor in (0..=python_version.1).rev() { tags.push(( - format!("py{}{}", python_version.0, minor), - "none".to_string(), + LanguageTag::Python { + major: python_version.0, + minor: Some(minor), + } + .to_string(), + AbiTag::None.to_string(), "any".to_string(), )); // After the matching version emit `none` tags for the major version i.e. `py3` if minor == python_version.1 { tags.push(( - format!("py{}", python_version.0), - "none".to_string(), + LanguageTag::Python { + major: python_version.0, + minor: None, + } + .to_string(), + AbiTag::None.to_string(), "any".to_string(), )); } @@ -321,64 +341,41 @@ enum Implementation { impl Implementation { /// Returns the "language implementation and version tag" for the current implementation and /// Python version (e.g., `cp39` or `pp37`). - fn language_tag(self, python_version: (u8, u8)) -> String { + fn language_tag(self, python_version: (u8, u8)) -> LanguageTag { match self { // Ex) `cp39` - Self::CPython { .. } => format!("cp{}{}", python_version.0, python_version.1), + Self::CPython { .. } => LanguageTag::CPython { python_version }, // Ex) `pp39` - Self::PyPy => format!("pp{}{}", python_version.0, python_version.1), + Self::PyPy => LanguageTag::PyPy { python_version }, // Ex) `graalpy310` - Self::GraalPy => format!("graalpy{}{}", python_version.0, python_version.1), + Self::GraalPy => LanguageTag::GraalPy { python_version }, // Ex) `pt38`` - Self::Pyston => format!("pt{}{}", python_version.0, python_version.1), + Self::Pyston => LanguageTag::Pyston { python_version }, } } - fn abi_tag(self, python_version: (u8, u8), implementation_version: (u8, u8)) -> String { + fn abi_tag(self, python_version: (u8, u8), implementation_version: (u8, u8)) -> AbiTag { match self { // Ex) `cp39` - Self::CPython { gil_disabled } => { - if python_version.1 <= 7 { - format!("cp{}{}m", python_version.0, python_version.1) - } else if gil_disabled { - // https://peps.python.org/pep-0703/#build-configuration-changes - // Python 3.13+ only, but it makes more sense to just rely on the sysconfig var. - format!("cp{}{}t", python_version.0, python_version.1) - } else { - format!( - "cp{}{}{}", - python_version.0, - python_version.1, - if gil_disabled { "t" } else { "" } - ) - } - } + Self::CPython { gil_disabled } => AbiTag::CPython { + gil_disabled, + python_version, + }, // Ex) `pypy39_pp73` - Self::PyPy => format!( - "pypy{}{}_pp{}{}", - python_version.0, - python_version.1, - implementation_version.0, - implementation_version.1 - ), + Self::PyPy => AbiTag::PyPy { + python_version, + implementation_version, + }, // Ex) `graalpy310_graalpy240_310_native - Self::GraalPy => format!( - "graalpy{}{}_graalpy{}{}_{}{}_native", - python_version.0, - python_version.1, - implementation_version.0, - implementation_version.1, - python_version.0, - python_version.1 - ), + Self::GraalPy => AbiTag::GraalPy { + python_version, + implementation_version, + }, // Ex) `pyston38-pyston_23` - Self::Pyston => format!( - "pyston{}{}-pyston_{}{}", - python_version.0, - python_version.1, - implementation_version.0, - implementation_version.1 - ), + Self::Pyston => AbiTag::Pyston { + python_version, + implementation_version, + }, } }