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, OperatingSystem, Riscv64Architecture, X86_32Architecture,
141        };
142
143        let Self { os, arch, libc } = &self;
144
145        let arch_name = match arch.family() {
146            // Special cases where Display doesn't match target triple
147            Architecture::X86_32(X86_32Architecture::I686) => "i686".to_string(),
148            Architecture::Riscv64(Riscv64Architecture::Riscv64) => "riscv64gc".to_string(),
149            _ => arch.to_string(),
150        };
151        let vendor = match &**os {
152            OperatingSystem::Darwin(_) => "apple",
153            OperatingSystem::Windows => "pc",
154            _ => "unknown",
155        };
156        let os_name = match &**os {
157            OperatingSystem::Darwin(_) => "darwin",
158            _ => &os.to_string(),
159        };
160
161        let abi = match (&**os, libc) {
162            (OperatingSystem::Windows, _) => Some("msvc".to_string()),
163            (OperatingSystem::Linux, Libc::Some(env)) => Some({
164                // Special suffix for ARM with hardware float
165                if matches!(arch.family(), Architecture::Arm(ArmArchitecture::Armv7)) {
166                    format!("{env}eabihf")
167                } else {
168                    env.to_string()
169                }
170            }),
171            _ => None,
172        };
173
174        format!(
175            "{arch_name}-{vendor}-{os_name}{abi}",
176            abi = abi.map(|abi| format!("-{abi}")).unwrap_or_default()
177        )
178    }
179}
180
181impl fmt::Display for Platform {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        write!(f, "{}-{}-{}", self.os, self.arch, self.libc)
184    }
185}
186
187impl FromStr for Platform {
188    type Err = Error;
189
190    fn from_str(s: &str) -> Result<Self, Self::Err> {
191        let parts: Vec<&str> = s.split('-').collect();
192
193        if parts.len() != 3 {
194            return Err(Error::InvalidPlatformFormat(format!(
195                "expected exactly 3 parts separated by '-', got {}",
196                parts.len()
197            )));
198        }
199
200        Self::from_parts(parts[0], parts[1], parts[2])
201    }
202}
203
204impl Ord for Platform {
205    fn cmp(&self, other: &Self) -> cmp::Ordering {
206        self.os
207            .to_string()
208            .cmp(&other.os.to_string())
209            // Then architecture
210            .then_with(|| {
211                if self.arch.family == other.arch.family {
212                    return self.arch.variant.cmp(&other.arch.variant);
213                }
214
215                // For the time being, manually make aarch64 windows disfavored on its own host
216                // platform, because most packages don't have wheels for aarch64 windows, making
217                // emulation more useful than native execution!
218                //
219                // The reason we do this in "sorting" and not "supports" is so that we don't
220                // *refuse* to use an aarch64 windows pythons if they happen to be installed and
221                // nothing else is available.
222                //
223                // Similarly if someone manually requests an aarch64 windows install, we should
224                // respect that request (this is the way users should "override" this behaviour).
225                let preferred = if self.os.is_windows() {
226                    Arch {
227                        family: target_lexicon::Architecture::X86_64,
228                        variant: None,
229                    }
230                } else {
231                    // Prefer native architectures
232                    Arch::from_env()
233                };
234
235                match (
236                    self.arch.family == preferred.family,
237                    other.arch.family == preferred.family,
238                ) {
239                    (true, true) => unreachable!(),
240                    (true, false) => cmp::Ordering::Less,
241                    (false, true) => cmp::Ordering::Greater,
242                    (false, false) => {
243                        // Both non-preferred, fallback to lexicographic order
244                        self.arch
245                            .family
246                            .to_string()
247                            .cmp(&other.arch.family.to_string())
248                    }
249                }
250            })
251            // Finally compare libc
252            .then_with(|| self.libc.to_string().cmp(&other.libc.to_string()))
253    }
254}
255
256impl PartialOrd for Platform {
257    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
258        Some(self.cmp(other))
259    }
260}
261
262impl From<&uv_platform_tags::Platform> for Platform {
263    fn from(value: &uv_platform_tags::Platform) -> Self {
264        Self {
265            os: Os::from(value.os()),
266            arch: Arch::from(&value.arch()),
267            libc: Libc::from(value.os()),
268        }
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn test_platform_display() {
278        let platform = Platform {
279            os: Os::from_str("linux").unwrap(),
280            arch: Arch::from_str("x86_64").unwrap(),
281            libc: Libc::from_str("gnu").unwrap(),
282        };
283        assert_eq!(platform.to_string(), "linux-x86_64-gnu");
284    }
285
286    #[test]
287    fn test_platform_from_str() {
288        let platform = Platform::from_str("macos-aarch64-none").unwrap();
289        assert_eq!(platform.os.to_string(), "macos");
290        assert_eq!(platform.arch.to_string(), "aarch64");
291        assert_eq!(platform.libc.to_string(), "none");
292    }
293
294    #[test]
295    fn test_platform_from_parts() {
296        let platform = Platform::from_parts("linux", "x86_64", "gnu").unwrap();
297        assert_eq!(platform.os.to_string(), "linux");
298        assert_eq!(platform.arch.to_string(), "x86_64");
299        assert_eq!(platform.libc.to_string(), "gnu");
300
301        // Test with arch variant
302        let platform = Platform::from_parts("linux", "x86_64_v3", "musl").unwrap();
303        assert_eq!(platform.os.to_string(), "linux");
304        assert_eq!(platform.arch.to_string(), "x86_64_v3");
305        assert_eq!(platform.libc.to_string(), "musl");
306
307        // Test error cases
308        assert!(Platform::from_parts("invalid_os", "x86_64", "gnu").is_err());
309        assert!(Platform::from_parts("linux", "invalid_arch", "gnu").is_err());
310        assert!(Platform::from_parts("linux", "x86_64", "invalid_libc").is_err());
311    }
312
313    #[test]
314    fn test_platform_from_str_with_arch_variant() {
315        let platform = Platform::from_str("linux-x86_64_v3-gnu").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(), "gnu");
319    }
320
321    #[test]
322    fn test_platform_from_str_error() {
323        // Too few parts
324        assert!(Platform::from_str("linux-x86_64").is_err());
325        assert!(Platform::from_str("invalid").is_err());
326
327        // Too many parts (would have been accepted by the old code)
328        assert!(Platform::from_str("linux-x86-64-gnu").is_err());
329        assert!(Platform::from_str("linux-x86_64-gnu-extra").is_err());
330    }
331
332    #[test]
333    fn test_platform_sorting_os_precedence() {
334        let linux = Platform::from_str("linux-x86_64-gnu").unwrap();
335        let macos = Platform::from_str("macos-x86_64-none").unwrap();
336        let windows = Platform::from_str("windows-x86_64-none").unwrap();
337
338        // OS sorting takes precedence (alphabetical)
339        assert!(linux < macos);
340        assert!(macos < windows);
341    }
342
343    #[test]
344    fn test_platform_sorting_libc() {
345        let gnu = Platform::from_str("linux-x86_64-gnu").unwrap();
346        let musl = Platform::from_str("linux-x86_64-musl").unwrap();
347
348        // Same OS and arch, libc comparison (alphabetical)
349        assert!(gnu < musl);
350    }
351
352    #[test]
353    fn test_platform_sorting_arch_linux() {
354        // Test that Linux prefers the native architecture
355        use crate::arch::test_support::{aarch64, run_with_arch, x86_64};
356
357        let linux_x86_64 = Platform::from_str("linux-x86_64-gnu").unwrap();
358        let linux_aarch64 = Platform::from_str("linux-aarch64-gnu").unwrap();
359
360        // On x86_64 Linux, x86_64 should be preferred over aarch64
361        run_with_arch(x86_64(), || {
362            assert!(linux_x86_64 < linux_aarch64);
363        });
364
365        // On aarch64 Linux, aarch64 should be preferred over x86_64
366        run_with_arch(aarch64(), || {
367            assert!(linux_aarch64 < linux_x86_64);
368        });
369    }
370
371    #[test]
372    fn test_platform_sorting_arch_macos() {
373        use crate::arch::test_support::{aarch64, run_with_arch, x86_64};
374
375        let macos_x86_64 = Platform::from_str("macos-x86_64-none").unwrap();
376        let macos_aarch64 = Platform::from_str("macos-aarch64-none").unwrap();
377
378        // On x86_64 macOS, x86_64 should be preferred over aarch64
379        run_with_arch(x86_64(), || {
380            assert!(macos_x86_64 < macos_aarch64);
381        });
382
383        // On aarch64 macOS, aarch64 should be preferred over x86_64
384        run_with_arch(aarch64(), || {
385            assert!(macos_aarch64 < macos_x86_64);
386        });
387    }
388
389    #[test]
390    fn test_platform_supports() {
391        let native = Platform::from_str("linux-x86_64-gnu").unwrap();
392        let same = Platform::from_str("linux-x86_64-gnu").unwrap();
393        let different_arch = Platform::from_str("linux-aarch64-gnu").unwrap();
394        let different_os = Platform::from_str("macos-x86_64-none").unwrap();
395        let different_libc = Platform::from_str("linux-x86_64-musl").unwrap();
396
397        // Exact match
398        assert!(native.supports(&same));
399
400        // Different OS - not supported
401        assert!(!native.supports(&different_os));
402
403        // Different libc - not supported
404        assert!(!native.supports(&different_libc));
405
406        // Different architecture but same family
407        // x86_64 doesn't support aarch64 on Linux
408        assert!(!native.supports(&different_arch));
409
410        // Test architecture family support
411        let x86_64_v2 = Platform::from_str("linux-x86_64_v2-gnu").unwrap();
412        let x86_64_v3 = Platform::from_str("linux-x86_64_v3-gnu").unwrap();
413
414        // These have the same architecture family (both x86_64)
415        assert_eq!(native.arch.family(), x86_64_v2.arch.family());
416        assert_eq!(native.arch.family(), x86_64_v3.arch.family());
417
418        // Due to the family check, these should support each other
419        assert!(native.supports(&x86_64_v2));
420        assert!(native.supports(&x86_64_v3));
421    }
422
423    #[test]
424    fn test_windows_aarch64_platform_sorting() {
425        // Test that on Windows, x86_64 is preferred over aarch64
426        let windows_x86_64 = Platform::from_str("windows-x86_64-none").unwrap();
427        let windows_aarch64 = Platform::from_str("windows-aarch64-none").unwrap();
428
429        // x86_64 should sort before aarch64 on Windows (preferred)
430        assert!(windows_x86_64 < windows_aarch64);
431
432        // Test with multiple Windows platforms
433        let mut platforms = [
434            Platform::from_str("windows-aarch64-none").unwrap(),
435            Platform::from_str("windows-x86_64-none").unwrap(),
436            Platform::from_str("windows-x86-none").unwrap(),
437        ];
438
439        platforms.sort();
440
441        // After sorting on Windows, the order should be: x86_64 (preferred), aarch64, x86
442        // x86_64 is preferred on Windows regardless of native architecture
443        assert_eq!(platforms[0].arch.to_string(), "x86_64");
444        assert_eq!(platforms[1].arch.to_string(), "aarch64");
445        assert_eq!(platforms[2].arch.to_string(), "x86");
446    }
447
448    #[test]
449    fn test_windows_sorting_always_prefers_x86_64() {
450        // Test that Windows always prefers x86_64 regardless of host architecture
451        use crate::arch::test_support::{aarch64, run_with_arch, x86_64};
452
453        let windows_x86_64 = Platform::from_str("windows-x86_64-none").unwrap();
454        let windows_aarch64 = Platform::from_str("windows-aarch64-none").unwrap();
455
456        // Even with aarch64 as host, Windows should still prefer x86_64
457        run_with_arch(aarch64(), || {
458            assert!(windows_x86_64 < windows_aarch64);
459        });
460
461        // With x86_64 as host, Windows should still prefer x86_64
462        run_with_arch(x86_64(), || {
463            assert!(windows_x86_64 < windows_aarch64);
464        });
465    }
466
467    #[test]
468    fn test_windows_aarch64_supports() {
469        // Test that Windows aarch64 can run x86_64 binaries through emulation
470        let windows_aarch64 = Platform::from_str("windows-aarch64-none").unwrap();
471        let windows_x86_64 = Platform::from_str("windows-x86_64-none").unwrap();
472
473        // aarch64 Windows supports x86_64 through transparent emulation
474        assert!(windows_aarch64.supports(&windows_x86_64));
475
476        // But x86_64 doesn't support aarch64
477        assert!(!windows_x86_64.supports(&windows_aarch64));
478
479        // Self-support should always work
480        assert!(windows_aarch64.supports(&windows_aarch64));
481        assert!(windows_x86_64.supports(&windows_x86_64));
482    }
483
484    #[test]
485    fn test_from_platform_tags_platform() {
486        // Test conversion from uv_platform_tags::Platform to uv_platform::Platform
487        let tags_platform = uv_platform_tags::Platform::new(
488            uv_platform_tags::Os::Windows,
489            uv_platform_tags::Arch::X86_64,
490        );
491        let platform = Platform::from(&tags_platform);
492
493        assert_eq!(platform.os.to_string(), "windows");
494        assert_eq!(platform.arch.to_string(), "x86_64");
495        assert_eq!(platform.libc.to_string(), "none");
496
497        // Test with manylinux
498        let tags_platform_linux = uv_platform_tags::Platform::new(
499            uv_platform_tags::Os::Manylinux {
500                major: 2,
501                minor: 17,
502            },
503            uv_platform_tags::Arch::Aarch64,
504        );
505        let platform_linux = Platform::from(&tags_platform_linux);
506
507        assert_eq!(platform_linux.os.to_string(), "linux");
508        assert_eq!(platform_linux.arch.to_string(), "aarch64");
509        assert_eq!(platform_linux.libc.to_string(), "gnu");
510    }
511}