1use 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#[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 pub fn new(os: Os, arch: Arch, libc: Libc) -> Self {
47 Self { os, arch, libc }
48 }
49
50 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 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 pub fn supports(&self, other: &Self) -> bool {
69 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 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 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 (Architecture::X86_64, Architecture::X86_32(_)) |
102 (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 (Architecture::Aarch64(_), Architecture::X86_64)
117 )
118 {
119 return true;
120 }
121
122 if other.arch.is_wasm() {
124 return true;
125 }
126
127 if self.arch.family() != other.arch.family() {
131 return false;
132 }
133
134 true
135 }
136
137 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 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 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_with(|| {
222 if self.arch.family == other.arch.family {
223 return self.arch.variant.cmp(&other.arch.variant);
224 }
225
226 let preferred = if self.os.is_windows() {
237 Arch {
238 family: target_lexicon::Architecture::X86_64,
239 variant: None,
240 }
241 } else {
242 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 self.arch
256 .family
257 .to_string()
258 .cmp(&other.arch.family.to_string())
259 }
260 }
261 })
262 .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 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 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 assert!(Platform::from_str("linux-x86_64").is_err());
336 assert!(Platform::from_str("invalid").is_err());
337
338 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 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 assert!(gnu < musl);
361 }
362
363 #[test]
364 fn test_platform_sorting_arch_linux() {
365 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 run_with_arch(x86_64(), || {
373 assert!(linux_x86_64 < linux_aarch64);
374 });
375
376 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 run_with_arch(x86_64(), || {
391 assert!(macos_x86_64 < macos_aarch64);
392 });
393
394 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 assert!(native.supports(&same));
410
411 assert!(!native.supports(&different_os));
413
414 assert!(!native.supports(&different_libc));
416
417 assert!(!native.supports(&different_arch));
420
421 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 assert_eq!(native.arch.family(), x86_64_v2.arch.family());
427 assert_eq!(native.arch.family(), x86_64_v3.arch.family());
428
429 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 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 assert!(windows_x86_64 < windows_aarch64);
442
443 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 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 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 run_with_arch(aarch64(), || {
469 assert!(windows_x86_64 < windows_aarch64);
470 });
471
472 run_with_arch(x86_64(), || {
474 assert!(windows_x86_64 < windows_aarch64);
475 });
476 }
477
478 #[test]
479 fn test_windows_aarch64_supports() {
480 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 assert!(windows_aarch64.supports(&windows_x86_64));
486
487 assert!(!windows_x86_64.supports(&windows_aarch64));
489
490 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 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 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 for (arch, libc, expected) in [
530 ("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", "gnu", "armv7-unknown-linux-gnueabihf"),
537 ("armv7", "musl", "armv7-unknown-linux-musleabihf"),
538 ("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}