diff --git a/Cargo.lock b/Cargo.lock index ff72f1418..5b2fd66b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4732,6 +4732,7 @@ dependencies = [ "uv-pep440", "uv-pep508", "uv-performance-memory-allocator", + "uv-platform", "uv-platform-tags", "uv-publish", "uv-pypi-types", @@ -5553,6 +5554,23 @@ dependencies = [ "tikv-jemallocator", ] +[[package]] +name = "uv-platform" +version = "0.0.1" +dependencies = [ + "fs-err", + "goblin", + "indoc", + "procfs", + "regex", + "target-lexicon", + "thiserror 2.0.12", + "tracing", + "uv-fs", + "uv-platform-tags", + "uv-static", +] + [[package]] name = "uv-platform-tags" version = "0.0.1" @@ -5646,14 +5664,12 @@ dependencies = [ "dunce", "fs-err", "futures", - "goblin", "indexmap", "indoc", "insta", "itertools 0.14.0", "once_cell", "owo-colors", - "procfs", "ref-cast", "regex", "reqwest", @@ -5687,6 +5703,7 @@ dependencies = [ "uv-install-wheel", "uv-pep440", "uv-pep508", + "uv-platform", "uv-platform-tags", "uv-pypi-types", "uv-redacted", diff --git a/Cargo.toml b/Cargo.toml index f11b91556..6e9742bc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ uv-once-map = { path = "crates/uv-once-map" } uv-options-metadata = { path = "crates/uv-options-metadata" } uv-pep440 = { path = "crates/uv-pep440", features = ["tracing", "rkyv", "version-ranges"] } uv-pep508 = { path = "crates/uv-pep508", features = ["non-pep508-extensions"] } +uv-platform = { path = "crates/uv-platform" } uv-platform-tags = { path = "crates/uv-platform-tags" } uv-publish = { path = "crates/uv-publish" } uv-pypi-types = { path = "crates/uv-pypi-types" } diff --git a/crates/uv-platform/Cargo.toml b/crates/uv-platform/Cargo.toml new file mode 100644 index 000000000..0bb891ed9 --- /dev/null +++ b/crates/uv-platform/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "uv-platform" +version = "0.0.1" +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +authors = { workspace = true } +license = { workspace = true } + +[lib] +doctest = false + +[lints] +workspace = true + +[dependencies] +uv-static = { workspace = true } +uv-fs = { workspace = true } +uv-platform-tags = { workspace = true } + +fs-err = { workspace = true } +goblin = { workspace = true } +regex = { workspace = true } +target-lexicon = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } + +[target.'cfg(target_os = "linux")'.dependencies] +procfs = { workspace = true } + +[dev-dependencies] +indoc = { workspace = true } diff --git a/crates/uv-platform/src/arch.rs b/crates/uv-platform/src/arch.rs new file mode 100644 index 000000000..f64312489 --- /dev/null +++ b/crates/uv-platform/src/arch.rs @@ -0,0 +1,249 @@ +use crate::Error; +use std::fmt::Display; +use std::str::FromStr; +use std::{cmp, fmt}; + +/// Architecture variants, e.g., with support for different instruction sets +#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, Ord, PartialOrd)] +pub enum ArchVariant { + /// Targets 64-bit Intel/AMD CPUs newer than Nehalem (2008). + /// Includes SSE3, SSE4 and other post-2003 CPU instructions. + V2, + /// Targets 64-bit Intel/AMD CPUs newer than Haswell (2013) and Excavator (2015). + /// Includes AVX, AVX2, MOVBE and other newer CPU instructions. + V3, + /// Targets 64-bit Intel/AMD CPUs with AVX-512 instructions (post-2017 Intel CPUs). + /// Many post-2017 Intel CPUs do not support AVX-512. + V4, +} + +#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)] +pub struct Arch { + pub(crate) family: target_lexicon::Architecture, + pub(crate) variant: Option, +} + +impl Ord for Arch { + fn cmp(&self, other: &Self) -> cmp::Ordering { + if self.family == other.family { + return self.variant.cmp(&other.variant); + } + + // For the time being, manually make aarch64 windows disfavored + // on its own host platform, because most packages don't have wheels for + // aarch64 windows, making emulation more useful than native execution! + // + // The reason we do this in "sorting" and not "supports" is so that we don't + // *refuse* to use an aarch64 windows pythons if they happen to be installed + // and nothing else is available. + // + // Similarly if someone manually requests an aarch64 windows install, we + // should respect that request (this is the way users should "override" + // this behaviour). + let preferred = if cfg!(all(windows, target_arch = "aarch64")) { + Arch { + family: target_lexicon::Architecture::X86_64, + variant: None, + } + } else { + // Prefer native architectures + Arch::from_env() + }; + + match ( + self.family == preferred.family, + other.family == preferred.family, + ) { + (true, true) => unreachable!(), + (true, false) => cmp::Ordering::Less, + (false, true) => cmp::Ordering::Greater, + (false, false) => { + // Both non-preferred, fallback to lexicographic order + self.family.to_string().cmp(&other.family.to_string()) + } + } + } +} + +impl PartialOrd for Arch { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Arch { + pub fn new(family: target_lexicon::Architecture, variant: Option) -> Self { + Self { family, variant } + } + + pub fn from_env() -> Self { + Self { + family: target_lexicon::HOST.architecture, + variant: None, + } + } + + /// Does the current architecture support running the other? + /// + /// When the architecture is equal, this is always true. Otherwise, this is true if the + /// architecture is transparently emulated or is a microarchitecture with worse performance + /// characteristics. + pub fn supports(self, other: Self) -> bool { + if self == other { + return true; + } + + // TODO: Implement `variant` support checks + + // Windows ARM64 runs emulated x86_64 binaries transparently + // Similarly, macOS aarch64 runs emulated x86_64 binaries transparently if you have Rosetta + // installed. We don't try to be clever and check if that's the case here, we just assume + // that if x86_64 distributions are available, they're usable. + if (cfg!(windows) || cfg!(target_os = "macos")) + && matches!(self.family, target_lexicon::Architecture::Aarch64(_)) + { + return other.family == target_lexicon::Architecture::X86_64; + } + + false + } + + pub fn family(&self) -> target_lexicon::Architecture { + self.family + } + + pub fn is_arm(&self) -> bool { + matches!(self.family, target_lexicon::Architecture::Arm(_)) + } +} + +impl Display for Arch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.family { + target_lexicon::Architecture::X86_32(target_lexicon::X86_32Architecture::I686) => { + write!(f, "x86")?; + } + inner => write!(f, "{inner}")?, + } + if let Some(variant) = self.variant { + write!(f, "_{variant}")?; + } + Ok(()) + } +} + +impl FromStr for Arch { + type Err = Error; + + fn from_str(s: &str) -> Result { + fn parse_family(s: &str) -> Result { + let inner = match s { + // Allow users to specify "x86" as a shorthand for the "i686" variant, they should not need + // to specify the exact architecture and this variant is what we have downloads for. + "x86" => { + target_lexicon::Architecture::X86_32(target_lexicon::X86_32Architecture::I686) + } + _ => target_lexicon::Architecture::from_str(s) + .map_err(|()| Error::UnknownArch(s.to_string()))?, + }; + if matches!(inner, target_lexicon::Architecture::Unknown) { + return Err(Error::UnknownArch(s.to_string())); + } + Ok(inner) + } + + // First check for a variant + if let Some((Ok(family), Ok(variant))) = s + .rsplit_once('_') + .map(|(family, variant)| (parse_family(family), ArchVariant::from_str(variant))) + { + // We only support variants for `x86_64` right now + if !matches!(family, target_lexicon::Architecture::X86_64) { + return Err(Error::UnsupportedVariant( + variant.to_string(), + family.to_string(), + )); + } + return Ok(Self { + family, + variant: Some(variant), + }); + } + + let family = parse_family(s)?; + + Ok(Self { + family, + variant: None, + }) + } +} + +impl FromStr for ArchVariant { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "v2" => Ok(Self::V2), + "v3" => Ok(Self::V3), + "v4" => Ok(Self::V4), + _ => Err(()), + } + } +} + +impl Display for ArchVariant { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::V2 => write!(f, "v2"), + Self::V3 => write!(f, "v3"), + Self::V4 => write!(f, "v4"), + } + } +} + +impl From<&uv_platform_tags::Arch> for Arch { + fn from(value: &uv_platform_tags::Arch) -> Self { + match value { + uv_platform_tags::Arch::Aarch64 => Arch::new( + target_lexicon::Architecture::Aarch64(target_lexicon::Aarch64Architecture::Aarch64), + None, + ), + uv_platform_tags::Arch::Armv5TEL => Arch::new( + target_lexicon::Architecture::Arm(target_lexicon::ArmArchitecture::Armv5te), + None, + ), + uv_platform_tags::Arch::Armv6L => Arch::new( + target_lexicon::Architecture::Arm(target_lexicon::ArmArchitecture::Armv6), + None, + ), + uv_platform_tags::Arch::Armv7L => Arch::new( + target_lexicon::Architecture::Arm(target_lexicon::ArmArchitecture::Armv7), + None, + ), + uv_platform_tags::Arch::S390X => Arch::new(target_lexicon::Architecture::S390x, None), + uv_platform_tags::Arch::Powerpc => { + Arch::new(target_lexicon::Architecture::Powerpc, None) + } + uv_platform_tags::Arch::Powerpc64 => { + Arch::new(target_lexicon::Architecture::Powerpc64, None) + } + uv_platform_tags::Arch::Powerpc64Le => { + Arch::new(target_lexicon::Architecture::Powerpc64le, None) + } + uv_platform_tags::Arch::X86 => Arch::new( + target_lexicon::Architecture::X86_32(target_lexicon::X86_32Architecture::I686), + None, + ), + uv_platform_tags::Arch::X86_64 => Arch::new(target_lexicon::Architecture::X86_64, None), + uv_platform_tags::Arch::LoongArch64 => { + Arch::new(target_lexicon::Architecture::LoongArch64, None) + } + uv_platform_tags::Arch::Riscv64 => Arch::new( + target_lexicon::Architecture::Riscv64(target_lexicon::Riscv64Architecture::Riscv64), + None, + ), + uv_platform_tags::Arch::Wasm32 => Arch::new(target_lexicon::Architecture::Wasm32, None), + } + } +} diff --git a/crates/uv-python/src/cpuinfo.rs b/crates/uv-platform/src/cpuinfo.rs similarity index 94% rename from crates/uv-python/src/cpuinfo.rs rename to crates/uv-platform/src/cpuinfo.rs index f0827886b..89a4f89e9 100644 --- a/crates/uv-python/src/cpuinfo.rs +++ b/crates/uv-platform/src/cpuinfo.rs @@ -1,6 +1,6 @@ //! Fetches CPU information. -use anyhow::Error; +use std::io::Error; #[cfg(target_os = "linux")] use procfs::{CpuInfo, Current}; @@ -14,7 +14,7 @@ use procfs::{CpuInfo, Current}; /// More information on this can be found in the [Debian ARM Hard Float Port documentation](https://wiki.debian.org/ArmHardFloatPort#VFP). #[cfg(target_os = "linux")] pub(crate) fn detect_hardware_floating_point_support() -> Result { - let cpu_info = CpuInfo::current()?; + let cpu_info = CpuInfo::current().map_err(Error::other)?; if let Some(features) = cpu_info.fields.get("Features") { if features.contains("vfp") { return Ok(true); // "vfp" found: hard-float (gnueabihf) detected diff --git a/crates/uv-platform/src/lib.rs b/crates/uv-platform/src/lib.rs new file mode 100644 index 000000000..7eb23875a --- /dev/null +++ b/crates/uv-platform/src/lib.rs @@ -0,0 +1,26 @@ +//! Platform detection for operating system, architecture, and libc. + +use thiserror::Error; + +pub use crate::arch::{Arch, ArchVariant}; +pub use crate::libc::{Libc, LibcDetectionError, LibcVersion}; +pub use crate::os::Os; + +mod arch; +mod cpuinfo; +mod libc; +mod os; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Unknown operating system: {0}")] + UnknownOs(String), + #[error("Unknown architecture: {0}")] + UnknownArch(String), + #[error("Unknown libc environment: {0}")] + UnknownLibc(String), + #[error("Unsupported variant `{0}` for architecture `{1}`")] + UnsupportedVariant(String, String), + #[error(transparent)] + LibcDetectionError(#[from] crate::libc::LibcDetectionError), +} diff --git a/crates/uv-python/src/libc.rs b/crates/uv-platform/src/libc.rs similarity index 77% rename from crates/uv-python/src/libc.rs rename to crates/uv-platform/src/libc.rs index 40950ae08..184f0487c 100644 --- a/crates/uv-python/src/libc.rs +++ b/crates/uv-platform/src/libc.rs @@ -3,18 +3,22 @@ //! Taken from `glibc_version` (), //! which used the Apache 2.0 license (but not the MIT license) +use crate::cpuinfo::detect_hardware_floating_point_support; use fs_err as fs; use goblin::elf::Elf; use regex::Regex; +use std::fmt::Display; use std::io; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; +use std::str::FromStr; use std::sync::LazyLock; -use thiserror::Error; +use std::{env, fmt}; use tracing::trace; use uv_fs::Simplified; +use uv_static::EnvVars; -#[derive(Debug, Error)] +#[derive(Debug, thiserror::Error)] pub enum LibcDetectionError { #[error( "Could not detect either glibc version nor musl libc version, at least one of which is required" @@ -45,11 +49,89 @@ pub enum LibcDetectionError { /// We support glibc (manylinux) and musl (musllinux) on linux. #[derive(Debug, PartialEq, Eq)] -pub(crate) enum LibcVersion { +pub enum LibcVersion { Manylinux { major: u32, minor: u32 }, Musllinux { major: u32, minor: u32 }, } +#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)] +pub enum Libc { + Some(target_lexicon::Environment), + None, +} + +impl Libc { + pub fn from_env() -> Result { + match env::consts::OS { + "linux" => { + if let Ok(libc) = env::var(EnvVars::UV_LIBC) { + if !libc.is_empty() { + return Self::from_str(&libc); + } + } + + Ok(Self::Some(match detect_linux_libc()? { + LibcVersion::Manylinux { .. } => match env::consts::ARCH { + // Checks if the CPU supports hardware floating-point operations. + // Depending on the result, it selects either the `gnueabihf` (hard-float) or `gnueabi` (soft-float) environment. + // download-metadata.json only includes armv7. + "arm" | "armv5te" | "armv7" => { + match detect_hardware_floating_point_support() { + Ok(true) => target_lexicon::Environment::Gnueabihf, + Ok(false) => target_lexicon::Environment::Gnueabi, + Err(_) => target_lexicon::Environment::Gnu, + } + } + _ => target_lexicon::Environment::Gnu, + }, + LibcVersion::Musllinux { .. } => target_lexicon::Environment::Musl, + })) + } + "windows" | "macos" => Ok(Self::None), + // Use `None` on platforms without explicit support. + _ => Ok(Self::None), + } + } + + pub fn is_musl(&self) -> bool { + matches!(self, Self::Some(target_lexicon::Environment::Musl)) + } +} + +impl FromStr for Libc { + type Err = crate::Error; + + fn from_str(s: &str) -> Result { + match s { + "gnu" => Ok(Self::Some(target_lexicon::Environment::Gnu)), + "gnueabi" => Ok(Self::Some(target_lexicon::Environment::Gnueabi)), + "gnueabihf" => Ok(Self::Some(target_lexicon::Environment::Gnueabihf)), + "musl" => Ok(Self::Some(target_lexicon::Environment::Musl)), + "none" => Ok(Self::None), + _ => Err(crate::Error::UnknownLibc(s.to_string())), + } + } +} + +impl Display for Libc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Some(env) => write!(f, "{env}"), + Self::None => write!(f, "none"), + } + } +} + +impl From<&uv_platform_tags::Os> for Libc { + fn from(value: &uv_platform_tags::Os) -> Self { + match value { + uv_platform_tags::Os::Manylinux { .. } => Libc::Some(target_lexicon::Environment::Gnu), + uv_platform_tags::Os::Musllinux { .. } => Libc::Some(target_lexicon::Environment::Musl), + _ => Libc::None, + } + } +} + /// Determine whether we're running glibc or musl and in which version, given we are on linux. /// /// Normally, we determine this from the python interpreter, which is more accurate, but when diff --git a/crates/uv-platform/src/os.rs b/crates/uv-platform/src/os.rs new file mode 100644 index 000000000..01f896f3f --- /dev/null +++ b/crates/uv-platform/src/os.rs @@ -0,0 +1,88 @@ +use crate::Error; +use std::fmt; +use std::fmt::Display; +use std::ops::Deref; +use std::str::FromStr; + +#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)] +pub struct Os(pub(crate) target_lexicon::OperatingSystem); + +impl Os { + pub fn new(os: target_lexicon::OperatingSystem) -> Self { + Self(os) + } + + pub fn from_env() -> Self { + Self(target_lexicon::HOST.operating_system) + } + + pub fn is_windows(&self) -> bool { + matches!(self.0, target_lexicon::OperatingSystem::Windows) + } +} + +impl Display for Os { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &**self { + target_lexicon::OperatingSystem::Darwin(_) => write!(f, "macos"), + inner => write!(f, "{inner}"), + } + } +} + +impl FromStr for Os { + type Err = Error; + + fn from_str(s: &str) -> Result { + let inner = match s { + "macos" => target_lexicon::OperatingSystem::Darwin(None), + _ => target_lexicon::OperatingSystem::from_str(s) + .map_err(|()| Error::UnknownOs(s.to_string()))?, + }; + if matches!(inner, target_lexicon::OperatingSystem::Unknown) { + return Err(Error::UnknownOs(s.to_string())); + } + Ok(Self(inner)) + } +} + +impl Deref for Os { + type Target = target_lexicon::OperatingSystem; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<&uv_platform_tags::Os> for Os { + fn from(value: &uv_platform_tags::Os) -> Self { + match value { + uv_platform_tags::Os::Dragonfly { .. } => { + Os::new(target_lexicon::OperatingSystem::Dragonfly) + } + uv_platform_tags::Os::FreeBsd { .. } => { + Os::new(target_lexicon::OperatingSystem::Freebsd) + } + uv_platform_tags::Os::Haiku { .. } => Os::new(target_lexicon::OperatingSystem::Haiku), + uv_platform_tags::Os::Illumos { .. } => { + Os::new(target_lexicon::OperatingSystem::Illumos) + } + uv_platform_tags::Os::Macos { .. } => { + Os::new(target_lexicon::OperatingSystem::Darwin(None)) + } + uv_platform_tags::Os::Manylinux { .. } + | uv_platform_tags::Os::Musllinux { .. } + | uv_platform_tags::Os::Android { .. } => { + Os::new(target_lexicon::OperatingSystem::Linux) + } + uv_platform_tags::Os::NetBsd { .. } => Os::new(target_lexicon::OperatingSystem::Netbsd), + uv_platform_tags::Os::OpenBsd { .. } => { + Os::new(target_lexicon::OperatingSystem::Openbsd) + } + uv_platform_tags::Os::Windows => Os::new(target_lexicon::OperatingSystem::Windows), + uv_platform_tags::Os::Pyodide { .. } => { + Os::new(target_lexicon::OperatingSystem::Emscripten) + } + } + } +} diff --git a/crates/uv-python/Cargo.toml b/crates/uv-python/Cargo.toml index 53c70ba5f..1c6f09b15 100644 --- a/crates/uv-python/Cargo.toml +++ b/crates/uv-python/Cargo.toml @@ -28,6 +28,7 @@ uv-fs = { workspace = true } uv-install-wheel = { workspace = true } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } +uv-platform = { workspace = true } uv-platform-tags = { workspace = true } uv-pypi-types = { workspace = true } uv-redacted = { workspace = true } @@ -42,7 +43,6 @@ configparser = { workspace = true } dunce = { workspace = true } fs-err = { workspace = true, features = ["tokio"] } futures = { workspace = true } -goblin = { workspace = true, default-features = false } indexmap = { workspace = true } itertools = { workspace = true } owo-colors = { workspace = true } @@ -68,9 +68,6 @@ url = { workspace = true } which = { workspace = true } once_cell = { workspace = true } -[target.'cfg(target_os = "linux")'.dependencies] -procfs = { workspace = true } - [target.'cfg(target_os = "windows")'.dependencies] windows-registry = { workspace = true } windows-result = { workspace = true } diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 466ea4b0f..496191818 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -3066,8 +3066,8 @@ mod tests { discovery::{PythonRequest, VersionRequest}, downloads::{ArchRequest, PythonDownloadRequest}, implementation::ImplementationName, - platform::{Arch, Libc, Os}, }; + use uv_platform::{Arch, Libc, Os}; use super::{Error, PythonVariant}; @@ -3154,11 +3154,11 @@ mod tests { PythonVariant::Default )), implementation: Some(ImplementationName::CPython), - arch: Some(ArchRequest::Explicit(Arch { - family: Architecture::Aarch64(Aarch64Architecture::Aarch64), - variant: None - })), - os: Some(Os(target_lexicon::OperatingSystem::Darwin(None))), + arch: Some(ArchRequest::Explicit(Arch::new( + Architecture::Aarch64(Aarch64Architecture::Aarch64), + None + ))), + os: Some(Os::new(target_lexicon::OperatingSystem::Darwin(None))), libc: Some(Libc::None), prereleases: None }) @@ -3189,10 +3189,10 @@ mod tests { PythonVariant::Default )), implementation: None, - arch: Some(ArchRequest::Explicit(Arch { - family: Architecture::Aarch64(Aarch64Architecture::Aarch64), - variant: None - })), + arch: Some(ArchRequest::Explicit(Arch::new( + Architecture::Aarch64(Aarch64Architecture::Aarch64), + None + ))), os: None, libc: None, prereleases: None diff --git a/crates/uv-python/src/downloads.rs b/crates/uv-python/src/downloads.rs index 05bf17cd1..9e1e03c91 100644 --- a/crates/uv-python/src/downloads.rs +++ b/crates/uv-python/src/downloads.rs @@ -25,6 +25,7 @@ use uv_client::{BaseClient, WrappedReqwestError, is_extended_transient_error}; use uv_distribution_filename::{ExtensionError, SourceDistExtension}; use uv_extract::hash::Hasher; use uv_fs::{Simplified, rename_with_retry}; +use uv_platform::{self as platform, Arch, Libc, Os}; use uv_pypi_types::{HashAlgorithm, HashDigest}; use uv_redacted::DisplaySafeUrl; use uv_static::EnvVars; @@ -34,9 +35,7 @@ use crate::implementation::{ Error as ImplementationError, ImplementationName, LenientImplementationName, }; use crate::installation::PythonInstallationKey; -use crate::libc::LibcDetectionError; use crate::managed::ManagedPythonInstallation; -use crate::platform::{self, Arch, Libc, Os}; use crate::{Interpreter, PythonRequest, PythonVersion, VersionRequest}; #[derive(Error, Debug)] @@ -98,7 +97,7 @@ pub enum Error { #[error("A mirror was provided via `{0}`, but the URL does not match the expected format: {0}")] Mirror(&'static str, &'static str), #[error("Failed to determine the libc used on the current platform")] - LibcDetection(#[from] LibcDetectionError), + LibcDetection(#[from] platform::LibcDetectionError), #[error("Remote Python downloads JSON is not yet supported, please use a local path")] RemoteJSONNotSupported, #[error("The JSON of the python downloads is invalid: {0}")] diff --git a/crates/uv-python/src/installation.rs b/crates/uv-python/src/installation.rs index 3f5b506a6..8cdc33106 100644 --- a/crates/uv-python/src/installation.rs +++ b/crates/uv-python/src/installation.rs @@ -10,6 +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 crate::discovery::{ EnvironmentPreference, PythonRequest, find_best_python_installation, find_python_installation, @@ -17,7 +18,6 @@ use crate::discovery::{ use crate::downloads::{DownloadResult, ManagedPythonDownload, PythonDownloadRequest, Reporter}; use crate::implementation::LenientImplementationName; use crate::managed::{ManagedPythonInstallation, ManagedPythonInstallations}; -use crate::platform::{Arch, Libc, Os}; use crate::{ Error, ImplementationName, Interpreter, PythonDownloads, PythonPreference, PythonSource, PythonVariant, PythonVersion, downloads, diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index dd9dd1cb4..3a7cce3f0 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -21,13 +21,13 @@ use uv_fs::{LockedFile, PythonExt, Simplified, write_atomic_sync}; use uv_install_wheel::Layout; use uv_pep440::Version; use uv_pep508::{MarkerEnvironment, StringVersion}; +use uv_platform::{Arch, Libc, Os}; use uv_platform_tags::Platform; use uv_platform_tags::{Tags, TagsError}; use uv_pypi_types::{ResolverMarkerEnvironment, Scheme}; use crate::implementation::LenientImplementationName; use crate::managed::ManagedPythonInstallations; -use crate::platform::{Arch, Libc, Os}; use crate::pointer_size::PointerSize; use crate::{ Prefix, PythonInstallationKey, PythonVariant, PythonVersion, Target, VersionRequest, diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index 8b8e9c129..f08198d97 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -29,19 +29,16 @@ pub use crate::version_files::{ }; pub use crate::virtualenv::{Error as VirtualEnvError, PyVenvConfiguration, VirtualEnvironment}; -mod cpuinfo; mod discovery; pub mod downloads; mod environment; mod implementation; mod installation; mod interpreter; -mod libc; pub mod macos_dylib; pub mod managed; #[cfg(windows)] mod microsoft_store; -pub mod platform; mod pointer_size; mod prefix; mod python_version; diff --git a/crates/uv-python/src/managed.rs b/crates/uv-python/src/managed.rs index d9b96e5ed..69d12a0a3 100644 --- a/crates/uv-python/src/managed.rs +++ b/crates/uv-python/src/managed.rs @@ -17,6 +17,8 @@ use uv_configuration::{Preview, PreviewFeatures}; use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT; use uv_fs::{LockedFile, Simplified, replace_symlink, symlink_or_copy_file}; +use uv_platform::Error as PlatformError; +use uv_platform::{Arch, Libc, LibcDetectionError, Os}; use uv_state::{StateBucket, StateStore}; use uv_static::EnvVars; use uv_trampoline_builder::{Launcher, windows_python_launcher}; @@ -26,9 +28,6 @@ use crate::implementation::{ Error as ImplementationError, ImplementationName, LenientImplementationName, }; use crate::installation::{self, PythonInstallationKey}; -use crate::libc::LibcDetectionError; -use crate::platform::Error as PlatformError; -use crate::platform::{Arch, Libc, Os}; use crate::python_version::PythonVersion; use crate::{ PythonInstallationMinorVersionKey, PythonRequest, PythonVariant, macos_dylib, sysconfig, @@ -271,7 +270,7 @@ impl ManagedPythonInstallations { && (arch.supports(installation.key.arch) // TODO(zanieb): Allow inequal variants, as `Arch::supports` does not // implement this yet. See https://github.com/astral-sh/uv/pull/9788 - || arch.family == installation.key.arch.family) + || arch.family() == installation.key.arch.family()) && installation.key.libc == libc }); @@ -545,7 +544,7 @@ impl ManagedPythonInstallation { /// standard `EXTERNALLY-MANAGED` file. pub fn ensure_externally_managed(&self) -> Result<(), Error> { // Construct the path to the `stdlib` directory. - let stdlib = if matches!(self.key.os, Os(target_lexicon::OperatingSystem::Windows)) { + let stdlib = if self.key.os.is_windows() { self.python_dir().join("Lib") } else { let lib_suffix = self.key.variant.suffix(); diff --git a/crates/uv-python/src/platform.rs b/crates/uv-python/src/platform.rs deleted file mode 100644 index 606e05e28..000000000 --- a/crates/uv-python/src/platform.rs +++ /dev/null @@ -1,427 +0,0 @@ -use crate::cpuinfo::detect_hardware_floating_point_support; -use crate::libc::{LibcDetectionError, LibcVersion, detect_linux_libc}; -use std::fmt::Display; -use std::ops::Deref; -use std::{fmt, str::FromStr}; -use thiserror::Error; - -use uv_static::EnvVars; - -#[derive(Error, Debug)] -pub enum Error { - #[error("Unknown operating system: {0}")] - UnknownOs(String), - #[error("Unknown architecture: {0}")] - UnknownArch(String), - #[error("Unknown libc environment: {0}")] - UnknownLibc(String), - #[error("Unsupported variant `{0}` for architecture `{1}`")] - UnsupportedVariant(String, String), - #[error(transparent)] - LibcDetectionError(#[from] LibcDetectionError), -} - -/// Architecture variants, e.g., with support for different instruction sets -#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, Ord, PartialOrd)] -pub enum ArchVariant { - /// Targets 64-bit Intel/AMD CPUs newer than Nehalem (2008). - /// Includes SSE3, SSE4 and other post-2003 CPU instructions. - V2, - /// Targets 64-bit Intel/AMD CPUs newer than Haswell (2013) and Excavator (2015). - /// Includes AVX, AVX2, MOVBE and other newer CPU instructions. - V3, - /// Targets 64-bit Intel/AMD CPUs with AVX-512 instructions (post-2017 Intel CPUs). - /// Many post-2017 Intel CPUs do not support AVX-512. - V4, -} - -#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)] -pub struct Arch { - pub(crate) family: target_lexicon::Architecture, - pub(crate) variant: Option, -} - -impl Ord for Arch { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - if self.family == other.family { - return self.variant.cmp(&other.variant); - } - - // For the time being, manually make aarch64 windows disfavored - // on its own host platform, because most packages don't have wheels for - // aarch64 windows, making emulation more useful than native execution! - // - // The reason we do this in "sorting" and not "supports" is so that we don't - // *refuse* to use an aarch64 windows pythons if they happen to be installed - // and nothing else is available. - // - // Similarly if someone manually requests an aarch64 windows install, we - // should respect that request (this is the way users should "override" - // this behaviour). - let preferred = if cfg!(all(windows, target_arch = "aarch64")) { - Arch { - family: target_lexicon::Architecture::X86_64, - variant: None, - } - } else { - // Prefer native architectures - Arch::from_env() - }; - - match ( - self.family == preferred.family, - other.family == preferred.family, - ) { - (true, true) => unreachable!(), - (true, false) => std::cmp::Ordering::Less, - (false, true) => std::cmp::Ordering::Greater, - (false, false) => { - // Both non-preferred, fallback to lexicographic order - self.family.to_string().cmp(&other.family.to_string()) - } - } - } -} - -impl PartialOrd for Arch { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)] -pub struct Os(pub(crate) target_lexicon::OperatingSystem); - -#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)] -pub enum Libc { - Some(target_lexicon::Environment), - None, -} - -impl Libc { - pub(crate) fn from_env() -> Result { - match std::env::consts::OS { - "linux" => { - if let Ok(libc) = std::env::var(EnvVars::UV_LIBC) { - if !libc.is_empty() { - return Self::from_str(&libc); - } - } - - Ok(Self::Some(match detect_linux_libc()? { - LibcVersion::Manylinux { .. } => match std::env::consts::ARCH { - // Checks if the CPU supports hardware floating-point operations. - // Depending on the result, it selects either the `gnueabihf` (hard-float) or `gnueabi` (soft-float) environment. - // download-metadata.json only includes armv7. - "arm" | "armv5te" | "armv7" => { - match detect_hardware_floating_point_support() { - Ok(true) => target_lexicon::Environment::Gnueabihf, - Ok(false) => target_lexicon::Environment::Gnueabi, - Err(_) => target_lexicon::Environment::Gnu, - } - } - _ => target_lexicon::Environment::Gnu, - }, - LibcVersion::Musllinux { .. } => target_lexicon::Environment::Musl, - })) - } - "windows" | "macos" => Ok(Self::None), - // Use `None` on platforms without explicit support. - _ => Ok(Self::None), - } - } - - pub fn is_musl(&self) -> bool { - matches!(self, Self::Some(target_lexicon::Environment::Musl)) - } -} - -impl FromStr for Libc { - type Err = Error; - - fn from_str(s: &str) -> Result { - match s { - "gnu" => Ok(Self::Some(target_lexicon::Environment::Gnu)), - "gnueabi" => Ok(Self::Some(target_lexicon::Environment::Gnueabi)), - "gnueabihf" => Ok(Self::Some(target_lexicon::Environment::Gnueabihf)), - "musl" => Ok(Self::Some(target_lexicon::Environment::Musl)), - "none" => Ok(Self::None), - _ => Err(Error::UnknownLibc(s.to_string())), - } - } -} - -impl Os { - pub fn from_env() -> Self { - Self(target_lexicon::HOST.operating_system) - } -} - -impl Arch { - pub fn from_env() -> Self { - Self { - family: target_lexicon::HOST.architecture, - variant: None, - } - } - - /// Does the current architecture support running the other? - /// - /// When the architecture is equal, this is always true. Otherwise, this is true if the - /// architecture is transparently emulated or is a microarchitecture with worse performance - /// characteristics. - pub(crate) fn supports(self, other: Self) -> bool { - if self == other { - return true; - } - - // TODO: Implement `variant` support checks - - // Windows ARM64 runs emulated x86_64 binaries transparently - // Similarly, macOS aarch64 runs emulated x86_64 binaries transparently if you have Rosetta - // installed. We don't try to be clever and check if that's the case here, we just assume - // that if x86_64 distributions are available, they're usable. - if (cfg!(windows) || cfg!(target_os = "macos")) - && matches!(self.family, target_lexicon::Architecture::Aarch64(_)) - { - return other.family == target_lexicon::Architecture::X86_64; - } - - false - } - - pub fn family(&self) -> target_lexicon::Architecture { - self.family - } - - pub fn is_arm(&self) -> bool { - matches!(self.family, target_lexicon::Architecture::Arm(_)) - } -} - -impl Display for Libc { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Some(env) => write!(f, "{env}"), - Self::None => write!(f, "none"), - } - } -} - -impl Display for Os { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match &**self { - target_lexicon::OperatingSystem::Darwin(_) => write!(f, "macos"), - inner => write!(f, "{inner}"), - } - } -} - -impl Display for Arch { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.family { - target_lexicon::Architecture::X86_32(target_lexicon::X86_32Architecture::I686) => { - write!(f, "x86")?; - } - inner => write!(f, "{inner}")?, - } - if let Some(variant) = self.variant { - write!(f, "_{variant}")?; - } - Ok(()) - } -} - -impl FromStr for Os { - type Err = Error; - - fn from_str(s: &str) -> Result { - let inner = match s { - "macos" => target_lexicon::OperatingSystem::Darwin(None), - _ => target_lexicon::OperatingSystem::from_str(s) - .map_err(|()| Error::UnknownOs(s.to_string()))?, - }; - if matches!(inner, target_lexicon::OperatingSystem::Unknown) { - return Err(Error::UnknownOs(s.to_string())); - } - Ok(Self(inner)) - } -} - -impl FromStr for Arch { - type Err = Error; - - fn from_str(s: &str) -> Result { - fn parse_family(s: &str) -> Result { - let inner = match s { - // Allow users to specify "x86" as a shorthand for the "i686" variant, they should not need - // to specify the exact architecture and this variant is what we have downloads for. - "x86" => { - target_lexicon::Architecture::X86_32(target_lexicon::X86_32Architecture::I686) - } - _ => target_lexicon::Architecture::from_str(s) - .map_err(|()| Error::UnknownArch(s.to_string()))?, - }; - if matches!(inner, target_lexicon::Architecture::Unknown) { - return Err(Error::UnknownArch(s.to_string())); - } - Ok(inner) - } - - // First check for a variant - if let Some((Ok(family), Ok(variant))) = s - .rsplit_once('_') - .map(|(family, variant)| (parse_family(family), ArchVariant::from_str(variant))) - { - // We only support variants for `x86_64` right now - if !matches!(family, target_lexicon::Architecture::X86_64) { - return Err(Error::UnsupportedVariant( - variant.to_string(), - family.to_string(), - )); - } - return Ok(Self { - family, - variant: Some(variant), - }); - } - - let family = parse_family(s)?; - - Ok(Self { - family, - variant: None, - }) - } -} - -impl FromStr for ArchVariant { - type Err = (); - - fn from_str(s: &str) -> Result { - match s { - "v2" => Ok(Self::V2), - "v3" => Ok(Self::V3), - "v4" => Ok(Self::V4), - _ => Err(()), - } - } -} - -impl Display for ArchVariant { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::V2 => write!(f, "v2"), - Self::V3 => write!(f, "v3"), - Self::V4 => write!(f, "v4"), - } - } -} - -impl Deref for Os { - type Target = target_lexicon::OperatingSystem; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From<&uv_platform_tags::Arch> for Arch { - fn from(value: &uv_platform_tags::Arch) -> Self { - match value { - uv_platform_tags::Arch::Aarch64 => Self { - family: target_lexicon::Architecture::Aarch64( - target_lexicon::Aarch64Architecture::Aarch64, - ), - variant: None, - }, - uv_platform_tags::Arch::Armv5TEL => Self { - family: target_lexicon::Architecture::Arm(target_lexicon::ArmArchitecture::Armv5te), - variant: None, - }, - uv_platform_tags::Arch::Armv6L => Self { - family: target_lexicon::Architecture::Arm(target_lexicon::ArmArchitecture::Armv6), - variant: None, - }, - uv_platform_tags::Arch::Armv7L => Self { - family: target_lexicon::Architecture::Arm(target_lexicon::ArmArchitecture::Armv7), - variant: None, - }, - uv_platform_tags::Arch::S390X => Self { - family: target_lexicon::Architecture::S390x, - variant: None, - }, - uv_platform_tags::Arch::Powerpc => Self { - family: target_lexicon::Architecture::Powerpc, - variant: None, - }, - uv_platform_tags::Arch::Powerpc64 => Self { - family: target_lexicon::Architecture::Powerpc64, - variant: None, - }, - uv_platform_tags::Arch::Powerpc64Le => Self { - family: target_lexicon::Architecture::Powerpc64le, - variant: None, - }, - uv_platform_tags::Arch::X86 => Self { - family: target_lexicon::Architecture::X86_32( - target_lexicon::X86_32Architecture::I686, - ), - variant: None, - }, - uv_platform_tags::Arch::X86_64 => Self { - family: target_lexicon::Architecture::X86_64, - variant: None, - }, - uv_platform_tags::Arch::LoongArch64 => Self { - family: target_lexicon::Architecture::LoongArch64, - variant: None, - }, - uv_platform_tags::Arch::Riscv64 => Self { - family: target_lexicon::Architecture::Riscv64( - target_lexicon::Riscv64Architecture::Riscv64, - ), - variant: None, - }, - uv_platform_tags::Arch::Wasm32 => Self { - family: target_lexicon::Architecture::Wasm32, - variant: None, - }, - } - } -} - -impl From<&uv_platform_tags::Os> for Libc { - fn from(value: &uv_platform_tags::Os) -> Self { - match value { - uv_platform_tags::Os::Manylinux { .. } => Self::Some(target_lexicon::Environment::Gnu), - uv_platform_tags::Os::Musllinux { .. } => Self::Some(target_lexicon::Environment::Musl), - _ => Self::None, - } - } -} - -impl From<&uv_platform_tags::Os> for Os { - fn from(value: &uv_platform_tags::Os) -> Self { - match value { - uv_platform_tags::Os::Dragonfly { .. } => { - Self(target_lexicon::OperatingSystem::Dragonfly) - } - uv_platform_tags::Os::FreeBsd { .. } => Self(target_lexicon::OperatingSystem::Freebsd), - uv_platform_tags::Os::Haiku { .. } => Self(target_lexicon::OperatingSystem::Haiku), - uv_platform_tags::Os::Illumos { .. } => Self(target_lexicon::OperatingSystem::Illumos), - uv_platform_tags::Os::Macos { .. } => { - Self(target_lexicon::OperatingSystem::Darwin(None)) - } - uv_platform_tags::Os::Manylinux { .. } - | uv_platform_tags::Os::Musllinux { .. } - | uv_platform_tags::Os::Android { .. } => Self(target_lexicon::OperatingSystem::Linux), - uv_platform_tags::Os::NetBsd { .. } => Self(target_lexicon::OperatingSystem::Netbsd), - uv_platform_tags::Os::OpenBsd { .. } => Self(target_lexicon::OperatingSystem::Openbsd), - uv_platform_tags::Os::Windows => Self(target_lexicon::OperatingSystem::Windows), - uv_platform_tags::Os::Pyodide { .. } => { - Self(target_lexicon::OperatingSystem::Emscripten) - } - } - } -} diff --git a/crates/uv-python/src/windows_registry.rs b/crates/uv-python/src/windows_registry.rs index 0020f95e9..cd6393aec 100644 --- a/crates/uv-python/src/windows_registry.rs +++ b/crates/uv-python/src/windows_registry.rs @@ -1,7 +1,6 @@ //! PEP 514 interactions with the Windows registry. use crate::managed::ManagedPythonInstallation; -use crate::platform::Arch; use crate::{COMPANY_DISPLAY_NAME, COMPANY_KEY, PythonInstallationKey, PythonVersion}; use anyhow::anyhow; use std::cmp::Ordering; @@ -11,6 +10,7 @@ use std::str::FromStr; use target_lexicon::PointerWidth; use thiserror::Error; use tracing::debug; +use uv_platform::Arch; use uv_warnings::{warn_user, warn_user_once}; use windows_registry::{CURRENT_USER, HSTRING, Key, LOCAL_MACHINE, Value}; use windows_result::HRESULT; diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 5acbb7f20..f37e8c2f0 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -38,6 +38,7 @@ uv-normalize = { workspace = true } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } uv-performance-memory-allocator = { path = "../uv-performance-memory-allocator", optional = true } +uv-platform = { workspace = true } uv-platform-tags = { workspace = true } uv-publish = { workspace = true } uv-pypi-types = { workspace = true } diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index 02e4c27e5..e3b1ef797 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -16,6 +16,7 @@ use tracing::{debug, trace}; use uv_configuration::{Preview, PreviewFeatures}; use uv_fs::Simplified; +use uv_platform::{Arch, Libc}; use uv_python::downloads::{ self, ArchRequest, DownloadResult, ManagedPythonDownload, PythonDownloadRequest, }; @@ -23,7 +24,6 @@ use uv_python::managed::{ ManagedPythonInstallation, ManagedPythonInstallations, PythonMinorVersionLink, create_link_to_executable, python_executable_dir, }; -use uv_python::platform::{Arch, Libc}; use uv_python::{ PythonDownloads, PythonInstallationKey, PythonInstallationMinorVersionKey, PythonRequest, PythonVersionFile, VersionFileDiscoveryOptions, VersionFilePreference, VersionRequest, diff --git a/crates/uv/tests/it/python_find.rs b/crates/uv/tests/it/python_find.rs index d2ae51e38..5086fe2df 100644 --- a/crates/uv/tests/it/python_find.rs +++ b/crates/uv/tests/it/python_find.rs @@ -2,7 +2,7 @@ use assert_fs::prelude::{FileTouch, PathChild}; use assert_fs::{fixture::FileWriteStr, prelude::PathCreateDir}; use indoc::indoc; -use uv_python::platform::{Arch, Os}; +use uv_platform::{Arch, Os}; use uv_static::EnvVars; use crate::common::{TestContext, uv_snapshot, venv_bin_path}; diff --git a/crates/uv/tests/it/python_list.rs b/crates/uv/tests/it/python_list.rs index 11472baec..5c23a3e93 100644 --- a/crates/uv/tests/it/python_list.rs +++ b/crates/uv/tests/it/python_list.rs @@ -1,4 +1,4 @@ -use uv_python::platform::{Arch, Os}; +use uv_platform::{Arch, Os}; use uv_static::EnvVars; use crate::common::{TestContext, uv_snapshot}; diff --git a/crates/uv/tests/it/python_pin.rs b/crates/uv/tests/it/python_pin.rs index 97093831c..0f01a0011 100644 --- a/crates/uv/tests/it/python_pin.rs +++ b/crates/uv/tests/it/python_pin.rs @@ -5,10 +5,8 @@ use anyhow::Result; use assert_cmd::assert::OutputAssertExt; use assert_fs::fixture::{FileWriteStr, PathChild, PathCreateDir}; use insta::assert_snapshot; -use uv_python::{ - PYTHON_VERSION_FILENAME, PYTHON_VERSIONS_FILENAME, - platform::{Arch, Os}, -}; +use uv_platform::{Arch, Os}; +use uv_python::{PYTHON_VERSION_FILENAME, PYTHON_VERSIONS_FILENAME}; #[test] fn python_pin() {