Skip to content

Commit 00efde0

Browse files
authored
Split platform detection code into a dedicated uv-platform crate (#14918)
In service of some subsequent work...
1 parent 5686771 commit 00efde0

22 files changed

+530
-469
lines changed

Cargo.lock

Lines changed: 19 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ uv-once-map = { path = "crates/uv-once-map" }
4949
uv-options-metadata = { path = "crates/uv-options-metadata" }
5050
uv-pep440 = { path = "crates/uv-pep440", features = ["tracing", "rkyv", "version-ranges"] }
5151
uv-pep508 = { path = "crates/uv-pep508", features = ["non-pep508-extensions"] }
52+
uv-platform = { path = "crates/uv-platform" }
5253
uv-platform-tags = { path = "crates/uv-platform-tags" }
5354
uv-publish = { path = "crates/uv-publish" }
5455
uv-pypi-types = { path = "crates/uv-pypi-types" }

crates/uv-platform/Cargo.toml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[package]
2+
name = "uv-platform"
3+
version = "0.0.1"
4+
edition = { workspace = true }
5+
rust-version = { workspace = true }
6+
homepage = { workspace = true }
7+
documentation = { workspace = true }
8+
repository = { workspace = true }
9+
authors = { workspace = true }
10+
license = { workspace = true }
11+
12+
[lib]
13+
doctest = false
14+
15+
[lints]
16+
workspace = true
17+
18+
[dependencies]
19+
uv-static = { workspace = true }
20+
uv-fs = { workspace = true }
21+
uv-platform-tags = { workspace = true }
22+
23+
fs-err = { workspace = true }
24+
goblin = { workspace = true }
25+
regex = { workspace = true }
26+
target-lexicon = { workspace = true }
27+
thiserror = { workspace = true }
28+
tracing = { workspace = true }
29+
30+
[target.'cfg(target_os = "linux")'.dependencies]
31+
procfs = { workspace = true }
32+
33+
[dev-dependencies]
34+
indoc = { workspace = true }

crates/uv-platform/src/arch.rs

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
use crate::Error;
2+
use std::fmt::Display;
3+
use std::str::FromStr;
4+
use std::{cmp, fmt};
5+
6+
/// Architecture variants, e.g., with support for different instruction sets
7+
#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, Ord, PartialOrd)]
8+
pub enum ArchVariant {
9+
/// Targets 64-bit Intel/AMD CPUs newer than Nehalem (2008).
10+
/// Includes SSE3, SSE4 and other post-2003 CPU instructions.
11+
V2,
12+
/// Targets 64-bit Intel/AMD CPUs newer than Haswell (2013) and Excavator (2015).
13+
/// Includes AVX, AVX2, MOVBE and other newer CPU instructions.
14+
V3,
15+
/// Targets 64-bit Intel/AMD CPUs with AVX-512 instructions (post-2017 Intel CPUs).
16+
/// Many post-2017 Intel CPUs do not support AVX-512.
17+
V4,
18+
}
19+
20+
#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
21+
pub struct Arch {
22+
pub(crate) family: target_lexicon::Architecture,
23+
pub(crate) variant: Option<ArchVariant>,
24+
}
25+
26+
impl Ord for Arch {
27+
fn cmp(&self, other: &Self) -> cmp::Ordering {
28+
if self.family == other.family {
29+
return self.variant.cmp(&other.variant);
30+
}
31+
32+
// For the time being, manually make aarch64 windows disfavored
33+
// on its own host platform, because most packages don't have wheels for
34+
// aarch64 windows, making emulation more useful than native execution!
35+
//
36+
// The reason we do this in "sorting" and not "supports" is so that we don't
37+
// *refuse* to use an aarch64 windows pythons if they happen to be installed
38+
// and nothing else is available.
39+
//
40+
// Similarly if someone manually requests an aarch64 windows install, we
41+
// should respect that request (this is the way users should "override"
42+
// this behaviour).
43+
let preferred = if cfg!(all(windows, target_arch = "aarch64")) {
44+
Arch {
45+
family: target_lexicon::Architecture::X86_64,
46+
variant: None,
47+
}
48+
} else {
49+
// Prefer native architectures
50+
Arch::from_env()
51+
};
52+
53+
match (
54+
self.family == preferred.family,
55+
other.family == preferred.family,
56+
) {
57+
(true, true) => unreachable!(),
58+
(true, false) => cmp::Ordering::Less,
59+
(false, true) => cmp::Ordering::Greater,
60+
(false, false) => {
61+
// Both non-preferred, fallback to lexicographic order
62+
self.family.to_string().cmp(&other.family.to_string())
63+
}
64+
}
65+
}
66+
}
67+
68+
impl PartialOrd for Arch {
69+
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
70+
Some(self.cmp(other))
71+
}
72+
}
73+
74+
impl Arch {
75+
pub fn new(family: target_lexicon::Architecture, variant: Option<ArchVariant>) -> Self {
76+
Self { family, variant }
77+
}
78+
79+
pub fn from_env() -> Self {
80+
Self {
81+
family: target_lexicon::HOST.architecture,
82+
variant: None,
83+
}
84+
}
85+
86+
/// Does the current architecture support running the other?
87+
///
88+
/// When the architecture is equal, this is always true. Otherwise, this is true if the
89+
/// architecture is transparently emulated or is a microarchitecture with worse performance
90+
/// characteristics.
91+
pub fn supports(self, other: Self) -> bool {
92+
if self == other {
93+
return true;
94+
}
95+
96+
// TODO: Implement `variant` support checks
97+
98+
// Windows ARM64 runs emulated x86_64 binaries transparently
99+
// Similarly, macOS aarch64 runs emulated x86_64 binaries transparently if you have Rosetta
100+
// installed. We don't try to be clever and check if that's the case here, we just assume
101+
// that if x86_64 distributions are available, they're usable.
102+
if (cfg!(windows) || cfg!(target_os = "macos"))
103+
&& matches!(self.family, target_lexicon::Architecture::Aarch64(_))
104+
{
105+
return other.family == target_lexicon::Architecture::X86_64;
106+
}
107+
108+
false
109+
}
110+
111+
pub fn family(&self) -> target_lexicon::Architecture {
112+
self.family
113+
}
114+
115+
pub fn is_arm(&self) -> bool {
116+
matches!(self.family, target_lexicon::Architecture::Arm(_))
117+
}
118+
}
119+
120+
impl Display for Arch {
121+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122+
match self.family {
123+
target_lexicon::Architecture::X86_32(target_lexicon::X86_32Architecture::I686) => {
124+
write!(f, "x86")?;
125+
}
126+
inner => write!(f, "{inner}")?,
127+
}
128+
if let Some(variant) = self.variant {
129+
write!(f, "_{variant}")?;
130+
}
131+
Ok(())
132+
}
133+
}
134+
135+
impl FromStr for Arch {
136+
type Err = Error;
137+
138+
fn from_str(s: &str) -> Result<Self, Self::Err> {
139+
fn parse_family(s: &str) -> Result<target_lexicon::Architecture, Error> {
140+
let inner = match s {
141+
// Allow users to specify "x86" as a shorthand for the "i686" variant, they should not need
142+
// to specify the exact architecture and this variant is what we have downloads for.
143+
"x86" => {
144+
target_lexicon::Architecture::X86_32(target_lexicon::X86_32Architecture::I686)
145+
}
146+
_ => target_lexicon::Architecture::from_str(s)
147+
.map_err(|()| Error::UnknownArch(s.to_string()))?,
148+
};
149+
if matches!(inner, target_lexicon::Architecture::Unknown) {
150+
return Err(Error::UnknownArch(s.to_string()));
151+
}
152+
Ok(inner)
153+
}
154+
155+
// First check for a variant
156+
if let Some((Ok(family), Ok(variant))) = s
157+
.rsplit_once('_')
158+
.map(|(family, variant)| (parse_family(family), ArchVariant::from_str(variant)))
159+
{
160+
// We only support variants for `x86_64` right now
161+
if !matches!(family, target_lexicon::Architecture::X86_64) {
162+
return Err(Error::UnsupportedVariant(
163+
variant.to_string(),
164+
family.to_string(),
165+
));
166+
}
167+
return Ok(Self {
168+
family,
169+
variant: Some(variant),
170+
});
171+
}
172+
173+
let family = parse_family(s)?;
174+
175+
Ok(Self {
176+
family,
177+
variant: None,
178+
})
179+
}
180+
}
181+
182+
impl FromStr for ArchVariant {
183+
type Err = ();
184+
185+
fn from_str(s: &str) -> Result<Self, Self::Err> {
186+
match s {
187+
"v2" => Ok(Self::V2),
188+
"v3" => Ok(Self::V3),
189+
"v4" => Ok(Self::V4),
190+
_ => Err(()),
191+
}
192+
}
193+
}
194+
195+
impl Display for ArchVariant {
196+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197+
match self {
198+
Self::V2 => write!(f, "v2"),
199+
Self::V3 => write!(f, "v3"),
200+
Self::V4 => write!(f, "v4"),
201+
}
202+
}
203+
}
204+
205+
impl From<&uv_platform_tags::Arch> for Arch {
206+
fn from(value: &uv_platform_tags::Arch) -> Self {
207+
match value {
208+
uv_platform_tags::Arch::Aarch64 => Arch::new(
209+
target_lexicon::Architecture::Aarch64(target_lexicon::Aarch64Architecture::Aarch64),
210+
None,
211+
),
212+
uv_platform_tags::Arch::Armv5TEL => Arch::new(
213+
target_lexicon::Architecture::Arm(target_lexicon::ArmArchitecture::Armv5te),
214+
None,
215+
),
216+
uv_platform_tags::Arch::Armv6L => Arch::new(
217+
target_lexicon::Architecture::Arm(target_lexicon::ArmArchitecture::Armv6),
218+
None,
219+
),
220+
uv_platform_tags::Arch::Armv7L => Arch::new(
221+
target_lexicon::Architecture::Arm(target_lexicon::ArmArchitecture::Armv7),
222+
None,
223+
),
224+
uv_platform_tags::Arch::S390X => Arch::new(target_lexicon::Architecture::S390x, None),
225+
uv_platform_tags::Arch::Powerpc => {
226+
Arch::new(target_lexicon::Architecture::Powerpc, None)
227+
}
228+
uv_platform_tags::Arch::Powerpc64 => {
229+
Arch::new(target_lexicon::Architecture::Powerpc64, None)
230+
}
231+
uv_platform_tags::Arch::Powerpc64Le => {
232+
Arch::new(target_lexicon::Architecture::Powerpc64le, None)
233+
}
234+
uv_platform_tags::Arch::X86 => Arch::new(
235+
target_lexicon::Architecture::X86_32(target_lexicon::X86_32Architecture::I686),
236+
None,
237+
),
238+
uv_platform_tags::Arch::X86_64 => Arch::new(target_lexicon::Architecture::X86_64, None),
239+
uv_platform_tags::Arch::LoongArch64 => {
240+
Arch::new(target_lexicon::Architecture::LoongArch64, None)
241+
}
242+
uv_platform_tags::Arch::Riscv64 => Arch::new(
243+
target_lexicon::Architecture::Riscv64(target_lexicon::Riscv64Architecture::Riscv64),
244+
None,
245+
),
246+
uv_platform_tags::Arch::Wasm32 => Arch::new(target_lexicon::Architecture::Wasm32, None),
247+
}
248+
}
249+
}

crates/uv-python/src/cpuinfo.rs renamed to crates/uv-platform/src/cpuinfo.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Fetches CPU information.
22
3-
use anyhow::Error;
3+
use std::io::Error;
44

55
#[cfg(target_os = "linux")]
66
use procfs::{CpuInfo, Current};
@@ -14,7 +14,7 @@ use procfs::{CpuInfo, Current};
1414
/// More information on this can be found in the [Debian ARM Hard Float Port documentation](https://wiki.debian.org/ArmHardFloatPort#VFP).
1515
#[cfg(target_os = "linux")]
1616
pub(crate) fn detect_hardware_floating_point_support() -> Result<bool, Error> {
17-
let cpu_info = CpuInfo::current()?;
17+
let cpu_info = CpuInfo::current().map_err(Error::other)?;
1818
if let Some(features) = cpu_info.fields.get("Features") {
1919
if features.contains("vfp") {
2020
return Ok(true); // "vfp" found: hard-float (gnueabihf) detected

crates/uv-platform/src/lib.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//! Platform detection for operating system, architecture, and libc.
2+
3+
use thiserror::Error;
4+
5+
pub use crate::arch::{Arch, ArchVariant};
6+
pub use crate::libc::{Libc, LibcDetectionError, LibcVersion};
7+
pub use crate::os::Os;
8+
9+
mod arch;
10+
mod cpuinfo;
11+
mod libc;
12+
mod os;
13+
14+
#[derive(Error, Debug)]
15+
pub enum Error {
16+
#[error("Unknown operating system: {0}")]
17+
UnknownOs(String),
18+
#[error("Unknown architecture: {0}")]
19+
UnknownArch(String),
20+
#[error("Unknown libc environment: {0}")]
21+
UnknownLibc(String),
22+
#[error("Unsupported variant `{0}` for architecture `{1}`")]
23+
UnsupportedVariant(String, String),
24+
#[error(transparent)]
25+
LibcDetectionError(#[from] crate::libc::LibcDetectionError),
26+
}

0 commit comments

Comments
 (0)