cargo_dist/platform.rs
1//! Logic for computing how different platforms are supported by a project's archives.
2//!
3//! The main entrypoint for this is [`PlatformSupport::new`][].
4//! [`PlatformSupport::platforms`][] is what you want to query.
5//!
6//!
7//! # Platform Support
8//!
9//! The complexity of this module is trying to handle things like:
10//!
11//! * linux-musl-static binaries work on linux-gnu platforms
12//! * but linux-gnu binaries are preferable if they're available
13//! * but if the target system has a really old version of glibc, the linux-gnu binaries won't work
14//! * so the linux-musl-static binaries need to still be eligible as a fallback
15//!
16//! ("x64 macos binaries can run on arm64 macos under rosetta2" is another good canonical example)
17//!
18//! [`PlatformSupport::platforms`][] is an index
19//! from "target I want to install to" ([`TripleName`][])
20//! to "list of archives we can potentially use to do that" ([`PlatformEntry`][]).
21//! The list is sorted in decreasing order from best-to-worst options. The basic idea
22//! is that you go down that list and try each option in order until one "works".
23//! Typically there will only be one option, and that option will always work.
24//!
25//!
26//!
27//! ## SupportQuality
28//!
29//! We get *multiple* options when there are targets that interop, typically
30//! with an emulation layer or some kind of compromise. The level of compromise
31//! is captured by [`SupportQuality`][], which is what we sort the options by.
32//! It can be found on [`PlatformEntry::quality`][].
33//!
34//! For instance, on linux-gnu, linux-gnu binaries have [`SupportQuality::HostNative`][] (best)
35//! while linux-musl-static binaris have [`SupportQuality::ImperfectNative`][] (excellent).
36//!
37//! Note that this `SupportQuality` is specific to the target platform. For instance
38//! x64 macos binaries are [`SupportQuality::HostNative`][] on x86_64-apple-darwin but
39//! [`SupportQuality::Emulated`][] on aarch64-apple-darwin (they run via Rosetta 2).
40//!
41//!
42//! ## RuntimeConditions
43//!
44//! A technically-superior option can *fail* if there are known runtime conditions for
45//! it to execute properly on the install-target system, and the system doesn't satisfy
46//! those conditions. These conditions are captured by [`RuntimeConditions`][].
47//! It can be found on [`PlatformEntry::runtime_conditions`][].
48//!
49//! For instance, linux-gnu binaries are built against a specific version of glibc.
50//! It can work with any glibc *newer* than that version, but it will hard error out
51//! with a glibc *older* than that version.
52//!
53//! It's up to each installer to check these conditions to the best of their ability
54//! and discard options that won't work. As of this writing, the shell installer
55//! does the best job of this, because linux has the most relevant fallback/conditions.
56//!
57//!
58//! ## Native RuntimeConditions
59//!
60//! Note that [`FetchableArchive::native_runtime_conditions`][] also exists but
61//! **YOU PROBABLY DON'T WANT THAT VALUE**. It contains runtime conditions that
62//! are *intrinsic* to the archive, which is a subset of [`PlatformEntry::runtime_conditions`][].
63//!
64//! For instance the glibc version is intrinsic to a linux-gnu archive, and
65//! is therefore a native_runtime_condition, so it will show up in both places.
66//! However "must have Rosetta2 installed" isn't intrinsic to x64 macos binaries,
67//! it *only* applies to "x64 macos binaries on arm64 macos", and so will *only*
68//! appear in [`PlatformEntry::runtime_conditions`][].
69//!
70//!
71//! # When To Invoke This Subsystem
72//!
73//! [`PlatformSupport::new`][] can be called at any time, and will do its best to produce
74//! the best possible results with the information it has. However, the later
75//! this function can be (re)run, the better information it will have.
76//!
77//! In particular, only once we have info from building and linkage-checking
78//! the binaries will we have all the [`RuntimeConditions`][]. In a typical
79//! CI run of dist this is fine, because the main use of this info
80//! is for installers, which are built with a fresh invocation on a machine
81//! with all binaries/platform info prefetched.
82//!
83//! However, if you were to run dist locally and try to build binaries
84//! and installers all at once, we currently fail to regenerate the platform
85//! info and update the installers. Doing this would necessitate some refactors
86//! to make the installers compute more of their archive/platform info "latebound"
87//! instead of the current very eager approach where we do that when building the
88//! DistGraph.
89//!
90//! In an ideal world we do an initial invocation of this API when building the DistGraph
91//! to get the list of platforms we expect to support (to know what an installer depends on),
92//! and then after building all binarier/archives and running linkage, we would rerun
93//! this API to get the final/complete picture. Then when we go to build installers we
94//! would lookup the *details* of PlatformSupport.
95//!
96//!
97//! # Compatibility Shims
98//!
99//! There's lots of things that care about platforms/archives, and they were written
100//! before this module. As of this writing we're in the process of gradually migrating
101//! them to using the full power of this API.
102//!
103//! To enable that migration, the PlatformSupport has a few APIs that will squash its
104//! richer information into legacy/simpler ones. In an ideal world we stop using these
105//! APIs and migrate all installers to just Doing It Right (but Doing It Right
106//! moves more logic into each installer, as it essentially requires each installer
107//! to have a full implementation for how to query [`PlatformSupport::platforms`][]
108//! and do the RuntimeCondition fallbacks.
109//!
110//!
111//! ## Fragments
112//!
113//! Fragments is the old platform support format that this API was made to replace.
114//!
115//! [`PlatformSupport::fragments`][] throws out all the fallback/condition information
116//! to produce a list of archives, each with a single target it claims to support.
117//! In cases where e.g. you have linux-musl-static build but no linux-gnu build, we
118//! will emit multiple copies of the linux-musl-static archive, one for each platform
119//! it's the best option for (so typically 3 copies covering linux-musl-static,
120//! linux-musl-dynamic, and linux-gnu).
121//!
122//! This system is a lot easier for an installer to handle, because all it needs to
123//! do is compute the target-triple it wants to try to install, and get the one
124//! archive that claims to support that (or error if none).
125//!
126//! Historically things like musl fallback were implemented in an installer during
127//! its target-triple selection with a single global hardcoded glibc version.
128//!
129//!
130//! ## Conflated Runtime Conditions
131//!
132//! [`PlatformSupport::safe_conflated_runtime_conditions`][] and
133//! [`PlatformSupport::conflated_runtime_conditions`][] exist to deal with
134//! installers that have the above "single global hardcoded glibc version"
135//! mentioned in the previous section.
136//!
137//! It represents a half-step to removing that, by removing the "hardcoded"
138//! part, having the version be baked into the installer when we generate it.
139//!
140//! The "conflation" occurs when you have multiple linux-gnu platforms.
141//! This is typical if you build for x64 and arm64 linux. In this case, the
142//! runners may have different glibc versions, so there's no "correct"
143//! global hardcoded version.
144//!
145//! Conflated conditions handle this by taking the maximum, which is *safe*
146//! but may prevent people from installing on a compatible system (see
147//! the next section for details).
148//!
149//!
150//! # The Importance of Glibc Versions
151//!
152//! Getting the right glibc version is important because it's used to:
153//!
154//! * Trigger musl fallback in installers if your glibc is too new
155//! * Informatively error out installers if there is no musl fallback
156//!
157//! If the version is wrong, there are two kinds of failure mode.
158//!
159//! If this version is too new, we may spuriously error during install for overly
160//! strict constraints, preventing users from installing the application at all.
161//! If there is a musl-static fallback this isn't a concern, and instead we'll just
162//! overly-aggressively use the musl fallback (though it's mildly unfortunate that
163//! a "more native" option is available and unused).
164//!
165//! If this version is too old we will fail to error and/or fail to invoke musl fallback,
166//! and may claim to successfully install linux-gnu binaries which will immediately error out
167//! when run.
168//!
169//!
170//! ### Madeup Glibc Versions
171//!
172//! There is currently a FIXME in `native_runtime_conditions_for_artifact` about us making up a fake
173//! glibc version if we can't find one, but we're clearing supposed to be linking linux-gnu.
174//!
175//! Under ideal conditions this only is "transiently" used when we're too-eagerly looking up
176//! runtime conditions, or doing tests without linkage info. As such, they
177//! generally won't appear in final production installers.
178//! In this case they will get an "arbitrary" glibc version ([`LibcVersion::default_glibc`][]).
179//!
180//! *HOWEVER* there are genuine situations where we don't run linkage in production.
181//! For instance, if the archives were built and packaged in custom build
182//! steps, because the user wanted to use maturin for cross-compilation.
183//!
184//!
185//! ### Approximating Glibc Versions
186//!
187//! To the best of our knowledge, there is no way to "ask" a binary what version of glibc
188//! it's linked against (if this is wrong PLEASE let us know that would be so useful).
189//! It will tell you it's linked against glibc, but not the version
190//! (there's a version in the library name but that never changes and is therefore irrelevant).
191//!
192//! We approximate the answer by asking the glibc on the system that built the binary
193//! "hey what version are you" and then *ASSUME ALL BINARIES BUILT ON THAT PLATFORM WERE
194//! BUILT AGAINST IT*.
195//!
196//! This is the default for most toolchains and is correct in 99% of cases.
197//! However, some tools may go above and beyond to try to link against older glibcs.
198//! Tools such as maturin and zig do this. In this case we are likely to pick a too-new
199//! glibc version, see the previous sections for the implications of this.
200//! It's possible in the case of maturin you "just" need to check the glibc in the
201//! docker image it used? This is a guess though.
202//!
203//!
204//! # targets vs target
205//!
206//! Ok so a lot of dist's code is *vaguely* trying to allow for a single archive
207//! to *natively* be built for multiple architectures. This would for instance be the
208//! case for any apple Universal Binary, which is just several binaries built for different
209//! architectures all stapled together.
210//!
211//! This is why you'll see several places where an archive/binary has `targets`, *plural*.
212//!
213//! In practice this is headache inducing, and because nothing we support *actually*
214//! is like this, code variously has punted on supporting it, or asserts against it.
215//! As such, there's a lot of random places where we use `target`, *singular*.
216//! Typically `target` is just `targets[0]`.
217//!
218//! So anyway it would be cool if code tried to work with `targets` but if you see stuff
219//! only using target, or weirdly throwing out parts of targets... that's why.
220//!
221//! In theory *this* is the module that would handle it for everyone else, because once
222//! we've constructed [`PlatformSupport`][] the information is indexed such that the
223//! difference doesn't actually matter (nothing should care what platform an archive
224//! is *natively* for, they should just do whatever [`PlatformSupport::platforms`][] says).
225//!
226//! But until we care about universal binaries, it's not really worth dealing with.
227
228#![allow(rustdoc::private_intra_doc_links)]
229
230pub mod github_runners;
231pub mod targets;
232
233use std::collections::HashMap;
234
235use cargo_dist_schema::{
236 ArtifactId, AssetId, BuildEnvironment, ChecksumExtension, ChecksumValue, DistManifest,
237 GlibcVersion, Linkage, SystemInfo, TripleName, TripleNameRef,
238};
239use serde::Serialize;
240
241use crate::{
242 backend::installer::{ExecutableZipFragment, UpdaterFragment},
243 config::ZipStyle,
244 tasks::Artifact,
245 BinaryKind, DistGraphBuilder, ReleaseIdx, SortedMap,
246};
247
248use targets::{
249 TARGET_ARM64_MAC, TARGET_ARM64_MINGW, TARGET_ARM64_WINDOWS, TARGET_X64_MAC, TARGET_X64_MINGW,
250 TARGET_X64_WINDOWS, TARGET_X86_MINGW, TARGET_X86_WINDOWS,
251};
252
253/// values of the form `min-glibc-version = { some-target-triple = "2.8" }
254pub type MinGlibcVersion = SortedMap<String, LibcVersion>;
255
256/// Suffixes of TargetTriples that refer to statically linked linux libcs.
257///
258/// On Linux it's preferred to dynamically link libc *but* because the One True ABI
259/// is actually the Linux kernel syscall interface, you *can* theoretically statically
260/// link libc. This comes with various tradeoffs but the big selling point is that the
261/// Linux kernel is a much more slowly moving target, so you can build a binary
262/// that's portable across way more systems by statically linking libc. As such,
263/// for any archive claiming to provide a static libc linux build, we can mark this
264/// archive as providing support for any linux distro (for that architecture)
265///
266/// Currently rust takes "linux-musl" to mean "statically linked musl", but
267/// in the future it will mean "dynamically linked musl":
268///
269/// https://github.com/rust-lang/compiler-team/issues/422
270///
271/// To avoid this ambiguity, we prefer "musl-static" and "musl-dynamic" aliases to
272/// disambiguate this situation. This module immediately rename "musl" to "musl-static",
273/// so in the following listings we don't need to deal with bare "musl".
274///
275/// Also note that known bonus ABI suffixes like "eabihf" are also already dealt with.
276const LINUX_STATIC_LIBCS: &[&str] = &["linux-musl-static"];
277/// Dynamically linked linux libcs that static libcs can replace
278const LINUX_STATIC_REPLACEABLE_LIBCS: &[&str] = &["linux-gnu", "linux-musl-dynamic"];
279/// A fake TargetTriple for apple's universal2 format (staples x64 and arm64 together)
280const TARGET_MACOS_UNIVERSAL2: &str = "universal2-apple-darwin";
281
282/// The quality of support an archive provides for a given platform
283#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
284pub enum SupportQuality {
285 /// The archive natively supports this platform, there's no beating it
286 HostNative,
287 /// The archive natively supports this platform, but it's a Universal binary that contains
288 /// multiple platforms stapled together, so if there are also more precise archives, prefer those.
289 BulkyNative,
290 /// The archive is still technically native to this platform, but it's in some sense
291 /// imperfect. This can happen for things like "running a 32-bit binary on 64-bit" or
292 /// "using a statically linked linux libc". This solution is acceptable, but a HostNative
293 /// (or BulkyNative) solution should always be preferred.
294 ImperfectNative,
295 /// The archive is only running by the grace of pretty heavyweight emulation like Rosetta2.
296 /// This should be treated as a last resort, but hey, it works!
297 Emulated,
298 /// The layers of emulation are out of control.
299 Hellmulated,
300 /// STOP
301 HighwayToHellmulated,
302}
303
304/// A unixy libc version
305#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Serialize)]
306pub struct LibcVersion {
307 /// Major version
308 pub major: u64,
309 /// Series (minor) version
310 pub series: u64,
311}
312
313impl LibcVersion {
314 /// Get the default glibc version for cases where we just need to guess
315 /// and make one up.
316 ///
317 /// This is the glibc of Ubuntu 22.04, which is the oldest supported
318 /// github linux runner, as of this writing.
319 pub fn default_glibc() -> Self {
320 Self {
321 major: 2,
322 series: 31,
323 }
324 }
325
326 fn glibc_from_schema(schema: &GlibcVersion) -> Self {
327 Self {
328 major: schema.major,
329 series: schema.series,
330 }
331 }
332}
333
334impl std::fmt::Display for LibcVersion {
335 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
336 write!(f, "{}.{}", self.major, self.series)
337 }
338}
339
340impl<'de> serde::de::Deserialize<'de> for LibcVersion {
341 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
342 where
343 D: serde::Deserializer<'de>,
344 {
345 let version_str = String::deserialize(deserializer)?;
346 let parts: Vec<&str> = version_str.split('.').collect();
347 if parts.len() != 2 {
348 return Err(serde::de::Error::custom(
349 "libc version must be {major}.{series} where major and series are numbers",
350 ));
351 }
352
353 let major = parts[0].parse().map_err(|_| {
354 serde::de::Error::custom(format!(
355 "expected {{major}}.{{series}} where major and series are numbers, but got input with major={}",
356 parts[0],
357 ))
358 })?;
359
360 let series = parts[1].parse().map_err(|_| {
361 serde::de::Error::custom(format!(
362 "expected {{major}}.{{series}} where major and series are numbers, but got input with series={}",
363 parts[1],
364 ))
365 })?;
366
367 Ok(LibcVersion { major, series })
368 }
369}
370
371/// Conditions that an installer should ideally check before using this an archive
372#[derive(Debug, Clone, Default, Serialize)]
373pub struct RuntimeConditions {
374 /// The system glibc should be at least this version
375 #[serde(skip_serializing_if = "Option::is_none")]
376 pub min_glibc_version: Option<LibcVersion>,
377 /// The system musl libc should be at least this version
378 #[serde(skip_serializing_if = "Option::is_none")]
379 pub min_musl_version: Option<LibcVersion>,
380 /// Rosetta2 should be installed
381 #[serde(skip_serializing_if = "std::ops::Not::not")]
382 pub rosetta2: bool,
383}
384
385/// Computed platform support details for a Release
386#[derive(Debug, Clone, Default, Serialize)]
387pub struct PlatformSupport {
388 /// The prebuilt archives for the Release
389 pub archives: Vec<FetchableArchive>,
390 /// The updaters for the Release
391 pub updaters: Vec<FetchableUpdater>,
392 /// Which options are available for the given target-triples.
393 ///
394 /// The list of PlatformEntries is pre-sorted in descending quality, so the first
395 /// is the best and should be used if possible (but maybe there's troublesome RuntimeConditions).
396 pub platforms: SortedMap<TripleName, Vec<PlatformEntry>>,
397}
398
399/// An archive of the prebuilt binaries for an app that can be fetched
400#[derive(Debug, Clone, Serialize)]
401pub struct FetchableArchive {
402 /// The unique id (and filename) of the archive
403 pub id: ArtifactId,
404 /// Runtime conditions that are native to this archive
405 ///
406 /// (You can largely ignore these in favour of the runtime_conditions in PlatformEntry)
407 pub native_runtime_conditions: RuntimeConditions,
408 /// "The" target triple to use
409 pub target_triple: TripleName,
410 /// What target triples does this archive natively support
411 pub target_triples: Vec<TripleName>,
412 /// The checksum of the archive, if any
413 pub checksum: Option<FetchableArchiveChecksum>,
414 /// The executables in the archive (may include .exe, assumed to be in root)
415 pub executables: Vec<String>,
416 /// The dynamic libraries in the archive (assumed to be in root)
417 pub cdylibs: Vec<String>,
418 /// The static libraries in the archive (assumed to be in root)
419 pub cstaticlibs: Vec<String>,
420 /// The kind of compression the archive has
421 pub zip_style: ZipStyle,
422 /// The updater you should also fetch if you install this archive
423 pub updater: Option<FetchableUpdaterIdx>,
424}
425
426/// The checksum for a fetchable archive
427#[derive(Debug, Clone, Serialize)]
428pub struct FetchableArchiveChecksum {
429 /// The checksum style (sha256, etc.)
430 pub style: ChecksumExtension,
431
432 /// The checksum value (lowercase hex)
433 pub value: ChecksumValue,
434}
435
436/// An updater for an app that can be fetched
437#[derive(Debug, Clone, Serialize)]
438pub struct FetchableUpdater {
439 /// The unique id (and filename) of the updater
440 pub id: ArtifactId,
441 /// The binary name of the updater
442 pub binary: ArtifactId,
443}
444
445/// An index into [`PlatformSupport::archives`][]
446pub type FetchableArchiveIdx = usize;
447/// An index into [`PlatformSupport::updaters`][]
448pub type FetchableUpdaterIdx = usize;
449
450/// An entry describing how well an archive supports a platform
451#[derive(Debug, Clone, Serialize)]
452pub struct PlatformEntry {
453 /// The quality of the support (prefer more "native" support over "emulated"/"fallback")
454 pub quality: SupportQuality,
455 /// Conditions the system being installed to must satisfy for the install to work.
456 /// Ideally installers should check these before using this archive, and fall back to
457 /// "worse" ones if the conditions aren't met.
458 ///
459 /// For instance if you have a linux-gnu build but the system glibc is too old to run it,
460 /// you will want to skip it in favour of a more portable musl-static build.
461 pub runtime_conditions: RuntimeConditions,
462 /// The archive
463 pub archive_idx: FetchableArchiveIdx,
464}
465
466impl PlatformSupport {
467 /// Compute the PlatformSupport for a Release
468 pub(crate) fn new(dist: &DistGraphBuilder, release_idx: ReleaseIdx) -> PlatformSupport {
469 let mut platforms = SortedMap::<TripleName, Vec<PlatformEntry>>::new();
470 let release = dist.release(release_idx);
471 let mut archives = vec![];
472 let mut updaters = vec![];
473 // Gather up all the fetchable archives
474 for &variant_idx in &release.variants {
475 // Compute the updater this variant *would* make *if* it were built
476 let updater_idx = if dist.inner.config.installers.updater {
477 let updater_artifact = dist.make_updater_for_variant(variant_idx);
478 let updater = FetchableUpdater {
479 id: updater_artifact.id.clone(),
480 binary: updater_artifact.id.clone(),
481 };
482 let updater_idx = updaters.len();
483 updaters.push(updater);
484 Some(updater_idx)
485 } else {
486 None
487 };
488
489 // Compute the artifact zip this variant *would* make *if* it were built
490 // FIXME: this is a kind of hacky workaround for the fact that we don't have a good
491 // way to add artifacts to the graph and then say "ok but don't build it".
492 let (artifact, binaries) =
493 dist.make_executable_zip_for_variant(release_idx, variant_idx);
494
495 let native_runtime_conditions = native_runtime_conditions_for_artifact(dist, &artifact);
496
497 let executables = binaries
498 .iter()
499 .filter(|(idx, _)| dist.binary(*idx).kind == BinaryKind::Executable);
500 let cdylibs = binaries
501 .iter()
502 .filter(|(idx, _)| dist.binary(*idx).kind == BinaryKind::DynamicLibrary);
503 let cstaticlibs = binaries
504 .iter()
505 .filter(|(idx, _)| dist.binary(*idx).kind == BinaryKind::StaticLibrary);
506
507 let archive = FetchableArchive {
508 id: artifact.id,
509 // computed later
510 target_triple: TripleName::new("".to_owned()),
511 target_triples: artifact.target_triples,
512 executables: executables
513 .map(|(_, dest_path)| dest_path.file_name().unwrap().to_owned())
514 .collect(),
515 cdylibs: cdylibs
516 .map(|(_, dest_path)| dest_path.file_name().unwrap().to_owned())
517 .collect(),
518 cstaticlibs: cstaticlibs
519 .map(|(_, dest_path)| dest_path.file_name().unwrap().to_owned())
520 .collect(),
521 zip_style: artifact.archive.as_ref().unwrap().zip_style,
522 checksum: None,
523 native_runtime_conditions,
524 updater: updater_idx,
525 };
526
527 archives.push(archive);
528 }
529
530 // Compute what platforms each archive Really supports
531 for (archive_idx, archive) in archives.iter_mut().enumerate() {
532 let supports = supports(archive_idx, archive);
533 // FIXME: some places need us to pick a simple single target triple
534 // and it needs to have desugarrings that `supports` computes, so we
535 // just grab the first triple, which is always going to be a native one
536 if let Some((target, _)) = supports.first() {
537 archive.target_triple.clone_from(target);
538 }
539 for (target, support) in supports {
540 platforms.entry(target).or_default().push(support);
541 }
542 }
543
544 // Now sort the platform-support so the best options come first
545 for support in platforms.values_mut() {
546 support.sort_by(|a, b| {
547 // Sort by SupportQuality, tie break by artifact name (for stability)
548 a.quality.cmp(&b.quality).then_with(|| {
549 let archive_a = &archives[a.archive_idx];
550 let archive_b = &archives[b.archive_idx];
551 archive_a.id.cmp(&archive_b.id)
552 })
553 });
554 }
555
556 PlatformSupport {
557 archives,
558 updaters,
559 platforms,
560 }
561 }
562
563 /// Convert to the old-style format so we can gradually migrate
564 pub fn fragments(&self) -> Vec<ExecutableZipFragment> {
565 let mut fragments = vec![];
566 for (target, options) in &self.platforms {
567 let Some(option) = options.first() else {
568 continue;
569 };
570 let archive = &self.archives[option.archive_idx];
571 let updater = if let Some(updater_idx) = archive.updater {
572 let updater = &self.updaters[updater_idx];
573 Some(UpdaterFragment {
574 id: updater.id.clone(),
575 binary: updater.binary.clone(),
576 })
577 } else {
578 None
579 };
580 let fragment = ExecutableZipFragment {
581 id: archive.id.clone(),
582 target_triple: target.clone(),
583 zip_style: archive.zip_style,
584 executables: archive.executables.clone(),
585 cdylibs: archive.cdylibs.clone(),
586 cstaticlibs: archive.cstaticlibs.clone(),
587 runtime_conditions: option.runtime_conditions.clone(),
588 updater,
589 };
590 fragments.push(fragment);
591 }
592 fragments
593 }
594
595 /// Conflate all the options that `fragments` suggests to create a single unified
596 /// RuntimeConditions that can be used in installers while we transition to implementations
597 /// that more granularly factor in these details.
598 pub fn conflated_runtime_conditions(&self) -> RuntimeConditions {
599 let mut runtime_conditions = RuntimeConditions::default();
600 for options in self.platforms.values() {
601 let Some(option) = options.first() else {
602 continue;
603 };
604 runtime_conditions.merge(&option.runtime_conditions);
605 }
606 runtime_conditions
607 }
608
609 /// Similar to conflated_runtime_conditions, but certain None values
610 /// are replaced by safe defaults.
611 /// Currently, a default value is provided for glibc; others may be
612 /// provided in the future.
613 pub fn safe_conflated_runtime_conditions(&self) -> RuntimeConditions {
614 let mut runtime_conditions = self.conflated_runtime_conditions();
615 if runtime_conditions.min_glibc_version.is_none() {
616 runtime_conditions.min_glibc_version = Some(LibcVersion::default_glibc());
617 }
618
619 runtime_conditions
620 }
621
622 /// Add checksum information for all archives built so far. They appeared
623 /// in the manifest after the initial platform support was computed.
624 pub fn fill_in_checksums_from_manifest(&mut self, manifest: &DistManifest) {
625 for archive in &mut self.archives {
626 if let Some(manifest_archive) = manifest.artifacts.get(&archive.id) {
627 if let Some((style, value)) = manifest_archive.checksums.first_key_value() {
628 archive.checksum = Some(FetchableArchiveChecksum {
629 style: style.clone(),
630 value: value.clone(),
631 });
632 }
633 }
634 }
635 }
636
637 /// A chainable version of [`Self::fill_in_checksums_from_manifest`]
638 pub fn with_checksums_from_manifest(mut self, manifest: &DistManifest) -> Self {
639 self.fill_in_checksums_from_manifest(manifest);
640 self
641 }
642}
643
644/// Given an archive, compute all the platforms it technically supports,
645/// and to what level of quality.
646///
647/// It's fine to be very generous and repetitive here as long as SupportQuality
648/// is honest and can be used to sort the options. Any "this is dubious" solutions
649/// will be buried by more native/legit ones if they're available.
650fn supports(
651 archive_idx: FetchableArchiveIdx,
652 archive: &FetchableArchive,
653) -> Vec<(TripleName, PlatformEntry)> {
654 let mut res: Vec<(TripleName, PlatformEntry)> = Vec::new();
655 for target in &archive.target_triples {
656 // this whole function manipulates targets as a string slice, which
657 // is unfortunate — these manipulations would be better done on a
658 // "parsed" version of the target
659 let target = target.as_str();
660
661 // For the following linux checks we want to pull off any "eabihf" suffix while
662 // comparing/parsing libc types.
663 let (degunked_target, abigunk) = if let Some(inner_target) = target.strip_suffix("eabihf") {
664 (inner_target, "eabihf")
665 } else {
666 (target, "")
667 };
668
669 // If this is the ambiguous-soon-to-be-changed "musl" target, rename it to musl-static,
670 // which is its current behaviour.
671 let (target, degunked_target) = if let Some(system) = degunked_target.strip_suffix("musl") {
672 (
673 format!("{system}musl-static{abigunk}"),
674 format!("{degunked_target}-static"),
675 )
676 } else {
677 (target.to_owned(), degunked_target.to_owned())
678 };
679
680 // First, add the target itself as a HostNative entry
681 res.push((
682 TripleName::new(target.clone()),
683 PlatformEntry {
684 quality: SupportQuality::HostNative,
685 runtime_conditions: archive.native_runtime_conditions.clone(),
686 archive_idx,
687 },
688 ));
689
690 // If this is a static linux libc, say it can support any linux at ImperfectNative quality
691 for &static_libc in LINUX_STATIC_LIBCS {
692 let Some(system) = degunked_target.strip_suffix(static_libc) else {
693 continue;
694 };
695 for &libc in LINUX_STATIC_REPLACEABLE_LIBCS {
696 res.push((
697 TripleName::new(format!("{system}{libc}{abigunk}")),
698 PlatformEntry {
699 quality: SupportQuality::ImperfectNative,
700 runtime_conditions: archive.native_runtime_conditions.clone(),
701 archive_idx,
702 },
703 ));
704 }
705 break;
706 }
707
708 // universal2 macos binaries are totally native for both arches, but bulkier than
709 // necessary if we have builds for the individual platforms too.
710 if target == TARGET_MACOS_UNIVERSAL2 {
711 res.push((
712 TARGET_X64_MAC.to_owned(),
713 PlatformEntry {
714 quality: SupportQuality::BulkyNative,
715 runtime_conditions: archive.native_runtime_conditions.clone(),
716 archive_idx,
717 },
718 ));
719 res.push((
720 TARGET_ARM64_MAC.to_owned(),
721 PlatformEntry {
722 quality: SupportQuality::BulkyNative,
723 runtime_conditions: archive.native_runtime_conditions.clone(),
724 archive_idx,
725 },
726 ));
727 }
728
729 let target = TripleName::new(target);
730
731 // FIXME?: technically we could add "run 32-bit intel macos on 64-bit intel"
732 // BUT this is unlikely to succeed as you increasingly need an EOL macOS,
733 // as support was dropped in macOS Catalina (macOS 10.15, October 2019).
734 // So this is unlikely to be helpful and DEFINITELY shouldn't be suggested
735 // unless all installers enforce the check for OS version.
736
737 // If this is x64 macos, say it can run on arm64 macos using Rosetta2
738 // Note that Rosetta2 is not *actually* installed by default on Apple Silicon,
739 // and the auto-installer for it only applies to GUI apps, not CLI apps, so ideally
740 // any installer that uses this fallback should check if Rosetta2 is installed!
741 if target == TARGET_X64_MAC {
742 let runtime_conditions = RuntimeConditions {
743 rosetta2: true,
744 ..archive.native_runtime_conditions.clone()
745 };
746 res.push((
747 TARGET_ARM64_MAC.to_owned(),
748 PlatformEntry {
749 quality: SupportQuality::Emulated,
750 runtime_conditions,
751 archive_idx,
752 },
753 ));
754 }
755
756 // x86_32 windows binaries run fine on x86_64, but it's Imperfect compared to actual x86_64 binaries
757 if target == TARGET_X86_WINDOWS {
758 res.push((
759 TARGET_X64_WINDOWS.to_owned(),
760 PlatformEntry {
761 quality: SupportQuality::ImperfectNative,
762 runtime_conditions: archive.native_runtime_conditions.clone(),
763 archive_idx,
764 },
765 ));
766 }
767 if target == TARGET_X86_MINGW {
768 res.push((
769 TARGET_X64_MINGW.to_owned(),
770 PlatformEntry {
771 quality: SupportQuality::ImperfectNative,
772 runtime_conditions: archive.native_runtime_conditions.clone(),
773 archive_idx,
774 },
775 ));
776 }
777
778 // Windows' equivalent to Rosetta2 (CHPE) is in fact installed-by-default so no need to detect!
779 if target == TARGET_X64_WINDOWS || target == TARGET_X86_WINDOWS {
780 // prefer x64 over x86 if we have the option
781 let quality = if target == TARGET_X86_WINDOWS {
782 SupportQuality::Hellmulated
783 } else {
784 SupportQuality::Emulated
785 };
786 res.push((
787 TARGET_ARM64_WINDOWS.to_owned(),
788 PlatformEntry {
789 quality,
790 runtime_conditions: archive.native_runtime_conditions.clone(),
791 archive_idx,
792 },
793 ));
794 }
795 if target == TARGET_X64_MINGW || target == TARGET_X86_MINGW {
796 // prefer x64 over x86 if we have the option
797 let quality = if target == TARGET_X86_MINGW {
798 SupportQuality::Hellmulated
799 } else {
800 SupportQuality::Emulated
801 };
802 res.push((
803 TARGET_ARM64_MINGW.to_owned(),
804 PlatformEntry {
805 quality,
806 runtime_conditions: archive.native_runtime_conditions.clone(),
807 archive_idx,
808 },
809 ));
810 }
811
812 // windows-msvc binaries should always be acceptable on windows-gnu (mingw)
813 if let Some(system) = target.as_str().strip_suffix("windows-msvc") {
814 res.push((
815 TripleName::new(format!("{system}windows-gnu")),
816 PlatformEntry {
817 quality: SupportQuality::ImperfectNative,
818 runtime_conditions: archive.native_runtime_conditions.clone(),
819 archive_idx,
820 },
821 ));
822 }
823 }
824 res
825}
826
827impl RuntimeConditions {
828 fn merge(&mut self, other: &Self) {
829 let RuntimeConditions {
830 min_glibc_version,
831 min_musl_version,
832 rosetta2,
833 } = other;
834
835 self.min_glibc_version =
836 max_of_min_libc_versions(&self.min_glibc_version, min_glibc_version);
837 self.min_musl_version = max_of_min_libc_versions(&self.min_musl_version, min_musl_version);
838 self.rosetta2 |= rosetta2;
839 }
840}
841
842/// Combine two min_libc_versions to get a new min that satisfies both
843fn max_of_min_libc_versions(
844 lhs: &Option<LibcVersion>,
845 rhs: &Option<LibcVersion>,
846) -> Option<LibcVersion> {
847 match (*lhs, *rhs) {
848 (None, None) => None,
849 (Some(ver), None) | (None, Some(ver)) => Some(ver),
850 (Some(lhs), Some(rhs)) => Some(lhs.max(rhs)),
851 }
852}
853
854/// Compute the requirements for running the binaries of this release on its host platform
855fn native_runtime_conditions_for_artifact(
856 dist: &DistGraphBuilder,
857 artifact: &Artifact,
858) -> RuntimeConditions {
859 let manifest = &dist.manifest;
860 let mut runtime_conditions = RuntimeConditions::default();
861 let artifact_id = &artifact.id;
862
863 if let Some(artifact) = manifest.artifacts.get(artifact_id) {
864 for asset in &artifact.assets {
865 let asset_conditions = native_runtime_conditions_for_asset(manifest, &asset.id);
866 runtime_conditions.merge(&asset_conditions);
867 }
868 };
869
870 if artifact_id.to_string().contains("linux") && artifact_id.to_string().contains("-gnu") {
871 if let Some(version) = get_glibc_override(dist, artifact) {
872 runtime_conditions.min_glibc_version = Some(version);
873 }
874
875 // FIXME: in our test suite we're running bare artifacts=global so we're missing
876 // all artifact/linkage info, preventing basic glibc bounds
877 if runtime_conditions.min_glibc_version.is_none() {
878 runtime_conditions.min_glibc_version = Some(LibcVersion::default_glibc());
879 }
880 }
881
882 runtime_conditions
883}
884
885fn get_glibc_override(dist: &DistGraphBuilder, artifact: &Artifact) -> Option<LibcVersion> {
886 let version_map = dist.inner.config.builds.min_glibc_version.clone();
887
888 version_map.and_then(|vmap| {
889 // if min-glibc-version config option is specified at all.
890 artifact
891 .target_triples
892 .first()
893 // if the target triple has a min-glibc-version specified, use it.
894 .and_then(|t: &TripleName| vmap.get(&t.to_string()).copied())
895 // or, try using the min-glibc-version for the "*" wildcard.
896 .or_else(|| vmap.get("*").copied())
897 })
898}
899
900fn native_runtime_conditions_for_asset(
901 manifest: &DistManifest,
902 asset_id: &Option<AssetId>,
903) -> RuntimeConditions {
904 let Some(asset_id) = asset_id else {
905 return RuntimeConditions::default();
906 };
907 let Some(asset) = &manifest.assets.get(asset_id) else {
908 return RuntimeConditions::default();
909 };
910 let Some(linkage) = &asset.linkage else {
911 return RuntimeConditions::default();
912 };
913 // This one's actually infallible but better safe than sorry...
914 let Some(system) = manifest.systems.get(&asset.system) else {
915 return RuntimeConditions::default();
916 };
917
918 // Get various libc versions
919 let min_glibc_version = native_glibc_version(system, linkage);
920 let min_musl_version = native_musl_version(system, linkage);
921
922 // rosetta2 is never required to run a binary on its *host* platform
923 let rosetta2 = false;
924 RuntimeConditions {
925 min_glibc_version,
926 min_musl_version,
927 rosetta2,
928 }
929}
930
931/// Get the native glibc version this binary links against, to the best of our ability
932fn native_glibc_version(system: &SystemInfo, linkage: &Linkage) -> Option<LibcVersion> {
933 for lib in &linkage.system {
934 // If this links against glibc, then we need to require that
935 if lib.is_glibc() {
936 if let BuildEnvironment::Linux {
937 glibc_version: Some(system_glibc),
938 } = &system.build_environment
939 {
940 // If there's a system libc, assume that's what it was built against
941 return Some(LibcVersion::glibc_from_schema(system_glibc));
942 } else {
943 // If the system has no known libc version use Ubuntu 22.04's glibc as a guess
944 return Some(LibcVersion::default_glibc());
945 }
946 }
947 }
948 None
949}
950
951/// Get the native musl libc version this binary links against, to the best of our ability
952fn native_musl_version(_system: &SystemInfo, _linkage: &Linkage) -> Option<LibcVersion> {
953 // FIXME: this should be the same as glibc_version but we don't get this info yet!
954 None
955}
956
957/// Translates a Rust triple into a human-readable display name
958pub fn triple_to_display_name(name: &TripleNameRef) -> Option<&'static str> {
959 if name.as_str() == "all" {
960 Some("All Platforms")
961 } else {
962 TARGET_TRIPLE_DISPLAY_NAMES.get(name).copied()
963 }
964}
965
966lazy_static::lazy_static! {
967 static ref TARGET_TRIPLE_DISPLAY_NAMES: HashMap<&'static TripleNameRef, &'static str> =
968 {
969 use targets::*;
970
971 let mut map = HashMap::new();
972 map.insert(TARGET_X86_LINUX_GNU, "x86 Linux");
973 map.insert(TARGET_X64_LINUX_GNU, "x64 Linux");
974 map.insert(TARGET_ARM64_LINUX_GNU, "ARM64 Linux");
975 map.insert(TARGET_ARMV7_LINUX_GNU, "ARMv7 Linux");
976 map.insert(TARGET_ARMV6_LINUX_GNU, "ARMv6 Linux");
977 map.insert(TARGET_ARMV6_LINUX_GNU_HARDFLOAT, "ARMv6 Linux (Hardfloat)");
978 map.insert(TARGET_PPC64_LINUX_GNU, "PPC64 Linux");
979 map.insert(TARGET_PPC64LE_LINUX_GNU, "PPC64LE Linux");
980 map.insert(TARGET_S390X_LINUX_GNU, "S390x Linux");
981 map.insert(TARGET_RISCV_LINUX_GNU, "RISCV Linux");
982 map.insert(TARGET_LOONGARCH64_LINUX_GNU, "LoongArch64 Linux");
983 map.insert(TARGET_SPARC64_LINUX_GNU, "SPARC64 Linux");
984
985 map.insert(TARGET_X86_LINUX_MUSL, "x86 MUSL Linux");
986 map.insert(TARGET_X64_LINUX_MUSL, "x64 MUSL Linux");
987 map.insert(TARGET_ARM64_LINUX_MUSL, "ARM64 MUSL Linux");
988 map.insert(TARGET_ARMV7_LINUX_MUSL, "ARMv7 MUSL Linux");
989 map.insert(TARGET_ARMV6_LINUX_MUSL, "ARMv6 MUSL Linux");
990 map.insert(
991 TARGET_ARMV6_LINUX_MUSL_HARDFLOAT,
992 "ARMv6 MUSL Linux (Hardfloat)",
993 );
994 map.insert(TARGET_PPC64_LINUX_MUSL, "PPC64 MUSL Linux");
995 map.insert(TARGET_PPC64LE_LINUX_MUSL, "PPC64LE MUSL Linux");
996 map.insert(TARGET_S390X_LINUX_MUSL, "S390x MUSL Linux");
997 map.insert(TARGET_RISCV_LINUX_MUSL, "RISCV MUSL Linux");
998 map.insert(TARGET_LOONGARCH64_LINUX_MUSL, "LoongArch64 MUSL Linux");
999 map.insert(TARGET_SPARC64_LINUX_MUSL, "SPARC64 MUSL Linux");
1000
1001 map.insert(TARGET_X86_WINDOWS, "x86 Windows");
1002 map.insert(TARGET_X64_WINDOWS, "x64 Windows");
1003 map.insert(TARGET_ARM64_WINDOWS, "ARM64 Windows");
1004 map.insert(TARGET_X86_MINGW, "x86 MinGW");
1005 map.insert(TARGET_X64_MINGW, "x64 MinGW");
1006 map.insert(TARGET_ARM64_MINGW, "ARM64 MinGW");
1007
1008 map.insert(TARGET_X86_MAC, "x86 macOS");
1009 map.insert(TARGET_X64_MAC, "Intel macOS");
1010 map.insert(TARGET_ARM64_MAC, "Apple Silicon macOS");
1011
1012 map.insert(TARGET_X64_FREEBSD, "x64 FreeBSD");
1013 map.insert(TARGET_X64_ILLUMOS, "x64 IllumOS");
1014 map.insert(TARGET_X64_NETBSD, "x64 NetBSD");
1015 map.insert(TARGET_ARM64_IOS, "iOS");
1016 map.insert(TARGET_ARM64_IOS_SIM, "ARM64 iOS SIM");
1017 map.insert(TARGET_X64_IOS, "x64 iOS");
1018 map.insert(TARGET_ARM64_FUCHSIA, "ARM64 Fuchsia");
1019 map.insert(TARGET_ARM64_ANDROID, "Android");
1020 map.insert(TARGET_X64_ANDROID, "x64 Android");
1021 map.insert(TARGET_ASMJS_EMSCRIPTEN, "asm.js Emscripten");
1022 map.insert(TARGET_WASM32_WASI, "WASI");
1023 map.insert(TARGET_WASM32, "WASM");
1024 map.insert(TARGET_SPARC_SOLARIS, "SPARC Solaris");
1025 map.insert(TARGET_X64_SOLARIS, "x64 Solaris");
1026
1027 map
1028 };
1029}