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