Refactor os, arch, and libc information into a shared Platform type (#15027)

Addresses this outstanding item from a previous review
https://github.com/astral-sh/uv/pull/13724#discussion_r2114867288

I'm interested in this now for consolidating some logic in #12731
This commit is contained in:
Zanie Blue
2025-08-13 09:02:55 -05:00
committed by GitHub
parent 323aa8f332
commit 78c8c711fa
8 changed files with 698 additions and 172 deletions
+162 -70
View File
@@ -10,7 +10,7 @@ use uv_cache::Cache;
use uv_client::BaseClientBuilder;
use uv_configuration::Preview;
use uv_pep440::{Prerelease, Version};
use uv_platform::{Arch, Libc, Os};
use uv_platform::{Arch, Libc, Os, Platform};
use crate::discovery::{
EnvironmentPreference, PythonRequest, find_best_python_installation, find_python_installation,
@@ -352,9 +352,7 @@ pub struct PythonInstallationKey {
pub(crate) minor: u8,
pub(crate) patch: u8,
pub(crate) prerelease: Option<Prerelease>,
pub(crate) os: Os,
pub(crate) arch: Arch,
pub(crate) libc: Libc,
pub(crate) platform: Platform,
pub(crate) variant: PythonVariant,
}
@@ -365,9 +363,7 @@ impl PythonInstallationKey {
minor: u8,
patch: u8,
prerelease: Option<Prerelease>,
os: Os,
arch: Arch,
libc: Libc,
platform: Platform,
variant: PythonVariant,
) -> Self {
Self {
@@ -376,9 +372,7 @@ impl PythonInstallationKey {
minor,
patch,
prerelease,
os,
arch,
libc,
platform,
variant,
}
}
@@ -386,9 +380,7 @@ impl PythonInstallationKey {
pub fn new_from_version(
implementation: LenientImplementationName,
version: &PythonVersion,
os: Os,
arch: Arch,
libc: Libc,
platform: Platform,
variant: PythonVariant,
) -> Self {
Self {
@@ -397,9 +389,7 @@ impl PythonInstallationKey {
minor: version.minor(),
patch: version.patch().unwrap_or_default(),
prerelease: version.pre(),
os,
arch,
libc,
platform,
variant,
}
}
@@ -434,16 +424,20 @@ impl PythonInstallationKey {
self.minor
}
pub fn platform(&self) -> &Platform {
&self.platform
}
pub fn arch(&self) -> &Arch {
&self.arch
&self.platform.arch
}
pub fn os(&self) -> &Os {
&self.os
&self.platform.os
}
pub fn libc(&self) -> &Libc {
&self.libc
&self.platform.libc
}
pub fn variant(&self) -> &PythonVariant {
@@ -489,7 +483,7 @@ impl fmt::Display for PythonInstallationKey {
};
write!(
f,
"{}-{}.{}.{}{}{}-{}-{}-{}",
"{}-{}.{}.{}{}{}-{}",
self.implementation,
self.major,
self.minor,
@@ -498,9 +492,7 @@ impl fmt::Display for PythonInstallationKey {
.map(|pre| pre.to_string())
.unwrap_or_default(),
variant,
self.os,
self.arch,
self.libc
self.platform
)
}
}
@@ -510,31 +502,25 @@ impl FromStr for PythonInstallationKey {
fn from_str(key: &str) -> Result<Self, Self::Err> {
let parts = key.split('-').collect::<Vec<_>>();
let [implementation, version, os, arch, libc] = parts.as_slice() else {
// We need exactly implementation-version-os-arch-libc
if parts.len() != 5 {
return Err(PythonInstallationKeyError::ParseError(
key.to_string(),
"not enough `-`-separated values".to_string(),
format!(
"expected exactly 5 `-`-separated values, got {}",
parts.len()
),
));
}
let [implementation_str, version_str, os, arch, libc] = parts.as_slice() else {
unreachable!()
};
let implementation = LenientImplementationName::from(*implementation);
let implementation = LenientImplementationName::from(*implementation_str);
let os = Os::from_str(os).map_err(|err| {
PythonInstallationKeyError::ParseError(key.to_string(), format!("invalid OS: {err}"))
})?;
let arch = Arch::from_str(arch).map_err(|err| {
PythonInstallationKeyError::ParseError(
key.to_string(),
format!("invalid architecture: {err}"),
)
})?;
let libc = Libc::from_str(libc).map_err(|err| {
PythonInstallationKeyError::ParseError(key.to_string(), format!("invalid libc: {err}"))
})?;
let (version, variant) = match version.split_once('+') {
let (version, variant) = match version_str.split_once('+') {
Some((version, variant)) => {
let variant = PythonVariant::from_str(variant).map_err(|()| {
PythonInstallationKeyError::ParseError(
@@ -544,7 +530,7 @@ impl FromStr for PythonInstallationKey {
})?;
(version, variant)
}
None => (*version, PythonVariant::Default),
None => (*version_str, PythonVariant::Default),
};
let version = PythonVersion::from_str(version).map_err(|err| {
@@ -554,14 +540,22 @@ impl FromStr for PythonInstallationKey {
)
})?;
Ok(Self::new_from_version(
let platform = Platform::from_parts(os, arch, libc).map_err(|err| {
PythonInstallationKeyError::ParseError(
key.to_string(),
format!("invalid platform: {err}"),
)
})?;
Ok(Self {
implementation,
&version,
os,
arch,
libc,
major: version.major(),
minor: version.minor(),
patch: version.patch().unwrap_or_default(),
prerelease: version.pre(),
platform,
variant,
))
})
}
}
@@ -576,10 +570,8 @@ impl Ord for PythonInstallationKey {
self.implementation
.cmp(&other.implementation)
.then_with(|| self.version().cmp(&other.version()))
.then_with(|| self.os.to_string().cmp(&other.os.to_string()))
// Architectures are sorted in preferred order, with native architectures first
.then_with(|| self.arch.cmp(&other.arch).reverse())
.then_with(|| self.libc.to_string().cmp(&other.libc.to_string()))
// Platforms are sorted in preferred order for the target
.then_with(|| self.platform.cmp(&other.platform).reverse())
// Python variants are sorted in preferred order, with `Default` first
.then_with(|| self.variant.cmp(&other.variant).reverse())
}
@@ -632,14 +624,8 @@ impl fmt::Display for PythonInstallationMinorVersionKey {
};
write!(
f,
"{}-{}.{}{}-{}-{}-{}",
self.0.implementation,
self.0.major,
self.0.minor,
variant,
self.0.os,
self.0.arch,
self.0.libc,
"{}-{}.{}{}-{}",
self.0.implementation, self.0.major, self.0.minor, variant, self.0.platform,
)
}
}
@@ -653,9 +639,9 @@ impl fmt::Debug for PythonInstallationMinorVersionKey {
.field("major", &self.0.major)
.field("minor", &self.0.minor)
.field("variant", &self.0.variant)
.field("os", &self.0.os)
.field("arch", &self.0.arch)
.field("libc", &self.0.libc)
.field("os", &self.0.platform.os)
.field("arch", &self.0.platform.arch)
.field("libc", &self.0.platform.libc)
.finish()
}
}
@@ -667,9 +653,7 @@ impl PartialEq for PythonInstallationMinorVersionKey {
self.0.implementation == other.0.implementation
&& self.0.major == other.0.major
&& self.0.minor == other.0.minor
&& self.0.os == other.0.os
&& self.0.arch == other.0.arch
&& self.0.libc == other.0.libc
&& self.0.platform == other.0.platform
&& self.0.variant == other.0.variant
}
}
@@ -681,9 +665,7 @@ impl Hash for PythonInstallationMinorVersionKey {
self.0.implementation.hash(state);
self.0.major.hash(state);
self.0.minor.hash(state);
self.0.os.hash(state);
self.0.arch.hash(state);
self.0.libc.hash(state);
self.0.platform.hash(state);
self.0.variant.hash(state);
}
}
@@ -693,3 +675,113 @@ impl From<PythonInstallationKey> for PythonInstallationMinorVersionKey {
Self(key)
}
}
#[cfg(test)]
mod tests {
use super::*;
use uv_platform::ArchVariant;
#[test]
fn test_python_installation_key_from_str() {
// Test basic parsing
let key = PythonInstallationKey::from_str("cpython-3.12.0-linux-x86_64-gnu").unwrap();
assert_eq!(
key.implementation,
LenientImplementationName::Known(ImplementationName::CPython)
);
assert_eq!(key.major, 3);
assert_eq!(key.minor, 12);
assert_eq!(key.patch, 0);
assert_eq!(
key.platform.os,
Os::new(target_lexicon::OperatingSystem::Linux)
);
assert_eq!(
key.platform.arch,
Arch::new(target_lexicon::Architecture::X86_64, None)
);
assert_eq!(
key.platform.libc,
Libc::Some(target_lexicon::Environment::Gnu)
);
// Test with architecture variant
let key = PythonInstallationKey::from_str("cpython-3.11.2-linux-x86_64_v3-musl").unwrap();
assert_eq!(
key.implementation,
LenientImplementationName::Known(ImplementationName::CPython)
);
assert_eq!(key.major, 3);
assert_eq!(key.minor, 11);
assert_eq!(key.patch, 2);
assert_eq!(
key.platform.os,
Os::new(target_lexicon::OperatingSystem::Linux)
);
assert_eq!(
key.platform.arch,
Arch::new(target_lexicon::Architecture::X86_64, Some(ArchVariant::V3))
);
assert_eq!(
key.platform.libc,
Libc::Some(target_lexicon::Environment::Musl)
);
// Test with Python variant (freethreaded)
let key = PythonInstallationKey::from_str("cpython-3.13.0+freethreaded-macos-aarch64-none")
.unwrap();
assert_eq!(
key.implementation,
LenientImplementationName::Known(ImplementationName::CPython)
);
assert_eq!(key.major, 3);
assert_eq!(key.minor, 13);
assert_eq!(key.patch, 0);
assert_eq!(key.variant, PythonVariant::Freethreaded);
assert_eq!(
key.platform.os,
Os::new(target_lexicon::OperatingSystem::Darwin(None))
);
assert_eq!(
key.platform.arch,
Arch::new(
target_lexicon::Architecture::Aarch64(target_lexicon::Aarch64Architecture::Aarch64),
None
)
);
assert_eq!(key.platform.libc, Libc::None);
// Test error cases
assert!(PythonInstallationKey::from_str("cpython-3.12.0-linux-x86_64").is_err());
assert!(PythonInstallationKey::from_str("cpython-3.12.0").is_err());
assert!(PythonInstallationKey::from_str("cpython").is_err());
}
#[test]
fn test_python_installation_key_display() {
let key = PythonInstallationKey {
implementation: LenientImplementationName::from("cpython"),
major: 3,
minor: 12,
patch: 0,
prerelease: None,
platform: Platform::from_str("linux-x86_64-gnu").unwrap(),
variant: PythonVariant::Default,
};
assert_eq!(key.to_string(), "cpython-3.12.0-linux-x86_64-gnu");
let key_with_variant = PythonInstallationKey {
implementation: LenientImplementationName::from("cpython"),
major: 3,
minor: 13,
patch: 0,
prerelease: None,
platform: Platform::from_str("macos-aarch64-none").unwrap(),
variant: PythonVariant::Freethreaded,
};
assert_eq!(
key_with_variant.to_string(),
"cpython-3.13.0+freethreaded-macos-aarch64-none"
);
}
}