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:
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user