From f2f01e2a82bcd089416929779dc4b1ea19266574 Mon Sep 17 00:00:00 2001 From: John Sirois Date: Fri, 29 May 2026 13:00:39 -0700 Subject: [PATCH 1/2] Fix PlatformDetails. This fixes PlatformDetails tag calculations for local interpreters and restores warm cache performance, which had regressed with the introduction of PlatformDetails. This work enables the 0.14.0 release. --- Cargo.lock | 185 +++++++- Cargo.toml | 1 + crates/interpreter/src/constraints.rs | 16 +- crates/interpreter/src/interpreter.rs | 382 +++++++--------- crates/interpreter/src/lib.rs | 2 +- crates/interpreter/src/platform.rs | 9 +- crates/pex/src/dependency_configuration.rs | 4 +- crates/pex/src/pex.rs | 8 +- crates/pexrs/src/lib.rs | 13 +- crates/python-platform/Cargo.toml | 1 + crates/python-platform/src/arch.rs | 29 +- crates/python-platform/src/implementation.rs | 14 +- crates/python-platform/src/lib.rs | 122 +++-- crates/python-platform/src/markers.rs | 49 +- crates/python-platform/src/os.rs | 77 ++-- crates/python-platform/src/platform.rs | 6 + crates/python-platform/src/tags.rs | 98 ++-- crates/python-platform/src/version.rs | 424 ++++++++++++++---- crates/python-platform/src/windows.rs | 11 +- crates/python-platform/tests/tags_test.rs | 27 +- crates/tools/src/commands/interpreter.rs | 13 +- .../tools/src/commands/repository/extract.rs | 4 +- crates/tools/src/commands/venv.rs | 6 +- crates/venv/src/venv_pex.rs | 10 +- crates/venv/src/virtualenv.rs | 104 ++--- python/testing/bin/run-tests.py | 13 +- python/testing/compare.py | 5 +- src/commands/platform/python.rs | 9 +- 28 files changed, 992 insertions(+), 650 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index beb463a..e6a7368 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,6 +189,15 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e" +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bstr" version = "1.12.1" @@ -302,6 +311,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] @@ -601,8 +611,18 @@ version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -618,13 +638,37 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + [[package]] name = "darling_macro" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core", + "darling_core 0.21.3", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn 2.0.117", ] @@ -656,6 +700,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", + "serde_core", ] [[package]] @@ -756,6 +801,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "either" version = "1.16.0" @@ -792,7 +843,7 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a" dependencies = [ - "darling", + "darling 0.21.3", "proc-macro2", "quote", "syn 2.0.117", @@ -1057,13 +1108,19 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -1372,6 +1429,17 @@ dependencies = [ "quote", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -1392,7 +1460,7 @@ dependencies = [ "cache", "elf", "fs-err", - "indexmap", + "indexmap 2.14.0", "log", "logging_timer", "ouroboros", @@ -1922,7 +1990,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faee7227064121fcadcd2ff788ea26f0d8f2bd23a0574da11eca23bc935bcc05" dependencies = [ "boxcar", - "indexmap", + "indexmap 2.14.0", "itertools 0.13.0", "once_cell", "pep440_rs", @@ -1996,7 +2064,7 @@ dependencies = [ "dashmap", "fs-err", "glob", - "indexmap", + "indexmap 2.14.0", "interpreter", "itertools 0.14.0", "log", @@ -2046,7 +2114,7 @@ dependencies = [ "env_logger", "fs-err", "include_dir", - "indexmap", + "indexmap 2.14.0", "interpreter", "itertools 0.14.0", "log", @@ -2118,7 +2186,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" dependencies = [ "base64", - "indexmap", + "indexmap 2.14.0", "quick-xml", "serde", "time", @@ -2260,6 +2328,7 @@ dependencies = [ "scripts", "serde", "serde_json", + "serde_with", "strum", "strum_macros", "target-lexicon", @@ -2441,6 +2510,26 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "regex" version = "1.12.3" @@ -2728,6 +2817,30 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2830,6 +2943,38 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_with" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3251,7 +3396,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde_core", "serde_spanned", "toml_datetime", @@ -3275,7 +3420,7 @@ version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ - "indexmap", + "indexmap 2.14.0", "toml_datetime", "toml_parser", "winnow", @@ -3312,7 +3457,7 @@ dependencies = [ "flate2", "fs-err", "graphviz-rust", - "indexmap", + "indexmap 2.14.0", "interpreter", "log", "logging", @@ -3509,7 +3654,7 @@ dependencies = [ "cache", "dashmap", "fs-err", - "indexmap", + "indexmap 2.14.0", "interpreter", "itertools 0.14.0", "log", @@ -3660,7 +3805,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -3673,7 +3818,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.14.0", "semver", ] @@ -4053,7 +4198,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap", + "indexmap 2.14.0", "prettyplease", "syn 2.0.117", "wasm-metadata", @@ -4084,7 +4229,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", - "indexmap", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -4103,7 +4248,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.14.0", "log", "semver", "serde", @@ -4256,7 +4401,7 @@ dependencies = [ "chrono", "crc32fast", "flate2", - "indexmap", + "indexmap 2.14.0", "memchr", "typed-path", "zopfli", diff --git a/Cargo.toml b/Cargo.toml index 03d78bb..0b5856b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -193,6 +193,7 @@ rustix = { version = "1.1", features = ["fs", "system"]} same-file = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +serde_with = { version = "3.20", features = ["macros"]} sha2 = "0.11" shell-quote = "0.7" strum = { version = "0.28", features = ["strum_macros"] } diff --git a/crates/interpreter/src/constraints.rs b/crates/interpreter/src/constraints.rs index 68d9e72..cde1c23 100644 --- a/crates/interpreter/src/constraints.rs +++ b/crates/interpreter/src/constraints.rs @@ -30,7 +30,7 @@ enum InterpreterImplementation { impl InterpreterImplementation { fn of(interpreter: &Interpreter) -> Option { - if let Some(abi_info) = interpreter.raw().cpython_abi_info { + if let Some(abi_info) = interpreter.details.cpython_abi_info { if let Some(free_threaded) = abi_info.free_threaded { if free_threaded { Some(Self::CPythonFreeThreaded) @@ -99,9 +99,9 @@ impl InterpreterConstraint { pub fn exact_version(interpreter: &Interpreter) -> Self { let python_version = Version::new( [ - u64::from(interpreter.raw().version.major), - u64::from(interpreter.raw().version.minor), - u64::from(interpreter.raw().version.micro), + u64::from(interpreter.details.version.major), + u64::from(interpreter.details.version.minor), + u64::from(interpreter.details.version.micro), ] .iter(), ); @@ -179,8 +179,8 @@ impl InterpreterConstraint { } } self.contains_version( - interpreter.raw().version.major, - interpreter.raw().version.minor, + interpreter.details.version.major, + interpreter.details.version.minor, ) } @@ -288,8 +288,8 @@ impl InterpreterConstraints { insert_specs( &mut binary_specs, implementation.as_ref(), - interpreter.raw().version.major, - interpreter.raw().version.minor, + interpreter.details.version.major, + interpreter.details.version.minor, ); } if !self.0.is_empty() { diff --git a/crates/interpreter/src/interpreter.rs b/crates/interpreter/src/interpreter.rs index 847a530..39c8b4e 100644 --- a/crates/interpreter/src/interpreter.rs +++ b/crates/interpreter/src/interpreter.rs @@ -3,138 +3,114 @@ use std::borrow::Cow; use std::collections::BTreeMap; -use std::fmt::{Display, Formatter}; +use std::fmt::Display; use std::hash::{Hash, Hasher}; -use std::io::{BufReader, BufWriter, Read, Write}; +use std::io::{BufReader, BufWriter, Write}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use anyhow::{anyhow, bail}; -use cache::{CacheDir, HashOptions, atomic_file, hash_file}; +use cache::{CacheDir, HashOptions, atomic_dir, hash_file}; use fs_err as fs; +use fs_err::File; use logging_timer::time; use ouroboros::self_referencing; use pep440_rs::Version; use pep508_rs::MarkerEnvironment; -use python_platform::{NonEmptyVec, PlatformDetails, PythonPlatform}; +use python_platform::{ + CPythonAbiInfo, + CPythonImplementation, + NonEmptyVec, + PlatformDetails, + PyPyImplementation, + PyPyVersion, + PythonImplementation, + PythonPlatform, + PythonVersion, +}; use scripts::{IdentifyInterpreter, Scripts}; use serde::{Deserialize, Serialize}; -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Deserialize, Serialize)] -pub struct PythonVersion<'a> { - pub major: u8, - pub minor: u8, - pub micro: u8, - pub releaselevel: &'a str, - pub serial: u8, -} - -impl<'a> Display for PythonVersion<'a> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!( - "{major}.{minor}.{micro}", - major = self.major, - minor = self.minor, - micro = self.micro - ))?; - - // N.B.: Using this for possible strings reference: - // https://peps.python.org/pep-0739/#implementation-version-releaselevel - - if let Some(level_abbrev) = match self.releaselevel { - "alpha" => Some("a"), - "beta" => Some("b"), - "candidate" => Some("rc"), - _ => None, - } { - f.write_fmt(format_args!("{level_abbrev}{serial}", serial = self.serial))?; - } - Ok(()) - } -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Deserialize, Serialize)] -pub struct PyPyVersion(u8, u8, u8); - -impl Display for PyPyVersion { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!( - "{major}.{minor}.{patch}", - major = self.0, - minor = self.1, - patch = self.2 - )) - } -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Deserialize, Serialize)] -pub struct CPythonAbiInfo { - pub free_threaded: Option, - pub debug: bool, - pub pymalloc: Option, - pub ucs4: Option, -} - #[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize, Serialize)] -pub struct RawInterpreter<'a> { - #[serde(borrow)] - pub path: Cow<'a, Path>, - #[serde(borrow)] - pub prefix: Cow<'a, Path>, - #[serde(borrow)] - pub base_prefix: Option>, - #[serde(borrow)] - pub version: PythonVersion<'a>, +pub struct InterpreterDetails { + pub path: PathBuf, + pub prefix: PathBuf, + pub base_prefix: Option, + pub version: PythonVersion, pub pypy_version: Option, pub cpython_abi_info: Option, pub paths: BTreeMap, pub has_ensurepip: bool, } -#[cfg(target_os = "linux")] -static LINUX_INFO: std::sync::Mutex> = std::sync::Mutex::new(None); +impl InterpreterDetails { + pub fn python_implementation(&self) -> PythonImplementation { + if let Some(cpython_abi_info) = self.cpython_abi_info { + PythonImplementation::CPython(CPythonImplementation { + version: self.version, + abi_info: cpython_abi_info, + }) + } else { + PythonImplementation::PyPy(PyPyImplementation { + version: self.version, + pypy_version: self.pypy_version, + }) + } + } +} +// N.B. The extra complexity of the JsonPlatformDetails container for parsing PlatformDetails nets +// a ~2.5x perf increase on warm cache loads as compared to storing a PlatformDetails using String +// tags directly. #[self_referencing] -pub struct Interpreter { - data: Vec, - realpath: PathBuf, - platform_details: PlatformDetails, - #[borrows(data)] +struct JsonPlatformDetails { + contents: String, + #[borrows(contents)] #[covariant] - interpreter: RawInterpreter<'this>, + platform_details: PlatformDetails<'this>, } -impl Eq for Interpreter {} +impl JsonPlatformDetails { + fn load(path: impl AsRef) -> anyhow::Result { + let contents = fs::read_to_string(path)?; + Self::try_new(contents, |contents| Ok(serde_json::from_str(contents)?)) + } +} -impl PartialEq for Interpreter { - fn eq(&self, other: &Self) -> bool { - self.raw().eq(other.raw()) +impl Clone for JsonPlatformDetails { + fn clone(&self) -> Self { + Self::new(self.borrow_contents().clone(), |contents| { + serde_json::from_str(contents) + .expect("We've already successfully parsed our JSON contents") + }) } } -impl Hash for Interpreter { - fn hash(&self, state: &mut H) - where - H: Hasher, - { - self.raw().hash(state) +impl Eq for JsonPlatformDetails {} + +impl PartialEq for JsonPlatformDetails { + fn eq(&self, other: &Self) -> bool { + self.borrow_platform_details() + .eq(other.borrow_platform_details()) } } -impl Clone for Interpreter { - fn clone(&self) -> Self { - Self::new( - self.borrow_data().clone(), - self.borrow_realpath().clone(), - self.borrow_platform_details().clone(), - |data| { - serde_json::from_slice(data) - .expect("We've already parsed out data successfully once.") - }, - ) +impl Hash for JsonPlatformDetails { + fn hash(&self, state: &mut H) { + self.borrow_platform_details().hash(state) } } +#[cfg(target_os = "linux")] +static LINUX_INFO: std::sync::Mutex> = std::sync::Mutex::new(None); + +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct Interpreter { + pub realpath: PathBuf, + pub details: InterpreterDetails, + platform_details: JsonPlatformDetails, +} + impl Interpreter { fn identify( python_exe: impl AsRef, @@ -188,42 +164,40 @@ impl Interpreter { pub fn load_uncached( python_exe: impl AsRef, identification_script: &IdentifyInterpreter, - platform_details: PlatformDetails, ) -> anyhow::Result { - let json_bytes = Self::identify(python_exe.as_ref(), identification_script)?; - Self::try_new( - json_bytes, - python_exe.as_ref().canonicalize()?, - platform_details, - |data| { - serde_json::from_slice(data).map_err(|err| { - anyhow!( - "Failed to identify Python interpreter {exe}: {err}", - exe = python_exe.as_ref().display() - ) - }) - }, - ) + let data = Self::identify(python_exe.as_ref(), identification_script)?; + let details: InterpreterDetails = + serde_json::from_slice(data.as_slice()).map_err(|err| { + anyhow!( + "Failed to identify Python interpreter {exe}: {err}", + exe = python_exe.as_ref().display() + ) + })?; + let platform_details = + PlatformDetails::python(python_exe.as_ref(), details.python_implementation())?; + Ok(Self { + realpath: python_exe.as_ref().canonicalize()?, + details, + platform_details: JsonPlatformDetails::new(String::new(), |_| platform_details), + }) } #[cfg(unix)] pub fn most_specific_exe_name(&self) -> String { - let interpreter = self.raw(); - let name = if interpreter.pypy_version.is_some() { + let name = if self.details.pypy_version.is_some() { "pypy" } else { "python" }; format!( "{name}{major}.{minor}", - major = interpreter.version.major, - minor = interpreter.version.minor + major = self.details.version.major, + minor = self.details.version.minor ) } pub fn prefix_rel_paths(&self) -> Vec> { - let interpreter = self.raw(); - Self::candidate_rel_paths(&interpreter.version, interpreter.pypy_version.is_some()) + Self::candidate_rel_paths(&self.details.version, self.details.pypy_version.is_some()) } #[cfg(unix)] @@ -298,7 +272,7 @@ impl Interpreter { ) -> anyhow::Result { let check_pypy_version = |interpreter: &Interpreter| match ( pypy_version.as_ref(), - interpreter.raw().pypy_version.as_ref(), + interpreter.details.pypy_version.as_ref(), ) { (Some(expected_pypy_version), Some(actual_pypy_version)) if expected_pypy_version == actual_pypy_version => @@ -314,11 +288,11 @@ impl Interpreter { for rel_path in candidate_rel_paths { let candidate_path = prefix.as_ref().join(rel_path); if let Ok(interpreter) = Self::load(&candidate_path, &identification_script) { - if interpreter.raw().version != version { + if interpreter.details.version != version { if re_cache_version_mismatch && ( - interpreter.raw().version.major, - interpreter.raw().version.minor, + interpreter.details.version.major, + interpreter.details.version.minor, ) == (version.major, version.minor) { re_cache_candidates.push(interpreter) @@ -332,7 +306,7 @@ impl Interpreter { } for interpreter in re_cache_candidates { let interpreter = interpreter.reload(&identification_script)?; - if interpreter.raw().version == version && check_pypy_version(&interpreter) { + if interpreter.details.version == version && check_pypy_version(&interpreter) { return Ok(interpreter); } } @@ -360,70 +334,82 @@ impl Interpreter { identification_script: &IdentifyInterpreter, ) -> anyhow::Result { let interpreter_info = Self::interpreter_info(python_exe)?; - let platform_details = PlatformDetails::spawn(python_exe)?; - Self::load_internal( - &interpreter_info, - python_exe, - identification_script, - platform_details, - ) + Self::load_internal(&interpreter_info, python_exe, identification_script) } fn load_internal( interpreter_info: &Path, python_exe: &Path, identification_script: &IdentifyInterpreter, - platform_details: impl FnOnce() -> anyhow::Result, ) -> anyhow::Result { - let file = atomic_file(interpreter_info, |file| { + if let Some((details, platform_details)) = atomic_dir(interpreter_info, |path| { let json_bytes = Self::identify(python_exe, identification_script)?; - BufWriter::new(file).write_all(&json_bytes)?; - Ok(()) - })?; - let size = file.metadata()?.len(); - let mut data = Vec::with_capacity(usize::try_from(size)?); - BufReader::new(file).read_to_end(&mut data)?; - Self::try_new( - data, - python_exe.canonicalize()?, - platform_details()?, - |data| { - serde_json::from_slice(data).map_err(|err| { + let details: InterpreterDetails = serde_json::from_slice(json_bytes.as_slice()) + .map_err(|err| { anyhow!( "Failed to identify Python interpreter {exe}: {err}", exe = python_exe.display() ) - }) - }, - ) + })?; + + let implementation_details = File::create_new(path.join("interpreter-details.json"))?; + BufWriter::new(implementation_details).write_all(&json_bytes)?; + + let platform_details = + PlatformDetails::python(python_exe, details.python_implementation())?; + let platform_details_json = path.join("platform-details.json"); + serde_json::to_writer( + BufWriter::new(File::create_new(&platform_details_json)?), + &platform_details, + )?; + Ok((details, JsonPlatformDetails::load(platform_details_json)?)) + })? { + Ok(Self { + realpath: python_exe.canonicalize()?, + details, + platform_details, + }) + } else { + let details: InterpreterDetails = serde_json::from_reader(BufReader::new(File::open( + interpreter_info.join("interpreter-details.json"), + )?))?; + let platform_details = + JsonPlatformDetails::load(interpreter_info.join("platform-details.json"))?; + Ok(Self { + realpath: python_exe.canonicalize()?, + details, + platform_details, + }) + } } fn reload(self, identification_script: &IdentifyInterpreter) -> anyhow::Result { - let python_exe = self.raw().path.as_ref(); + let python_exe = self.details.path.as_ref(); let interpreter_info = Self::interpreter_info(python_exe)?; - fs::remove_file(&interpreter_info)?; - let platform_details = PlatformDetails::spawn(python_exe)?; - Self::load_internal( - &interpreter_info, - python_exe, - identification_script, - platform_details, - ) + fs::remove_dir_all(&interpreter_info)?; + Self::load_internal(&interpreter_info, python_exe, identification_script) } #[time("debug", "Interpreter.{}")] pub fn store(&self) -> anyhow::Result<()> { - let hash = hash_file(self.raw().path.as_ref(), &Self::INTERPRETER_HASH_CONFIG)?; + let hash = hash_file(self.details.path.as_ref(), &Self::INTERPRETER_HASH_CONFIG)?; let interpreter_info = CacheDir::Interpreter.path()?.join(hash.base64_digest()); - atomic_file(&interpreter_info, |file| { - serde_json::to_writer(BufWriter::new(file), self.raw())?; + atomic_dir(&interpreter_info, |path| { + serde_json::to_writer( + BufWriter::new(File::create_new(path.join("interpreter-details.json"))?), + &self.details, + )?; + serde_json::to_writer( + BufWriter::new(File::create_new(path.join("platform-details.json"))?), + &self.platform_details(), + )?; Ok(()) })?; Ok(()) } pub fn hermetic_args(&self) -> &'static str { - if self.raw().version.major == 3 && self.raw().version.minor >= 4 { + if self.details.version.major == 3 && self.details.version.minor >= 4 { "-I" } else { "-sE" @@ -432,13 +418,13 @@ impl Interpreter { #[time("debug", "Interpreter.{}")] pub fn resolve_base_interpreter(self, scripts: &mut Scripts) -> anyhow::Result { - if let Some(base_prefix) = self.raw().base_prefix.as_ref() - && base_prefix != &self.raw().prefix + if let Some(base_prefix) = self.details.base_prefix.as_ref() + && base_prefix != &self.details.prefix { let resolved = Self::at_prefix( base_prefix, - self.raw().version, - self.raw().pypy_version, + self.details.version, + self.details.pypy_version, scripts, true, )?; @@ -448,8 +434,8 @@ impl Interpreter { } pub fn is_venv(&self) -> bool { - if let Some(base_prefix) = self.raw().base_prefix.as_deref() - && base_prefix != self.raw().prefix + if let Some(base_prefix) = self.details.base_prefix.as_deref() + && base_prefix != self.details.prefix { true } else { @@ -457,50 +443,29 @@ impl Interpreter { } } - #[inline] - pub fn raw(&self) -> &RawInterpreter<'_> { - self.borrow_interpreter() - } - - #[inline] - pub fn with_raw_mut(&mut self, func: impl FnOnce(&mut RawInterpreter) -> R) -> R { - self.with_interpreter_mut(func) - } - - pub fn realpath(&self) -> &Path { - self.borrow_realpath() - } - - pub fn set_realpath(&mut self, value: &Path) { - self.with_realpath_mut(|realpath| { - realpath.clear(); - realpath.push(value) - }) - } - - pub fn platform_details(&self) -> &PlatformDetails { - self.borrow_platform_details() + pub fn platform_details(&self) -> &PlatformDetails<'_> { + self.platform_details.borrow_platform_details() } } -impl PythonPlatform for Interpreter { +impl<'a> PythonPlatform<'a> for Interpreter { fn description(&self) -> impl Display { format!( "interpreter at {python_exe}", - python_exe = self.raw().path.as_ref().display() + python_exe = self.details.path.display() ) } fn marker_env(&self) -> &MarkerEnvironment { - self.borrow_platform_details().marker_env() + self.platform_details().marker_env() } - fn supported_tags(&self) -> &NonEmptyVec { - self.borrow_platform_details().supported_tags() + fn supported_tags(&self) -> NonEmptyVec<&'_ str> { + self.platform_details().supported_tags() } fn version(&self) -> Cow<'_, Version> { - let version = self.raw().version; + let version = self.details.version; Cow::Owned(Version::new([ u64::from(version.major), u64::from(version.minor), @@ -516,7 +481,7 @@ mod tests { use anyhow::Context; use pretty_assertions::assert_eq; - use python_platform::{PlatformDetails, PythonPlatform}; + use python_platform::PythonPlatform; use rstest::rstest; use scripts::{IdentifyInterpreter, Scripts}; use testing::{ @@ -573,19 +538,15 @@ mod tests { let expected_tags: Vec = serde_json::from_str(String::from_utf8(output.stdout).unwrap().as_str()).unwrap(); - let platform_details = PlatformDetails::python(&venv_python_exe).unwrap(); - let interpreter = Interpreter::load_uncached( - &venv_python_exe, - &interpreter_identification_script, - platform_details, - ) - .with_context(|| { - format!( - "Failed to load interpreter info for {python}", - python = venv_python_exe.display() - ) - }) - .unwrap(); + let interpreter = + Interpreter::load_uncached(&venv_python_exe, &interpreter_identification_script) + .with_context(|| { + format!( + "Failed to load interpreter info for {python}", + python = venv_python_exe.display() + ) + }) + .unwrap(); assert_eq!( expected_tags, interpreter @@ -616,9 +577,8 @@ mod tests { venv_interpreter .resolve_base_interpreter(&mut embedded_scripts) .unwrap() - .raw() + .details .path - .as_ref() ) } } diff --git a/crates/interpreter/src/lib.rs b/crates/interpreter/src/lib.rs index 7e89a8e..8e8da5a 100644 --- a/crates/interpreter/src/lib.rs +++ b/crates/interpreter/src/lib.rs @@ -17,7 +17,7 @@ pub use constraints::{ SelectionStrategy, VersionSpec, }; -pub use interpreter::{Interpreter, RawInterpreter}; +pub use interpreter::{Interpreter, InterpreterDetails}; pub use platform::Platform; pub use search_path::SearchPath; pub use tag::Tag; diff --git a/crates/interpreter/src/platform.rs b/crates/interpreter/src/platform.rs index b54a111..65663f4 100644 --- a/crates/interpreter/src/platform.rs +++ b/crates/interpreter/src/platform.rs @@ -3,24 +3,23 @@ use std::fmt::{Display, Formatter}; -use python_platform::PythonPlatform; +use python_platform::{PythonPlatform, PythonVersion}; -use crate::interpreter::PythonVersion; use crate::{Interpreter, Tag}; pub struct Platform<'a> { implementation: &'a str, - version: &'a PythonVersion<'a>, + version: PythonVersion, abi: &'a str, platform: &'a str, } impl<'a> Platform<'a> { pub fn of(interpreter: &'a Interpreter) -> anyhow::Result { - let tag = Tag::parse(interpreter.primary_tag())?; + let tag = Tag::parse(interpreter.supported_tags().first())?; Ok(Self { implementation: &tag.python[0..2], - version: &interpreter.raw().version, + version: interpreter.details.version, abi: tag.abi, platform: tag.platform, }) diff --git a/crates/pex/src/dependency_configuration.rs b/crates/pex/src/dependency_configuration.rs index b96a3cb..4cb31b8 100644 --- a/crates/pex/src/dependency_configuration.rs +++ b/crates/pex/src/dependency_configuration.rs @@ -101,10 +101,10 @@ impl DependencyConfiguration { } } - pub(crate) fn overridden( + pub(crate) fn overridden<'a>( &self, requirement: &Requirement, - target: &impl PythonPlatform, + target: &impl PythonPlatform<'a>, extras: &[ExtraName], ) -> anyhow::Result>> { if let Some(overrides) = self.overridden.get(&requirement.name) { diff --git a/crates/pex/src/pex.rs b/crates/pex/src/pex.rs index 3215c53..f15b0fd 100644 --- a/crates/pex/src/pex.rs +++ b/crates/pex/src/pex.rs @@ -253,7 +253,7 @@ impl<'a> Pex<'a> { #[time("debug", "Pex.{}")] fn resolve_wheels( &'a self, - target: &impl PythonPlatform, + target: &impl PythonPlatform<'a>, dependency_configuration: &DependencyConfiguration, collect_extra_metadata: Option>, ) -> anyhow::Result>> { @@ -533,7 +533,7 @@ impl<'a> Pex<'a> { wheels: selected_wheels, }), Err(err) => Err(ResolveError { - python_exe: interpreter.raw().path.to_path_buf(), + python_exe: interpreter.details.path.to_path_buf(), err, }), } @@ -583,7 +583,7 @@ impl<'a> Pex<'a> { additional_wheels, }); } - Err(err) => errors.push((interpreter.raw().path.to_path_buf(), err)), + Err(err) => errors.push((interpreter.details.path.to_path_buf(), err)), } } @@ -679,7 +679,7 @@ impl<'a> Pex<'a> { fn load_wheel_metadata( &'a self, - target: &impl PythonPlatform, + target: &impl PythonPlatform<'a>, wheel_files: Vec>, ) -> anyhow::Result>> { let python_version = target.version(); diff --git a/crates/pexrs/src/lib.rs b/crates/pexrs/src/lib.rs index ab9193a..44a73c2 100644 --- a/crates/pexrs/src/lib.rs +++ b/crates/pexrs/src/lib.rs @@ -132,7 +132,7 @@ fn prepare_boot( #[cfg(unix)] env::var_os("_PEXRC_SH_BOOT_SEED_DIR").map(PathBuf::from), )?; - let mut command = Command::new(venv.interpreter.raw().path.as_ref()); + let mut command = Command::new(&venv.interpreter.details.path); command .args(python_args) .arg(venv.prefix().as_os_str()) @@ -192,9 +192,9 @@ fn prepare_venv<'a>( let interpreter_relpath = venv .interpreter - .raw() + .details .path - .strip_prefix(&venv.interpreter.raw().prefix)?; + .strip_prefix(&venv.interpreter.details.prefix)?; let shebang_interpreter = venv_dir.join(interpreter_relpath); let shebang_arg = if (pex_info.venv && pex_info.venv_hermetic_scripts) || (!pex_info.venv @@ -249,12 +249,11 @@ fn prepare_venv<'a>( // N.B.: This symlink is probed by the --sh-boot script to confirm the venv is still // linked to an existing base Python (no uninstalls or upgrades). platform::unix::symlink( - venv_interpreter + &venv_interpreter .clone() .resolve_base_interpreter(&mut pex.scripts()?)? - .raw() - .path - .as_ref(), + .details + .path, sh_boot_seed_dir.join(format!("base-{python}")), false, )?; diff --git a/crates/python-platform/Cargo.toml b/crates/python-platform/Cargo.toml index 73227d2..c6d6786 100644 --- a/crates/python-platform/Cargo.toml +++ b/crates/python-platform/Cargo.toml @@ -14,6 +14,7 @@ pep508_rs = { workspace = true } plist = { workspace = true } regex = { workspace = true } serde_json = { workspace = true } +serde_with = { workspace = true } serde = { workspace = true } strum = { workspace = true } strum_macros = { workspace = true } diff --git a/crates/python-platform/src/arch.rs b/crates/python-platform/src/arch.rs index ffb7b81..b316e3c 100644 --- a/crates/python-platform/src/arch.rs +++ b/crates/python-platform/src/arch.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use std::fmt::{Display, Formatter}; +use std::str::FromStr; use anyhow::bail; use target_lexicon::{ @@ -44,18 +45,6 @@ impl Arch { } } - pub(crate) fn parse(value: &str) -> anyhow::Result { - match value { - "aarch64" | "arm64" => Ok(Self::Arm64), - "armv7" => Ok(Self::Arm64), - "ppc64le" => Ok(Self::Arm64), - "riscv64" => Ok(Self::Arm64), - "s390x" => Ok(Self::Arm64), - "amd64" | "x86_64" | "x64" => Ok(Self::X64), - _ => bail!("Un-supported chip architecture: {value}"), - } - } - pub(crate) fn as_str(&self) -> &'static str { self.as_linux_arch() } @@ -72,6 +61,22 @@ impl Arch { } } +impl FromStr for Arch { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "aarch64" | "arm64" => Ok(Self::Arm64), + "armv7" => Ok(Self::Arm64), + "ppc64le" => Ok(Self::Arm64), + "riscv64" => Ok(Self::Arm64), + "s390x" => Ok(Self::Arm64), + "amd64" | "x86_64" | "x64" => Ok(Self::X64), + _ => bail!("Un-supported chip architecture: {s}"), + } + } +} + impl Display for Arch { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(self.as_str()) diff --git a/crates/python-platform/src/implementation.rs b/crates/python-platform/src/implementation.rs index 7e514ba..55bcafd 100644 --- a/crates/python-platform/src/implementation.rs +++ b/crates/python-platform/src/implementation.rs @@ -1,6 +1,8 @@ // Copyright 2026 Pex project contributors. // SPDX-License-Identifier: Apache-2.0 +use std::str::FromStr; + use anyhow::bail; #[derive(Copy, Clone)] @@ -9,14 +11,16 @@ pub(crate) enum Implementation { PyPy, } -impl Implementation { - pub(crate) fn parse(value: &str) -> anyhow::Result { - if value.eq_ignore_ascii_case("cpython") { +impl FromStr for Implementation { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + if s.eq_ignore_ascii_case("cpython") { Ok(Self::CPython) - } else if value.eq_ignore_ascii_case("pypy") { + } else if s.eq_ignore_ascii_case("pypy") { Ok(Self::PyPy) } else { - bail!("Un-supported Python implementation: {value}") + bail!("Un-supported Python implementation: {s}") } } } diff --git a/crates/python-platform/src/lib.rs b/crates/python-platform/src/lib.rs index acea65b..ae08e58 100644 --- a/crates/python-platform/src/lib.rs +++ b/crates/python-platform/src/lib.rs @@ -17,24 +17,31 @@ mod windows; use std::borrow::Cow; use std::fmt::Display; -use std::io::{BufRead, BufReader, Cursor}; use std::ops::Deref; use std::path::Path; -use std::process::{Command, Stdio}; +use std::str::FromStr; use anyhow::{anyhow, bail}; -pub use linux::LinuxInfo; use logging_timer::time; -pub use markers::{PlatformRelease, PlatformVersion}; use pep508_rs::MarkerEnvironment; use pep508_rs::pep440_rs::Version; use serde::{Deserialize, Serialize}; pub use crate::arch::Arch; use crate::implementation::Implementation; +pub use crate::linux::LinuxInfo; use crate::mac::Release; +pub use crate::markers::{PlatformRelease, PlatformVersion}; pub use crate::os::{Libc, Os}; use crate::platform::Platform; +pub use crate::version::{ + CPythonAbiInfo, + CPythonImplementation, + PyPyImplementation, + PyPyVersion, + PythonImplementation, + PythonVersion, +}; #[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize, Serialize)] pub struct NonEmptyVec(Vec); @@ -46,6 +53,14 @@ impl NonEmptyVec { } Ok(Self(vec)) } + + pub fn first(&self) -> &T { + &self.0[0] + } + + fn transformed(&self, transform: impl Fn(&T) -> &U) -> NonEmptyVec<&U> { + NonEmptyVec(self.0.iter().map(transform).collect()) + } } impl Deref for NonEmptyVec { @@ -56,30 +71,27 @@ impl Deref for NonEmptyVec { } } -pub trait PythonPlatform { +pub trait PythonPlatform<'a> { fn description(&self) -> impl Display; fn marker_env(&self) -> &MarkerEnvironment; - fn supported_tags(&self) -> &NonEmptyVec; + fn supported_tags(&self) -> NonEmptyVec<&'_ str>; fn version(&self) -> Cow<'_, Version> { Cow::Borrowed(&self.marker_env().python_full_version().version) } - fn primary_tag(&self) -> &str { - self.supported_tags()[0].as_ref() - } } #[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize, Serialize)] -pub struct PlatformDetails { +pub struct PlatformDetails<'a> { pub source: String, pub marker_env: MarkerEnvironment, - pub supported_tags: NonEmptyVec, + pub supported_tags: NonEmptyVec>, } -impl PlatformDetails { +impl<'a> PlatformDetails<'a> { pub fn new( source: impl Display, marker_env: MarkerEnvironment, - supported_tags: Vec, + supported_tags: Vec>, ) -> anyhow::Result { Ok(Self { source: source.to_string(), @@ -88,48 +100,31 @@ impl PlatformDetails { }) } - pub fn spawn( - python: &Path, - ) -> anyhow::Result anyhow::Result> { - let child = Command::new(python) - .arg("-V") - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn()?; - Ok(move || { - let output = child.wait_with_output()?; - let version = parse_version(output.stdout).or_else(|_| parse_version(output.stderr))?; - parse(&version, None, None) + pub fn python( + python_exe: &Path, + python_version: PythonImplementation, + ) -> anyhow::Result> { + let platform = Platform::current()?; + let release_info = Os::current_release()?; + Ok(Self { + source: python_exe.display().to_string(), + marker_env: markers::calculate( + python_version, + platform, + Some(PlatformRelease(release_info.release)), + Some(PlatformVersion(release_info.version)), + )?, + supported_tags: NonEmptyVec::new( + tags::calculate(python_version, platform) + .into_iter() + .map(Cow::Owned) + .collect(), + )?, }) } - - pub fn python(python: &Path) -> anyhow::Result { - Self::spawn(python)?() - } -} - -fn parse_version(data: Vec) -> anyhow::Result { - let line = BufReader::new(Cursor::new(data)) - .lines() - .next() - .ok_or_else(|| anyhow!("No Python version output found."))?; - let mut text = line?; - let start = text - .find(" ") - .ok_or_else(|| anyhow!("No Python version output found."))?; - text.drain(..start + 1); - let version = text - .split(" ") - .next() - .expect("Should split at least 1 element."); - if version.is_empty() { - bail!("No Python version output found.") - } - text.truncate(version.len()); - Ok(text) } -impl PythonPlatform for PlatformDetails { +impl<'a> PythonPlatform<'a> for PlatformDetails<'a> { fn description(&self) -> impl Display { &self.source } @@ -138,8 +133,8 @@ impl PythonPlatform for PlatformDetails { &self.marker_env } - fn supported_tags(&self) -> &NonEmptyVec { - &self.supported_tags + fn supported_tags(&self) -> NonEmptyVec<&'_ str> { + self.supported_tags.transformed(|tag| tag.as_ref()) } } @@ -148,13 +143,13 @@ pub fn parse<'a>( spec: &'a str, platform_release: Option>, platform_version: Option>, -) -> anyhow::Result { +) -> anyhow::Result> { let mut components = spec.split("-"); let implementation_or_version = components .next() .expect("There is always at least one split component."); let python_version = - if let Ok(implementation) = Implementation::parse(implementation_or_version) { + if let Ok(implementation) = Implementation::from_str(implementation_or_version) { let version = components.next().ok_or_else(|| { anyhow!( "Expected a Python platform specification starting with \ @@ -169,9 +164,9 @@ pub fn parse<'a>( let (platform, platform_release, platform_version) = { let (os, arch, platform_release, platform_version) = if let Some(os) = components.next() { - let os = Os::parse(os)?; + let os = os.parse()?; let arch = if let Some(arch) = components.next() { - Arch::parse(arch)? + arch.parse()? } else { match &os { Os::Linux(_) => Arch::X64, @@ -219,14 +214,13 @@ pub fn parse<'a>( ) } - let marker_env = markers::calculate( - &python_version, - &platform, - platform_release, - platform_version, - )?; + let marker_env = + markers::calculate(python_version, platform, platform_release, platform_version)?; - let supported_tags = tags::calculate(&python_version, platform); + let supported_tags = tags::calculate(python_version, platform) + .into_iter() + .map(Cow::Owned) + .collect(); PlatformDetails::new( format!("abbreviated platform {spec}"), diff --git a/crates/python-platform/src/markers.rs b/crates/python-platform/src/markers.rs index 43f74dc..c36a8d2 100644 --- a/crates/python-platform/src/markers.rs +++ b/crates/python-platform/src/markers.rs @@ -2,15 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use std::borrow::Cow; -use std::fmt::{Display, Formatter}; use std::ops::Deref; -use pep440_rs::{PrereleaseKind, Version}; use pep508_rs::{MarkerEnvironment, MarkerEnvironmentBuilder}; use crate::arch::Arch; use crate::platform::{Linux, Mac, Platform, Windows}; -use crate::version::{PythonVersion, SimpleVersion}; +use crate::version::PythonImplementation; macro_rules! generate_marker_variable { ( $marker_variable_type:ident ) => { @@ -36,17 +34,17 @@ generate_marker_variable!(PlatformRelease); generate_marker_variable!(PlatformVersion); pub(crate) fn calculate( - python_version: &PythonVersion, - platform: &Platform, + python_version: PythonImplementation, + platform: Platform, platform_release: Option, platform_version: Option, ) -> anyhow::Result { let (implementation_name, platform_python_implementation) = match python_version { - PythonVersion::CPython(_) => ("cpython", "CPython"), - PythonVersion::PyPy(_) => ("pypy", "PyPy"), + PythonImplementation::CPython(_) => ("cpython", "CPython"), + PythonImplementation::PyPy(_) => ("pypy", "PyPy"), }; - let major_version = python_version.major_version(); + let major_version = python_version.major; let (os_name, platform_system, sys_platform) = match platform { Platform::Linux(_) => ( "posix", @@ -71,14 +69,14 @@ pub(crate) fn calculate( Arch::X64 => "x86_64", }, Platform::Mac(Mac { arm64, .. }) => { - if *arm64 { + if arm64 { "arm64" } else { "x86_64" } } Platform::Windows(Windows { arm64, .. }) => { - if *arm64 { + if arm64 { "ARM64" } else { "AMD64" @@ -105,38 +103,11 @@ pub(crate) fn calculate( } }; - struct ImplementationVersion<'a>(&'a Version); - impl<'a> Display for ImplementationVersion<'a> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let release = self.0.release(); - write!( - f, - "{major}.{minor}.{patch}", - major = release[0], - minor = release[1], - patch = release[2] - )?; - if let Some(prerelease) = self.0.pre() { - write!( - f, - "{}", - match prerelease.kind { - PrereleaseKind::Alpha => "a", - PrereleaseKind::Beta => "b", - PrereleaseKind::Rc => "rc", - } - )?; - write!(f, "{}", prerelease.number)?; - } - Ok(()) - } - } - - let implementation_version = ImplementationVersion(python_version.version()).to_string(); + let implementation_version = python_version.to_string(); let python_version_str = format!( "{major}.{minor}", major = major_version, - minor = python_version.minor_version() + minor = python_version.minor ); Ok(MarkerEnvironment::try_from(MarkerEnvironmentBuilder { diff --git a/crates/python-platform/src/os.rs b/crates/python-platform/src/os.rs index 2e7717a..1a05e3e 100644 --- a/crates/python-platform/src/os.rs +++ b/crates/python-platform/src/os.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use std::borrow::Cow; +use std::str::FromStr; use anyhow::bail; #[cfg(target_os = "linux")] @@ -29,42 +30,6 @@ pub(crate) struct ReleaseInfo<'a> { } impl Os { - pub(crate) fn parse(value: &str) -> anyhow::Result { - match value { - "linux" => { - // We default to manylinux2014 support like out rust builds do. - Ok(Self::Linux(Libc::Gnu(LibcVersion::new(2, 17)))) - } - "macos" => { - // We default to 11.3 support like our rust builds do. - // This is macOS Big Sur from April 2021. - Ok(Self::Mac(MacRelease::new(11, 3))) - } - "windows" => Ok(Self::Windows(None)), - value if let Some(version) = value.strip_prefix("macos_") => { - Ok(Self::Mac(MacRelease::parse(version, '_')?)) - } - // See: https://peps.python.org/pep-0600/ - value if let Some(version) = value.strip_prefix("manylinux") => match version { - "1" => Ok(Self::Linux(Libc::Gnu(LibcVersion::new(2, 5)))), - "2010" => Ok(Self::Linux(Libc::Gnu(LibcVersion::new(2, 12)))), - "2014" => Ok(Self::Linux(Libc::Gnu(LibcVersion::new(2, 17)))), - _ if let Some(glibc_version) = version.strip_prefix("_") => Ok(Self::Linux( - Libc::Gnu(LibcVersion::parse(glibc_version, '_')?), - )), - _ => bail!("Invalid manylinux specification: {value}"), - }, - // See: https://peps.python.org/pep-0656/ - value if let Some(version) = value.strip_prefix("musllinux_") => { - Ok(Self::Linux(Libc::Musl(LibcVersion::parse(version, '_')?))) - } - value if let Some(release) = value.strip_prefix("windows_") => { - Ok(Self::Windows(Some(WindowsRelease::parse(release)?))) - } - value => bail!("Un-supported operating system: {value}"), - } - } - #[cfg(target_os = "linux")] #[time("debug", "Os.{}")] pub fn current() -> anyhow::Result { @@ -118,3 +83,43 @@ impl Os { } } } + +impl FromStr for Os { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "linux" => { + // We default to manylinux2014 support like out rust builds do. + Ok(Self::Linux(Libc::Gnu(LibcVersion::new(2, 17)))) + } + "macos" => { + // We default to 11.3 support like our rust builds do. + // This is macOS Big Sur from April 2021. + Ok(Self::Mac(MacRelease::new(11, 3))) + } + "windows" => Ok(Self::Windows(None)), + value if let Some(version) = value.strip_prefix("macos_") => { + Ok(Self::Mac(MacRelease::parse(version, '_')?)) + } + // See: https://peps.python.org/pep-0600/ + value if let Some(version) = value.strip_prefix("manylinux") => match version { + "1" => Ok(Self::Linux(Libc::Gnu(LibcVersion::new(2, 5)))), + "2010" => Ok(Self::Linux(Libc::Gnu(LibcVersion::new(2, 12)))), + "2014" => Ok(Self::Linux(Libc::Gnu(LibcVersion::new(2, 17)))), + _ if let Some(glibc_version) = version.strip_prefix("_") => Ok(Self::Linux( + Libc::Gnu(LibcVersion::parse(glibc_version, '_')?), + )), + _ => bail!("Invalid manylinux specification: {value}"), + }, + // See: https://peps.python.org/pep-0656/ + value if let Some(version) = value.strip_prefix("musllinux_") => { + Ok(Self::Linux(Libc::Musl(LibcVersion::parse(version, '_')?))) + } + value if let Some(release) = value.strip_prefix("windows_") => { + Ok(Self::Windows(Some(release.parse()?))) + } + value => bail!("Un-supported operating system: {value}"), + } + } +} diff --git a/crates/python-platform/src/platform.rs b/crates/python-platform/src/platform.rs index 6e28c85..55003ca 100644 --- a/crates/python-platform/src/platform.rs +++ b/crates/python-platform/src/platform.rs @@ -20,11 +20,13 @@ pub struct Mac { pub(crate) release: MacRelease, } +#[derive(Copy, Clone)] pub struct Windows { pub(crate) arm64: bool, pub(crate) release: Option, } +#[derive(Copy, Clone)] pub(crate) enum Platform { Linux(Linux), Mac(Mac), @@ -57,4 +59,8 @@ impl Platform { } }) } + + pub(crate) fn current() -> anyhow::Result { + Self::from_parts(Os::current()?, Arch::current()?) + } } diff --git a/crates/python-platform/src/tags.rs b/crates/python-platform/src/tags.rs index e2b0b0a..c674f34 100644 --- a/crates/python-platform/src/tags.rs +++ b/crates/python-platform/src/tags.rs @@ -6,16 +6,18 @@ use crate::linux::LibcVersion; use crate::mac::Release; use crate::os::Libc; use crate::platform::{Linux, Mac, Platform, Windows}; -use crate::version::{CPythonVersion, PyPyVersion, PythonVersion, SimpleVersion}; +use crate::version::{CPythonImplementation, PyPyImplementation, PythonImplementation}; -pub(crate) fn calculate(python_version: &PythonVersion, platform: Platform) -> Vec { +pub(crate) fn calculate(python_version: PythonImplementation, platform: Platform) -> Vec { let platforms = calculate_supported_platforms(platform); let mut tags = Vec::with_capacity(2048); match python_version { - PythonVersion::CPython(version) => { + PythonImplementation::CPython(version) => { add_cpython_tags(&mut tags, platforms.as_slice(), version) } - PythonVersion::PyPy(version) => add_pypy_tags(&mut tags, platforms.as_slice(), version), + PythonImplementation::PyPy(version) => { + add_pypy_tags(&mut tags, platforms.as_slice(), version) + } } add_compatible_tags(&mut tags, platforms.as_slice(), python_version); tags @@ -161,23 +163,27 @@ fn calculate_windows_platform(windows: Windows) -> &'static str { } } -fn add_cpython_tags(tags: &mut Vec, platforms: &[String], python_version: &CPythonVersion) { - let major = python_version.major_version(); - let minor = python_version.minor_version(); +fn add_cpython_tags( + tags: &mut Vec, + platforms: &[String], + python_version: CPythonImplementation, +) { + let major = python_version.major; + let minor = python_version.minor; let interpreter = format_args!("cp{major}{minor}"); - let threading = if python_version.free_threaded { + let threading = if python_version.free_threaded() { "t" } else { "" }; - let debug = if python_version.debug { "d" } else { "" }; - let pymalloc = if python_version.pymalloc { "m" } else { "" }; - let ucs4 = if python_version.ucs4 { "u" } else { "" }; + let debug = if python_version.debug() { "d" } else { "" }; + let pymalloc = if python_version.pymalloc() { "m" } else { "" }; + let ucs4 = if python_version.ucs4() { "u" } else { "" }; let abi = format!("cp{major}{minor}{threading}{debug}{pymalloc}{ucs4}"); - let abis: &[String] = if python_version.debug { + let abis: &[String] = if python_version.debug() { &[format!("cp{major}{minor}{threading}"), abi] } else { &[abi] @@ -191,12 +197,12 @@ fn add_cpython_tags(tags: &mut Vec, platforms: &[String], python_version // N.B.: PEP 384 was first implemented in Python 3.2. The free-threaded builds do not support // abi3. - let abi3 = (major, minor) >= (3, 2) && !python_version.free_threaded; + let abi3 = (major, minor) >= (3, 2) && !python_version.free_threaded(); // PEP 803 was first implemented in Python 3.15 but, per PEP 803, this returns tags going back // to Python 3.2 to mirror the abi3 implementation and leave open the possibility of abi3t // wheels supporting older Python versions. - let abi3t = (major, minor) >= (3, 2) && python_version.free_threaded; + let abi3t = (major, minor) >= (3, 2) && python_version.free_threaded(); if abi3 { for platform in platforms { @@ -228,12 +234,12 @@ fn add_cpython_tags(tags: &mut Vec, platforms: &[String], python_version } } -fn add_pypy_tags(tags: &mut Vec, platforms: &[String], python_version: &PyPyVersion) { - let major = python_version.major_version(); - let minor = python_version.minor_version(); +fn add_pypy_tags(tags: &mut Vec, platforms: &[String], python_version: PyPyImplementation) { + let major = python_version.major; + let minor = python_version.minor; if let Some(pypy_version) = python_version.pypy_version.as_ref() { - let pypy_major = pypy_version.major_version(); - let pypy_minor = pypy_version.minor_version(); + let pypy_major = pypy_version.major(); + let pypy_minor = pypy_version.minor(); for platform in platforms { tags.push(format!( "pp{major}{minor}-pypy{major}{minor}_pp{pypy_major}{pypy_minor}-{platform}" @@ -246,31 +252,24 @@ fn add_pypy_tags(tags: &mut Vec, platforms: &[String], python_version: & } mod py_interpreter { - use pep440_rs::Version; + use crate::PythonVersion; enum State { Latest, MajorOnly, - Descending(u64), + Descending(u8), Finished, } pub(super) struct Range { - major: u64, - minor: Option, + version: PythonVersion, state: State, } impl Range { - pub(super) fn new(version: &Version) -> Self { - let release = version.release(); + pub(super) fn new(version: &PythonVersion) -> Self { Self { - major: release[0], - minor: if release.len() > 1 { - Some(release[1]) - } else { - None - }, + version: *version, state: State::Latest, } } @@ -283,19 +282,15 @@ mod py_interpreter { match self.state { State::Latest => { self.state = State::MajorOnly; - if let Some(minor) = self.minor { - Some(format!("py{major}{minor}", major = self.major)) - } else { - self.next() - } + Some(format!( + "py{major}{minor}", + major = self.version.major, + minor = self.version.minor + )) } State::MajorOnly => { - if let Some(minor) = self.minor { - self.state = State::Descending(minor - 1); - } else { - self.state = State::Finished; - } - Some(format!("py{major}", major = self.major)) + self.state = State::Descending(self.version.minor - 1); + Some(format!("py{major}", major = self.version.major)) } State::Descending(minor) => { if minor == 0 { @@ -303,7 +298,7 @@ mod py_interpreter { } else { self.state = State::Descending(minor - 1); } - Some(format!("py{major}{minor}", major = self.major)) + Some(format!("py{major}{minor}", major = self.version.major)) } State::Finished => None, } @@ -314,25 +309,24 @@ mod py_interpreter { fn add_compatible_tags( tags: &mut Vec, platforms: &[String], - python_version: &PythonVersion, + python_version: PythonImplementation, ) { - let version = python_version.version(); - for version in py_interpreter::Range::new(version) { + for version in py_interpreter::Range::new(&python_version) { for platform in platforms { tags.push(format!("{version}-none-{platform}")) } } tags.push(match python_version { - PythonVersion::CPython(_) => format!( + PythonImplementation::CPython(_) => format!( "cp{major}{minor}-none-any", - major = python_version.major_version(), - minor = python_version.minor_version() + major = python_version.major, + minor = python_version.minor ), - PythonVersion::PyPy(_) => { - format!("pp{major}-none-any", major = python_version.major_version()) + PythonImplementation::PyPy(_) => { + format!("pp{major}-none-any", major = python_version.major) } }); - for version in py_interpreter::Range::new(version) { + for version in py_interpreter::Range::new(&python_version) { tags.push(format!("{version}-none-any")) } } diff --git a/crates/python-platform/src/version.rs b/crates/python-platform/src/version.rs index 2671990..5ca1c9d 100644 --- a/crates/python-platform/src/version.rs +++ b/crates/python-platform/src/version.rs @@ -1,161 +1,334 @@ // Copyright 2026 Pex project contributors. // SPDX-License-Identifier: Apache-2.0 +use std::fmt::{Display, Formatter}; +use std::ops::Deref; use std::str::FromStr; use std::sync::LazyLock; use anyhow::bail; -use pep440_rs::Version; -use regex::Regex; +use regex::{Regex, RegexBuilder}; +use serde::{Deserialize, Serialize}; +use serde_with::{DeserializeFromStr, SerializeDisplay}; use crate::implementation::Implementation; -pub(crate) struct CPythonVersion { - version: Version, - pub(crate) debug: bool, - pub(crate) free_threaded: bool, - pub(crate) pymalloc: bool, - pub(crate) ucs4: bool, +#[derive( + Copy, + Clone, + Debug, + Default, + Eq, + PartialEq, + Ord, + PartialOrd, + Hash, + DeserializeFromStr, + SerializeDisplay, +)] +pub enum ReleaseLevel { + #[default] + Final, + Rc, + Beta, + Alpha, } -pub(crate) trait SimpleVersion { - fn major_version(&self) -> u64; - fn minor_version(&self) -> u64; +impl FromStr for ReleaseLevel { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "final" => Self::Final, + "rc" => Self::Rc, + "beta" | "b" => Self::Beta, + "alpha" | "a" => Self::Alpha, + _ => bail!("Not a recognized CPython releaselevel: {s}"), + }) + } } -impl SimpleVersion for Version { - fn major_version(&self) -> u64 { - self.release()[0] +impl Display for ReleaseLevel { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + ReleaseLevel::Final => "final", + ReleaseLevel::Rc => "rc", + ReleaseLevel::Beta => "b", + ReleaseLevel::Alpha => "a", + }) } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)] +pub struct PythonVersion { + pub major: u8, + pub minor: u8, + pub micro: u8, + pub releaselevel: ReleaseLevel, + pub serial: u8, +} - fn minor_version(&self) -> u64 { - self.release()[1] +impl Display for PythonVersion { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{major}.{minor}.{micro}", + major = self.major, + minor = self.minor, + micro = self.micro + )?; + match (self.releaselevel, self.serial) { + (ReleaseLevel::Alpha, serial) => write!(f, "a{serial}")?, + (ReleaseLevel::Beta, serial) => write!(f, "b{serial}")?, + (ReleaseLevel::Rc, serial) => write!(f, "rc{serial}")?, + (ReleaseLevel::Final, _) => {} + } + Ok(()) } } -impl SimpleVersion for CPythonVersion { - fn major_version(&self) -> u64 { - self.version.major_version() +impl PythonVersion { + pub fn simple(major: u8, minor: u8) -> Self { + Self::new(major, minor, None) } - fn minor_version(&self) -> u64 { - self.version.minor_version() + pub fn new(major: u8, minor: u8, micro: Option) -> Self { + Self { + major, + minor, + micro: micro.unwrap_or_default(), + releaselevel: ReleaseLevel::Final, + serial: 0, + } } } -pub(crate) struct PyPyVersion { - version: Version, - pub(crate) pypy_version: Option, +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash, Deserialize, Serialize)] +pub struct CPythonAbiInfo { + pub free_threaded: Option, + pub debug: bool, + pub pymalloc: Option, + pub ucs4: Option, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct CPythonImplementation { + pub version: PythonVersion, + pub abi_info: CPythonAbiInfo, +} + +impl Deref for CPythonImplementation { + type Target = PythonVersion; + + fn deref(&self) -> &Self::Target { + &self.version + } } -impl SimpleVersion for PyPyVersion { - fn major_version(&self) -> u64 { - self.version.major_version() +impl CPythonImplementation { + pub fn free_threaded(&self) -> bool { + self.abi_info.free_threaded.unwrap_or_default() + } + + pub fn debug(&self) -> bool { + self.abi_info.debug + } + + pub fn pymalloc(&self) -> bool { + self.abi_info.pymalloc.unwrap_or_default() } - fn minor_version(&self) -> u64 { - self.version.minor_version() + pub fn ucs4(&self) -> bool { + self.abi_info.ucs4.unwrap_or_default() } } -pub(crate) enum PythonVersion { - CPython(CPythonVersion), - PyPy(PyPyVersion), +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash, Deserialize, Serialize)] +pub struct PyPyVersion(u8, u8, u8); + +impl PyPyVersion { + pub fn major(&self) -> u8 { + self.0 + } + + pub fn minor(&self) -> u8 { + self.1 + } + + pub fn patch(&self) -> u8 { + self.2 + } } -impl PythonVersion { - pub(crate) fn version(&self) -> &Version { - match self { - PythonVersion::CPython(CPythonVersion { version, .. }) - | PythonVersion::PyPy(PyPyVersion { version, .. }) => version, - } +impl Display for PyPyVersion { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{major}.{minor}.{patch}", + major = self.0, + minor = self.1, + patch = self.2 + ) + } +} + +impl FromStr for PyPyVersion { + type Err = anyhow::Error; + + fn from_str(value: &str) -> Result { + let mut components = value.split("."); + let major = components + .next() + .expect("A split always yields one component") + .parse::()?; + let minor = if let Some(minor) = components.next() { + minor.parse::()? + } else { + 0 + }; + let patch = if let Some(patch) = components.next() { + patch.parse::()? + } else { + 0 + }; + Ok(PyPyVersion(major, minor, patch)) } } -impl SimpleVersion for PythonVersion { - fn major_version(&self) -> u64 { - self.version().major_version() +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct PyPyImplementation { + pub version: PythonVersion, + pub pypy_version: Option, +} + +impl Deref for PyPyImplementation { + type Target = PythonVersion; + + fn deref(&self) -> &Self::Target { + &self.version } +} - fn minor_version(&self) -> u64 { - self.version().minor_version() +#[derive(Copy, Clone)] +pub enum PythonImplementation { + CPython(CPythonImplementation), + PyPy(PyPyImplementation), +} + +impl Deref for PythonImplementation { + type Target = PythonVersion; + + fn deref(&self) -> &Self::Target { + match self { + PythonImplementation::CPython(CPythonImplementation { version, .. }) + | PythonImplementation::PyPy(PyPyImplementation { version, .. }) => version, + } } } pub(crate) fn parse( implementation: Implementation, version: &str, -) -> anyhow::Result { +) -> anyhow::Result { Ok(match implementation { - Implementation::CPython => PythonVersion::CPython(parse_cpython_version(version)?), - Implementation::PyPy => PythonVersion::PyPy(parse_pypy_version(version)?), + Implementation::CPython => { + PythonImplementation::CPython(parse_cpython_implementation(version)?) + } + Implementation::PyPy => PythonImplementation::PyPy(parse_pypy_implementation(version)?), }) } static PYTHON_VERSION_REGEX: LazyLock = LazyLock::new(|| { - Regex::new(r"^(?\d+\.\d+(\.\d+)?)(?:(?:a|b|rc)\d+)?(?[dtmu]+)?$") - .expect("This is a known valid regex.") + RegexBuilder::new( + r"^ +(?\d+\.\d+) +(?: + \.(?\d+) + (?: + (?a|b|rc|final)(?\d+) + )? +)? +$ +", + ) + .ignore_whitespace(true) + .build() + .expect("This is a known valid regex.") }); -fn parse_python_version(value: &str) -> anyhow::Result { - let mut version = Version::from_str(value)?; - let release = version.release(); - if release.len() < 2 { - bail!( - "The Python version must have at least a major version and minor version, but \ - given version of {version}" - ) +fn parse_python_version(value: &str) -> anyhow::Result { + if let Some(captures) = PYTHON_VERSION_REGEX.captures(value) { + let mut major_minor = captures + .name("major_minor") + .expect("The major_minor capture was required.") + .as_str() + .split("."); + let major = major_minor + .next() + .expect("We captured major") + .parse::()?; + let minor = major_minor + .next() + .expect("We captured minor") + .parse::()?; + let mut version = PythonVersion::simple(major, minor); + if let Some(micro) = captures.name("micro") { + version.micro = micro.as_str().parse::()?; + if let (Some(release), Some(serial)) = + (captures.name("release"), captures.name("serial")) + { + version.releaselevel = release.as_str().parse()?; + version.serial = serial.as_str().parse::()?; + } + } + return Ok(version); } - if release.len() == 2 { - let major = version.major_version(); - let minor = version.minor_version(); - version = version.with_release([major, minor, 0]) - }; - Ok(version) + bail!("Not a valid Python version: {value}") } -fn parse_cpython_version(value: &str) -> anyhow::Result { - if let Some(captures) = PYTHON_VERSION_REGEX.captures(value) { +static CPYTHON_VERSION_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"^(?.+[^dtmu])(?[dtmu]+)?$").expect("This is a known valid regex.") +}); + +fn parse_cpython_implementation(value: &str) -> anyhow::Result { + if let Some(captures) = CPYTHON_VERSION_REGEX.captures(value) { let version = parse_python_version( captures .name("version") - .expect("The version capture was required.") + .expect("The version capture was required") .as_str(), )?; - let mut debug = false; - let mut free_threaded = false; - let mut pymalloc = false; - let mut ucs4 = false; + let mut abi_info = CPythonAbiInfo::default(); if let Some(flags) = captures.name("flags") { for flag in flags.as_str().chars() { match flag { - 'd' => debug = true, + 'd' => abi_info.debug = true, 't' => { - if version < Version::new([3, 13]) { + if version < PythonVersion::simple(3, 13) { bail!( "The t version flag indicating a free-threaded CPython build only \ applies for versions 3.13 and newer; but given version of {version}" ) } - free_threaded = true; + abi_info.free_threaded = Some(true); } 'm' => { - if version >= Version::new([3, 8]) { + if version >= PythonVersion::simple(3, 8) { bail!( "The m version flag indicating a pymalloc CPython build only \ applies for versions prior to 3.8; but given version of {version}" ) } - pymalloc = true; + abi_info.pymalloc = Some(true); } 'u' => { - if version >= Version::new([3, 3]) { + if version >= PythonVersion::simple(3, 3) { bail!( "The m version flag indicating a pymalloc CPython build only \ applies for versions prior to 3.3; but given version of {version}" ) } - ucs4 = true; + abi_info.ucs4 = Some(true); } flag => { bail!("Un-recognized version flag {flag} for Cpython version {version}") @@ -163,19 +336,13 @@ fn parse_cpython_version(value: &str) -> anyhow::Result { } } } - Ok(CPythonVersion { - version, - debug, - free_threaded, - pymalloc, - ucs4, - }) + Ok(CPythonImplementation { version, abi_info }) } else { bail!("Invalid CPython version: {value}") } } -fn parse_pypy_version(value: &str) -> anyhow::Result { +fn parse_pypy_implementation(value: &str) -> anyhow::Result { let mut components = value.splitn(2, "_"); let version = parse_python_version( components @@ -183,12 +350,97 @@ fn parse_pypy_version(value: &str) -> anyhow::Result { .expect("The version split will always yield at least one component."), )?; let pypy_version = if let Some(pypy_version) = components.next() { - Some(Version::from_str(pypy_version)?) + Some(pypy_version.parse()?) } else { None }; - Ok(PyPyVersion { + Ok(PyPyImplementation { version, pypy_version, }) } + +#[cfg(test)] +mod tests { + use crate::version::{ReleaseLevel, parse_cpython_implementation}; + use crate::{CPythonAbiInfo, CPythonImplementation, PythonVersion}; + + #[test] + fn test_parse_cpython_implementation() { + assert_eq!( + CPythonImplementation { + version: PythonVersion { + major: 3, + minor: 14, + micro: 0, + releaselevel: ReleaseLevel::Final, + serial: 0 + }, + abi_info: CPythonAbiInfo { + free_threaded: None, + debug: false, + pymalloc: None, + ucs4: None + }, + }, + parse_cpython_implementation("3.14").unwrap() + ); + + assert_eq!( + CPythonImplementation { + version: PythonVersion { + major: 3, + minor: 14, + micro: 5, + releaselevel: ReleaseLevel::Final, + serial: 0 + }, + abi_info: CPythonAbiInfo { + free_threaded: None, + debug: false, + pymalloc: None, + ucs4: None + }, + }, + parse_cpython_implementation("3.14.5").unwrap() + ); + + assert_eq!( + CPythonImplementation { + version: PythonVersion { + major: 3, + minor: 14, + micro: 0, + releaselevel: ReleaseLevel::Final, + serial: 0 + }, + abi_info: CPythonAbiInfo { + free_threaded: Some(true), + debug: true, + pymalloc: None, + ucs4: None + }, + }, + parse_cpython_implementation("3.14dt").unwrap() + ); + + assert_eq!( + CPythonImplementation { + version: PythonVersion { + major: 3, + minor: 15, + micro: 0, + releaselevel: ReleaseLevel::Beta, + serial: 1 + }, + abi_info: CPythonAbiInfo { + free_threaded: Some(true), + debug: false, + pymalloc: None, + ucs4: None + }, + }, + parse_cpython_implementation("3.15.0b1t").unwrap() + ); + } +} diff --git a/crates/python-platform/src/windows.rs b/crates/python-platform/src/windows.rs index f698f03..8c6c9f2 100644 --- a/crates/python-platform/src/windows.rs +++ b/crates/python-platform/src/windows.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use std::fmt::{Display, Formatter}; +use std::str::FromStr; use anyhow::bail; use strum::IntoEnumIterator; @@ -60,10 +61,14 @@ impl Release { Release::Windows2000Server => "2000Server", } } +} + +impl FromStr for Release { + type Err = anyhow::Error; - pub fn parse(release: &str) -> anyhow::Result { + fn from_str(s: &str) -> Result { for value in Self::iter() { - if release.eq_ignore_ascii_case(value.as_str()) { + if s.eq_ignore_ascii_case(value.as_str()) { return Ok(value); } } @@ -77,7 +82,7 @@ impl Release { } } bail!( - "Invalid Windows release: {release}\n\ + "Invalid Windows release: {s}\n\ The following releases are currently supported:\n\ {Releases}" ) diff --git a/crates/python-platform/tests/tags_test.rs b/crates/python-platform/tests/tags_test.rs index b554ee6..051891e 100644 --- a/crates/python-platform/tests/tags_test.rs +++ b/crates/python-platform/tests/tags_test.rs @@ -5,16 +5,7 @@ use std::path::Path; use interpreter::Interpreter; use pretty_assertions::assert_eq; -use python_platform::{ - Arch, - Libc, - Os, - PlatformDetails, - PlatformRelease, - PlatformVersion, - PythonPlatform, - parse, -}; +use python_platform::{Arch, Libc, Os, PlatformRelease, PlatformVersion, PythonPlatform, parse}; use rstest::rstest; use scripts::IdentifyInterpreter; use testing::{interpreter_identification_script, python_exe}; @@ -24,20 +15,14 @@ fn test_abbreviated_platform( python_exe: &Path, interpreter_identification_script: IdentifyInterpreter, ) { - let platform_details = PlatformDetails::python(python_exe).unwrap(); - let interpreter = Interpreter::load_uncached( - python_exe, - &interpreter_identification_script, - platform_details, - ) - .unwrap(); + let interpreter = + Interpreter::load_uncached(python_exe, &interpreter_identification_script).unwrap(); - let raw_interpreter = interpreter.raw(); let spec = format!( "cpython-{major}.{minor}.{patch}-{os}-{arch}", - major = raw_interpreter.version.major, - minor = raw_interpreter.version.minor, - patch = raw_interpreter.version.micro, + major = interpreter.details.version.major, + minor = interpreter.details.version.minor, + patch = interpreter.details.version.micro, os = match Os::current().unwrap() { Os::Linux(libc) => match libc { Libc::Gnu(libc_version) => format!( diff --git a/crates/tools/src/commands/interpreter.rs b/crates/tools/src/commands/interpreter.rs index aa63a7f..e85b987 100644 --- a/crates/tools/src/commands/interpreter.rs +++ b/crates/tools/src/commands/interpreter.rs @@ -44,17 +44,16 @@ pub(crate) struct InterpreterArgs { pub(crate) fn display(python: &Path, pex: Pex, args: InterpreterArgs) -> anyhow::Result<()> { let mut out = args.output.writer()?; for interpreter in compatible_interpreters(python, &pex, args.all)? { - let raw_interpeter = interpreter.raw(); match args.verbose { 0 => writeln!( &mut out, "{path}", - path = raw_interpeter.path.as_ref().display() + path = interpreter.details.path.display() )?, 1 => args.json.serialize( &mut out, &json!({ - "path": raw_interpeter.path.as_ref(), + "path": interpreter.details.path, "requirement": InterpreterConstraint::exact_version(&interpreter).to_string(), "platform": Platform::of(&interpreter)?.to_string() }), @@ -62,7 +61,7 @@ pub(crate) fn display(python: &Path, pex: Pex, args: InterpreterArgs) -> anyhow: 2 => args.json.serialize( &mut out, &json!({ - "path": raw_interpeter.path.as_ref(), + "path": interpreter.details.path, "requirement": InterpreterConstraint::exact_version(&interpreter).to_string(), "platform": Platform::of(&interpreter)?.to_string(), "supported_tags": interpreter.supported_tags() @@ -76,20 +75,20 @@ pub(crate) fn display(python: &Path, pex: Pex, args: InterpreterArgs) -> anyhow: args.json.serialize( &mut out, &json!({ - "path": raw_interpeter.path.as_ref(), + "path": interpreter.details.path, "requirement": InterpreterConstraint::exact_version(&interpreter).to_string(), "platform": Platform::of(&interpreter)?.to_string(), "supported_tags": interpreter.supported_tags(), "env_markers": interpreter.marker_env(), "venv": true, - "base_interpreter": base_interpreter.raw().path.as_ref() + "base_interpreter": base_interpreter.details.path }), )? } else { args.json.serialize( &mut out, &json!({ - "path": raw_interpeter.path.as_ref(), + "path": interpreter.details.path, "requirement": InterpreterConstraint::exact_version(&interpreter).to_string(), "platform": Platform::of(&interpreter)?.to_string(), "supported_tags": interpreter.supported_tags(), diff --git a/crates/tools/src/commands/repository/extract.rs b/crates/tools/src/commands/repository/extract.rs index e7a06ba..6ba69d0 100644 --- a/crates/tools/src/commands/repository/extract.rs +++ b/crates/tools/src/commands/repository/extract.rs @@ -485,12 +485,12 @@ fn serve( pid_file: Option<&Path>, timeout: f32, ) -> anyhow::Result<()> { - let module = if interpreter.raw().version.major == 3 { + let module = if interpreter.details.version.major == 3 { "http.server" } else { "SimpleHTTPServer" }; - let mut child = Command::new(interpreter.raw().path.as_ref()) + let mut child = Command::new(&interpreter.details.path) // N.B.: Running Python in unbuffered mode here is critical to being able to read stdout. .arg("-u") .args(["-m", module]) diff --git a/crates/tools/src/commands/venv.rs b/crates/tools/src/commands/venv.rs index 6ca9077..dfcfa10 100644 --- a/crates/tools/src/commands/venv.rs +++ b/crates/tools/src/commands/venv.rs @@ -357,7 +357,7 @@ pub(crate) fn create(python: &Path, pex: Pex, args: VenvArgs) -> anyhow::Result< ))); venv_pex::populate( &venv, - venv.interpreter.raw().path.as_ref(), + venv.interpreter.details.path.as_ref(), shebang_arg, &pex, resolve.wheels, @@ -369,7 +369,7 @@ pub(crate) fn create(python: &Path, pex: Pex, args: VenvArgs) -> anyhow::Result< for (pex, wheels) in resolve.additional_wheels { venv_pex::populate_user_code_and_wheels( &venv, - venv.interpreter.raw().path.as_ref(), + venv.interpreter.details.path.as_ref(), shebang_arg, pex, wheels, @@ -390,7 +390,7 @@ pub(crate) fn create(python: &Path, pex: Pex, args: VenvArgs) -> anyhow::Result< } if args.compile { - let exit_status = Command::new(venv.interpreter.raw().path.as_ref()) + let exit_status = Command::new(&venv.interpreter.details.path) .args(["-m", "compileall"]) .arg(&args.venv_dir) .stdout(Stdio::null()) diff --git a/crates/venv/src/venv_pex.rs b/crates/venv/src/venv_pex.rs index 7bcf213..eeee347 100644 --- a/crates/venv/src/venv_pex.rs +++ b/crates/venv/src/venv_pex.rs @@ -361,13 +361,13 @@ fn calculate_spread_path( .join("site") .join(format!( "python{major}.{minor}", - major = venv.interpreter.raw().version.major, - minor = venv.interpreter.raw().version.minor + major = venv.interpreter.details.version.major, + minor = venv.interpreter.details.version.minor )) .join(wheel_details.project_name) .join(components.collect::()), )) - } else if let Some(spread_path) = venv.interpreter.raw().paths.get(key) { + } else if let Some(spread_path) = venv.interpreter.details.paths.get(key) { Ok(Some(spread_path.join(components.collect::()))) } else { bail!( @@ -402,7 +402,7 @@ fn calculate_spread_path( .chain(stash_rel_path.components()) .collect(), )) - } else if let Some(spread_path) = venv.interpreter.raw().paths.get(key) { + } else if let Some(spread_path) = venv.interpreter.details.paths.get(key) { Ok(Some(spread_path.components().chain(components).collect())) } else { bail!( @@ -1081,7 +1081,7 @@ fn write_pex_extra_sys_path_support_files( .write_all(VenvPexExtraSysPathPy::read(scripts)?.contents().as_bytes())?; let python_version = { - let version = venv.interpreter.raw().version; + let version = venv.interpreter.details.version; (version.major, version.minor) }; diff --git a/crates/venv/src/virtualenv.rs b/crates/venv/src/virtualenv.rs index 52cf695..3799065 100644 --- a/crates/venv/src/virtualenv.rs +++ b/crates/venv/src/virtualenv.rs @@ -21,18 +21,17 @@ const SCRIPTS_DIR: &str = "bin"; #[cfg(unix)] fn executable_rel_path(interpreter: &Interpreter) -> Cow<'static, str> { - let interpreter = interpreter.raw(); - if interpreter.pypy_version.is_some() { + if interpreter.details.pypy_version.is_some() { Cow::Owned(format!( "{SCRIPTS_DIR}/pypy{major}.{minor}", - major = interpreter.version.major, - minor = interpreter.version.minor + major = interpreter.details.version.major, + minor = interpreter.details.version.minor )) } else { Cow::Owned(format!( "{SCRIPTS_DIR}/python{major}.{minor}", - major = interpreter.version.major, - minor = interpreter.version.minor + major = interpreter.details.version.major, + minor = interpreter.details.version.minor )) } } @@ -42,11 +41,11 @@ const SCRIPTS_DIR: &str = "Scripts"; #[cfg(windows)] fn executable_rel_path(interpreter: &Interpreter) -> Cow<'static, str> { - if interpreter.raw().pypy_version.is_some() { + if interpreter.details.pypy_version.is_some() { Cow::Owned(format!( "{SCRIPTS_DIR}\\pypy{major}.{minor}.exe", - major = interpreter.raw().version.major, - minor = interpreter.raw().version.minor + major = interpreter.details.version.major, + minor = interpreter.details.version.minor )) } else { Cow::Borrowed("Scripts\\python.exe") @@ -105,20 +104,18 @@ impl<'a> Virtualenv<'a> { let mut venv_interpreter = interpreter.clone(); if HOST.operating_system == OperatingSystem::Windows { - venv_interpreter.set_realpath(&path); + venv_interpreter.realpath = path.clone(); } - venv_interpreter.with_raw_mut(|venv_interpreter| { - if venv_interpreter.base_prefix.is_none() { - venv_interpreter.base_prefix = Some(venv_interpreter.prefix.clone()); - } - venv_interpreter.prefix = Cow::Owned(venv_dir.to_path_buf()); - venv_interpreter.path = Cow::Owned(path); - for path in venv_interpreter.paths.values_mut() { - if let Ok(prefix_rel_path) = path.strip_prefix(&interpreter.raw().prefix) { - *path = venv_interpreter.prefix.join(prefix_rel_path); - } + if venv_interpreter.details.base_prefix.is_none() { + venv_interpreter.details.base_prefix = Some(venv_interpreter.details.prefix.clone()); + } + venv_interpreter.details.prefix = venv_dir.to_path_buf(); + venv_interpreter.details.path = path; + for path in venv_interpreter.details.paths.values_mut() { + if let Ok(prefix_rel_path) = path.strip_prefix(&interpreter.details.prefix) { + *path = venv_interpreter.details.prefix.join(prefix_rel_path); } - }); + } Ok(venv_interpreter) } @@ -135,7 +132,7 @@ impl<'a> Virtualenv<'a> { let venv_interpreter = Self::host_interpreter(path.as_ref(), &interpreter)?; let site_packages_relpath = - if interpreter.raw().version.major == 3 && interpreter.raw().version.minor >= 3 { + if interpreter.details.version.major == 3 && interpreter.details.version.minor >= 3 { create_pep_405_venv( interpreter, path.as_ref(), @@ -166,12 +163,12 @@ impl<'a> Virtualenv<'a> { } pub fn prefix(&self) -> &Path { - &self.interpreter.raw().prefix + &self.interpreter.details.prefix } pub fn script_path(&self, rel_path: impl AsRef) -> PathBuf { self.interpreter - .raw() + .details .prefix .join(self.bin_dir_relpath) .join(rel_path) @@ -179,7 +176,7 @@ impl<'a> Virtualenv<'a> { pub fn site_packages_path(&self, rel_path: impl AsRef) -> PathBuf { self.interpreter - .raw() + .details .prefix .join(self.site_packages_relpath.as_ref()) .join(rel_path) @@ -187,9 +184,9 @@ impl<'a> Virtualenv<'a> { pub fn create_additional_pythons(&self) -> anyhow::Result<()> { for rel_path in self.interpreter.prefix_rel_paths() { - let dest = self.interpreter.raw().prefix.join(rel_path.as_ref()); + let dest = self.interpreter.details.prefix.join(rel_path.as_ref()); if !dest.exists() { - symlink_or_link_or_copy(self.interpreter.raw().path.as_ref(), dest, true)?; + symlink_or_link_or_copy(&self.interpreter.details.path, dest, true)?; } } Ok(()) @@ -337,11 +334,10 @@ fn create_pep_405_venv<'a>( ) -> anyhow::Result> { // See: https://peps.python.org/pep-0405/ let base_interpreter = interpreter.resolve_base_interpreter(scripts)?; - let raw_base_interpreter = base_interpreter.raw(); - let home = base_interpreter.realpath().parent().ok_or_else(|| { + let home = base_interpreter.realpath.parent().ok_or_else(|| { anyhow!( "Failed to calculate the home dir of venv base python {path}", - path = base_interpreter.realpath().display() + path = base_interpreter.realpath.display() ) })?; let executable_rel_path = executable_rel_path(&base_interpreter); @@ -349,9 +345,9 @@ fn create_pep_405_venv<'a>( let pyvenv_cfg = PyVenvCfg { home: Cow::Borrowed(home), include_system_site_packages, - version: Some(Cow::Owned(raw_base_interpreter.version.to_string())), + version: Some(Cow::Owned(base_interpreter.details.version.to_string())), prompt: prompt.map(Cow::Borrowed), - executable: Some(Cow::Borrowed(base_interpreter.realpath())), + executable: Some(Cow::Borrowed(&base_interpreter.realpath)), executable_rel_path: Cow::Borrowed(executable_rel_path), }; pyvenv_cfg.write(path)?; @@ -360,17 +356,17 @@ fn create_pep_405_venv<'a>( if let Some(parent) = venv_python.parent() { fs::create_dir_all(parent)?; } - linker.link(&venv_python, Some(base_interpreter.realpath()), false)?; + linker.link(&venv_python, Some(&base_interpreter.realpath), false)?; #[cfg(windows)] linker.link( &venv_python.with_file_name("pythonw.exe"), - Some(&base_interpreter.realpath().with_file_name("pythonw.exe")), + Some(&base_interpreter.realpath.with_file_name("pythonw.exe")), true, )?; let site_packages_relpath = site_packages_relpath(&base_interpreter); fs::create_dir_all(path.join(site_packages_relpath.as_ref()))?; if pip { - ensure_pip(raw_base_interpreter, &venv_python)?; + ensure_pip(&base_interpreter.details, &venv_python)?; } Ok(site_packages_relpath) } @@ -389,8 +385,7 @@ fn create_virtualenv_venv<'a>( .suffix(".py") .tempfile()?; script.write_all(virtualenv_script.contents().as_bytes())?; - let raw_interpreter = interpreter.raw(); - let mut command = Command::new(raw_interpreter.path.as_ref()); + let mut command = Command::new(&interpreter.details.path); command .arg(interpreter.hermetic_args()) .arg(script.path()) @@ -414,15 +409,15 @@ fn create_virtualenv_venv<'a>( workdir = path.display(), python_implementation = interpreter.marker_env().platform_python_implementation(), python_version = interpreter.marker_env().python_full_version(), - python_exe = raw_interpreter.path.as_ref().display(), + python_exe = interpreter.details.path.display(), stderr = String::from_utf8_lossy(&output.stderr) ) } - let home = interpreter.realpath().parent().ok_or_else(|| { + let home = interpreter.realpath.parent().ok_or_else(|| { anyhow!( "Failed to calculate the home dir of venv base python {path}", - path = interpreter.realpath().display() + path = interpreter.realpath.display() ) })?; let executable_rel_path = executable_rel_path(interpreter); @@ -432,9 +427,9 @@ fn create_virtualenv_venv<'a>( let pyvenv_cfg = PyVenvCfg { home: Cow::Borrowed(home), include_system_site_packages, - version: Some(Cow::Owned(raw_interpreter.version.to_string())), + version: Some(Cow::Owned(interpreter.details.version.to_string())), prompt: prompt.map(Cow::Borrowed), - executable: Some(Cow::Borrowed(interpreter.realpath())), + executable: Some(Cow::Borrowed(&interpreter.realpath)), executable_rel_path: Cow::Borrowed(executable_rel_path), }; pyvenv_cfg.write(path.as_ref())?; @@ -443,18 +438,21 @@ fn create_virtualenv_venv<'a>( #[cfg(windows)] linker.link(&venv_python.with_file_name("pythonw.exe"), None, true)?; if pip { - ensure_pip(raw_interpreter, &venv_python)?; + ensure_pip(&interpreter.details, &venv_python)?; } Ok(site_packages_relpath(interpreter)) } -fn ensure_pip(base_interpreter: &interpreter::RawInterpreter, python: &Path) -> anyhow::Result<()> { +fn ensure_pip( + base_interpreter: &interpreter::InterpreterDetails, + python: &Path, +) -> anyhow::Result<()> { if !base_interpreter.has_ensurepip { // TODO: Consider embedding or fetching: https://bootstrap.pypa.io/pip/ bail!( "Cannot install Pip since the selected interpreter does not have the `ensurepip` \ module: {interpreter}", - interpreter = base_interpreter.path.as_ref().display() + interpreter = base_interpreter.path.display() ) } @@ -473,9 +471,11 @@ fn site_packages_relpath<'a>(interpreter: &Interpreter) -> Cow<'a, Path> { // TODO: XXX: Confirm venv layouts for PyPy under Windows. return Cow::Borrowed(Path::new("Lib\\site-packages")); } - let raw_interpreter = interpreter.raw(); if interpreter.marker_env().platform_python_implementation() == "PyPy" - && (raw_interpreter.version.major, raw_interpreter.version.minor) < (3, 8) + && ( + interpreter.details.version.major, + interpreter.details.version.minor, + ) < (3, 8) { Cow::Borrowed(Path::new("site-packages")) } else { @@ -489,8 +489,8 @@ fn site_packages_relpath<'a>(interpreter: &Interpreter) -> Cow<'a, Path> { } else { "python" }, - major = raw_interpreter.version.major, - minor = raw_interpreter.version.minor + major = interpreter.details.version.major, + minor = interpreter.details.version.minor )) .join("site-packages"), ) @@ -514,10 +514,10 @@ mod tests { let identification_script = IdentifyInterpreter::read(&mut embedded_scripts).unwrap(); let interpreter = Interpreter::load(python_exe, &identification_script).unwrap(); let expected_prefix = interpreter - .raw() + .details .base_prefix .as_deref() - .unwrap_or(interpreter.raw().prefix.as_ref()) + .unwrap_or(interpreter.details.prefix.as_ref()) .to_owned(); let venv = Virtualenv::create( interpreter, @@ -531,7 +531,7 @@ mod tests { .unwrap(); assert_eq!( expected_prefix, - venv.interpreter.raw().base_prefix.as_deref().unwrap() + venv.interpreter.details.base_prefix.as_deref().unwrap() ) } } diff --git a/python/testing/bin/run-tests.py b/python/testing/bin/run-tests.py index 7ce1a40..1972576 100644 --- a/python/testing/bin/run-tests.py +++ b/python/testing/bin/run-tests.py @@ -70,12 +70,19 @@ def run_tests(): ), formatter_class=RawTextHelpFormatter, ) - arg_parser.add_argument( + profile = arg_parser.add_mutually_exclusive_group() + profile.add_argument( "--release", default=None, action="store_true", help="Build pexrc for use in tests in release mode.", ) + profile.add_argument( + "--debug", + default=None, + action="store_true", + help="Build pexrc for use in tests in debug mode and enable Rust backtraces.", + ) run_test_args = [] explicit_pytest_args = None # type: Optional[List[str]] @@ -101,6 +108,10 @@ def run_tests(): _PEXRC_TEST_SESSION_PEXRC_ROOT=seed_pexrc_root(session_dir, pexrc), PYTHONPATH=PYTHONPATH, ) + if options.debug: + env.update(RUST_BACKTRACE="1") + if options.debug or not options.release: + env.update(_PEXRC_TEST_DISABLE_ASSERT_FASTER="1") return subprocess.call( args=["pytest", "-n", "auto"] + pytest_args, cwd=os.path.abspath(os.path.join("python", "tests")), diff --git a/python/testing/compare.py b/python/testing/compare.py index f32e1cc..f577ec1 100644 --- a/python/testing/compare.py +++ b/python/testing/compare.py @@ -97,6 +97,9 @@ def _compare_results( assert traditional_result.stdout == injected_result.stdout +ASSERT_FASTER = not IS_WINDOWS and os.environ.get("_PEXRC_TEST_DISABLE_ASSERT_FASTER", "0") != "1" + + def compare( pex, # type: str python_args=(), # type: Iterable[str] @@ -136,7 +139,7 @@ def compare( # complication of virus scans that do slow things down much like the Mac case, leading to some # CI instability for no obvious gain. assert ( - IS_WINDOWS + not ASSERT_FASTER or not assert_faster or (injected_result.elapsed < traditional_result.elapsed) ), ( diff --git a/src/commands/platform/python.rs b/src/commands/platform/python.rs index 0f648a5..9d75b89 100644 --- a/src/commands/platform/python.rs +++ b/src/commands/platform/python.rs @@ -5,7 +5,8 @@ use std::path::{Path, PathBuf}; use clap::Args; use cli::{Json, Output}; -use python_platform::PlatformDetails; +use interpreter::Interpreter; +use scripts::{IdentifyInterpreter, Scripts}; #[derive(Clone)] enum PythonPlatform { @@ -122,8 +123,10 @@ impl Python { self.json.serialize(&mut out, &platform) } PythonPlatform::Interpreter(path) => { - let platform = PlatformDetails::python(path)?; - self.json.serialize(&mut out, &platform) + let identification_script = IdentifyInterpreter::read(&mut Scripts::Embedded)?; + let interpreter = Interpreter::load(path, &identification_script)?; + self.json + .serialize(&mut out, interpreter.platform_details()) } } } From f13639b4c15e2eec6067bd2df8fd7fa360012970 Mon Sep 17 00:00:00 2001 From: John Sirois Date: Sat, 30 May 2026 14:08:03 -0700 Subject: [PATCH 2/2] Use `PythonVersion` comparisons. --- crates/python-platform/src/tags.rs | 5 +-- crates/python-platform/src/version.rs | 10 ++++-- crates/venv/src/venv_pex.rs | 10 ++---- crates/venv/src/virtualenv.rs | 52 +++++++++++++-------------- 4 files changed, 38 insertions(+), 39 deletions(-) diff --git a/crates/python-platform/src/tags.rs b/crates/python-platform/src/tags.rs index c674f34..25f82e9 100644 --- a/crates/python-platform/src/tags.rs +++ b/crates/python-platform/src/tags.rs @@ -197,12 +197,13 @@ fn add_cpython_tags( // N.B.: PEP 384 was first implemented in Python 3.2. The free-threaded builds do not support // abi3. - let abi3 = (major, minor) >= (3, 2) && !python_version.free_threaded(); + let of_abi3_era = (major, minor) >= (3, 2); + let abi3 = of_abi3_era && !python_version.free_threaded(); // PEP 803 was first implemented in Python 3.15 but, per PEP 803, this returns tags going back // to Python 3.2 to mirror the abi3 implementation and leave open the possibility of abi3t // wheels supporting older Python versions. - let abi3t = (major, minor) >= (3, 2) && python_version.free_threaded(); + let abi3t = of_abi3_era && python_version.free_threaded(); if abi3 { for platform in platforms { diff --git a/crates/python-platform/src/version.rs b/crates/python-platform/src/version.rs index 5ca1c9d..aaa3a57 100644 --- a/crates/python-platform/src/version.rs +++ b/crates/python-platform/src/version.rs @@ -88,8 +88,14 @@ impl Display for PythonVersion { } impl PythonVersion { - pub fn simple(major: u8, minor: u8) -> Self { - Self::new(major, minor, None) + pub const fn simple(major: u8, minor: u8) -> Self { + Self { + major, + minor, + micro: 0, + releaselevel: ReleaseLevel::Final, + serial: 0, + } } pub fn new(major: u8, minor: u8, micro: Option) -> Self { diff --git a/crates/venv/src/venv_pex.rs b/crates/venv/src/venv_pex.rs index eeee347..9fddc8f 100644 --- a/crates/venv/src/venv_pex.rs +++ b/crates/venv/src/venv_pex.rs @@ -31,6 +31,7 @@ use pex::{ filter_zipped_user_source, }; use platform::{Perms, mark_executable, path_as_bytes, path_as_str, symlink_or_link_or_copy}; +use python_platform::PythonVersion; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use scripts::{ Scripts, @@ -1080,14 +1081,9 @@ fn write_pex_extra_sys_path_support_files( pex_extra_sys_path_py_fp .write_all(VenvPexExtraSysPathPy::read(scripts)?.contents().as_bytes())?; - let python_version = { - let version = venv.interpreter.details.version; - (version.major, version.minor) - }; - // Starting with Python 3.15 .start files trump import lines in .pth files. // See: https://peps.python.org/pep-0829/#abstract - if python_version >= (3, 15) { + if venv.interpreter.details.version >= PythonVersion::simple(3, 15) { let mut pex_extra_sys_path_start_fp = File::create_new(venv.site_packages_path("PEX_EXTRA_SYS_PATH.start"))?; pex_extra_sys_path_start_fp.write_all( @@ -1099,7 +1095,7 @@ fn write_pex_extra_sys_path_support_files( // After ~Python 3.20 .pth import lines will start to raise warnings; so we no longer emit a // .pth compatibility bridge. See: https://peps.python.org/pep-0829/#abstract - if python_version < (3, 20) { + if venv.interpreter.details.version < PythonVersion::simple(3, 20) { let mut pex_extra_sys_path_pth_fp = File::create_new(venv.site_packages_path("PEX_EXTRA_SYS_PATH.pth"))?; pex_extra_sys_path_pth_fp diff --git a/crates/venv/src/virtualenv.rs b/crates/venv/src/virtualenv.rs index 3799065..c72beb5 100644 --- a/crates/venv/src/virtualenv.rs +++ b/crates/venv/src/virtualenv.rs @@ -12,7 +12,7 @@ use fs_err::File; use interpreter::Interpreter; use logging_timer::time; use platform::symlink_or_link_or_copy; -use python_platform::PythonPlatform; +use python_platform::{PythonPlatform, PythonVersion}; use scripts::{IdentifyInterpreter, Scripts, VendoredVirtualenv}; use target_lexicon::{HOST, OperatingSystem}; @@ -131,29 +131,28 @@ impl<'a> Virtualenv<'a> { ) -> anyhow::Result { let venv_interpreter = Self::host_interpreter(path.as_ref(), &interpreter)?; - let site_packages_relpath = - if interpreter.details.version.major == 3 && interpreter.details.version.minor >= 3 { - create_pep_405_venv( - interpreter, - path.as_ref(), - linker, - include_system_site_packages, - scripts, - pip, - prompt, - )? - } else { - let virtualenv_script = VendoredVirtualenv::read(scripts)?; - create_virtualenv_venv( - &interpreter, - path.as_ref(), - linker, - virtualenv_script, - include_system_site_packages, - pip, - prompt, - )? - }; + let site_packages_relpath = if interpreter.details.version >= PythonVersion::simple(3, 3) { + create_pep_405_venv( + interpreter, + path.as_ref(), + linker, + include_system_site_packages, + scripts, + pip, + prompt, + )? + } else { + let virtualenv_script = VendoredVirtualenv::read(scripts)?; + create_virtualenv_venv( + &interpreter, + path.as_ref(), + linker, + virtualenv_script, + include_system_site_packages, + pip, + prompt, + )? + }; Ok(Self { interpreter: venv_interpreter, @@ -472,10 +471,7 @@ fn site_packages_relpath<'a>(interpreter: &Interpreter) -> Cow<'a, Path> { return Cow::Borrowed(Path::new("Lib\\site-packages")); } if interpreter.marker_env().platform_python_implementation() == "PyPy" - && ( - interpreter.details.version.major, - interpreter.details.version.minor, - ) < (3, 8) + && interpreter.details.version < PythonVersion::simple(3, 8) { Cow::Borrowed(Path::new("site-packages")) } else {