Skip to main content

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}