Skip to main content

uv_platform/
lib.rs

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