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