Skip to main content

rucc_driver/
link.rs

1//! Finding a linker and telling it what to link.
2//!
3//! Design: `spec/04-driver-and-cli.md` section 4.9. There is no linker of our own before 1.0, so
4//! this finds one on the machine and builds the command line it wants.
5//!
6//! The linker is invoked directly rather than through the system compiler driver. Going through
7//! `cc` would be shorter to write and would borrow that compiler's idea of where everything is,
8//! and it would also mean this compiler cannot link on a machine that has no other compiler on
9//! it, which is most of the machines a compiler ends up on. It would also make `-###` output a
10//! line that does not say what happens, since the interesting half would be inside the program
11//! being spawned.
12//!
13//! # What is not decided here
14//!
15//! The startup files and the library directories are looked for rather than configured, for the
16//! same reason `library` looks for the headers: gcc settles this when it is built because a gcc
17//! is built for the machine it will run on, and this is one binary that runs wherever it is
18//! copied. So the shape of the answer is a list of candidates per platform of which the ones
19//! that exist are taken, and a cross build says where the rest is with `--sysroot`.
20//!
21//! # The compiler's own runtime
22//!
23//! `crtbegin`, `crtend` and the runtime libraries are found the same way, on the machine rather
24//! than by configuration. Ours is `librucc_builtins.a`, looked for beside the compiler, and the
25//! machine's `libgcc` goes on after it for the parts we have not written, which today is the
26//! unwinder and its personality routine. The C library goes in front of both, so that on a target
27//! that has one its `memcpy` is the one that answers rather than ours. `-fno-builtins-lib` leaves
28//! ours off, for somebody who wants libgcc to answer for everything.
29//!
30//! On a static link the three archives go inside `--start-group`, because `libc.a` refers to the
31//! unwinder and the unwinder refers back to `libc.a`, and a linker walking a list once resolves
32//! whichever of the two it reaches first and leaves the other undefined. That circularity is the
33//! whole reason `-static` failed before this, and it is issue #277.
34//!
35//! # Linking for a machine that is not this one
36//!
37//! Everything above describes a link against the machine running the compiler, and it is what runs
38//! when the target is that machine. A target that is not is a different problem: there is no
39//! `crt1.o` for it in `/usr/lib`, the `libc.so` there is the wrong architecture, and a line built
40//! out of what is lying around either fails at the first input or, worse, links. So a cross link
41//! does not look at this machine at all. It is built by [`rucc_sysroot::argv`] out of the target
42//! and a sysroot under the cache directory, and `spec/cross-compile/11-linking.md` section 11.3 is
43//! the design. [`cross_sysroot`] is the one place that decides which of the two it is.
44//!
45//! Two conditions keep that out of the way of everything that works today. The target has to differ
46//! from the host, and `--sysroot` must not have been given: somebody who assembled a tree and named
47//! it is asking for the line above with their own root in front of every path, which is what a
48//! cross compile with a real distribution tree in it has always been.
49//!
50//! That second condition is also the escape hatch for a machine which has a distribution's own cross
51//! files installed, where `/usr/lib/aarch64-linux-gnu` really does hold an AArch64 `crt1.o`.
52//! `--sysroot=/` takes the line above, and then every directory it decides is that machine's again.
53//!
54//! # What is not here yet
55//!
56//! Darwin, and Windows in Microsoft's ABI. `ld64` wants a platform version load command and a
57//! different set of default libraries, and `lld-link` wants a `/`-style command line and an import
58//! library set out of an SDK nobody may redistribute. Each arrives with the target that needs it,
59//! and a cross link to either is refused by name rather than approximated. A mingw-w64 target does
60//! have a line, because PE in that environment is written in the GNU style and the import libraries
61//! for it are ours to produce.
62//!
63//! The headers are the other half of a cross compile and [`crate::library::header_dirs`] is where
64//! they are decided. It asks [`cross_sysroot`] the same question this file asks it, which is the
65//! point: a compile that took its libc from the sysroot and its declarations from this machine would
66//! be wrong in the quietest way available, and one function answering for both is what stops that
67//! being possible.
68
69use std::ffi::OsString;
70use std::fs;
71use std::path::{Path, PathBuf};
72use std::process::Command;
73
74use rucc_sysroot::layout::{Kernel, Sysroot};
75use rucc_sysroot::{LinkMode, argv};
76use rucc_target::{Arch, Env, Os, Triple};
77use rucc_tuple::TargetTuple;
78
79/// What the command line said about linking.
80///
81/// Kept apart from `Options` because none of it reaches the compilation. A flag here changes what
82/// the linker is told and changes nothing about the object files handed to it, which is why `-lm`
83/// on a `-c` line is a note rather than an error.
84#[derive(Debug, Default, Clone, PartialEq, Eq)]
85pub struct LinkOptions {
86    /// `-fuse-ld=<name>`, which names a linker rather than a path to one.
87    pub use_ld: Option<String>,
88    /// `-L<dir>`, in order, because the linker takes the first library it finds.
89    pub search: Vec<PathBuf>,
90    /// `-B<prefix>`, which is where to look for the linker before looking on the path.
91    pub prefixes: Vec<PathBuf>,
92    /// `--sysroot=<dir>`, which prefixes the directories this looks in.
93    pub sysroot: Option<PathBuf>,
94    /// Where the generated sysroots are, which is [`crate::cache::dir`] on a real command line.
95    ///
96    /// [`None`] is a caller that was not given one, which outside a test is nothing, and then there
97    /// is no cross link line and a foreign target is refused the way it was before there was one.
98    /// It is a field rather than a call inside this module because a link line that read the
99    /// environment could only be tested on a machine whose environment said the right thing.
100    pub cache: Option<PathBuf>,
101    /// Where a distribution's cross packages put the tree for another architecture, which is
102    /// `/usr` on a real command line.
103    ///
104    /// Debian and Ubuntu install `libc6-dev-arm64-cross` and its friends as `/usr/<multiarch>/include`
105    /// and `/usr/<multiarch>/lib`, and gcc's own files for that target under
106    /// `/usr/lib/gcc-cross/<multiarch>`. [`distro_cross`] reads it. A field for the reason
107    /// [`LinkOptions::cache`] is one, and [`None`] in a test is a machine with no such packages.
108    pub usr: Option<PathBuf>,
109    /// `-static`.
110    pub is_static: bool,
111    /// `-shared`.
112    pub shared: bool,
113    /// `-pie` or `-no-pie`, and the platform's default when neither was written.
114    pub pie: Option<bool>,
115    /// `-nostdlib`, which is `-nostartfiles` and `-nodefaultlibs` together.
116    pub no_stdlib: bool,
117    /// `-nostartfiles`.
118    pub no_startfiles: bool,
119    /// `-nodefaultlibs`.
120    pub no_defaultlibs: bool,
121    /// `-rdynamic`, which puts every symbol in the dynamic table so a program can look itself up.
122    pub export_dynamic: bool,
123    /// `-s`, which drops the symbol table.
124    pub strip: bool,
125    /// `-fno-builtins-lib`, which leaves our own runtime off the line so that the machine's
126    /// libgcc answers for everything instead.
127    pub no_builtins_lib: bool,
128    /// The whole ten field target when `--target=` spelled one, which is where a pinned libc
129    /// release is.
130    ///
131    /// [`None`] is a command line that named no target at all, and then there is nothing pinned and
132    /// this machine is the target. A `Triple` has room for an architecture, an OS and an
133    /// environment and nowhere to put a release, so the release arrives here instead of there, and
134    /// [`cross_sysroot`] reads it for both of the things it decides: whether this is a cross link
135    /// and which directory under the cache it is against.
136    pub pinned: Option<TargetTuple>,
137    /// `-pg`, which changes the link as well as the code.
138    ///
139    /// The counts a profiled program keeps have to be started before `main` runs and written out
140    /// after it returns, and what does both is a start file of its own. So a build that compiles
141    /// with the flag and links without it produces a program that calls the hook on every function
142    /// and never writes a profile.
143    pub profile: bool,
144    /// Whether `-Ofast`, `-ffast-math` or `-funsafe-math-optimizations` was in force at the end
145    /// of the command line, which links `crtfastmath.o` into anything that is not a shared object.
146    ///
147    /// That file is a constructor which sets flush to zero and denormals are zero before `main`,
148    /// so the mode is the process's rather than the unit's, and it is the half of fast math the
149    /// compiler cannot give from inside a function.
150    pub fast_math: bool,
151    /// `-mdaz-ftz` or `-mno-daz-ftz`, which decides the same file outright and for a shared
152    /// object as well.
153    pub daz_ftz: Option<bool>,
154}
155
156impl LinkOptions {
157    /// Whether gcc's `crtfastmath.o` goes on the line, which is its end file spec on x86-64.
158    fn wants_fastmath(&self) -> bool {
159        self.daz_ftz.unwrap_or(self.fast_math && !self.shared)
160    }
161
162    /// Whether the startup files go on the line.
163    fn wants_startfiles(&self) -> bool {
164        !self.no_stdlib && !self.no_startfiles
165    }
166
167    /// Whether the library the program was written against goes on the line.
168    fn wants_defaultlibs(&self) -> bool {
169        !self.no_stdlib && !self.no_defaultlibs
170    }
171
172    /// Whether the compiler's own runtime goes on the line.
173    ///
174    /// The same switch as the C library, because `-nodefaultlibs` in GCC means the compiler's
175    /// runtime too, and a link that keeps `libgcc` while dropping `libc` is not a thing anyone
176    /// asks for on purpose.
177    fn wants_runtime(&self) -> bool {
178        !self.no_stdlib && !self.no_defaultlibs
179    }
180}
181
182/// One item on the link line, in the order it was written, because link order is semantic.
183///
184/// A library named before the object that needs it is not found on a static link, which is the
185/// oldest surprise in the toolchain and the reason this is one ordered list rather than a list of
186/// files and a list of libraries.
187#[derive(Debug, Clone, PartialEq, Eq)]
188pub enum Item {
189    /// A file: an object this compilation produced, or one named on the command line.
190    File(String),
191    /// `-l<name>`, which the linker resolves against its search path.
192    Library(String),
193    /// One word from `-Wl,` or `-Xlinker`, handed to the linker where the user wrote it.
194    ///
195    /// Here rather than in a list of its own because a great many of the linker's options are a
196    /// bracket around the files after them, and an option moved away from what it brackets means
197    /// something else or nothing at all. `--whole-archive` says that every member of every archive
198    /// named after it goes in whether anything referenced it or not, `--start-group` says that the
199    /// archives after it are searched again until nothing more comes out, and `-Bstatic` says which
200    /// half of a library that ships both is wanted. Collecting them and appending them to the end
201    /// leaves each of those pointing at nothing.
202    Linker(String),
203}
204
205impl std::fmt::Display for Item {
206    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207        match self {
208            Item::File(path) => f.write_str(path),
209            Item::Library(name) => write!(f, "-l{name}"),
210            Item::Linker(arg) => write!(f, "-Wl,{arg}"),
211        }
212    }
213}
214
215/// Why a link could not be run.
216#[derive(Debug, Clone, PartialEq, Eq)]
217pub enum Error {
218    /// No linker was found, after looking everywhere there was to look.
219    NoLinker {
220        /// The names that were tried, in the order they were tried.
221        tried: Vec<String>,
222    },
223    /// `-fuse-ld=` named one that is not on this machine.
224    Named {
225        /// What it named.
226        name: String,
227    },
228    /// A target this does not know how to build a link line for.
229    Target {
230        /// The triple that was asked for.
231        triple: String,
232    },
233    /// A cross link this scheme cannot produce, which [`rucc_sysroot::argv`] has explained.
234    ///
235    /// The reason is carried as a sentence rather than as a variant per cause, because the causes
236    /// live in `rucc-sysroot` and a second enumeration here would be a second thing to keep in step
237    /// with them. What this adds is that the sentence came from a link rather than from a
238    /// compilation.
239    Cross {
240        /// Why, in full, ready to print.
241        why: String,
242    },
243    /// The sysroot a cross link needs is not on this machine.
244    Sysroot {
245        /// The target that was asked for.
246        target: String,
247        /// Where its sysroot would be.
248        dir: String,
249        /// Whether this release pins an artifact for that target, which decides whether the message
250        /// can name a command that would fix it.
251        pinned: bool,
252    },
253    /// The linker was found and cannot do this target's link.
254    ///
255    /// Separate from [`Error::NoLinker`] because the linker is there and runs, and separate from
256    /// [`Error::Refused`] because the refusal is ours rather than its own: this is the case the
257    /// linker would not complain about at all.
258    TooOld {
259        /// What it was found as, which is what to look for when replacing it.
260        name: String,
261        /// The major version it reported.
262        found: u32,
263        /// The target whose link it cannot do.
264        target: String,
265    },
266    /// The linker was found and could not be started.
267    Spawn {
268        /// Where it was.
269        path: String,
270        /// What the operating system said.
271        why: String,
272    },
273    /// The linker ran and said no.
274    Refused {
275        /// What it exited with, or a description when it was killed instead.
276        status: String,
277    },
278}
279
280impl std::fmt::Display for Error {
281    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282        match self {
283            Error::NoLinker { tried } => {
284                write!(f, "no linker was found; tried {}", tried.join(", "))
285            }
286            Error::Named { name } => {
287                write!(f, "-fuse-ld={name} asks for a linker that is not on this machine")
288            }
289            Error::Target { triple } => {
290                write!(f, "there is no link line for {triple} in this compiler yet")
291            }
292            Error::Cross { why } => f.write_str(why),
293            // Two sentences and the second one changes, because a person whose link just failed
294            // wants the command that fixes it and there is only a command to name when this release
295            // pins an artifact for that target. Section 13.8's rule is that a compile which is
296            // missing a sysroot says what to run rather than running it, and this is where it says
297            // it.
298            Error::Sysroot { target, dir, pinned: true } => write!(
299                f,
300                "there is no sysroot for {target} at {dir}, so there is nothing to link it \
301                 against. `rucc --fetch {target}` gets the one this release pins, or pass \
302                 --sysroot=<dir> to name a tree you have already"
303            ),
304            Error::Sysroot { target, dir, pinned: false } => write!(
305                f,
306                "there is no sysroot for {target} at {dir}, so there is nothing to link it \
307                 against, and this release pins none for it to fetch. Pass --sysroot=<dir> to name \
308                 a tree you have already, or see spec/cross-compile/13-distribution.md section \
309                 13.2 for the cache that will hold one"
310            ),
311            // The whole message, because the person reading it has a linker that works, a link that
312            // succeeded on their last try, and no reason to suspect the thing that is wrong.
313            Error::TooOld { name, found, target } => write!(
314                f,
315                "{name} is lld {found} and cannot link for {target}. mingw-w64 writes a few hundred \
316                 of its aliases, `_crt_atexit == atexit` among them, as IMPORT_NAME_EXPORTAS \
317                 records in its import libraries, which lld learned to read in {LLD_EXPORTAS}. An \
318                 older one neither reads them nor says so: it writes an import by ordinal zero, the \
319                 link succeeds, and the program dies at startup. Install lld {LLD_EXPORTAS} or \
320                 newer, or name one with -fuse-ld="
321            ),
322            Error::Spawn { path, why } => write!(f, "could not run the linker at {path}: {why}"),
323            Error::Refused { status } => write!(f, "the linker {status}"),
324        }
325    }
326}
327
328impl std::error::Error for Error {}
329
330/// A linker, found.
331#[derive(Debug, Clone, PartialEq, Eq)]
332pub struct Linker {
333    /// The name it is known by, which is what `--print-config` reports.
334    pub name: String,
335    /// Where it is, which is what gets spawned.
336    pub path: PathBuf,
337}
338
339/// The names to look for, in the order section 4.9 gives.
340///
341/// `mold` first because it is dramatically faster, and a compiler that is twice the speed of
342/// another one while the link takes twelve seconds has not helped anybody. Then `lld`, then the
343/// platform's own. Each is looked for under both the bare name and the `ld.` prefix, because a
344/// distribution installs `mold` under its own name and `ld.mold` for exactly this lookup.
345#[must_use]
346pub fn order(target: Triple, opts: &LinkOptions) -> Vec<String> {
347    if let Some(named) = &opts.use_ld {
348        // A name rather than a path, so `-fuse-ld=mold` finds a `mold` that is not `ld.mold`.
349        return vec![format!("ld.{named}"), named.clone()];
350    }
351    if cross_sysroot(target, opts).is_some() || distro_cross(target, opts).is_some() {
352        return cross_order(target);
353    }
354    match target.os {
355        Os::Windows => vec!["lld-link".to_owned(), "link.exe".to_owned()],
356        _ => vec![
357            "ld.mold".to_owned(),
358            "mold".to_owned(),
359            "ld.lld".to_owned(),
360            "lld".to_owned(),
361            "ld".to_owned(),
362        ],
363    }
364}
365
366/// The names to look for when the target is not this machine.
367///
368/// A shorter list than the one above and a different one, because most of that list cannot do this.
369/// `spec/cross-compile/11-linking.md` section 11.2 settles it: `ld.lld` is the ELF cross linker,
370/// since one binary of it links for every architecture it was built with and that is all of them.
371/// mold is off the list because it links for the host and `wild` likewise, which is why section 11.2
372/// has them as `-fuse-ld=` choices for a native link rather than as defaults. The platform's own
373/// `ld` is off it for the same reason: a distribution's `/usr/bin/ld` is built for one architecture,
374/// and `-fuse-ld=` is still there for somebody whose is not.
375///
376/// A cross binutils under its prefixed name is last, because a machine that has
377/// `aarch64-linux-gnu-ld` installed has it on purpose. The prefix is a distribution convention and
378/// there are two of them: a Linux target is filed under its multiarch name and a mingw-w64 one under
379/// `<arch>-w64-mingw32`, which is what every distribution's mingw packages install. `ld.lld` is the
380/// same binary for both, because its MinGW mode is a mode of the one linker rather than a second one.
381fn cross_order(target: Triple) -> Vec<String> {
382    let mut names = vec!["ld.lld".to_owned(), "lld".to_owned()];
383    match (target.os, target.env) {
384        (Os::Linux, _) => names.push(format!("{}-ld", multiarch(target))),
385        (Os::Windows, Env::Gnu) => names.push(format!("{}-w64-mingw32-ld", target.arch.as_str())),
386        _ => {}
387    }
388    names
389}
390
391/// The sysroot a cross link would use, or [`None`] for a link against this machine.
392///
393/// The one place the two paths are told apart, so that the linker that is looked for and the line it
394/// is handed cannot disagree about which kind of link this is.
395///
396/// Three conditions, and two of them are about leaving working configurations alone. A target that
397/// is this machine is linked against this machine, which is what every native compile has always
398/// done and what the directories under `/usr/lib` are for. A `--sysroot` the user wrote is taken as
399/// the root of a tree they assembled, and the line above prefixes every path it decides with it,
400/// which is what cross compiling against a real distribution tree has always meant here. The third
401/// is that there has to be a cache directory to look in, which on a real command line there always
402/// is.
403///
404/// An unknown host counts as different from every target. A machine this compiler cannot name is a
405/// machine whose `/usr/lib` it should not be guessing at.
406///
407/// # A pinned release is a cross compile
408///
409/// The first of those three conditions is about the machine and not about the triple, and a target
410/// that names a libc release is not this machine even when it is this architecture. Somebody on a
411/// 2.44 box writing `--target=x86_64-linux-gnu.2.28` is asking for a binary that runs on a 2.28
412/// machine, and handing them their own headers and their own libc gives them a binary that does not.
413/// So the condition is the triple being the host *and* no release named, and what it costs is that a
414/// pin equal to this machine's own release also stops using this machine's libc. That is not a loss:
415/// the two should be the same text, and if they are not then this machine's copy is patched and the
416/// bundled tree is the one the pin asked for. tamnd/rucc#956.
417#[must_use]
418pub fn cross_sysroot(target: Triple, opts: &LinkOptions) -> Option<Sysroot> {
419    cross_for(target, opts, Triple::host())
420}
421
422/// The same answer with the host as a parameter, so that both branches are testable on one machine.
423fn cross_for(target: Triple, opts: &LinkOptions, host: Option<Triple>) -> Option<Sysroot> {
424    if opts.sysroot.is_some() {
425        return None;
426    }
427    let tuple = target_tuple(target, opts);
428    if host == Some(target) && tuple.env_version().is_none() {
429        return None;
430    }
431    if distro_for(target, opts, host).is_some() {
432        return None;
433    }
434    let cache = opts.cache.as_deref()?;
435    Some(Sysroot::in_cache(cache, tuple))
436}
437
438/// A tree a distribution's cross packages installed for a Linux target that is not this machine.
439///
440/// What `apt install gcc-aarch64-linux-gnu` leaves behind: the C library's headers and files under
441/// `/usr/aarch64-linux-gnu`, and gcc's `crtbegin.o` and `libgcc.a` for that target under
442/// `/usr/lib/gcc-cross/aarch64-linux-gnu/<version>`. The library's `libc.so` script names its files
443/// by their full path, so the tree is linked where it is and not under a `--sysroot`.
444#[derive(Debug, Clone, PartialEq, Eq)]
445pub struct Distro {
446    /// `/usr/<multiarch>`, which has `include` and `lib` under it.
447    pub root: PathBuf,
448    /// gcc's directories for the target, newest version first, and empty when only the library
449    /// was installed.
450    pub gcc: Vec<PathBuf>,
451}
452
453impl Distro {
454    /// The C library's headers, and the kernel's, which the same packages put in the same place.
455    #[must_use]
456    pub fn include(&self) -> PathBuf {
457        self.root.join("include")
458    }
459
460    /// The C library's startup files and libraries.
461    #[must_use]
462    pub fn lib(&self) -> PathBuf {
463        self.root.join("lib")
464    }
465}
466
467/// The distribution's tree for this target, when a cross compile should use it.
468///
469/// Only when nothing better was asked for or is there. A `--sysroot` is a tree somebody named, a
470/// pinned release is a request for our tree cut at that release, and a sysroot already fetched into
471/// the cache is the one this release pins, so each of those wins. What is left is a machine that
472/// has the distribution's cross packages and nothing of ours, and there the packages are what a
473/// prefixed gcc on the same machine would use, so they are what this uses too.
474#[must_use]
475pub fn distro_cross(target: Triple, opts: &LinkOptions) -> Option<Distro> {
476    distro_for(target, opts, Triple::host())
477}
478
479/// The same answer with the host as a parameter, for the same reason as [`cross_for`].
480fn distro_for(target: Triple, opts: &LinkOptions, host: Option<Triple>) -> Option<Distro> {
481    if opts.sysroot.is_some() || target.os != Os::Linux || host == Some(target) {
482        return None;
483    }
484    let tuple = target_tuple(target, opts);
485    if tuple.env_version().is_some() {
486        return None;
487    }
488    if opts.cache.as_deref().is_some_and(|cache| Sysroot::in_cache(cache, tuple).lib().is_dir()) {
489        return None;
490    }
491    let usr = opts.usr.as_deref()?;
492    let name = multiarch(target);
493    let root = usr.join(&name);
494    if !root.join("include").is_dir() || !root.join("lib").is_dir() {
495        return None;
496    }
497    let gcc = newest_first(&usr.join("lib/gcc-cross").join(&name));
498    Some(Distro { root, gcc })
499}
500
501/// The target as the model that has room for a release, which is what names the cache directory.
502///
503/// The pinned spelling when there is one, because `x86_64-linux-gnu` and `x86_64-linux-gnu.2.28` are
504/// two sysroots and not one: the release is in the tuple for the reason
505/// `spec/cross-compile/03-target-model.md` section 3.2 admits a field at all, which is that it
506/// changes what is compiled. A command line that named no target, or one whose spelling the ten
507/// field parser did not take, falls back to what the three field one did.
508fn target_tuple(target: Triple, opts: &LinkOptions) -> TargetTuple {
509    opts.pinned.unwrap_or_else(|| target.tuple())
510}
511
512/// The kernel headers that go with [`cross_sysroot`], for the targets that have any.
513///
514/// The same three conditions, asked through the same function, because the two halves of one
515/// target's system headers have to be decided together or a compile could read glibc's `sys/stat.h`
516/// against this machine's `asm/stat.h`. A `None` here on a Linux target where the sysroot is `Some`
517/// means only one thing, which is that the cache has no kernel tree for that architecture, and the
518/// directory is still named for the reason [`crate::library::header_dirs`] gives.
519///
520/// Not under the sysroot, because `linux/` and `asm-generic/` are the same nine megabytes for every
521/// target that shares an architecture, and a copy per target is eight copies of one thing.
522#[must_use]
523pub fn cross_kernel(target: Triple, opts: &LinkOptions) -> Option<Kernel> {
524    kernel_for(target, opts, Triple::host())
525}
526
527/// The same answer with the host as a parameter, for the same reason as [`cross_for`].
528fn kernel_for(target: Triple, opts: &LinkOptions, host: Option<Triple>) -> Option<Kernel> {
529    cross_for(target, opts, host)?;
530    Kernel::for_target(opts.cache.as_deref()?, target.tuple())
531}
532
533/// How the result is linked, as the five cases a sysroot link line is written over.
534///
535/// Four booleans reach here and five cases leave, because static and position independent are not
536/// independent of each other and the start file differs in four of the five. The default for `pie`
537/// is the one the native line above uses, so that a command line that says neither gets the same
538/// answer whichever path it takes.
539fn mode(opts: &LinkOptions) -> LinkMode {
540    let pie = opts.pie.unwrap_or(!opts.is_static && !opts.shared);
541    if opts.shared {
542        LinkMode::Shared
543    } else if opts.is_static {
544        if pie { LinkMode::StaticPie } else { LinkMode::Static }
545    } else if pie {
546        LinkMode::Dynamic
547    } else {
548        LinkMode::DynamicNoPie
549    }
550}
551
552/// The line for a machine that is not this one, from the target and the sysroot and nothing else.
553///
554/// Everything this knows is already in `opts`, and all it does is say it in the shape
555/// [`rucc_sysroot::argv`] is written over. There is deliberately no decision here: a second place
556/// that decided what goes on a cross link line would be a second place to get it wrong, and the
557/// recorded lines under `tests/link-lines` would stop describing what this compiler does.
558fn cross_line(
559    target: Triple,
560    opts: &LinkOptions,
561    items: &[Item],
562    output: &str,
563    sysroot: &Sysroot,
564) -> Result<Vec<String>, Error> {
565    if opts.profile {
566        // `gcrt1.o` is a compiled object out of the C library's own sources, and a generated sysroot
567        // has the names a libc exports rather than the bodies behind them. Said here rather than
568        // left to the linker, because what the linker would say is that `main` is undefined.
569        return Err(Error::Cross {
570            why: format!(
571                "-pg needs gcrt1.o, or gcrt2.o on Windows, the startup file that starts and stops \
572                 the counting, and a generated sysroot for {target} does not have one. Profile on \
573                 the host, or pass --sysroot=<dir> naming a tree that has it"
574            ),
575        });
576    }
577    let inputs: Vec<argv::Item> = items
578        .iter()
579        .map(|item| match item {
580            Item::File(path) => argv::Item::File(PathBuf::from(path)),
581            Item::Library(name) => argv::Item::Library(name.clone()),
582            Item::Linker(arg) => argv::Item::Linker(arg.clone()),
583        })
584        .collect();
585    let output = PathBuf::from(output);
586    // Ours, from beside the compiler, because that is where `cargo xtask builtins` writes it and a
587    // fetched sysroot will never hold it. The cross line used to name it inside the sysroot, which
588    // is a file nothing puts there, so every cross link either failed at the linker or quietly ran
589    // against somebody else's `libgcc` copied in under the name. tamnd/rucc#1514.
590    let ours = builtins_archive(target, &opts.prefixes);
591    if ours.is_none() && opts.wants_runtime() && !opts.no_builtins_lib {
592        // Said here rather than left to the linker, which on a Windows target says `___chkstk_ms`
593        // is undefined and names mingw-w64's objects as the callers, and on a musl one says
594        // `__udivti3` is. Neither of those is a person's first guess at a missing archive.
595        let tuple = target.tuple().to_canonical_string();
596        return Err(Error::Cross {
597            why: format!(
598                "a cross link ends with librucc_builtins.a, this compiler's own runtime for \
599                 {tuple}, and there is none beside the compiler or under a -B prefix. A sysroot \
600                 does not carry it, because it is our output rather than the platform's. Build it \
601                 with `cargo xtask builtins --target={tuple}`, or pass -fno-builtins-lib to link \
602                 without it"
603            ),
604        });
605    }
606    let invocation = argv::Invocation {
607        inputs: &inputs,
608        output: Some(&output),
609        mode: mode(opts),
610        search: &opts.search,
611        no_startfiles: !opts.wants_startfiles(),
612        no_defaultlibs: !opts.wants_defaultlibs(),
613        no_builtins_lib: opts.no_builtins_lib,
614        builtins: ours.as_deref(),
615        export_dynamic: opts.export_dynamic,
616        strip: opts.strip,
617    };
618    argv::argv(target.tuple(), sysroot, &invocation)
619        .map_err(|why| Error::Cross { why: why.to_string() })
620}
621
622/// Whether this link can be run at all, asked before anything is compiled.
623///
624/// Two questions that have answers before the first object exists: whether there is a line for this
625/// target and mode at all, and whether the sysroot it would read is on the machine. Both are worth a
626/// second at the start rather than a message after a minute of compiling, which is the same reason
627/// the linker itself is looked for first.
628///
629/// The line is built rather than inspected, with no inputs and a name nothing will be written to,
630/// because the refusals belong to the one function that builds it. A link against this machine has
631/// nothing to answer here: its directories are looked for as the line is built and a missing one is
632/// simply a directory that is not offered.
633///
634/// # Errors
635///
636/// [`Error::Cross`] for a target or a mode that has no line, and [`Error::Sysroot`] when the sysroot
637/// it would be linked against is not there.
638pub fn preflight(target: Triple, opts: &LinkOptions) -> Result<(), Error> {
639    let Some(sysroot) = cross_sysroot(target, opts) else { return Ok(()) };
640    // Whether there is a line for this target and mode at all, asked with our own runtime left off
641    // it. Otherwise a target nothing here can link and a machine where nobody built the runtime
642    // report the same thing, and the archive is the smaller of the two problems by a long way.
643    let shape = LinkOptions { no_builtins_lib: true, ..opts.clone() };
644    cross_line(target, &shape, &[], "a.out", &sysroot)?;
645    // The library directory rather than the root, because the root of a cache directory that has
646    // been created and never populated is there and holds nothing. Section 11.6's rule is that
647    // suitable is checked and not assumed, and this is the cheapest form of that.
648    if !sysroot.lib().is_dir() {
649        let tuple = target_tuple(target, opts).to_canonical_string();
650        return Err(Error::Sysroot {
651            dir: sysroot.root().display().to_string(),
652            pinned: rucc_sysroot::pinned_for_target(target_tuple(target, opts)).is_some(),
653            target: tuple,
654        });
655    }
656    // And now the whole line, which is the sysroot's files plus ours, so that a missing runtime is
657    // said here rather than by the linker after everything has been compiled.
658    cross_line(target, opts, &[], "a.out", &sysroot)?;
659    Ok(())
660}
661
662/// Writes the stub libraries a glibc cross link reads, into [`Sysroot::stubs`].
663///
664/// `spec/cross-compile/09-libc-stubs.md` section 9.1: the stubs are generated on demand rather than
665/// shipped, out of the description `rucc-stub` carries, cut at the release the tuple names or at
666/// the bundled one when it names none. So there is nothing to fetch for them and nothing to go
667/// stale, and a pin is a different directory rather than a different download.
668///
669/// A file is written only when its bytes differ from what is there, and then through a temporary
670/// name and a rename, because two builds for one target run side by side all the time and a
671/// linker must never read a half written `libc.so`. The bytes are the same on every host, so two
672/// processes racing to write them race to write the same thing.
673///
674/// Nothing for a link against this machine, a `--sysroot` the user named, or a target that is not
675/// glibc. A glibc release newer than the bundled tree was already refused when the headers were
676/// chosen, so it is quietly nothing here too.
677///
678/// # Errors
679///
680/// [`Error::Cross`] when the stubs cannot be generated for this target or cannot be written.
681pub fn write_stubs(target: Triple, opts: &LinkOptions) -> Result<(), Error> {
682    let Some(sysroot) = cross_sysroot(target, opts) else { return Ok(()) };
683    let tuple = sysroot.target();
684    if rucc_stub::glibc::architecture(tuple).is_none() {
685        return Ok(());
686    }
687    let Ok(Some(minor)) = rucc_sysroot::bundled_glibc_minor(tuple) else { return Ok(()) };
688    let files = rucc_stub::glibc::stubs(tuple, minor).map_err(|why| Error::Cross {
689        why: format!("the glibc stubs for {}: {why}", tuple.to_canonical_string()),
690    })?;
691    let dir = sysroot.stubs();
692    let failed = |path: &Path, why: std::io::Error| Error::Cross {
693        why: format!("{} cannot be written: {why}", path.display()),
694    };
695    fs::create_dir_all(dir).map_err(|why| failed(dir, why))?;
696    for file in files {
697        let path = dir.join(&file.name);
698        if fs::read(&path).is_ok_and(|there| there == file.bytes) {
699            continue;
700        }
701        let temporary = dir.join(format!(".{}.{}", file.name, std::process::id()));
702        fs::write(&temporary, &file.bytes).map_err(|why| failed(&temporary, why))?;
703        fs::rename(&temporary, &path).map_err(|why| {
704            let _ = fs::remove_file(&temporary);
705            failed(&path, why)
706        })?;
707    }
708    Ok(())
709}
710
711/// The linker to use, looked for where a linker is.
712///
713/// `-B` prefixes first, since the point of one is to put a toolchain in front of the machine's,
714/// then the path. A name that contains a separator is a path and is taken as one, which is what
715/// gcc does with `-fuse-ld=/usr/bin/ld.gold` and what a build system relying on that expects.
716///
717/// # Errors
718///
719/// [`Error::Named`] when `-fuse-ld=` asked for one that is not here, and [`Error::NoLinker`] when
720/// nothing was, which name the candidates so that the message says what was looked for.
721pub fn find(target: Triple, opts: &LinkOptions) -> Result<Linker, Error> {
722    let tried = order(target, opts);
723    for name in &tried {
724        if name.contains(std::path::MAIN_SEPARATOR) || name.contains('/') {
725            let path = PathBuf::from(name);
726            if path.is_file() {
727                return Ok(Linker { name: name.clone(), path });
728            }
729            continue;
730        }
731        for dir in &opts.prefixes {
732            let path = dir.join(name);
733            if path.is_file() {
734                return Ok(Linker { name: name.clone(), path });
735            }
736        }
737        if let Some(path) = on_path(name) {
738            return Ok(Linker { name: name.clone(), path });
739        }
740    }
741    match &opts.use_ld {
742        Some(name) => Err(Error::Named { name: name.clone() }),
743        None => Err(Error::NoLinker { tried }),
744    }
745}
746
747/// The first lld that reads `IMPORT_NAME_EXPORTAS`, which is what a windows-gnu link needs.
748///
749/// 18 does not read it and does not say so, so the number is not a convenience: below it the
750/// answer is wrong rather than absent. tamnd/rucc#1515.
751pub const LLD_EXPORTAS: u32 = 19;
752
753/// Whether a found linker can do this target's link, asked before it is handed anything.
754///
755/// Section 11.6's rule is that suitable is checked and not assumed, and this is the one check that
756/// cannot be made by looking at a file. A windows-gnu link reads import libraries that mingw-w64's
757/// `==` aliases compiled into `IMPORT_NAME_EXPORTAS` records, which lld reads from
758/// [`LLD_EXPORTAS`] on. An older lld writes an import by ordinal zero instead, without a warning
759/// and with a successful exit, so nothing later in the toolchain has anything to notice: the
760/// program is wrong at startup and the link that made it said nothing. Ubuntu 24.04 is the current
761/// LTS and ships 18, so the machine this happens on is an ordinary one.
762///
763/// Every other target is left alone, and so is anything that is not an lld, because this is the one
764/// version of the one linker that is known to answer wrongly rather than not at all.
765///
766/// A linker that will not run or whose version cannot be read is allowed through. What the check
767/// can establish is that a specific old lld is here, and it should not turn every unusual linker
768/// into a refusal on the strength of failing to recognise it.
769///
770/// # Errors
771///
772/// [`Error::TooOld`] when the linker is an lld older than [`LLD_EXPORTAS`] and the target is
773/// windows-gnu.
774pub fn suitable(target: Triple, linker: &Linker) -> Result<(), Error> {
775    if (target.os, target.env) != (Os::Windows, Env::Gnu) {
776        return Ok(());
777    }
778    let Some(found) = lld_major(&reported_version(&linker.path)) else { return Ok(()) };
779    if found >= LLD_EXPORTAS {
780        return Ok(());
781    }
782    Err(Error::TooOld {
783        name: linker.name.clone(),
784        found,
785        target: target.tuple().to_canonical_string(),
786    })
787}
788
789/// What `<linker> --version` prints, or an empty string when it will not say.
790///
791/// A linker that cannot be started is not this function's problem to report, because the link is
792/// about to start it again and say so properly. What this returns for such a one is nothing to
793/// read, which is the same as a linker that ran and said something unrecognisable.
794fn reported_version(path: &Path) -> String {
795    let Ok(out) = Command::new(path).arg("--version").output() else { return String::new() };
796    String::from_utf8_lossy(&out.stdout).into_owned()
797}
798
799/// The major version in an lld's `--version`, when the program that printed it was an lld.
800///
801/// What lld prints is `LLD 18.1.8 (compatible with GNU linkers)`, with a distribution's own prefix
802/// in front of it often enough that the word is looked for rather than the line starting with it:
803/// Ubuntu's says `Ubuntu LLD 18.1.3`. Binutils prints `GNU ld (GNU Binutils for Ubuntu) 2.42` and
804/// mold prints its own name, and neither has the word, so both come back as [`None`] and are left
805/// alone.
806fn lld_major(text: &str) -> Option<u32> {
807    let mut words = text.split_whitespace();
808    words.find(|word| *word == "LLD")?;
809    words.next()?.split('.').next()?.parse().ok()
810}
811
812/// The first executable of that name on `PATH`.
813///
814/// Executability is checked rather than assumed, because a directory of that name on `PATH` is
815/// not a thing to try to run and neither is a file nobody may execute.
816fn on_path(name: &str) -> Option<PathBuf> {
817    let path = std::env::var_os("PATH")?;
818    std::env::split_paths(&path).map(|dir| dir.join(name)).find(|p| executable(p))
819}
820
821/// Whether a path is a file this process could run.
822#[cfg(unix)]
823fn executable(path: &Path) -> bool {
824    use std::os::unix::fs::PermissionsExt as _;
825    path.metadata().is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
826}
827
828/// Whether a path is a file this process could run.
829///
830/// Windows has no executable bit and decides by extension, and the names looked for above carry
831/// theirs, so being a file is the whole of the question here.
832#[cfg(not(unix))]
833fn executable(path: &Path) -> bool {
834    path.is_file()
835}
836
837/// What the linker is told, in order, not counting the linker itself.
838///
839/// Two lines and [`cross_sysroot`] picks which: the one above for this machine, and
840/// [`rucc_sysroot::argv`]'s for any other. Nothing about the machine is read on the second path, so
841/// `-###` prints the same line on every host and prints it whether the sysroot has been built or
842/// not, which is what makes it worth printing.
843///
844/// # Errors
845///
846/// [`Error::Target`] for a platform there is no native line for yet, which is every one but Linux,
847/// and [`Error::Cross`] for a cross link that cannot be produced at all.
848pub fn line(
849    target: Triple,
850    opts: &LinkOptions,
851    items: &[Item],
852    output: &str,
853) -> Result<Vec<String>, Error> {
854    if let Some(sysroot) = cross_sysroot(target, opts) {
855        return cross_line(target, opts, items, output, &sysroot);
856    }
857    if target.os != Os::Linux {
858        return Err(Error::Target { triple: target.to_string() });
859    }
860    let machine = emulation(target);
861    let root = opts.sysroot.as_deref();
862    // A distribution's cross tree is linked by the same line, with its two directories in place of
863    // this machine's, because it is laid out the way this machine's own library is.
864    let distro = distro_cross(target, opts);
865    let dirs = match &distro {
866        Some(distro) => vec![distro.lib()],
867        None => library_dirs(target, root),
868    };
869    // Where a gcc on this machine keeps its own runtime, which is a different place from where
870    // the C library keeps its own, and where our runtime is if it was built for this target.
871    let runtime = match distro {
872        Some(distro) => distro.gcc,
873        None => runtime_dirs(target, root),
874    };
875    let ours = if opts.no_builtins_lib { None } else { builtins_archive(target, &opts.prefixes) };
876    let mut args = vec![
877        "-o".to_owned(),
878        output.to_owned(),
879        // Which of the several formats one `ld` can write is meant. A linker built for more than
880        // one machine guesses from its first input otherwise, and a link of no objects at all has
881        // nothing to guess from.
882        "-m".to_owned(),
883        machine.to_owned(),
884        // The table a program unwinds through, which a C program with no exceptions in it still
885        // needs because `backtrace` and every crash handler read it.
886        "--eh-frame-hdr".to_owned(),
887        // The symbol hash a dynamic loader from this century reads. The old one is still written
888        // alongside by default on some distributions, and asking for this one is what stops a link
889        // from carrying a table nothing has needed since 2006.
890        "--hash-style=gnu".to_owned(),
891    ];
892
893    let pie = opts.pie.unwrap_or(!opts.is_static && !opts.shared);
894    if opts.shared {
895        args.push("-shared".to_owned());
896    } else if opts.is_static {
897        args.push("-static".to_owned());
898    } else if pie {
899        args.push("-pie".to_owned());
900    } else {
901        args.push("-no-pie".to_owned());
902    }
903    if !opts.is_static && !opts.shared {
904        args.push("-dynamic-linker".to_owned());
905        args.push(target_path(root, loader(target)));
906    }
907    if opts.export_dynamic {
908        args.push("--export-dynamic".to_owned());
909    }
910    if opts.strip {
911        args.push("-s".to_owned());
912    }
913
914    if opts.wants_startfiles() {
915        for name in startfile(opts, pie).into_iter().chain(["crti.o"]) {
916            if let Some(path) = find_file(&dirs, name) {
917                args.push(path.display().to_string());
918            }
919        }
920        // The compiler's own startup file, which runs the static constructors. Three spellings
921        // of the same thing, and which one is right is about how the code in it refers to
922        // itself: `S` for a position independent result, `T` for a static one, plain for the
923        // rest. Skipped when there is no gcc on the machine to take it from, because a program
924        // with no constructor in it does not miss it.
925        let begin = if opts.shared || pie {
926            "crtbeginS.o"
927        } else if opts.is_static {
928            "crtbeginT.o"
929        } else {
930            "crtbegin.o"
931        };
932        if let Some(path) = find_file(&runtime, begin).or_else(|| find_file(&runtime, "crtbegin.o"))
933        {
934            args.push(path.display().to_string());
935        }
936    }
937
938    for dir in &opts.search {
939        args.push(format!("-L{}", dir.display()));
940    }
941    for dir in &dirs {
942        args.push(format!("-L{}", dir.display()));
943    }
944    // Where `libgcc.a` and `libgcc_eh.a` are, which is not where the C library is. Nothing is
945    // added when there is no gcc on the machine, and then the `-l` names below are left off too.
946    for dir in &runtime {
947        args.push(format!("-L{}", dir.display()));
948    }
949
950    for item in items {
951        match item {
952            Item::File(path) => args.push(path.clone()),
953            Item::Library(name) => args.push(format!("-l{name}")),
954            Item::Linker(arg) => args.push(arg.clone()),
955        }
956    }
957    // After the objects, because a static archive is searched for what is undefined at the point
958    // it is reached and a library named before the object that needs it contributes nothing.
959    args.extend(runtime_items(opts, &runtime, ours.as_deref()));
960
961    if opts.wants_startfiles() {
962        // The other end of `crtbegin`, and it goes before `crtn.o` for the same reason `crti.o`
963        // goes before `crtbegin`: the four are two nested pairs and not four separate files.
964        // The fast math constructor, ahead of `crtend` where gcc puts it. Skipped when there is
965        // no gcc to take it from, as `crtbegin` is.
966        if opts.wants_fastmath() {
967            if let Some(path) = find_file(&runtime, "crtfastmath.o") {
968                args.push(path.display().to_string());
969            }
970        }
971        let end = if opts.shared || pie { "crtendS.o" } else { "crtend.o" };
972        if let Some(path) = find_file(&runtime, end).or_else(|| find_file(&runtime, "crtend.o")) {
973            args.push(path.display().to_string());
974        }
975        if let Some(path) = find_file(&dirs, "crtn.o") {
976            args.push(path.display().to_string());
977        }
978    }
979
980    Ok(args)
981}
982
983/// The startup file the C library brings, or `None` for a link that calls nothing.
984///
985/// This is what calls `main` and what passes it the arguments, so a shared object takes none of
986/// them: nothing starts one and it has no `main` to be started at. `Scrt1.o` rather than `crt1.o`
987/// when the result moves, because the two differ in whether the reference to `main` in them is one
988/// a loader may relocate.
989///
990/// A profiled program gets a different one again, which does all of that and starts and stops the
991/// counting around it. There are two of those rather than three: the one that relocates itself is
992/// only needed by a static position independent link, and every other link takes the plain one,
993/// which is what gcc does with the same flag.
994fn startfile(opts: &LinkOptions, pie: bool) -> Option<&'static str> {
995    if opts.shared {
996        None
997    } else if opts.profile {
998        Some(if pie && opts.is_static { "grcrt1.o" } else { "gcrt1.o" })
999    } else if pie {
1000        Some("Scrt1.o")
1001    } else {
1002        Some("crt1.o")
1003    }
1004}
1005
1006/// The libraries the compiler's own runtime contributes, in the order the linker wants them.
1007///
1008/// The C library first, then ours, then the machine's `libgcc`. Order inside this list is not
1009/// about whether a symbol resolves, it is about which archive supplies one that more than one of
1010/// them defines, and the two places that happens both have a right answer.
1011///
1012/// `memcpy` and its three neighbours are in the C library on a hosted target and in ours only for
1013/// a freestanding one, which is what `spec/12-abi-and-runtime.md` section 12.8 says they are for.
1014/// glibc's are written in assembly per microarchitecture and ours is a word at a time loop, so a
1015/// link that took ours over glibc's would be slower at the one routine every program reaches.
1016///
1017/// The wide arithmetic is in ours and in `libgcc` both, and the two are ABI-identical on purpose,
1018/// so which one answers is not a correctness question. Ours comes first because it is ours, and
1019/// `-fno-builtins-lib` leaves it off for somebody who would rather it were not.
1020///
1021/// A static link puts the whole list inside `--start-group`. `libc.a` refers to `_Unwind_Resume`,
1022/// and the unwinder refers back into `libc.a`, so a linker walking the list once resolves
1023/// whichever it reaches first and reports the other as undefined. That is exactly the failure
1024/// issue #277 describes and the group is the fix for it.
1025///
1026/// A dynamic link needs no group, because the shared `libc` resolves its own references inside
1027/// itself. `libgcc_s` is asked for `--as-needed` there, the way gcc asks for it, so a program that
1028/// never unwinds does not acquire a dependency on it.
1029fn runtime_items(opts: &LinkOptions, runtime: &[PathBuf], ours: Option<&Path>) -> Vec<String> {
1030    let mut args = Vec::new();
1031    if !opts.wants_defaultlibs() && !opts.wants_runtime() {
1032        return args;
1033    }
1034    // Only when there is a gcc to take them from. On a machine without one the names would be an
1035    // error about a library that was never going to be there, and a program that needs neither
1036    // the unwinder nor a wide divide links and runs without them.
1037    let has_gcc = find_file(runtime, "libgcc.a").is_some();
1038
1039    if opts.is_static {
1040        args.push("--start-group".to_owned());
1041    }
1042    if opts.wants_defaultlibs() {
1043        args.push("-lc".to_owned());
1044    }
1045    if opts.wants_runtime() {
1046        if let Some(path) = ours {
1047            args.push(path.display().to_string());
1048        }
1049        if has_gcc {
1050            args.push("-lgcc".to_owned());
1051            if opts.is_static {
1052                args.push("-lgcc_eh".to_owned());
1053            }
1054        }
1055    }
1056    if opts.is_static {
1057        args.push("--end-group".to_owned());
1058    } else if opts.wants_runtime() && has_gcc {
1059        // The shared half, and only if something still wants it after everything above.
1060        args.push("--as-needed".to_owned());
1061        args.push("-lgcc_s".to_owned());
1062        args.push("--no-as-needed".to_owned());
1063    }
1064    args
1065}
1066
1067/// Where a gcc on this machine keeps `crtbegin.o`, `crtend.o` and `libgcc.a`, newest first.
1068///
1069/// This is not where the C library's files are. A distribution puts them under a directory named
1070/// for the gcc version, and there may be several, so the answer is every one that exists with the
1071/// highest version in front. Newest first because a newer `libgcc` is a superset of an older one
1072/// and because that is the one the C library on the same machine was built against.
1073#[must_use]
1074pub fn runtime_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
1075    let libc = match target.env {
1076        Env::Musl => "musl",
1077        Env::None | Env::Gnu | Env::Msvc => "gnu",
1078    };
1079    let arch = target.arch.as_str();
1080    // The spellings the distributions use for the same triple. Debian and Ubuntu drop the vendor
1081    // field, the source builds and Arch keep `pc`, and Red Hat and SUSE write their own name in
1082    // it, so all of them are looked for and the ones that are there are taken.
1083    let names = [
1084        format!("{arch}-linux-{libc}"),
1085        format!("{arch}-pc-linux-{libc}"),
1086        format!("{arch}-redhat-linux"),
1087        format!("{arch}-suse-linux"),
1088        format!("{arch}-alpine-linux-{libc}"),
1089    ];
1090    let mut found = Vec::new();
1091    for base in ["/usr/lib/gcc", "/usr/lib64/gcc", "/usr/local/lib/gcc"] {
1092        for name in &names {
1093            found.extend(newest_first(&under(sysroot, &format!("{base}/{name}"))));
1094        }
1095    }
1096    found
1097}
1098
1099/// The version directories under one of gcc's, highest version first.
1100fn newest_first(dir: &Path) -> Vec<PathBuf> {
1101    let Ok(entries) = fs::read_dir(dir) else { return Vec::new() };
1102    let mut versions: Vec<(Vec<u64>, PathBuf)> = entries
1103        .flatten()
1104        .map(|e| e.path())
1105        .filter(|p| p.is_dir())
1106        .map(|p| (version_key(&p), p))
1107        .collect();
1108    // Descending, so the highest version is the first place `find_file` looks. Ties keep the order
1109    // the directory gave, which is arbitrary and does not matter because two directories that sort
1110    // the same hold the same version.
1111    versions.sort_by(|a, b| b.0.cmp(&a.0));
1112    versions.into_iter().map(|(_, path)| path).collect()
1113}
1114
1115/// A directory name read as a version, so that `13` sorts above `9` and `10.2` above `10`.
1116///
1117/// A name that is not a version at all sorts below every name that is, rather than being left
1118/// out, because a directory holding a `libgcc.a` is worth looking in whatever it is called.
1119fn version_key(dir: &Path) -> Vec<u64> {
1120    let name = dir.file_name().unwrap_or_default().to_string_lossy();
1121    name.split('.').map(|part| part.parse::<u64>().unwrap_or(0)).collect()
1122}
1123
1124/// Our own runtime library for this target, if it was built.
1125///
1126/// Looked for beside the compiler rather than at a path decided when the compiler was built, for
1127/// the same reason everything else here is looked for: one binary runs wherever it is copied. A
1128/// `-B` prefix is asked first, because that is what a `-B` prefix is for.
1129#[must_use]
1130pub fn builtins_archive(target: Triple, prefixes: &[PathBuf]) -> Option<PathBuf> {
1131    // The name from the crate that puts it on a line, rather than a second spelling of it here,
1132    // which is what that constant asks of anybody who needs the name.
1133    const NAME: &str = rucc_sysroot::link::BUILTINS;
1134    // The four field triple first and then the tuple the sysroots are named by, because `cargo
1135    // xtask builtins --target=aarch64-linux-musl` names its directory after what it was given, and
1136    // that shorter spelling is the one people type.
1137    let spellings = [target.to_string(), target.tuple().to_string()];
1138    let mut places: Vec<PathBuf> = Vec::new();
1139    for prefix in prefixes {
1140        for triple in &spellings {
1141            places.push(prefix.join(triple).join(NAME));
1142        }
1143        places.push(prefix.join(NAME));
1144    }
1145    if let Some(dir) =
1146        std::env::current_exe().ok().and_then(|exe| exe.parent().map(Path::to_path_buf))
1147    {
1148        if let Some(up) = dir.parent() {
1149            for triple in &spellings {
1150                // An install: the compiler in `bin` and its runtime in `lib/rucc/<triple>`.
1151                places.push(up.join("lib").join("rucc").join(triple).join(NAME));
1152                // A build tree: the compiler in `target/release` and the runtime, which is built
1153                // for the target and not the host, in `target/<triple>/release`.
1154                for profile in ["release", "debug"] {
1155                    places.push(up.join(triple).join(profile).join(NAME));
1156                }
1157            }
1158        }
1159        places.push(dir.join(NAME));
1160    }
1161    places.into_iter().find(|path| path.is_file())
1162}
1163
1164/// Which output format this `ld` should write, in the name `ld` knows it by.
1165fn emulation(target: Triple) -> &'static str {
1166    match target.arch {
1167        Arch::X86_64 => "elf_x86_64",
1168        Arch::Aarch64 => "aarch64linux",
1169        Arch::Riscv64 => "elf64lriscv",
1170    }
1171}
1172
1173/// The program that starts a dynamically linked program, whose path is part of the file.
1174///
1175/// It is a per-target constant rather than something to look for, because the name is fixed by
1176/// the platform's ABI and a program naming a different one does not start.
1177fn loader(target: Triple) -> &'static str {
1178    match (target.arch, target.env) {
1179        (Arch::X86_64, Env::Musl) => "/lib/ld-musl-x86_64.so.1",
1180        (Arch::X86_64, _) => "/lib64/ld-linux-x86-64.so.2",
1181        (Arch::Aarch64, Env::Musl) => "/lib/ld-musl-aarch64.so.1",
1182        (Arch::Aarch64, _) => "/lib/ld-linux-aarch64.so.1",
1183        (Arch::Riscv64, Env::Musl) => "/lib/ld-musl-riscv64.so.1",
1184        (Arch::Riscv64, _) => "/lib/ld-linux-riscv64-lp64d.so.1",
1185    }
1186}
1187
1188/// Where the library's own files might be, in search order.
1189///
1190/// The multiarch directory first for the reason it comes first in the header search: it is where
1191/// a distribution that can hold two architectures at once puts the one being asked for, and a
1192/// distribution that cannot simply does not have it. `lib64` after it, which is what the
1193/// distributions that split by word size use instead, and `lib` last, which is every other one.
1194#[must_use]
1195pub fn candidates(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
1196    let multiarch = multiarch(target);
1197    [
1198        format!("/usr/lib/{multiarch}"),
1199        format!("/lib/{multiarch}"),
1200        "/usr/lib64".to_owned(),
1201        "/lib64".to_owned(),
1202        "/usr/lib".to_owned(),
1203        "/lib".to_owned(),
1204    ]
1205    .into_iter()
1206    .map(|dir| under(sysroot, &dir))
1207    .collect()
1208}
1209
1210/// The name a distribution that holds two architectures at once files this target under.
1211///
1212/// `x86_64-linux-gnu` and its friends, which is what `gcc -print-multiarch` prints and what a
1213/// build system pastes into a path when it is looking for a library itself.
1214#[must_use]
1215pub fn multiarch(target: Triple) -> String {
1216    let libc = match target.env {
1217        Env::Musl => "musl",
1218        Env::None | Env::Gnu | Env::Msvc => "gnu",
1219    };
1220    format!("{}-linux-{libc}", target.arch.as_str())
1221}
1222
1223/// The candidates that are there.
1224fn library_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
1225    candidates(target, sysroot).into_iter().filter(|dir| dir.is_dir()).collect()
1226}
1227
1228/// Where a library is looked for, in the order it is looked for in.
1229///
1230/// The command line first and the target's own after it, which is the order the linker is handed
1231/// and therefore the order `-print-search-dirs` has to print.
1232#[must_use]
1233pub fn search_dirs(link: &LinkOptions, target: Triple) -> Vec<PathBuf> {
1234    let mut dirs = link.search.clone();
1235    // A cross link searches the sysroot's directory and, for a libc that is a stub, the one its
1236    // stubs are written to, so this is those and not the machine's. What `-print-search-dirs` says is what a build
1237    // system pastes into a link line of its own, and an answer that named `/usr/lib` for a target
1238    // whose link line never goes near it would be worse than no answer at all.
1239    if let Some(sysroot) = cross_sysroot(target, link) {
1240        dirs.push(sysroot.lib());
1241        if rucc_sysroot::link::libc(sysroot.target()) == rucc_sysroot::link::Libc::Stub {
1242            dirs.push(sysroot.stubs().to_path_buf());
1243        }
1244        return dirs;
1245    }
1246    if let Some(distro) = distro_cross(target, link) {
1247        dirs.push(distro.lib());
1248        return dirs;
1249    }
1250    dirs.extend(candidates(target, link.sysroot.as_deref()));
1251    dirs
1252}
1253
1254/// The full path of a file with that name, when one of the search directories holds it.
1255///
1256/// What `-print-file-name=` answers. GCC prints the name back unchanged when it finds nothing,
1257/// which is what makes the flag safe to paste into a link line either way.
1258#[must_use]
1259pub fn find_in_search(link: &LinkOptions, target: Triple, name: &str) -> Option<PathBuf> {
1260    find_file(&search_dirs(link, target), name)
1261}
1262
1263/// The first of those directories holding a file of that name.
1264fn find_file(dirs: &[PathBuf], name: &str) -> Option<PathBuf> {
1265    dirs.iter().map(|dir| dir.join(name)).find(|path| path.is_file())
1266}
1267
1268/// A path under the sysroot, when there is one.
1269fn under(sysroot: Option<&Path>, path: &str) -> PathBuf {
1270    match sysroot {
1271        // `strip_prefix` because joining an absolute path replaces the root rather than extending
1272        // it, which would make every entry the unprefixed one.
1273        Some(root) => root.join(path.strip_prefix('/').unwrap_or(path)),
1274        None => PathBuf::from(path),
1275    }
1276}
1277
1278/// A path on the machine that will run the program, rather than on the one compiling it.
1279///
1280/// Written with the separator of the target and not of the host, which matters for the one path
1281/// that is not looked at here but stored in the file and read by something else later: the loader
1282/// a dynamic program names. A Windows host joining it would put a backslash in the middle of a
1283/// name that a Linux loader has to find, and the program would not start.
1284fn target_path(sysroot: Option<&Path>, path: &str) -> String {
1285    match sysroot {
1286        Some(root) => {
1287            let root = root.display().to_string();
1288            format!("{}/{}", root.trim_end_matches(['/', '\\']), path.trim_start_matches('/'))
1289        }
1290        None => path.to_owned(),
1291    }
1292}
1293
1294/// The whole invocation as one line, quoted the way `-###` prints it.
1295#[must_use]
1296pub fn render(linker: &Linker, args: &[String]) -> String {
1297    let mut out = linker.path.display().to_string();
1298    for arg in args {
1299        out.push(' ');
1300        if arg.is_empty() || arg.contains(char::is_whitespace) {
1301            out.push('"');
1302            out.push_str(arg);
1303            out.push('"');
1304        } else {
1305            out.push_str(arg);
1306        }
1307    }
1308    out
1309}
1310
1311/// Runs the linker and waits for it.
1312///
1313/// # Errors
1314///
1315/// [`Error::Spawn`] when it could not be started, which is a machine problem, and
1316/// [`Error::Refused`] when it ran and said no, which is a program problem and one the linker has
1317/// already explained on its own error output.
1318pub fn run(linker: &Linker, args: &[String]) -> Result<(), Error> {
1319    let args: Vec<OsString> = args.iter().map(OsString::from).collect();
1320    let status = Command::new(&linker.path).args(&args).status().map_err(|why| Error::Spawn {
1321        path: linker.path.display().to_string(),
1322        why: why.to_string(),
1323    })?;
1324    if status.success() {
1325        return Ok(());
1326    }
1327    // Nothing is added to what the linker printed. It has already named the symbol or the file,
1328    // and a second message from here saying that linking failed would only push the first one
1329    // further up the screen.
1330    Err(Error::Refused {
1331        status: match status.code() {
1332            Some(code) => format!("exited with status {code}"),
1333            None => "was killed before it finished".to_owned(),
1334        },
1335    })
1336}
1337
1338#[cfg(test)]
1339mod tests {
1340    use super::*;
1341
1342    fn linux() -> Triple {
1343        Triple::new(Arch::X86_64, Os::Linux, Env::Gnu)
1344    }
1345
1346    fn one(name: &str) -> Vec<Item> {
1347        vec![Item::File(name.to_owned())]
1348    }
1349
1350    #[test]
1351    fn the_fast_one_is_looked_for_first_and_the_platforms_own_last() {
1352        let names = order(linux(), &LinkOptions::default());
1353        assert_eq!(names.first().map(String::as_str), Some("ld.mold"));
1354        assert_eq!(names.last().map(String::as_str), Some("ld"));
1355    }
1356
1357    #[test]
1358    fn naming_one_is_the_whole_of_the_order() {
1359        let opts = LinkOptions { use_ld: Some("gold".to_owned()), ..LinkOptions::default() };
1360        assert_eq!(order(linux(), &opts), ["ld.gold", "gold"]);
1361    }
1362
1363    #[test]
1364    fn a_dynamic_program_names_the_loader_that_will_start_it() {
1365        let args = line(linux(), &LinkOptions::default(), &one("a.o"), "a.out").expect("a line");
1366        let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
1367        assert!(args[at + 1].ends_with("/lib64/ld-linux-x86-64.so.2"), "{args:?}");
1368    }
1369
1370    #[test]
1371    fn a_static_program_names_no_loader_because_nothing_will_start_it() {
1372        let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
1373        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
1374        assert!(args.contains(&"-static".to_owned()), "{args:?}");
1375        assert!(!args.contains(&"-dynamic-linker".to_owned()), "{args:?}");
1376    }
1377
1378    #[test]
1379    fn the_startup_file_of_a_program_that_moves_is_not_the_one_of_a_program_that_does_not() {
1380        let moving = LinkOptions { pie: Some(true), ..LinkOptions::default() };
1381        let fixed = LinkOptions { pie: Some(false), ..LinkOptions::default() };
1382        let named = |opts: &LinkOptions| {
1383            line(linux(), opts, &one("a.o"), "a.out")
1384                .expect("a line")
1385                .iter()
1386                .filter_map(|a| Path::new(a).file_name().map(|n| n.to_string_lossy().into_owned()))
1387                .find(|n| n.ends_with("crt1.o"))
1388        };
1389        // Only when the machine running this has them, which is what makes this two assertions
1390        // rather than one: a machine with no glibc development files has neither to find.
1391        if let Some(name) = named(&moving) {
1392            assert_eq!(name, "Scrt1.o");
1393            assert_eq!(named(&fixed).as_deref(), Some("crt1.o"));
1394        }
1395    }
1396
1397    /// A profiled program is started by a startup file of its own.
1398    ///
1399    /// The counts it keeps have to be started before `main` runs and written out after it returns,
1400    /// and what does both is this file rather than anything the compiler wrote. So a build that
1401    /// compiles with the flag and links without it produces a program that calls the hook on every
1402    /// function and never writes a profile, which is the failure this is here to keep out.
1403    ///
1404    /// A shared object takes none of them either way, since nothing starts one.
1405    #[test]
1406    fn a_profiled_program_is_started_by_the_startup_file_that_counts() {
1407        let profile = LinkOptions { profile: true, ..LinkOptions::default() };
1408        assert_eq!(startfile(&profile, false), Some("gcrt1.o"));
1409        assert_eq!(startfile(&profile, true), Some("gcrt1.o"));
1410        let still = LinkOptions { is_static: true, ..profile.clone() };
1411        assert_eq!(startfile(&still, true), Some("grcrt1.o"));
1412        assert_eq!(startfile(&still, false), Some("gcrt1.o"));
1413        let shared = LinkOptions { shared: true, ..profile };
1414        assert_eq!(startfile(&shared, false), None);
1415    }
1416
1417    /// And a program that is not profiled is started by the one it always was.
1418    #[test]
1419    fn a_program_that_is_not_profiled_is_started_by_the_usual_one() {
1420        let plain = LinkOptions::default();
1421        assert_eq!(startfile(&plain, false), Some("crt1.o"));
1422        assert_eq!(startfile(&plain, true), Some("Scrt1.o"));
1423    }
1424
1425    #[test]
1426    fn asking_for_no_startup_files_leaves_out_both_ends_of_them() {
1427        let opts = LinkOptions { no_startfiles: true, ..LinkOptions::default() };
1428        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
1429        assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
1430        assert!(!args.iter().any(|a| a.ends_with("crtn.o")), "{args:?}");
1431        // And still links against the library, because that is the other flag.
1432        assert!(args.contains(&"-lc".to_owned()), "{args:?}");
1433    }
1434
1435    #[test]
1436    fn asking_for_no_library_at_all_leaves_out_the_startup_files_too() {
1437        let opts = LinkOptions { no_stdlib: true, ..LinkOptions::default() };
1438        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
1439        assert!(!args.contains(&"-lc".to_owned()), "{args:?}");
1440        assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
1441    }
1442
1443    #[test]
1444    fn the_library_comes_after_the_objects_that_need_it() {
1445        let items = vec![Item::File("a.o".to_owned()), Item::Library("m".to_owned())];
1446        let args = line(linux(), &LinkOptions::default(), &items, "a.out").expect("a line");
1447        let obj = args.iter().position(|a| a == "a.o").expect("the object");
1448        let m = args.iter().position(|a| a == "-lm").expect("the library");
1449        let c = args.iter().position(|a| a == "-lc").expect("the library");
1450        assert!(obj < m && m < c, "{args:?}");
1451    }
1452
1453    #[test]
1454    fn what_the_user_told_the_linker_stays_where_the_user_wrote_it() {
1455        // The pair libtool writes around a set of convenience archives, which is what found this.
1456        // Both words are about the files between them, so a line that collects them and puts them
1457        // at the end has two options that do nothing and an archive whose members were all dropped.
1458        let items = vec![
1459            Item::File("a.o".to_owned()),
1460            Item::Linker("--whole-archive".to_owned()),
1461            Item::File("libaesni.a".to_owned()),
1462            Item::Linker("--no-whole-archive".to_owned()),
1463            Item::Library("m".to_owned()),
1464        ];
1465        let args = line(linux(), &LinkOptions::default(), &items, "a.out").expect("a line");
1466        let at = |what: &str| args.iter().position(|a| a == what).expect(what);
1467        assert!(at("a.o") < at("--whole-archive"), "{args:?}");
1468        assert!(at("--whole-archive") < at("libaesni.a"), "{args:?}");
1469        assert!(at("libaesni.a") < at("--no-whole-archive"), "{args:?}");
1470        assert!(at("--no-whole-archive") < at("-lm"), "{args:?}");
1471        assert!(at("-lm") < at("-lc"), "{args:?}");
1472    }
1473
1474    #[test]
1475    fn a_sysroot_moves_every_path_this_decided_and_none_the_user_wrote() {
1476        let opts = LinkOptions {
1477            sysroot: Some(PathBuf::from("/nowhere-at-all")),
1478            search: vec![PathBuf::from("/opt/mine")],
1479            ..LinkOptions::default()
1480        };
1481        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
1482        let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
1483        assert_eq!(args[at + 1], "/nowhere-at-all/lib64/ld-linux-x86-64.so.2");
1484        assert!(args.contains(&"-L/opt/mine".to_owned()), "{args:?}");
1485    }
1486
1487    #[test]
1488    fn a_platform_with_no_link_line_is_said_so_rather_than_linked_wrongly() {
1489        for triple in [
1490            Triple::new(Arch::X86_64, Os::Darwin, Env::Gnu),
1491            Triple::new(Arch::X86_64, Os::Windows, Env::Msvc),
1492        ] {
1493            let error = line(triple, &LinkOptions::default(), &one("a.o"), "a.out")
1494                .expect_err("no line for it");
1495            assert!(matches!(error, Error::Target { .. }), "{error:?}");
1496        }
1497    }
1498
1499    #[test]
1500    fn the_line_is_printed_the_way_it_would_be_typed() {
1501        let linker = Linker { name: "ld".to_owned(), path: PathBuf::from("/usr/bin/ld") };
1502        let args = ["-o".to_owned(), "a b".to_owned()];
1503        assert_eq!(render(&linker, &args), "/usr/bin/ld -o \"a b\"");
1504    }
1505
1506    #[test]
1507    fn a_linker_that_is_not_there_is_said_by_name() {
1508        let opts = LinkOptions {
1509            use_ld: Some("a-linker-nobody-has".to_owned()),
1510            ..LinkOptions::default()
1511        };
1512        let error = find(linux(), &opts).expect_err("not on this machine");
1513        assert_eq!(error, Error::Named { name: "a-linker-nobody-has".to_owned() });
1514    }
1515    /// A directory with a `libgcc.a` in it, so a test can say what a machine with a gcc on it
1516    /// looks like without needing one.
1517    fn a_gcc_dir(name: &str) -> PathBuf {
1518        let dir = std::env::temp_dir().join(format!("rucc-link-{name}-{}", std::process::id()));
1519        fs::create_dir_all(&dir).expect("a temporary directory");
1520        fs::write(dir.join("libgcc.a"), b"not really an archive").expect("a file in it");
1521        dir
1522    }
1523
1524    #[test]
1525    fn the_c_library_supplies_the_block_routines_and_our_runtime_does_not_displace_them() {
1526        let gcc = a_gcc_dir("order");
1527        let ours = PathBuf::from("/somewhere/librucc_builtins.a");
1528        let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
1529        let at_libc = args.iter().position(|a| a == "-lc").expect("libc");
1530        let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
1531        // glibc's `memcpy` is assembly per microarchitecture and ours is a word at a time loop,
1532        // so on a target that has one, its is the one that should answer.
1533        assert!(at_libc < at_ours, "{args:?}");
1534    }
1535
1536    #[test]
1537    fn a_static_link_puts_them_in_a_group_because_two_of_them_refer_to_each_other() {
1538        let gcc = a_gcc_dir("group");
1539        let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
1540        let args = runtime_items(&opts, &[gcc], None);
1541        assert_eq!(args.first().map(String::as_str), Some("--start-group"), "{args:?}");
1542        assert_eq!(args.last().map(String::as_str), Some("--end-group"), "{args:?}");
1543        // The unwinder, which is what `libc.a` refers to and what a static link fails on without
1544        // it. Issue #277.
1545        assert!(args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
1546    }
1547
1548    #[test]
1549    fn a_dynamic_link_needs_no_group_and_asks_for_the_shared_half_only_if_something_wants_it() {
1550        let gcc = a_gcc_dir("dynamic");
1551        let args = runtime_items(&LinkOptions::default(), &[gcc], None);
1552        assert!(!args.contains(&"--start-group".to_owned()), "{args:?}");
1553        assert!(!args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
1554        let at = args.iter().position(|a| a == "-lgcc_s").expect("the shared half");
1555        assert_eq!(args[at - 1], "--as-needed", "{args:?}");
1556        assert_eq!(args[at + 1], "--no-as-needed", "{args:?}");
1557    }
1558
1559    #[test]
1560    fn our_own_runtime_comes_before_the_machines_because_the_two_are_interchangeable() {
1561        let gcc = a_gcc_dir("ours");
1562        let ours = PathBuf::from("/somewhere/librucc_builtins.a");
1563        let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
1564        let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
1565        let at_gcc = args.iter().position(|a| a == "-lgcc").expect("libgcc");
1566        assert!(at_ours < at_gcc, "{args:?}");
1567    }
1568
1569    #[test]
1570    fn no_builtins_lib_leaves_ours_off_and_keeps_the_machines() {
1571        let gcc = a_gcc_dir("theirs");
1572        let opts = LinkOptions { no_builtins_lib: true, ..LinkOptions::default() };
1573        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
1574        assert!(!args.iter().any(|a| a.ends_with("librucc_builtins.a")), "{args:?}");
1575        // And the machine's half is still decided the same way it was, from the directories
1576        // that are there, which on the machine running this test may be none.
1577        assert!(runtime_items(&opts, &[gcc], None).contains(&"-lgcc".to_owned()));
1578    }
1579
1580    #[test]
1581    fn nodefaultlibs_leaves_the_whole_runtime_off_and_not_only_the_c_library() {
1582        let gcc = a_gcc_dir("none");
1583        let opts = LinkOptions { no_defaultlibs: true, ..LinkOptions::default() };
1584        assert!(runtime_items(&opts, &[gcc], None).is_empty());
1585    }
1586
1587    #[test]
1588    fn a_machine_with_no_gcc_on_it_gets_no_names_for_libraries_that_are_not_there() {
1589        let empty = std::env::temp_dir().join("rucc-link-empty-not-a-gcc");
1590        let args = runtime_items(&LinkOptions::default(), &[empty], None);
1591        assert_eq!(args, ["-lc"], "{args:?}");
1592    }
1593
1594    #[test]
1595    fn a_gcc_version_directory_is_read_as_a_version_and_not_as_a_word() {
1596        assert!(version_key(Path::new("/usr/lib/gcc/x/13")) > version_key(Path::new("/x/9")));
1597        assert!(version_key(Path::new("/x/10.2")) > version_key(Path::new("/x/10")));
1598        // Something that is not a version at all still sorts, and sorts below one that is.
1599        assert!(version_key(Path::new("/x/snapshot")) < version_key(Path::new("/x/1")));
1600    }
1601
1602    /// A command line that has a cache to find generated sysroots in, which a real one always has.
1603    ///
1604    /// And a `-B` prefix with our runtime in it, because a cross link refuses without one and
1605    /// every machine that does this for real has the archive `cargo xtask builtins` wrote. What
1606    /// happens when it is missing is its own test below.
1607    /// The fast math startup file follows gcc's end file spec: the family puts it in anything
1608    /// that is not a shared object, and `-mdaz-ftz` decides it outright either way.
1609    #[test]
1610    fn the_fast_math_startup_file_is_wanted_where_gccs_spec_wants_it() {
1611        let fast = LinkOptions { fast_math: true, ..LinkOptions::default() };
1612        assert!(!LinkOptions::default().wants_fastmath());
1613        assert!(fast.wants_fastmath());
1614        assert!(!LinkOptions { shared: true, ..fast.clone() }.wants_fastmath());
1615        assert!(!LinkOptions { daz_ftz: Some(false), ..fast.clone() }.wants_fastmath());
1616        let forced = LinkOptions { shared: true, daz_ftz: Some(true), ..LinkOptions::default() };
1617        assert!(forced.wants_fastmath());
1618    }
1619
1620    fn cached() -> LinkOptions {
1621        LinkOptions {
1622            cache: Some(PathBuf::from("/cache")),
1623            prefixes: vec![a_builtins_dir()],
1624            ..LinkOptions::default()
1625        }
1626    }
1627
1628    /// A directory with our runtime archive in it, so that a test can say what a machine where the
1629    /// runtime was built looks like without building one.
1630    ///
1631    /// One directory for every test rather than one each, since none of them writes to it and the
1632    /// name of the file is the whole of what they read.
1633    fn a_builtins_dir() -> PathBuf {
1634        let dir = std::env::temp_dir().join(format!("rucc-link-ours-{}", std::process::id()));
1635        fs::create_dir_all(&dir).expect("a temporary directory");
1636        fs::write(dir.join("librucc_builtins.a"), b"not really an archive").expect("a file in it");
1637        dir
1638    }
1639
1640    /// An archive in a directory named by the short tuple is found, which is where `cargo xtask
1641    /// builtins --target=aarch64-linux-musl` puts it.
1642    #[test]
1643    fn the_runtime_is_found_under_the_tuple_as_well_as_the_triple() {
1644        let dir = std::env::temp_dir().join(format!("rucc-link-tuple-{}", std::process::id()));
1645        let target: Triple = "aarch64-linux-musl".parse().expect("a triple");
1646        let under = dir.join(target.tuple().to_string());
1647        fs::create_dir_all(&under).expect("a temporary directory");
1648        fs::write(under.join("librucc_builtins.a"), b"not really an archive")
1649            .expect("a file in it");
1650        let found = builtins_archive(target, std::slice::from_ref(&dir));
1651        assert_eq!(found, Some(under.join("librucc_builtins.a")));
1652        let _ = fs::remove_dir_all(&dir);
1653    }
1654
1655    /// Where that cache would keep this target's sysroot.
1656    fn a_sysroot(target: Triple) -> Sysroot {
1657        Sysroot::in_cache(Path::new("/cache"), target.tuple())
1658    }
1659
1660    /// A target that is not the machine running this test, whatever machine that is.
1661    ///
1662    /// A freestanding one, because [`Triple::host`] answers Linux, Darwin or Windows and never
1663    /// `Os::None`. Every other triple is somebody's host, so a test that wants the cross path out of
1664    /// [`line`] itself has to use this one and the rest go through [`cross_line`].
1665    fn foreign() -> Triple {
1666        Triple::new(Arch::X86_64, Os::None, Env::None)
1667    }
1668
1669    #[test]
1670    fn a_cross_link_reads_the_targets_own_sysroot_and_nothing_of_this_machine() {
1671        let target = Triple::new(Arch::Aarch64, Os::Linux, Env::Musl);
1672        let sysroot = a_sysroot(target);
1673        // The paths as this host spells them, because what is being checked is which directory the
1674        // files are in and a Windows separator is a backslash.
1675        let root = sysroot.root().display().to_string();
1676        let lib = sysroot.lib();
1677        let args = cross_line(target, &cached(), &one("a.o"), "a.out", &sysroot).expect("a line");
1678        assert!(args.contains(&format!("--sysroot={root}")), "{args:?}");
1679        assert!(args.contains(&format!("-L{}", lib.display())), "{args:?}");
1680        assert!(args.contains(&lib.join("libc.a").display().to_string()), "{args:?}");
1681        let at = args.iter().position(|a| a == "-dynamic-linker").expect("the loader");
1682        assert_eq!(args[at + 1], "/lib/ld-musl-aarch64.so.1", "{args:?}");
1683        // The whole point of the other path not being taken: not one directory of this machine is
1684        // on the line, so the line is the same on every host and the recorded ones describe it.
1685        for arg in &args {
1686            assert!(!arg.contains("/usr/lib"), "{arg} in {args:?}");
1687            assert!(!arg.contains("/lib64"), "{arg} in {args:?}");
1688        }
1689    }
1690
1691    #[test]
1692    fn a_freestanding_target_links_against_our_runtime_instead_of_being_refused() {
1693        let args = line(foreign(), &cached(), &one("a.o"), "a.out").expect("a line");
1694        assert!(args.iter().any(|a| a.ends_with("librucc_builtins.a")), "{args:?}");
1695        // No libc, because there is not one, and no start file either: what runs before `main` on a
1696        // freestanding target comes from whatever is being built.
1697        assert!(!args.iter().any(|a| a.ends_with("libc.a")), "{args:?}");
1698        assert!(!args.contains(&"-lc".to_owned()), "{args:?}");
1699        assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
1700    }
1701
1702    /// And with nothing to find sysroots in it is refused, which is what it was before this.
1703    #[test]
1704    fn a_driver_with_no_cache_to_look_in_says_so_rather_than_guessing() {
1705        let error = line(foreign(), &LinkOptions::default(), &one("a.o"), "a.out")
1706            .expect_err("no line for it");
1707        assert!(matches!(error, Error::Target { .. }), "{error:?}");
1708    }
1709
1710    #[test]
1711    fn a_static_link_against_a_libc_that_is_a_stub_is_refused_rather_than_attempted() {
1712        let target = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
1713        let opts = LinkOptions { is_static: true, ..cached() };
1714        let error = cross_line(target, &opts, &one("a.o"), "a.out", &a_sysroot(target))
1715            .expect_err("there is no libc.a in a stub sysroot");
1716        let Error::Cross { why } = &error else { panic!("{error:?}") };
1717        // Because a stub carries the names a library exports and none of the bodies, which is
1718        // everything a dynamic link reads and nothing a static one does.
1719        assert!(why.contains("stub"), "{why}");
1720    }
1721
1722    #[test]
1723    fn a_target_whose_linker_wants_a_different_line_is_refused_by_name() {
1724        for target in [
1725            Triple::new(Arch::Aarch64, Os::Darwin, Env::None),
1726            Triple::new(Arch::X86_64, Os::Windows, Env::Msvc),
1727        ] {
1728            let error = cross_line(target, &cached(), &one("a.o"), "a.out", &a_sysroot(target))
1729                .expect_err("no line for that format");
1730            let Error::Cross { why } = &error else { panic!("{error:?}") };
1731            assert!(why.contains(&target.tuple().to_canonical_string()), "{why}");
1732        }
1733    }
1734
1735    #[test]
1736    fn a_mingw_target_links_and_looks_for_a_linker_that_can_write_a_pe_image() {
1737        let target = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
1738        let args = cross_line(target, &cached(), &one("a.o"), "a.exe", &a_sysroot(target))
1739            .expect("a line for mingw-w64");
1740        let at = |flag: &str| args.iter().position(|arg| arg == flag).expect(flag);
1741        assert_eq!(args[at("-m") + 1], "i386pep");
1742        assert_eq!(args[at("--subsystem") + 1], "console");
1743        assert!(args.iter().any(|arg| arg.ends_with("libmsvcrt.a")), "{args:?}");
1744        // And the prefixed name a distribution files its mingw binutils under, which is not the
1745        // multiarch one.
1746        let names = cross_order(target);
1747        assert_eq!(names.first().map(String::as_str), Some("ld.lld"));
1748        assert!(names.contains(&"x86_64-w64-mingw32-ld".to_owned()), "{names:?}");
1749    }
1750
1751    #[test]
1752    fn our_runtime_comes_from_beside_the_compiler_rather_than_from_inside_the_sysroot() {
1753        // The two halves of tamnd/rucc#1514. The line used to name it under the sysroot's `lib`,
1754        // where nothing ever put it: it is this compiler's output for the target and a sysroot
1755        // fetched from a release holds the platform's files and not ours. So the path on the line
1756        // is the one the driver found, and the only `librucc_builtins.a` on the line is that one.
1757        let target = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
1758        let sysroot = a_sysroot(target);
1759        let opts = cached();
1760        let args = cross_line(target, &opts, &one("a.o"), "a.exe", &sysroot).expect("a line");
1761        let ours: Vec<&String> =
1762            args.iter().filter(|arg| arg.ends_with("librucc_builtins.a")).collect();
1763        assert_eq!(ours.len(), 1, "{args:?}");
1764        assert_eq!(ours[0], &opts.prefixes[0].join("librucc_builtins.a").display().to_string());
1765        assert!(!ours[0].starts_with(&sysroot.lib().display().to_string()), "{args:?}");
1766        // And it is still last, after everything that calls into it.
1767        assert_eq!(args.last(), Some(ours[0]), "{args:?}");
1768    }
1769
1770    #[test]
1771    fn a_cross_link_with_no_runtime_to_find_says_which_command_writes_one() {
1772        // What the linker would say instead is that `___chkstk_ms` is undefined, referenced from
1773        // mingw-w64's own objects, which is tamnd/rucc#1513 and is nobody's first guess at a
1774        // missing archive.
1775        let target = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
1776        let opts = LinkOptions { prefixes: Vec::new(), ..cached() };
1777        let error = cross_line(target, &opts, &one("a.o"), "a.exe", &a_sysroot(target))
1778            .expect_err("there is no runtime for it to find");
1779        let Error::Cross { why } = &error else { panic!("{error:?}") };
1780        assert!(why.contains("cargo xtask builtins"), "{why}");
1781        assert!(why.contains("-fno-builtins-lib"), "{why}");
1782
1783        // And that flag is the way through it, for somebody who meant to link without ours.
1784        let without = LinkOptions { no_builtins_lib: true, ..opts };
1785        let args = cross_line(target, &without, &one("a.o"), "a.exe", &a_sysroot(target))
1786            .expect("a line without ours on it");
1787        assert!(!args.iter().any(|arg| arg.ends_with("librucc_builtins.a")), "{args:?}");
1788    }
1789
1790    #[test]
1791    fn the_version_an_lld_prints_is_read_and_nothing_elses_is() {
1792        // What each of these programs actually prints, because the word being in the line is the
1793        // whole of how one is told from another.
1794        assert_eq!(lld_major("LLD 18.1.8 (compatible with GNU linkers)\n"), Some(18));
1795        assert_eq!(lld_major("Ubuntu LLD 18.1.3 (compatible with GNU linkers)\n"), Some(18));
1796        assert_eq!(lld_major("LLD 20.1.2 (compatible with GNU linkers)\n"), Some(20));
1797
1798        // Binutils and mold do not have it, and neither of them has this problem, so the answer
1799        // for both is that this check has nothing to say about them.
1800        assert_eq!(lld_major("GNU ld (GNU Binutils for Ubuntu) 2.42\n"), None);
1801        assert_eq!(lld_major("mold 2.4.1 (compatible with GNU ld)\n"), None);
1802        assert_eq!(lld_major(""), None);
1803    }
1804
1805    /// A program that prints `text` and exits, which is as much of a linker as this check reads.
1806    ///
1807    /// Named after what it says, so that two of them in one test are two files.
1808    #[cfg(unix)]
1809    fn a_linker_that_says(tag: &str, text: &str) -> Linker {
1810        use std::os::unix::fs::PermissionsExt as _;
1811        let dir = std::env::temp_dir().join(format!("rucc-link-ld-{}", std::process::id()));
1812        fs::create_dir_all(&dir).expect("a temporary directory");
1813        let path = dir.join(format!("ld.lld-{tag}"));
1814        fs::write(&path, format!("#!/bin/sh\necho '{text}'\n")).expect("a script");
1815        fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).expect("an executable one");
1816        Linker { name: "ld.lld".to_owned(), path }
1817    }
1818
1819    #[test]
1820    #[cfg(unix)]
1821    fn an_lld_too_old_to_read_exportas_is_refused_for_windows_gnu_and_nowhere_else() {
1822        // The failure this replaces has no diagnostic at all: 18 writes an import by ordinal zero,
1823        // exits successfully, and the program dies at startup under wine. tamnd/rucc#1515.
1824        let windows = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
1825        let old = a_linker_that_says("18", "LLD 18.1.8 (compatible with GNU linkers)");
1826        let error = suitable(windows, &old).expect_err("18 cannot link this");
1827        let Error::TooOld { name, found, target } = &error else { panic!("{error:?}") };
1828        assert_eq!((name.as_str(), *found, target.as_str()), ("ld.lld", 18, "x86_64-windows-gnu"));
1829        assert!(error.to_string().contains("IMPORT_NAME_EXPORTAS"), "{error}");
1830
1831        // The same linker for a target whose import libraries have no such records in them, which
1832        // is every other target, since this is one encoding in one format.
1833        let linux = Triple::new(Arch::X86_64, Os::Linux, Env::Musl);
1834        assert_eq!(suitable(linux, &old), Ok(()));
1835
1836        // And the first one that reads them.
1837        let new = a_linker_that_says("19", "LLD 19.1.0 (compatible with GNU linkers)");
1838        assert_eq!(suitable(windows, &new), Ok(()));
1839    }
1840
1841    #[test]
1842    #[cfg(unix)]
1843    fn a_linker_that_will_not_say_what_it_is_is_left_alone() {
1844        // Every linker that is not an lld reaches this check too, and what it can establish is
1845        // that a specific old lld is here rather than that anything else is fit. Turning "I did
1846        // not recognise this" into a refusal would break machines this problem never touched.
1847        let windows = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
1848        let quiet = a_linker_that_says("gnu", "GNU ld (GNU Binutils for Ubuntu) 2.42");
1849        assert_eq!(suitable(windows, &quiet), Ok(()));
1850
1851        let missing = Linker { name: "ld.lld".to_owned(), path: PathBuf::from("/no/such/linker") };
1852        assert_eq!(suitable(windows, &missing), Ok(()));
1853    }
1854
1855    #[test]
1856    fn profiling_a_cross_link_is_refused_because_the_startup_file_is_compiled_code() {
1857        let target = Triple::new(Arch::X86_64, Os::Linux, Env::Musl);
1858        let opts = LinkOptions { profile: true, ..cached() };
1859        let error = cross_line(target, &opts, &one("a.o"), "a.out", &a_sysroot(target))
1860            .expect_err("there is no gcrt1.o in a generated sysroot");
1861        let Error::Cross { why } = &error else { panic!("{error:?}") };
1862        assert!(why.contains("gcrt1.o"), "{why}");
1863    }
1864
1865    #[test]
1866    fn a_distributions_cross_tree_is_used_when_there_is_no_sysroot_of_ours() {
1867        let usr = std::env::temp_dir().join(format!("rucc-link-usr-{}", std::process::id()));
1868        let root = usr.join("aarch64-linux-gnu");
1869        for dir in ["include", "lib"] {
1870            fs::create_dir_all(root.join(dir)).expect("a scratch tree");
1871        }
1872        for version in ["9", "13"] {
1873            fs::create_dir_all(usr.join("lib/gcc-cross/aarch64-linux-gnu").join(version))
1874                .expect("a scratch gcc");
1875        }
1876        let host = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
1877        let arm = Triple::new(Arch::Aarch64, Os::Linux, Env::Gnu);
1878        let opts = LinkOptions { usr: Some(usr.clone()), ..cached() };
1879        let distro = distro_for(arm, &opts, Some(host)).expect("the packages are there");
1880        assert_eq!(distro.include(), root.join("include"));
1881        assert_eq!(distro.lib(), root.join("lib"));
1882        assert_eq!(
1883            distro.gcc,
1884            [13, 9].map(|v| usr.join("lib/gcc-cross/aarch64-linux-gnu").join(v.to_string()))
1885        );
1886        // And then it is not a link against a sysroot of ours, which is what decides the line.
1887        assert!(cross_for(arm, &opts, Some(host)).is_none());
1888        // The host itself, a target with no tree, a named tree and a pinned release all leave it.
1889        assert!(distro_for(host, &opts, Some(host)).is_none());
1890        let riscv = Triple::new(Arch::Riscv64, Os::Linux, Env::Gnu);
1891        assert!(distro_for(riscv, &opts, Some(host)).is_none());
1892        let named = LinkOptions { sysroot: Some(PathBuf::from("/opt/root")), ..opts.clone() };
1893        assert!(distro_for(arm, &named, Some(host)).is_none());
1894        let pinned = LinkOptions {
1895            pinned: Some("aarch64-linux-gnu.2.28".parse::<TargetTuple>().expect("a release")),
1896            ..opts.clone()
1897        };
1898        assert!(distro_for(arm, &pinned, Some(host)).is_none());
1899        // A sysroot of ours in the cache wins over the packages, because it is the one pinned.
1900        let cache = usr.join("cache");
1901        fs::create_dir_all(Sysroot::in_cache(&cache, arm.tuple()).lib()).expect("a sysroot");
1902        let fetched = LinkOptions { cache: Some(cache), ..opts };
1903        assert!(distro_for(arm, &fetched, Some(host)).is_none());
1904        let _ = fs::remove_dir_all(&usr);
1905    }
1906
1907    #[test]
1908    fn the_host_takes_the_host_line_and_a_tree_the_user_named_takes_it_too() {
1909        let host = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
1910        let other = Triple::new(Arch::Riscv64, Os::Linux, Env::Musl);
1911        assert!(cross_for(host, &cached(), Some(host)).is_none());
1912        assert!(cross_for(other, &cached(), Some(host)).is_some());
1913        // A tree somebody assembled and named is what `--sysroot` has always meant here, and the
1914        // native line prefixes every path it decides with it.
1915        let named = LinkOptions { sysroot: Some(PathBuf::from("/opt/root")), ..cached() };
1916        assert!(cross_for(other, &named, Some(host)).is_none());
1917        // A host this compiler cannot name is a host whose directories it should not be guessing at.
1918        assert!(cross_for(other, &cached(), None).is_some());
1919    }
1920
1921    #[test]
1922    fn a_pinned_release_on_this_machines_own_target_is_a_cross_compile() {
1923        // The case that used to be dropped on the floor. `--target=x86_64-linux-gnu.2.28` on an
1924        // x86-64 glibc machine read that machine's headers and linked that machine's libc, and the
1925        // release reached nothing, so what came out was a binary for whatever release the build
1926        // machine happened to have. A pin is the one thing a person writes to say otherwise.
1927        let host = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
1928        let pinned = LinkOptions {
1929            pinned: Some(
1930                "x86_64-linux-gnu.2.28".parse::<TargetTuple>().expect("a spelling with a release"),
1931            ),
1932            ..cached()
1933        };
1934        let at = cross_for(host, &pinned, Some(host)).expect("a pin is a cross compile");
1935        // And against the release's own directory, because the release is in the cache key: a tree
1936        // produced for 2.28 and a tree produced for 2.44 are two trees and the path has to say which.
1937        assert!(at.root().ends_with("x86_64-linux-gnu.2.28"), "{:?}", at.root());
1938        // The release is the whole of the difference. The same command line without it is this
1939        // machine, which is what every native compile has always been.
1940        let bare = LinkOptions { pinned: None, ..cached() };
1941        assert!(cross_for(host, &bare, Some(host)).is_none());
1942    }
1943
1944    #[test]
1945    fn a_glibc_cross_link_writes_its_stubs_beside_the_sysroot_once() {
1946        let cache = std::env::temp_dir().join(format!("rucc-link-stubs-{}", std::process::id()));
1947        let _ = fs::remove_dir_all(&cache);
1948        let target = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
1949        let pinned = LinkOptions {
1950            cache: Some(cache.clone()),
1951            pinned: Some("x86_64-linux-gnu.2.28".parse().expect("a spelling with a release")),
1952            ..LinkOptions::default()
1953        };
1954        write_stubs(target, &pinned).expect("x86_64 glibc has a description");
1955        let dir = cache.join("stubs").join("x86_64-linux-gnu.2.28");
1956        let libc = dir.join("libc.so");
1957        let bytes = fs::read(&libc).expect("libc.so was written");
1958        assert!(bytes.starts_with(b"\x7fELF"));
1959        assert!(dir.join("libm.so").is_file());
1960        // 2.28 is before the release that emptied libpthread, so there is no empty one to write.
1961        assert!(!dir.join("libpthread.so").exists());
1962        // The second time finds the same bytes and leaves the file alone, which is what keeps a
1963        // linker in another build from ever reading one that is being replaced.
1964        let before = fs::metadata(&libc).and_then(|m| m.modified()).expect("a time");
1965        write_stubs(target, &pinned).expect("again");
1966        let after = fs::metadata(&libc).and_then(|m| m.modified()).expect("a time");
1967        assert_eq!(before, after);
1968        // And nothing for a libc that is not glibc, whose sysroot has a real one in it.
1969        let musl = LinkOptions {
1970            cache: Some(cache.clone()),
1971            pinned: Some("x86_64-linux-musl".parse().expect("musl")),
1972            ..LinkOptions::default()
1973        };
1974        write_stubs(Triple::new(Arch::X86_64, Os::Linux, Env::Musl), &musl).expect("nothing");
1975        assert!(!cache.join("stubs").join("x86_64-linux-musl").exists());
1976        let _ = fs::remove_dir_all(&cache);
1977    }
1978
1979    #[test]
1980    fn what_a_cross_link_searches_is_the_sysroot_and_not_this_machine() {
1981        let dirs = search_dirs(&cached(), foreign());
1982        // One directory, because that is what the line has, and the same one the line has, because
1983        // `-print-search-dirs` is what a build system reads to write a link line of its own.
1984        assert_eq!(dirs.len(), 1, "{dirs:?}");
1985        assert!(dirs[0].starts_with("/cache"), "{dirs:?}");
1986        assert!(dirs[0].ends_with("lib"), "{dirs:?}");
1987        // And what the user wrote still comes first, the way it does on the line itself.
1988        let mine = LinkOptions { search: vec![PathBuf::from("/opt/mine")], ..cached() };
1989        assert_eq!(search_dirs(&mine, foreign())[0], PathBuf::from("/opt/mine"));
1990    }
1991
1992    #[test]
1993    fn the_linker_looked_for_on_a_cross_link_is_one_that_can_cross() {
1994        let names = cross_order(Triple::new(Arch::Aarch64, Os::Linux, Env::Gnu));
1995        assert_eq!(names.first().map(String::as_str), Some("ld.lld"));
1996        assert!(names.contains(&"aarch64-linux-gnu-ld".to_owned()), "{names:?}");
1997        // mold links for the machine it is running on, and so does a distribution's own `ld`, so
1998        // neither is a default here. `-fuse-ld=` is still there for somebody whose is different.
1999        assert!(!names.iter().any(|name| name.contains("mold")), "{names:?}");
2000        assert!(!names.contains(&"ld".to_owned()), "{names:?}");
2001        // And the lookup the driver really does for a target that is not this machine.
2002        assert_eq!(order(foreign(), &cached()), ["ld.lld", "lld"]);
2003    }
2004
2005    #[test]
2006    fn the_four_flags_become_the_five_modes_they_describe() {
2007        let plain = LinkOptions::default();
2008        assert_eq!(mode(&plain), LinkMode::Dynamic);
2009        assert_eq!(
2010            mode(&LinkOptions { pie: Some(false), ..plain.clone() }),
2011            LinkMode::DynamicNoPie
2012        );
2013        assert_eq!(mode(&LinkOptions { is_static: true, ..plain.clone() }), LinkMode::Static);
2014        let both = LinkOptions { is_static: true, pie: Some(true), ..plain.clone() };
2015        assert_eq!(mode(&both), LinkMode::StaticPie);
2016        assert_eq!(mode(&LinkOptions { shared: true, ..plain }), LinkMode::Shared);
2017    }
2018
2019    #[test]
2020    fn a_sysroot_that_has_not_been_built_is_named_before_anything_is_compiled() {
2021        let opts = LinkOptions {
2022            cache: Some(std::env::temp_dir().join("rucc-a-cache-nobody-filled")),
2023            ..LinkOptions::default()
2024        };
2025        let error = preflight(foreign(), &opts).expect_err("nothing has built one");
2026        let Error::Sysroot { dir, pinned, .. } = &error else { panic!("{error:?}") };
2027        assert!(dir.ends_with("x86_64-none"), "{dir}");
2028        // Nothing is pinned for that target, or for any target yet, so the message says that rather
2029        // than naming a command that would not work.
2030        assert!(!pinned, "nothing should be pinned for a bare metal target");
2031        let said = error.to_string();
2032        assert!(said.contains("pins none for it to fetch"), "{said}");
2033    }
2034
2035    /// The other half of the same message, which is what a target this release does pin an artifact
2036    /// for is told. Built by hand rather than through `preflight`, because what is being checked is
2037    /// the message and not which targets `rucc_sysroot::artifact` happens to pin this release.
2038    #[test]
2039    fn a_sysroot_that_could_be_fetched_is_told_what_to_run() {
2040        let said = Error::Sysroot {
2041            target: "x86_64-linux-musl".to_owned(),
2042            dir: "/somewhere/sysroots/x86_64-linux-musl".to_owned(),
2043            pinned: true,
2044        }
2045        .to_string();
2046        assert!(said.contains("`rucc --fetch x86_64-linux-musl`"), "{said}");
2047        // And the other way out of it, because a person who has a tree already does not want a
2048        // download.
2049        assert!(said.contains("--sysroot=<dir>"), "{said}");
2050    }
2051
2052    #[test]
2053    fn a_link_against_this_machine_has_nothing_to_check_before_it_starts() {
2054        // Its directories are looked for as the line is built, and one that is not there is simply
2055        // one that is not offered, so there is no question to answer early.
2056        assert!(preflight(linux(), &LinkOptions::default()).is_ok());
2057    }
2058
2059    #[test]
2060    fn a_runtime_directory_that_is_not_on_this_machine_is_not_offered() {
2061        let dirs = runtime_dirs(linux(), Some(Path::new("/definitely/not/a/sysroot")));
2062        assert!(dirs.is_empty(), "{dirs:?}");
2063    }
2064}