Skip to main content

uv_platform/
lib.rs

1//! Platform detection for operating system, architecture, and libc.
2
3use std::borrow::Cow;
4use std::cmp;
5use std::fmt;
6use std::str::FromStr;
7use target_lexicon::Architecture;
8use thiserror::Error;
9use tracing::trace;
10
11pub use crate::arch::{Arch, ArchVariant};
12pub use crate::host::{LinuxOsRelease, OsRelease, OsType};
13pub use crate::libc::{Libc, LibcDetectionError};
14pub use crate::os::Os;
15
16mod arch;
17mod cpuinfo;
18mod host;
19mod libc;
20mod os;
21
22#[derive(Error, Debug)]
23pub enum Error {
24    #[error("Unknown operating system: {0}")]
25    UnknownOs(String),
26    #[error("Unknown architecture: {0}")]
27    UnknownArch(String),
28    #[error("Unknown libc environment: {0}")]
29    UnknownLibc(String),
30    #[error("Unsupported variant `{0}` for architecture `{1}`")]
31    UnsupportedVariant(String, String),
32    #[error(transparent)]
33    LibcDetectionError(#[from] crate::libc::LibcDetectionError),
34    #[error("Invalid platform format: {0}")]
35    InvalidPlatformFormat(String),
36}
37
38/// A platform identifier that combines operating system, architecture, and libc.
39#[derive(Debug, Clone, PartialEq, Eq, Hash)]
40pub struct Platform {
41    pub os: Os,
42    pub arch: Arch,
43    pub libc: Libc,
44}
45
46impl Platform {
47    /// Create a new platform with the given components.
48    pub fn new(os: Os, arch: Arch, libc: Libc) -> Self {
49        Self { os, arch, libc }
50    }
51
52    /// Create a platform from string parts (os, arch, libc).
53    pub fn from_parts(os: &str, arch: &str, libc: &str) -> Result<Self, Error> {
54        Ok(Self {
55            os: Os::from_str(os)?,
56            arch: Arch::from_str(arch)?,
57            libc: Libc::from_str(libc)?,
58        })
59    }
60
61    /// Detect the platform from the current environment.
62    pub fn from_env() -> Result<Self, Error> {
63        let os = Os::from_env();
64        let arch = Arch::from_env();
65        let libc = Libc::from_env()?;
66        Ok(Self { os, arch, libc })
67    }
68
69    /// Check if this platform supports running another platform.
70    pub fn supports(&self, other: &Self) -> bool {
71        // If platforms are exactly equal, they're compatible
72        if self == other {
73            return true;
74        }
75
76        if !self.os.supports(other.os) {
77            trace!(
78                "Operating system `{}` is not compatible with `{}`",
79                self.os, other.os
80            );
81            return false;
82        }
83
84        // Libc must match exactly, unless we're on emscripten — in which case it doesn't matter
85        if self.libc != other.libc && !(other.os.is_emscripten() || self.os.is_emscripten()) {
86            trace!(
87                "Libc `{}` is not compatible with `{}`",
88                self.libc, other.libc
89            );
90            return false;
91        }
92
93        // Check architecture compatibility
94        if self.arch == other.arch {
95            return true;
96        }
97
98        #[expect(clippy::unnested_or_patterns)]
99        if self.os.is_windows()
100            && matches!(
101                (self.arch.family(), other.arch.family()),
102                // 32-bit x86 binaries work fine on 64-bit x86 windows
103                (Architecture::X86_64, Architecture::X86_32(_)) |
104                // Both 32-bit and 64-bit binaries are transparently emulated on aarch64 windows
105                (Architecture::Aarch64(_), Architecture::X86_64) |
106                (Architecture::Aarch64(_), Architecture::X86_32(_))
107            )
108        {
109            return true;
110        }
111
112        if self.os.is_macos()
113            && matches!(
114                (self.arch.family(), other.arch.family()),
115                // macOS aarch64 runs emulated x86_64 binaries transparently if you have Rosetta
116                // installed. We don't try to be clever and check if that's the case here,
117                // we just assume that if x86_64 distributions are available, they're usable.
118                (Architecture::Aarch64(_), Architecture::X86_64)
119            )
120        {
121            return true;
122        }
123
124        // Wasm32 can run on any architecture
125        if other.arch.is_wasm() {
126            return true;
127        }
128
129        // TODO: Allow inequal variants, as we don't implement variant support checks yet.
130        // See https://github.com/astral-sh/uv/pull/9788
131        // For now, allow same architecture family as a fallback
132        if self.arch.family() != other.arch.family() {
133            return false;
134        }
135
136        true
137    }
138
139    /// Convert this platform to a `cargo-dist` style triple string.
140    pub fn as_cargo_dist_triple(&self) -> String {
141        use target_lexicon::{
142            Architecture, ArmArchitecture, Environment, OperatingSystem, Riscv64Architecture,
143            X86_32Architecture,
144        };
145
146        let Self { os, arch, libc } = &self;
147
148        let arch_name = match arch.family() {
149            // Special cases where Display doesn't match target triple
150            Architecture::X86_32(X86_32Architecture::I686) => Cow::Borrowed("i686"),
151            Architecture::Riscv64(Riscv64Architecture::Riscv64) => Cow::Borrowed("riscv64gc"),
152            _ => Cow::Owned(arch.to_string()),
153        };
154        let vendor = match &**os {
155            OperatingSystem::Darwin(_) => "apple",
156            OperatingSystem::Windows => "pc",
157            _ => "unknown",
158        };
159        let os_name = match &**os {
160            OperatingSystem::Darwin(_) => Cow::Borrowed("darwin"),
161            _ => Cow::Owned(os.to_string()),
162        };
163
164        let abi = match (&**os, libc) {
165            (OperatingSystem::Windows, _) => Some(Cow::Borrowed("msvc")),
166            (OperatingSystem::Linux, Libc::Some(env)) => Some({
167                // If we've detected a bare `gnu` or `musl` environment on ARMv7,
168                // that means our floating point environment detection failed.
169                // We currently assume hard-float in that case, for two reasons:
170                // 1. Statistically, we expect the overwhelming majority of ARMv7 Linux hosts to
171                //    be hard-float.
172                // 2. We currently only ship hard-float ARMv7 builds of ruff and uv anyways,
173                //    so we don't even have a soft-float build to fall back to.
174                // By contrast, we *do* ship soft-float Python builds via PBS, but PBS
175                // installations don't take this pathway.
176                if matches!(arch.family(), Architecture::Arm(ArmArchitecture::Armv7))
177                    && matches!(env, Environment::Gnu | Environment::Musl)
178                {
179                    Cow::Owned(format!("{env}eabihf"))
180                } else {
181                    Cow::Owned(env.to_string())
182                }
183            }),
184            _ => None,
185        };
186
187        format!(
188            "{arch_name}-{vendor}-{os_name}{abi}",
189            abi = abi.map(|abi| format!("-{abi}")).unwrap_or_default()
190        )
191    }
192}
193
194impl fmt::Display for Platform {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        write!(f, "{}-{}-{}", self.os, self.arch, self.libc)
197    }
198}
199
200impl FromStr for Platform {
201    type Err = Error;
202
203    fn from_str(s: &str) -> Result<Self, Self::Err> {
204        let parts: Vec<&str> = s.split('-').collect();
205
206        if parts.len() != 3 {
207            return Err(Error::InvalidPlatformFormat(format!(
208                "expected exactly 3 parts separated by '-', got {}",
209                parts.len()
210            )));
211        }
212
213        Self::from_parts(parts[0], parts[1], parts[2])
214    }
215}
216
217impl Ord for Platform {
218    fn cmp(&self, other: &Self) -> cmp::Ordering {
219        self.os
220            .to_string()
221            .cmp(&other.os.to_string())
222            // Then architecture
223            .then_with(|| self.arch.cmp_for_os(other.arch, self.os, Arch::from_env()))
224            // Finally compare libc
225            .then_with(|| self.libc.to_string().cmp(&other.libc.to_string()))
226    }
227}
228
229impl PartialOrd for Platform {
230    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
231        Some(self.cmp(other))
232    }
233}
234
235impl From<&uv_platform_tags::Platform> for Platform {
236    fn from(value: &uv_platform_tags::Platform) -> Self {
237        Self {
238            os: Os::from(value.os()),
239            arch: Arch::from(&value.arch()),
240            libc: Libc::from(value.os()),
241        }
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn test_platform_display() {
251        let platform = Platform {
252            os: Os::from_str("linux").unwrap(),
253            arch: Arch::from_str("x86_64").unwrap(),
254            libc: Libc::from_str("gnu").unwrap(),
255        };
256        assert_eq!(platform.to_string(), "linux-x86_64-gnu");
257    }
258
259    #[test]
260    fn test_platform_from_str() {
261        let platform = Platform::from_str("macos-aarch64-none").unwrap();
262        assert_eq!(platform.os.to_string(), "macos");
263        assert_eq!(platform.arch.to_string(), "aarch64");
264        assert_eq!(platform.libc.to_string(), "none");
265    }
266
267    #[test]
268    fn test_platform_from_parts() {
269        let platform = Platform::from_parts("linux", "x86_64", "gnu").unwrap();
270        assert_eq!(platform.os.to_string(), "linux");
271        assert_eq!(platform.arch.to_string(), "x86_64");
272        assert_eq!(platform.libc.to_string(), "gnu");
273
274        // Test with arch variant
275        let platform = Platform::from_parts("linux", "x86_64_v3", "musl").unwrap();
276        assert_eq!(platform.os.to_string(), "linux");
277        assert_eq!(platform.arch.to_string(), "x86_64_v3");
278        assert_eq!(platform.libc.to_string(), "musl");
279
280        // Test error cases
281        assert!(Platform::from_parts("invalid_os", "x86_64", "gnu").is_err());
282        assert!(Platform::from_parts("linux", "invalid_arch", "gnu").is_err());
283        assert!(Platform::from_parts("linux", "x86_64", "invalid_libc").is_err());
284    }
285
286    #[test]
287    fn test_platform_from_str_with_arch_variant() {
288        let platform = Platform::from_str("linux-x86_64_v3-gnu").unwrap();
289        assert_eq!(platform.os.to_string(), "linux");
290        assert_eq!(platform.arch.to_string(), "x86_64_v3");
291        assert_eq!(platform.libc.to_string(), "gnu");
292    }
293
294    #[test]
295    fn test_platform_from_str_error() {
296        // Too few parts
297        assert!(Platform::from_str("linux-x86_64").is_err());
298        assert!(Platform::from_str("invalid").is_err());
299
300        // Too many parts (would have been accepted by the old code)
301        assert!(Platform::from_str("linux-x86-64-gnu").is_err());
302        assert!(Platform::from_str("linux-x86_64-gnu-extra").is_err());
303    }
304
305    #[test]
306    fn test_platform_sorting_os_precedence() {
307        let linux = Platform::from_str("linux-x86_64-gnu").unwrap();
308        let macos = Platform::from_str("macos-x86_64-none").unwrap();
309        let windows = Platform::from_str("windows-x86_64-none").unwrap();
310
311        // OS sorting takes precedence (alphabetical)
312        assert!(linux < macos);
313        assert!(macos < windows);
314    }
315
316    #[test]
317    fn test_platform_sorting_libc() {
318        let gnu = Platform::from_str("linux-x86_64-gnu").unwrap();
319        let musl = Platform::from_str("linux-x86_64-musl").unwrap();
320
321        // Same OS and arch, libc comparison (alphabetical)
322        assert!(gnu < musl);
323    }
324
325    #[test]
326    fn test_platform_sorting_arch_linux() {
327        // Test that Linux prefers the native architecture
328        use crate::arch::test_support::{aarch64, run_with_arch, x86_64};
329
330        let linux_x86_64 = Platform::from_str("linux-x86_64-gnu").unwrap();
331        let linux_aarch64 = Platform::from_str("linux-aarch64-gnu").unwrap();
332
333        // On x86_64 Linux, x86_64 should be preferred over aarch64
334        run_with_arch(x86_64(), || {
335            assert!(linux_x86_64 < linux_aarch64);
336        });
337
338        // On aarch64 Linux, aarch64 should be preferred over x86_64
339        run_with_arch(aarch64(), || {
340            assert!(linux_aarch64 < linux_x86_64);
341        });
342    }
343
344    #[test]
345    fn test_platform_sorting_arch_macos() {
346        use crate::arch::test_support::{aarch64, run_with_arch, x86_64};
347
348        let macos_x86_64 = Platform::from_str("macos-x86_64-none").unwrap();
349        let macos_aarch64 = Platform::from_str("macos-aarch64-none").unwrap();
350
351        // On x86_64 macOS, x86_64 should be preferred over aarch64
352        run_with_arch(x86_64(), || {
353            assert!(macos_x86_64 < macos_aarch64);
354        });
355
356        // On aarch64 macOS, aarch64 should be preferred over x86_64
357        run_with_arch(aarch64(), || {
358            assert!(macos_aarch64 < macos_x86_64);
359        });
360    }
361
362    #[test]
363    fn test_platform_supports() {
364        let native = Platform::from_str("linux-x86_64-gnu").unwrap();
365        let same = Platform::from_str("linux-x86_64-gnu").unwrap();
366        let different_arch = Platform::from_str("linux-aarch64-gnu").unwrap();
367        let different_os = Platform::from_str("macos-x86_64-none").unwrap();
368        let different_libc = Platform::from_str("linux-x86_64-musl").unwrap();
369
370        // Exact match
371        assert!(native.supports(&same));
372
373        // Different OS - not supported
374        assert!(!native.supports(&different_os));
375
376        // Different libc - not supported
377        assert!(!native.supports(&different_libc));
378
379        // Different architecture but same family
380        // x86_64 doesn't support aarch64 on Linux
381        assert!(!native.supports(&different_arch));
382
383        // Test architecture family support
384        let x86_64_v2 = Platform::from_str("linux-x86_64_v2-gnu").unwrap();
385        let x86_64_v3 = Platform::from_str("linux-x86_64_v3-gnu").unwrap();
386
387        // These have the same architecture family (both x86_64)
388        assert_eq!(native.arch.family(), x86_64_v2.arch.family());
389        assert_eq!(native.arch.family(), x86_64_v3.arch.family());
390
391        // Due to the family check, these should support each other
392        assert!(native.supports(&x86_64_v2));
393        assert!(native.supports(&x86_64_v3));
394    }
395
396    #[test]
397    fn test_windows_aarch64_platform_sorting() {
398        use crate::arch::test_support::{aarch64, run_with_arch};
399
400        let mut platforms = [
401            Platform::from_str("windows-aarch64-none").unwrap(),
402            Platform::from_str("windows-x86_64-none").unwrap(),
403            Platform::from_str("windows-x86-none").unwrap(),
404        ];
405
406        run_with_arch(aarch64(), || platforms.sort());
407
408        assert_eq!(platforms[0].arch.to_string(), "aarch64");
409        assert_eq!(platforms[1].arch.to_string(), "x86_64");
410        assert_eq!(platforms[2].arch.to_string(), "x86");
411    }
412
413    #[test]
414    fn test_windows_sorting_prefers_native_arch() {
415        use crate::arch::test_support::{aarch64, run_with_arch, x86_64};
416
417        let windows_x86_64 = Platform::from_str("windows-x86_64-none").unwrap();
418        let windows_aarch64 = Platform::from_str("windows-aarch64-none").unwrap();
419
420        run_with_arch(aarch64(), || {
421            assert!(windows_aarch64 < windows_x86_64);
422        });
423
424        run_with_arch(x86_64(), || {
425            assert!(windows_x86_64 < windows_aarch64);
426        });
427    }
428
429    #[test]
430    fn test_windows_aarch64_supports() {
431        // Test that Windows aarch64 can run x86_64 binaries through emulation
432        let windows_aarch64 = Platform::from_str("windows-aarch64-none").unwrap();
433        let windows_x86_64 = Platform::from_str("windows-x86_64-none").unwrap();
434
435        // aarch64 Windows supports x86_64 through transparent emulation
436        assert!(windows_aarch64.supports(&windows_x86_64));
437
438        // But x86_64 doesn't support aarch64
439        assert!(!windows_x86_64.supports(&windows_aarch64));
440
441        // Self-support should always work
442        assert!(windows_aarch64.supports(&windows_aarch64));
443        assert!(windows_x86_64.supports(&windows_x86_64));
444    }
445
446    #[test]
447    fn test_from_platform_tags_platform() {
448        // Test conversion from uv_platform_tags::Platform to uv_platform::Platform
449        let tags_platform = uv_platform_tags::Platform::new(
450            uv_platform_tags::Os::Windows,
451            uv_platform_tags::Arch::X86_64,
452        );
453        let platform = Platform::from(&tags_platform);
454
455        assert_eq!(platform.os.to_string(), "windows");
456        assert_eq!(platform.arch.to_string(), "x86_64");
457        assert_eq!(platform.libc.to_string(), "none");
458
459        // Test with manylinux
460        let tags_platform_linux = uv_platform_tags::Platform::new(
461            uv_platform_tags::Os::Manylinux {
462                major: 2,
463                minor: 17,
464            },
465            uv_platform_tags::Arch::Aarch64,
466        );
467        let platform_linux = Platform::from(&tags_platform_linux);
468
469        assert_eq!(platform_linux.os.to_string(), "linux");
470        assert_eq!(platform_linux.arch.to_string(), "aarch64");
471        assert_eq!(platform_linux.libc.to_string(), "gnu");
472    }
473
474    #[test]
475    fn test_as_cargo_dist_triple_armv7_libc_handling() {
476        // This mapping reflects our current behavior: we currently ship
477        // only hard-float artifacts for ARMv7, so if we fail to detect
478        // the float ABI (i.e., we get the generic `Gnu` or `Musl`),
479        // we assume hard-float.
480        for (arch, libc, expected) in [
481            // ARMv7: detected ABI passes through unchanged.
482            ("armv7", "gnueabihf", "armv7-unknown-linux-gnueabihf"),
483            ("armv7", "gnueabi", "armv7-unknown-linux-gnueabi"),
484            ("armv7", "musleabihf", "armv7-unknown-linux-musleabihf"),
485            ("armv7", "musleabi", "armv7-unknown-linux-musleabi"),
486            // ARMv7: generic libc (detection failure) is biased to hard-float.
487            ("armv7", "gnu", "armv7-unknown-linux-gnueabihf"),
488            ("armv7", "musl", "armv7-unknown-linux-musleabihf"),
489            // Non-ARMv7: libc passes through unchanged.
490            ("aarch64", "gnu", "aarch64-unknown-linux-gnu"),
491            ("x86_64", "musl", "x86_64-unknown-linux-musl"),
492        ] {
493            let platform = Platform::from_parts("linux", arch, libc).unwrap();
494            assert_eq!(
495                platform.as_cargo_dist_triple(),
496                expected,
497                "linux-{arch}-{libc}"
498            );
499        }
500    }
501}