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(|| {
224                if self.arch.family == other.arch.family {
225                    return self.arch.variant.cmp(&other.arch.variant);
226                }
227
228                // For the time being, manually make aarch64 windows disfavored on its own host
229                // platform, because most packages don't have wheels for aarch64 windows, making
230                // emulation more useful than native execution!
231                //
232                // The reason we do this in "sorting" and not "supports" is so that we don't
233                // *refuse* to use an aarch64 windows pythons if they happen to be installed and
234                // nothing else is available.
235                //
236                // Similarly if someone manually requests an aarch64 windows install, we should
237                // respect that request (this is the way users should "override" this behaviour).
238                let preferred = if self.os.is_windows() {
239                    Arch {
240                        family: target_lexicon::Architecture::X86_64,
241                        variant: None,
242                    }
243                } else {
244                    // Prefer native architectures
245                    Arch::from_env()
246                };
247
248                match (
249                    self.arch.family == preferred.family,
250                    other.arch.family == preferred.family,
251                ) {
252                    (true, true) => unreachable!(),
253                    (true, false) => cmp::Ordering::Less,
254                    (false, true) => cmp::Ordering::Greater,
255                    (false, false) => {
256                        // Both non-preferred, fallback to lexicographic order
257                        self.arch
258                            .family
259                            .to_string()
260                            .cmp(&other.arch.family.to_string())
261                    }
262                }
263            })
264            // Finally compare libc
265            .then_with(|| self.libc.to_string().cmp(&other.libc.to_string()))
266    }
267}
268
269impl PartialOrd for Platform {
270    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
271        Some(self.cmp(other))
272    }
273}
274
275impl From<&uv_platform_tags::Platform> for Platform {
276    fn from(value: &uv_platform_tags::Platform) -> Self {
277        Self {
278            os: Os::from(value.os()),
279            arch: Arch::from(&value.arch()),
280            libc: Libc::from(value.os()),
281        }
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn test_platform_display() {
291        let platform = Platform {
292            os: Os::from_str("linux").unwrap(),
293            arch: Arch::from_str("x86_64").unwrap(),
294            libc: Libc::from_str("gnu").unwrap(),
295        };
296        assert_eq!(platform.to_string(), "linux-x86_64-gnu");
297    }
298
299    #[test]
300    fn test_platform_from_str() {
301        let platform = Platform::from_str("macos-aarch64-none").unwrap();
302        assert_eq!(platform.os.to_string(), "macos");
303        assert_eq!(platform.arch.to_string(), "aarch64");
304        assert_eq!(platform.libc.to_string(), "none");
305    }
306
307    #[test]
308    fn test_platform_from_parts() {
309        let platform = Platform::from_parts("linux", "x86_64", "gnu").unwrap();
310        assert_eq!(platform.os.to_string(), "linux");
311        assert_eq!(platform.arch.to_string(), "x86_64");
312        assert_eq!(platform.libc.to_string(), "gnu");
313
314        // Test with arch variant
315        let platform = Platform::from_parts("linux", "x86_64_v3", "musl").unwrap();
316        assert_eq!(platform.os.to_string(), "linux");
317        assert_eq!(platform.arch.to_string(), "x86_64_v3");
318        assert_eq!(platform.libc.to_string(), "musl");
319
320        // Test error cases
321        assert!(Platform::from_parts("invalid_os", "x86_64", "gnu").is_err());
322        assert!(Platform::from_parts("linux", "invalid_arch", "gnu").is_err());
323        assert!(Platform::from_parts("linux", "x86_64", "invalid_libc").is_err());
324    }
325
326    #[test]
327    fn test_platform_from_str_with_arch_variant() {
328        let platform = Platform::from_str("linux-x86_64_v3-gnu").unwrap();
329        assert_eq!(platform.os.to_string(), "linux");
330        assert_eq!(platform.arch.to_string(), "x86_64_v3");
331        assert_eq!(platform.libc.to_string(), "gnu");
332    }
333
334    #[test]
335    fn test_platform_from_str_error() {
336        // Too few parts
337        assert!(Platform::from_str("linux-x86_64").is_err());
338        assert!(Platform::from_str("invalid").is_err());
339
340        // Too many parts (would have been accepted by the old code)
341        assert!(Platform::from_str("linux-x86-64-gnu").is_err());
342        assert!(Platform::from_str("linux-x86_64-gnu-extra").is_err());
343    }
344
345    #[test]
346    fn test_platform_sorting_os_precedence() {
347        let linux = Platform::from_str("linux-x86_64-gnu").unwrap();
348        let macos = Platform::from_str("macos-x86_64-none").unwrap();
349        let windows = Platform::from_str("windows-x86_64-none").unwrap();
350
351        // OS sorting takes precedence (alphabetical)
352        assert!(linux < macos);
353        assert!(macos < windows);
354    }
355
356    #[test]
357    fn test_platform_sorting_libc() {
358        let gnu = Platform::from_str("linux-x86_64-gnu").unwrap();
359        let musl = Platform::from_str("linux-x86_64-musl").unwrap();
360
361        // Same OS and arch, libc comparison (alphabetical)
362        assert!(gnu < musl);
363    }
364
365    #[test]
366    fn test_platform_sorting_arch_linux() {
367        // Test that Linux prefers the native architecture
368        use crate::arch::test_support::{aarch64, run_with_arch, x86_64};
369
370        let linux_x86_64 = Platform::from_str("linux-x86_64-gnu").unwrap();
371        let linux_aarch64 = Platform::from_str("linux-aarch64-gnu").unwrap();
372
373        // On x86_64 Linux, x86_64 should be preferred over aarch64
374        run_with_arch(x86_64(), || {
375            assert!(linux_x86_64 < linux_aarch64);
376        });
377
378        // On aarch64 Linux, aarch64 should be preferred over x86_64
379        run_with_arch(aarch64(), || {
380            assert!(linux_aarch64 < linux_x86_64);
381        });
382    }
383
384    #[test]
385    fn test_platform_sorting_arch_macos() {
386        use crate::arch::test_support::{aarch64, run_with_arch, x86_64};
387
388        let macos_x86_64 = Platform::from_str("macos-x86_64-none").unwrap();
389        let macos_aarch64 = Platform::from_str("macos-aarch64-none").unwrap();
390
391        // On x86_64 macOS, x86_64 should be preferred over aarch64
392        run_with_arch(x86_64(), || {
393            assert!(macos_x86_64 < macos_aarch64);
394        });
395
396        // On aarch64 macOS, aarch64 should be preferred over x86_64
397        run_with_arch(aarch64(), || {
398            assert!(macos_aarch64 < macos_x86_64);
399        });
400    }
401
402    #[test]
403    fn test_platform_supports() {
404        let native = Platform::from_str("linux-x86_64-gnu").unwrap();
405        let same = Platform::from_str("linux-x86_64-gnu").unwrap();
406        let different_arch = Platform::from_str("linux-aarch64-gnu").unwrap();
407        let different_os = Platform::from_str("macos-x86_64-none").unwrap();
408        let different_libc = Platform::from_str("linux-x86_64-musl").unwrap();
409
410        // Exact match
411        assert!(native.supports(&same));
412
413        // Different OS - not supported
414        assert!(!native.supports(&different_os));
415
416        // Different libc - not supported
417        assert!(!native.supports(&different_libc));
418
419        // Different architecture but same family
420        // x86_64 doesn't support aarch64 on Linux
421        assert!(!native.supports(&different_arch));
422
423        // Test architecture family support
424        let x86_64_v2 = Platform::from_str("linux-x86_64_v2-gnu").unwrap();
425        let x86_64_v3 = Platform::from_str("linux-x86_64_v3-gnu").unwrap();
426
427        // These have the same architecture family (both x86_64)
428        assert_eq!(native.arch.family(), x86_64_v2.arch.family());
429        assert_eq!(native.arch.family(), x86_64_v3.arch.family());
430
431        // Due to the family check, these should support each other
432        assert!(native.supports(&x86_64_v2));
433        assert!(native.supports(&x86_64_v3));
434    }
435
436    #[test]
437    fn test_windows_aarch64_platform_sorting() {
438        // Test that on Windows, x86_64 is preferred over aarch64
439        let windows_x86_64 = Platform::from_str("windows-x86_64-none").unwrap();
440        let windows_aarch64 = Platform::from_str("windows-aarch64-none").unwrap();
441
442        // x86_64 should sort before aarch64 on Windows (preferred)
443        assert!(windows_x86_64 < windows_aarch64);
444
445        // Test with multiple Windows platforms
446        let mut platforms = [
447            Platform::from_str("windows-aarch64-none").unwrap(),
448            Platform::from_str("windows-x86_64-none").unwrap(),
449            Platform::from_str("windows-x86-none").unwrap(),
450        ];
451
452        platforms.sort();
453
454        // After sorting on Windows, the order should be: x86_64 (preferred), aarch64, x86
455        // x86_64 is preferred on Windows regardless of native architecture
456        assert_eq!(platforms[0].arch.to_string(), "x86_64");
457        assert_eq!(platforms[1].arch.to_string(), "aarch64");
458        assert_eq!(platforms[2].arch.to_string(), "x86");
459    }
460
461    #[test]
462    fn test_windows_sorting_always_prefers_x86_64() {
463        // Test that Windows always prefers x86_64 regardless of host architecture
464        use crate::arch::test_support::{aarch64, run_with_arch, x86_64};
465
466        let windows_x86_64 = Platform::from_str("windows-x86_64-none").unwrap();
467        let windows_aarch64 = Platform::from_str("windows-aarch64-none").unwrap();
468
469        // Even with aarch64 as host, Windows should still prefer x86_64
470        run_with_arch(aarch64(), || {
471            assert!(windows_x86_64 < windows_aarch64);
472        });
473
474        // With x86_64 as host, Windows should still prefer x86_64
475        run_with_arch(x86_64(), || {
476            assert!(windows_x86_64 < windows_aarch64);
477        });
478    }
479
480    #[test]
481    fn test_windows_aarch64_supports() {
482        // Test that Windows aarch64 can run x86_64 binaries through emulation
483        let windows_aarch64 = Platform::from_str("windows-aarch64-none").unwrap();
484        let windows_x86_64 = Platform::from_str("windows-x86_64-none").unwrap();
485
486        // aarch64 Windows supports x86_64 through transparent emulation
487        assert!(windows_aarch64.supports(&windows_x86_64));
488
489        // But x86_64 doesn't support aarch64
490        assert!(!windows_x86_64.supports(&windows_aarch64));
491
492        // Self-support should always work
493        assert!(windows_aarch64.supports(&windows_aarch64));
494        assert!(windows_x86_64.supports(&windows_x86_64));
495    }
496
497    #[test]
498    fn test_from_platform_tags_platform() {
499        // Test conversion from uv_platform_tags::Platform to uv_platform::Platform
500        let tags_platform = uv_platform_tags::Platform::new(
501            uv_platform_tags::Os::Windows,
502            uv_platform_tags::Arch::X86_64,
503        );
504        let platform = Platform::from(&tags_platform);
505
506        assert_eq!(platform.os.to_string(), "windows");
507        assert_eq!(platform.arch.to_string(), "x86_64");
508        assert_eq!(platform.libc.to_string(), "none");
509
510        // Test with manylinux
511        let tags_platform_linux = uv_platform_tags::Platform::new(
512            uv_platform_tags::Os::Manylinux {
513                major: 2,
514                minor: 17,
515            },
516            uv_platform_tags::Arch::Aarch64,
517        );
518        let platform_linux = Platform::from(&tags_platform_linux);
519
520        assert_eq!(platform_linux.os.to_string(), "linux");
521        assert_eq!(platform_linux.arch.to_string(), "aarch64");
522        assert_eq!(platform_linux.libc.to_string(), "gnu");
523    }
524
525    #[test]
526    fn test_as_cargo_dist_triple_armv7_libc_handling() {
527        // This mapping reflects our current behavior: we currently ship
528        // only hard-float artifacts for ARMv7, so if we fail to detect
529        // the float ABI (i.e., we get the generic `Gnu` or `Musl`),
530        // we assume hard-float.
531        for (arch, libc, expected) in [
532            // ARMv7: detected ABI passes through unchanged.
533            ("armv7", "gnueabihf", "armv7-unknown-linux-gnueabihf"),
534            ("armv7", "gnueabi", "armv7-unknown-linux-gnueabi"),
535            ("armv7", "musleabihf", "armv7-unknown-linux-musleabihf"),
536            ("armv7", "musleabi", "armv7-unknown-linux-musleabi"),
537            // ARMv7: generic libc (detection failure) is biased to hard-float.
538            ("armv7", "gnu", "armv7-unknown-linux-gnueabihf"),
539            ("armv7", "musl", "armv7-unknown-linux-musleabihf"),
540            // Non-ARMv7: libc passes through unchanged.
541            ("aarch64", "gnu", "aarch64-unknown-linux-gnu"),
542            ("x86_64", "musl", "x86_64-unknown-linux-musl"),
543        ] {
544            let platform = Platform::from_parts("linux", arch, libc).unwrap();
545            assert_eq!(
546                platform.as_cargo_dist_triple(),
547                expected,
548                "linux-{arch}-{libc}"
549            );
550        }
551    }
552}