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//! # Windows in Microsoft's environment
55//!
56//! `lld-link` takes a line of its own, which [`rucc_sysroot::argv`] writes as it writes the others,
57//! and the libraries on it come out of a tree nobody may redistribute. `--fetch` and
58//! `--fetch-msvc-sdk` lay that tree out from Microsoft's own downloads once the person asking has
59//! accepted Microsoft's licence, and say which `--sysroot` to pass, so the tree is always one
60//! somebody named, and `msvc_sysroot` is the one place that says so. A mingw-w64 target links
61//! against the cache like every other cross target, because PE in that environment is written in
62//! the GNU style and the import libraries for it are ours.
63//!
64//! Darwin has a line of its own, [`darwin_line`], and it is the same line on a Mac and anywhere
65//! else. Everything it links against is in the SDK, which is found the way the header search finds
66//! it, so a machine that is not a Mac links for one exactly when somebody has named an SDK with
67//! `-isysroot` or `SDKROOT` and there is an `ld64.lld` to hand it to.
68//!
69//! The headers are the other half of a cross compile and [`crate::library::header_dirs`] is where
70//! they are decided. It asks [`cross_sysroot`] the same question this file asks it, which is the
71//! point: a compile that took its libc from the sysroot and its declarations from this machine would
72//! be wrong in the quietest way available, and one function answering for both is what stops that
73//! being possible.
74
75use std::ffi::OsString;
76use std::fs;
77use std::path::{Path, PathBuf};
78use std::process::Command;
79
80use rucc_sysroot::layout::{Kernel, Sysroot};
81use rucc_sysroot::{Chip, Crt, LinkMode, argv};
82use rucc_target::{Arch, Env, Os, Triple};
83use rucc_tuple::TargetTuple;
84
85/// What the command line said about linking.
86///
87/// Kept apart from `Options` because none of it reaches the compilation. A flag here changes what
88/// the linker is told and changes nothing about the object files handed to it, which is why `-lm`
89/// on a `-c` line is a note rather than an error.
90#[derive(Debug, Default, Clone, PartialEq, Eq)]
91pub struct LinkOptions {
92    /// `-fuse-ld=<name>`, which names a linker rather than a path to one.
93    pub use_ld: Option<String>,
94    /// `-L<dir>`, in order, because the linker takes the first library it finds.
95    pub search: Vec<PathBuf>,
96    /// `-B<prefix>`, which is where to look for the linker before looking on the path.
97    pub prefixes: Vec<PathBuf>,
98    /// `--sysroot=<dir>`, which prefixes the directories this looks in.
99    pub sysroot: Option<PathBuf>,
100    /// Where the generated sysroots are, which is [`crate::cache::dir`] on a real command line.
101    ///
102    /// [`None`] is a caller that was not given one, which outside a test is nothing, and then there
103    /// is no cross link line and a foreign target is refused the way it was before there was one.
104    /// It is a field rather than a call inside this module because a link line that read the
105    /// environment could only be tested on a machine whose environment said the right thing.
106    pub cache: Option<PathBuf>,
107    /// Where a distribution's cross packages put the tree for another architecture, which is
108    /// `/usr` on a real command line.
109    ///
110    /// Debian and Ubuntu install `libc6-dev-arm64-cross` and its friends as `/usr/<multiarch>/include`
111    /// and `/usr/<multiarch>/lib`, and gcc's own files for that target under
112    /// `/usr/lib/gcc-cross/<multiarch>`. [`distro_cross`] reads it. A field for the reason
113    /// [`LinkOptions::cache`] is one, and [`None`] in a test is a machine with no such packages.
114    pub usr: Option<PathBuf>,
115    /// `-static`.
116    pub is_static: bool,
117    /// `-shared`.
118    pub shared: bool,
119    /// `-pie` or `-no-pie`, and the platform's default when neither was written.
120    pub pie: Option<bool>,
121    /// `-nostdlib`, which is `-nostartfiles` and `-nodefaultlibs` together.
122    pub no_stdlib: bool,
123    /// `-nostartfiles`.
124    pub no_startfiles: bool,
125    /// `-nodefaultlibs`.
126    pub no_defaultlibs: bool,
127    /// `-rdynamic`, which puts every symbol in the dynamic table so a program can look itself up.
128    pub export_dynamic: bool,
129    /// `-s`, which drops the symbol table.
130    pub strip: bool,
131    /// `-mwindows` rather than `-mconsole`, whichever came last. Only a Windows line reads it.
132    pub gui: bool,
133    /// `-municode`, which only a Windows line reads.
134    pub unicode: bool,
135    /// `-fms-runtime-lib=`, which is `/MT` or `/MD` and which only an MSVC line reads.
136    pub crt: Crt,
137    /// `-fno-builtins-lib`, which leaves our own runtime off the line so that the machine's
138    /// libgcc answers for everything instead.
139    pub no_builtins_lib: bool,
140    /// The whole ten field target when `--target=` spelled one, which is where a pinned libc
141    /// release is.
142    ///
143    /// [`None`] is a command line that named no target at all, and then there is nothing pinned and
144    /// this machine is the target. A `Triple` has room for an architecture, an OS and an
145    /// environment and nowhere to put a release, so the release arrives here instead of there, and
146    /// [`cross_sysroot`] reads it for both of the things it decides: whether this is a cross link
147    /// and which directory under the cache it is against.
148    pub pinned: Option<TargetTuple>,
149    /// `-pg`, which changes the link as well as the code.
150    ///
151    /// The counts a profiled program keeps have to be started before `main` runs and written out
152    /// after it returns, and what does both is a start file of its own. So a build that compiles
153    /// with the flag and links without it produces a program that calls the hook on every function
154    /// and never writes a profile.
155    pub profile: bool,
156    /// Whether `-Ofast`, `-ffast-math` or `-funsafe-math-optimizations` was in force at the end
157    /// of the command line, which links `crtfastmath.o` into anything that is not a shared object.
158    ///
159    /// That file is a constructor which sets flush to zero and denormals are zero before `main`,
160    /// so the mode is the process's rather than the unit's, and it is the half of fast math the
161    /// compiler cannot give from inside a function.
162    pub fast_math: bool,
163    /// `-mdaz-ftz` or `-mno-daz-ftz`, which decides the same file outright and for a shared
164    /// object as well.
165    pub daz_ftz: Option<bool>,
166    /// `-r`, which joins objects into one bigger object rather than into something that runs.
167    ///
168    /// Kbuild builds every directory into a `built-in.o` this way. The result is still an input to a
169    /// later link, so it takes no startup files, no libraries and nothing about how it will be
170    /// loaded, which is what gcc leaves off the line when it sees the flag.
171    pub relocatable: bool,
172    /// The deployment target on an Apple platform, from `-mmacosx-version-min=` and its friends or
173    /// from the tuple, which `ld64` is told in `-platform_version` and checks every object against.
174    ///
175    /// [`None`] is the platform's default, the same one the object writer puts in
176    /// `LC_BUILD_VERSION`, so that the two agree when neither was told anything.
177    pub os_version: Option<rucc_tuple::Version>,
178}
179
180impl LinkOptions {
181    /// Whether gcc's `crtfastmath.o` goes on the line, which is its end file spec on x86-64.
182    fn wants_fastmath(&self) -> bool {
183        self.daz_ftz.unwrap_or(self.fast_math && !self.shared)
184    }
185
186    /// Whether the startup files go on the line.
187    fn wants_startfiles(&self) -> bool {
188        !self.no_stdlib && !self.no_startfiles && !self.relocatable
189    }
190
191    /// Whether the library the program was written against goes on the line.
192    fn wants_defaultlibs(&self) -> bool {
193        !self.no_stdlib && !self.no_defaultlibs && !self.relocatable
194    }
195
196    /// Whether the compiler's own runtime goes on the line.
197    ///
198    /// The same switch as the C library, because `-nodefaultlibs` in GCC means the compiler's
199    /// runtime too, and a link that keeps `libgcc` while dropping `libc` is not a thing anyone
200    /// asks for on purpose.
201    fn wants_runtime(&self) -> bool {
202        !self.no_stdlib && !self.no_defaultlibs && !self.relocatable
203    }
204}
205
206/// One item on the link line, in the order it was written, because link order is semantic.
207///
208/// A library named before the object that needs it is not found on a static link, which is the
209/// oldest surprise in the toolchain and the reason this is one ordered list rather than a list of
210/// files and a list of libraries.
211#[derive(Debug, Clone, PartialEq, Eq)]
212pub enum Item {
213    /// A file: an object this compilation produced, or one named on the command line.
214    File(String),
215    /// `-l<name>`, which the linker resolves against its search path.
216    Library(String),
217    /// One word from `-Wl,` or `-Xlinker`, handed to the linker where the user wrote it.
218    ///
219    /// Here rather than in a list of its own because a great many of the linker's options are a
220    /// bracket around the files after them, and an option moved away from what it brackets means
221    /// something else or nothing at all. `--whole-archive` says that every member of every archive
222    /// named after it goes in whether anything referenced it or not, `--start-group` says that the
223    /// archives after it are searched again until nothing more comes out, and `-Bstatic` says which
224    /// half of a library that ships both is wanted. Collecting them and appending them to the end
225    /// leaves each of those pointing at nothing.
226    Linker(String),
227}
228
229impl std::fmt::Display for Item {
230    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231        match self {
232            Item::File(path) => f.write_str(path),
233            Item::Library(name) => write!(f, "-l{name}"),
234            Item::Linker(arg) => write!(f, "-Wl,{arg}"),
235        }
236    }
237}
238
239/// Why a link could not be run.
240#[derive(Debug, Clone, PartialEq, Eq)]
241pub enum Error {
242    /// No linker was found, after looking everywhere there was to look.
243    NoLinker {
244        /// The names that were tried, in the order they were tried.
245        tried: Vec<String>,
246    },
247    /// `-fuse-ld=` named one that is not on this machine.
248    Named {
249        /// What it named.
250        name: String,
251    },
252    /// A target this does not know how to build a link line for.
253    Target {
254        /// The triple that was asked for.
255        triple: String,
256    },
257    /// A cross link this scheme cannot produce, which [`rucc_sysroot::argv`] has explained.
258    ///
259    /// The reason is carried as a sentence rather than as a variant per cause, because the causes
260    /// live in `rucc-sysroot` and a second enumeration here would be a second thing to keep in step
261    /// with them. What this adds is that the sentence came from a link rather than from a
262    /// compilation.
263    Cross {
264        /// Why, in full, ready to print.
265        why: String,
266    },
267    /// The sysroot a cross link needs is not on this machine.
268    Sysroot {
269        /// The target that was asked for.
270        target: String,
271        /// Where its sysroot would be.
272        dir: String,
273        /// Whether this release pins an artifact for that target, which decides whether the message
274        /// can name a command that would fix it.
275        pinned: bool,
276    },
277    /// The linker was found and cannot do this target's link.
278    ///
279    /// Separate from [`Error::NoLinker`] because the linker is there and runs, and separate from
280    /// [`Error::Refused`] because the refusal is ours rather than its own: this is the case the
281    /// linker would not complain about at all.
282    TooOld {
283        /// What it was found as, which is what to look for when replacing it.
284        name: String,
285        /// The major version it reported.
286        found: u32,
287        /// The target whose link it cannot do.
288        target: String,
289    },
290    /// The linker was found and could not be started.
291    Spawn {
292        /// Where it was.
293        path: String,
294        /// What the operating system said.
295        why: String,
296    },
297    /// The linker ran and said no.
298    Refused {
299        /// What it exited with, or a description when it was killed instead.
300        status: String,
301    },
302}
303
304impl std::fmt::Display for Error {
305    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306        match self {
307            Error::NoLinker { tried } => {
308                write!(f, "no linker was found; tried {}", tried.join(", "))?;
309                // Only when lld was one of the names, because that is the linker every cross
310                // target here is linked with and the one there is a single answer for.
311                if tried.iter().any(|name| is_lld(name)) {
312                    write!(
313                        f,
314                        ". lld {LLD_EXPORTAS} or newer links for every target, and {}",
315                        lld_advice()
316                    )?;
317                }
318                Ok(())
319            }
320            Error::Named { name } => {
321                write!(f, "-fuse-ld={name} asks for a linker that is not on this machine")
322            }
323            Error::Target { triple } => {
324                write!(f, "there is no link line for {triple} in this compiler yet")
325            }
326            Error::Cross { why } => f.write_str(why),
327            // Two sentences and the second one changes, because a person whose link just failed
328            // wants the command that fixes it and there is only a command to name when this release
329            // pins an artifact for that target. Section 13.8's rule is that a compile which is
330            // missing a sysroot says what to run rather than running it, and this is where it says
331            // it.
332            Error::Sysroot { target, dir, pinned: true } => write!(
333                f,
334                "there is no sysroot for {target} at {dir}, so there is nothing to link it \
335                 against. `rucc --fetch {target}` gets the one this release pins, or pass \
336                 --sysroot=<dir> to name a tree you have already"
337            ),
338            Error::Sysroot { target, dir, pinned: false } => write!(
339                f,
340                "there is no sysroot for {target} at {dir}, so there is nothing to link it \
341                 against, and this release pins none for it to fetch. Pass --sysroot=<dir> to name \
342                 a tree you have already, or see spec/cross-compile/13-distribution.md section \
343                 13.2 for the cache that will hold one"
344            ),
345            // The whole message, because the person reading it has a linker that works, a link that
346            // succeeded on their last try, and no reason to suspect the thing that is wrong.
347            Error::TooOld { name, found, target } => write!(
348                f,
349                "{name} is lld {found} and cannot link for {target}. mingw-w64 writes a few hundred \
350                 of its aliases, `_crt_atexit == atexit` among them, as IMPORT_NAME_EXPORTAS \
351                 records in its import libraries, which lld learned to read in {LLD_EXPORTAS}. An \
352                 older one neither reads them nor says so: it writes an import by ordinal zero, the \
353                 link succeeds, and the program dies at startup. No newer lld was found either. \
354                 For lld {LLD_EXPORTAS} or newer, {}, or name one with -fuse-ld=",
355                lld_advice()
356            ),
357            Error::Spawn { path, why } => write!(f, "could not run the linker at {path}: {why}"),
358            Error::Refused { status } => write!(f, "the linker {status}"),
359        }
360    }
361}
362
363impl std::error::Error for Error {}
364
365/// A linker, found.
366#[derive(Debug, Clone, PartialEq, Eq)]
367pub struct Linker {
368    /// The name it is known by, which is what `--print-config` reports.
369    pub name: String,
370    /// Where it is, which is what gets spawned.
371    pub path: PathBuf,
372}
373
374/// The names to look for, in the order section 4.9 gives.
375///
376/// `mold` first because it is dramatically faster, and a compiler that is twice the speed of
377/// another one while the link takes twelve seconds has not helped anybody. Then `lld`, then the
378/// platform's own. Each is looked for under both the bare name and the `ld.` prefix, because a
379/// distribution installs `mold` under its own name and `ld.mold` for exactly this lookup.
380#[must_use]
381pub fn order(target: Triple, opts: &LinkOptions) -> Vec<String> {
382    if let Some(named) = &opts.use_ld {
383        // A name rather than a path, so `-fuse-ld=mold` finds a `mold` that is not `ld.mold`.
384        return vec![format!("ld.{named}"), named.clone()];
385    }
386    // Apple's `ld` and lld's Mach-O flavour, and nothing from the list below. mold and `ld.lld` write
387    // ELF, and a Mac with Homebrew's llvm on its `PATH` has an `ld.lld` that would take the line and
388    // fail on its first flag.
389    if target.os == Os::Darwin {
390        return vec!["ld".to_owned(), "ld64.lld".to_owned()];
391    }
392    // The two linkers that read Microsoft's line, on every host. `ld.lld` is the same program as
393    // `lld-link`, and which line it reads is decided by the name it was started under, so the name
394    // is the whole of the choice.
395    if is_msvc(target) {
396        return vec!["lld-link".to_owned(), "link.exe".to_owned()];
397    }
398    if cross_sysroot(target, opts).is_some() {
399        let mut names = cross_order(target);
400        // The mingw-w64 sysroot this release fetches has import libraries that GNU ld cannot link
401        // against: the `==` aliases are IMPORT_NAME_EXPORTAS records, and binutils reports every
402        // one of them as an undefined reference to a symbol like `__imp__fmode`. So on this path
403        // lld is the only linker worth finding, and a machine without one is told how to get it
404        // instead of being handed a page of undefined references.
405        if (target.os, target.env) == (Os::Windows, Env::Gnu) {
406            names.retain(|name| is_lld(name));
407        }
408        return names;
409    }
410    if distro_cross(target, opts).is_some() {
411        return cross_order(target);
412    }
413    match target.os {
414        Os::Windows => vec!["lld-link".to_owned(), "link.exe".to_owned()],
415        _ => vec![
416            "ld.mold".to_owned(),
417            "mold".to_owned(),
418            "ld.lld".to_owned(),
419            "lld".to_owned(),
420            "ld".to_owned(),
421        ],
422    }
423}
424
425/// Whether this is a Windows target in Microsoft's environment, which is linked by `lld-link`.
426fn is_msvc(target: Triple) -> bool {
427    (target.os, target.env) == (Os::Windows, Env::Msvc)
428}
429
430/// The tree an MSVC link is against, which is always the one `--sysroot` named.
431///
432/// Never the cache, because the cache holds what a release of this compiler pins and nothing for
433/// this environment ever will be: `spec/cross-compile/13-distribution.md` section 13.4. What
434/// `--fetch` lays out is under a directory named for the versions it fetched, and it ends
435/// by printing the `--sysroot` to pass, so a link with no `--sysroot` is one where that has not
436/// happened yet and the answer is to say what to run.
437///
438/// # Errors
439///
440/// [`Error::Cross`] when no `--sysroot` was given.
441fn msvc_sysroot(target: Triple, opts: &LinkOptions) -> Result<Sysroot, Error> {
442    let tuple = target_tuple(target, opts);
443    match &opts.sysroot {
444        Some(root) => Ok(Sysroot::at(root.clone(), tuple)),
445        None => Err(Error::Cross {
446            why: format!(
447                "a link for {tuple} is against Microsoft's C runtime and the Windows SDK, which \
448                 this compiler may not ship. `rucc --fetch {tuple}` gets them from Microsoft once \
449                 you accept Microsoft's licence and prints the --sysroot=<dir> to pass",
450                tuple = tuple.to_canonical_string()
451            ),
452        }),
453    }
454}
455
456/// The names to look for when the target is not this machine.
457///
458/// A shorter list than the one above and a different one, because most of that list cannot do this.
459/// `spec/cross-compile/11-linking.md` section 11.2 settles it: `ld.lld` is the ELF cross linker,
460/// since one binary of it links for every architecture it was built with and that is all of them.
461/// mold is off the list because it links for the host and `wild` likewise, which is why section 11.2
462/// has them as `-fuse-ld=` choices for a native link rather than as defaults. The platform's own
463/// `ld` is off it for the same reason: a distribution's `/usr/bin/ld` is built for one architecture,
464/// and `-fuse-ld=` is still there for somebody whose is not.
465///
466/// A cross binutils under its prefixed name is last, because a machine that has
467/// `aarch64-linux-gnu-ld` installed has it on purpose. The prefix is a distribution convention and
468/// there are two of them: a Linux target is filed under its multiarch name and a mingw-w64 one under
469/// `<arch>-w64-mingw32`, which is what every distribution's mingw packages install. `ld.lld` is the
470/// same binary for both, because its MinGW mode is a mode of the one linker rather than a second one.
471fn cross_order(target: Triple) -> Vec<String> {
472    let mut names = vec!["ld.lld".to_owned(), "lld".to_owned()];
473    match (target.os, target.env) {
474        (Os::Linux, _) => names.push(format!("{}-ld", multiarch(target))),
475        (Os::Windows, Env::Gnu) => names.push(format!("{}-w64-mingw32-ld", target.arch.as_str())),
476        _ => {}
477    }
478    names
479}
480
481/// The sysroot a cross link would use, or [`None`] for a link against this machine.
482///
483/// The one place the two paths are told apart, so that the linker that is looked for and the line it
484/// is handed cannot disagree about which kind of link this is.
485///
486/// Three conditions, and two of them are about leaving working configurations alone. A target that
487/// is this machine is linked against this machine, which is what every native compile has always
488/// done and what the directories under `/usr/lib` are for. A `--sysroot` the user wrote is taken as
489/// the root of a tree they assembled, and the line above prefixes every path it decides with it,
490/// which is what cross compiling against a real distribution tree has always meant here. The third
491/// is that there has to be a cache directory to look in, which on a real command line there always
492/// is.
493///
494/// An unknown host counts as different from every target. A machine this compiler cannot name is a
495/// machine whose `/usr/lib` it should not be guessing at.
496///
497/// # A pinned release is a cross compile
498///
499/// The first of those three conditions is about the machine and not about the triple, and a target
500/// that names a libc release is not this machine even when it is this architecture. Somebody on a
501/// 2.44 box writing `--target=x86_64-linux-gnu.2.28` is asking for a binary that runs on a 2.28
502/// machine, and handing them their own headers and their own libc gives them a binary that does not.
503/// So the condition is the triple being the host *and* no release named, and what it costs is that a
504/// pin equal to this machine's own release also stops using this machine's libc. That is not a loss:
505/// the two should be the same text, and if they are not then this machine's copy is patched and the
506/// bundled tree is the one the pin asked for. tamnd/rucc#956.
507#[must_use]
508pub fn cross_sysroot(target: Triple, opts: &LinkOptions) -> Option<Sysroot> {
509    cross_for(target, opts, Triple::host())
510}
511
512/// The same answer with the host as a parameter, so that both branches are testable on one machine.
513fn cross_for(target: Triple, opts: &LinkOptions, host: Option<Triple>) -> Option<Sysroot> {
514    if opts.sysroot.is_some() {
515        return None;
516    }
517    let tuple = target_tuple(target, opts);
518    // Windows is left out because a Windows machine has no C library of its own to compile
519    // against. On an x86-64 Windows box the default target is the host, and the only headers
520    // it can have are the mingw-w64 tree `--fetch` put in the cache.
521    if host == Some(target) && target.os != Os::Windows && tuple.env_version().is_none() {
522        return None;
523    }
524    if distro_for(target, opts, host).is_some() {
525        return None;
526    }
527    let cache = opts.cache.as_deref()?;
528    Some(Sysroot::in_cache(cache, tuple))
529}
530
531/// A tree a distribution's cross packages installed for a Linux target that is not this machine.
532///
533/// What `apt install gcc-aarch64-linux-gnu` leaves behind: the C library's headers and files under
534/// `/usr/aarch64-linux-gnu`, and gcc's `crtbegin.o` and `libgcc.a` for that target under
535/// `/usr/lib/gcc-cross/aarch64-linux-gnu/<version>`. The library's `libc.so` script names its files
536/// by their full path, so the tree is linked where it is and not under a `--sysroot`.
537#[derive(Debug, Clone, PartialEq, Eq)]
538pub struct Distro {
539    /// `/usr/<multiarch>`, which has `include` and `lib` under it.
540    pub root: PathBuf,
541    /// gcc's directories for the target, newest version first, and empty when only the library
542    /// was installed.
543    pub gcc: Vec<PathBuf>,
544}
545
546impl Distro {
547    /// The C library's headers, and the kernel's, which the same packages put in the same place.
548    #[must_use]
549    pub fn include(&self) -> PathBuf {
550        self.root.join("include")
551    }
552
553    /// The C library's startup files and libraries.
554    #[must_use]
555    pub fn lib(&self) -> PathBuf {
556        self.root.join("lib")
557    }
558}
559
560/// The distribution's tree for this target, when a cross compile should use it.
561///
562/// Only when nothing better was asked for or is there. A `--sysroot` is a tree somebody named, a
563/// pinned release is a request for our tree cut at that release, and a sysroot already fetched into
564/// the cache is the one this release pins, so each of those wins. What is left is a machine that
565/// has the distribution's cross packages and nothing of ours, and there the packages are what a
566/// prefixed gcc on the same machine would use, so they are what this uses too.
567#[must_use]
568pub fn distro_cross(target: Triple, opts: &LinkOptions) -> Option<Distro> {
569    distro_for(target, opts, Triple::host())
570}
571
572/// The same answer with the host as a parameter, for the same reason as [`cross_for`].
573fn distro_for(target: Triple, opts: &LinkOptions, host: Option<Triple>) -> Option<Distro> {
574    if opts.sysroot.is_some() || target.os != Os::Linux || host == Some(target) {
575        return None;
576    }
577    let tuple = target_tuple(target, opts);
578    if tuple.env_version().is_some() {
579        return None;
580    }
581    if opts.cache.as_deref().is_some_and(|cache| Sysroot::in_cache(cache, tuple).lib().is_dir()) {
582        return None;
583    }
584    let usr = opts.usr.as_deref()?;
585    let name = multiarch(target);
586    let root = usr.join(&name);
587    if !root.join("include").is_dir() || !root.join("lib").is_dir() {
588        return None;
589    }
590    let gcc = newest_first(&usr.join("lib/gcc-cross").join(&name));
591    Some(Distro { root, gcc })
592}
593
594/// The target as the model that has room for a release, which is what names the cache directory.
595///
596/// The pinned spelling when there is one, because `x86_64-linux-gnu` and `x86_64-linux-gnu.2.28` are
597/// two sysroots and not one: the release is in the tuple for the reason
598/// `spec/cross-compile/03-target-model.md` section 3.2 admits a field at all, which is that it
599/// changes what is compiled. A command line that named no target, or one whose spelling the ten
600/// field parser did not take, falls back to what the three field one did.
601fn target_tuple(target: Triple, opts: &LinkOptions) -> TargetTuple {
602    opts.pinned.unwrap_or_else(|| target.tuple())
603}
604
605/// The kernel headers that go with [`cross_sysroot`], for the targets that have any.
606///
607/// The same three conditions, asked through the same function, because the two halves of one
608/// target's system headers have to be decided together or a compile could read glibc's `sys/stat.h`
609/// against this machine's `asm/stat.h`. A `None` here on a Linux target where the sysroot is `Some`
610/// means only one thing, which is that the cache has no kernel tree for that architecture, and the
611/// directory is still named for the reason [`crate::library::header_dirs`] gives.
612///
613/// Not under the sysroot, because `linux/` and `asm-generic/` are the same nine megabytes for every
614/// target that shares an architecture, and a copy per target is eight copies of one thing.
615#[must_use]
616pub fn cross_kernel(target: Triple, opts: &LinkOptions) -> Option<Kernel> {
617    kernel_for(target, opts, Triple::host())
618}
619
620/// The same answer with the host as a parameter, for the same reason as [`cross_for`].
621fn kernel_for(target: Triple, opts: &LinkOptions, host: Option<Triple>) -> Option<Kernel> {
622    cross_for(target, opts, host)?;
623    Kernel::for_target(opts.cache.as_deref()?, target.tuple())
624}
625
626/// How the result is linked, as the five cases a sysroot link line is written over.
627///
628/// Four booleans reach here and five cases leave, because static and position independent are not
629/// independent of each other and the start file differs in four of the five. The default for `pie`
630/// is the one the native line above uses, so that a command line that says neither gets the same
631/// answer whichever path it takes.
632fn mode(opts: &LinkOptions) -> LinkMode {
633    let pie = opts.pie.unwrap_or(!opts.is_static && !opts.shared);
634    if opts.shared {
635        LinkMode::Shared
636    } else if opts.is_static {
637        if pie { LinkMode::StaticPie } else { LinkMode::Static }
638    } else if pie {
639        LinkMode::Dynamic
640    } else {
641        LinkMode::DynamicNoPie
642    }
643}
644
645/// The line for a machine that is not this one, from the target and the sysroot and nothing else.
646///
647/// Everything this knows is already in `opts`, and all it does is say it in the shape
648/// [`rucc_sysroot::argv`] is written over. There is deliberately no decision here: a second place
649/// that decided what goes on a cross link line would be a second place to get it wrong, and the
650/// recorded lines under `tests/link-lines` would stop describing what this compiler does.
651fn cross_line(
652    target: Triple,
653    opts: &LinkOptions,
654    items: &[Item],
655    output: &str,
656    sysroot: &Sysroot,
657) -> Result<Vec<String>, Error> {
658    if opts.profile {
659        // `gcrt1.o` is a compiled object out of the C library's own sources, and a generated sysroot
660        // has the names a libc exports rather than the bodies behind them. Said here rather than
661        // left to the linker, because what the linker would say is that `main` is undefined.
662        return Err(Error::Cross {
663            why: format!(
664                "-pg needs gcrt1.o, or gcrt2.o on Windows, the startup file that starts and stops \
665                 the counting, and a generated sysroot for {target} does not have one. Profile on \
666                 the host, or pass --sysroot=<dir> naming a tree that has it"
667            ),
668        });
669    }
670    let inputs: Vec<argv::Item> = items
671        .iter()
672        .map(|item| match item {
673            Item::File(path) => argv::Item::File(PathBuf::from(path)),
674            Item::Library(name) => argv::Item::Library(name.clone()),
675            Item::Linker(arg) => argv::Item::Linker(arg.clone()),
676        })
677        .collect();
678    let output = PathBuf::from(output);
679    // Ours, from beside the compiler, because that is where `cargo xtask builtins` writes it and a
680    // fetched sysroot will never hold it. The cross line used to name it inside the sysroot, which
681    // is a file nothing puts there, so every cross link either failed at the linker or quietly ran
682    // against somebody else's `libgcc` copied in under the name. tamnd/rucc#1514.
683    let ours = builtins_archive(target, &opts.prefixes);
684    if ours.is_none() && opts.wants_runtime() && !opts.no_builtins_lib {
685        // Said here rather than left to the linker, which on a Windows target says `___chkstk_ms`
686        // is undefined and names mingw-w64's objects as the callers, and on a musl one says
687        // `__udivti3` is. Neither of those is a person's first guess at a missing archive.
688        let tuple = target.tuple().to_canonical_string();
689        return Err(Error::Cross {
690            why: format!(
691                "a cross link ends with librucc_builtins.a, this compiler's own runtime for \
692                 {tuple}, and there is none beside the compiler or under a -B prefix. A sysroot \
693                 does not carry it, because it is our output rather than the platform's. Build it \
694                 with `cargo xtask builtins --target={tuple}`, or pass -fno-builtins-lib to link \
695                 without it"
696            ),
697        });
698    }
699    let invocation = argv::Invocation {
700        inputs: &inputs,
701        output: Some(&output),
702        mode: mode(opts),
703        search: &opts.search,
704        no_startfiles: !opts.wants_startfiles(),
705        no_defaultlibs: !opts.wants_defaultlibs(),
706        no_builtins_lib: opts.no_builtins_lib,
707        builtins: ours.as_deref(),
708        export_dynamic: opts.export_dynamic,
709        strip: opts.strip,
710        gui: opts.gui,
711        unicode: opts.unicode,
712        crt: opts.crt,
713    };
714    argv::argv(target.tuple(), sysroot, &invocation)
715        .map_err(|why| Error::Cross { why: why.to_string() })
716}
717
718/// Whether this link can be run at all, asked before anything is compiled.
719///
720/// Two questions that have answers before the first object exists: whether there is a line for this
721/// target and mode at all, and whether the sysroot it would read is on the machine. Both are worth a
722/// second at the start rather than a message after a minute of compiling, which is the same reason
723/// the linker itself is looked for first.
724///
725/// The line is built rather than inspected, with no inputs and a name nothing will be written to,
726/// because the refusals belong to the one function that builds it. A link against this machine has
727/// nothing to answer here: its directories are looked for as the line is built and a missing one is
728/// simply a directory that is not offered.
729///
730/// # Errors
731///
732/// [`Error::Cross`] for a target or a mode that has no line, and [`Error::Sysroot`] when the sysroot
733/// it would be linked against is not there.
734pub fn preflight(target: Triple, opts: &LinkOptions) -> Result<(), Error> {
735    if opts.relocatable {
736        return relocatable_line(target, opts, &[], "a.out").map(drop);
737    }
738    // Whether there is an SDK, which is the one thing a Darwin link can be missing that is worth
739    // saying before everything has been compiled.
740    if target.os == Os::Darwin {
741        return darwin_line(target, opts, &[], "a.out").map(drop);
742    }
743    if is_msvc(target) {
744        return msvc_preflight(target, opts);
745    }
746    let Some(sysroot) = cross_sysroot(target, opts) else { return Ok(()) };
747    // Whether there is a line for this target and mode at all, asked with our own runtime left off
748    // it. Otherwise a target nothing here can link and a machine where nobody built the runtime
749    // report the same thing, and the archive is the smaller of the two problems by a long way.
750    let shape = LinkOptions { no_builtins_lib: true, ..opts.clone() };
751    cross_line(target, &shape, &[], "a.out", &sysroot)?;
752    // The library directory rather than the root, because the root of a cache directory that has
753    // been created and never populated is there and holds nothing. Section 11.6's rule is that
754    // suitable is checked and not assumed, and this is the cheapest form of that.
755    if !sysroot.lib().is_dir() {
756        let tuple = target_tuple(target, opts).to_canonical_string();
757        return Err(Error::Sysroot {
758            dir: sysroot.root().display().to_string(),
759            pinned: rucc_sysroot::pinned_for_target(target_tuple(target, opts)).is_some(),
760            target: tuple,
761        });
762    }
763    // And now the whole line, which is the sysroot's files plus ours, so that a missing runtime is
764    // said here rather than by the linker after everything has been compiled.
765    cross_line(target, opts, &[], "a.out", &sysroot)?;
766    Ok(())
767}
768
769/// [`preflight`] for Microsoft's environment, which is the same three questions about a tree
770/// somebody named rather than one in the cache.
771///
772/// Whether the tree is one is asked of the CRT's directory for this architecture, because that is
773/// the one every line needs and the one a tree fetched for another architecture does not have.
774fn msvc_preflight(target: Triple, opts: &LinkOptions) -> Result<(), Error> {
775    let sysroot = msvc_sysroot(target, opts)?;
776    let shape = LinkOptions { no_builtins_lib: true, ..opts.clone() };
777    cross_line(target, &shape, &[], "a.exe", &sysroot)?;
778    let tuple = target_tuple(target, opts);
779    if let Some(chip) = Chip::of(tuple) {
780        let crt = sysroot.root().join("crt").join("lib").join(chip.in_tree());
781        if !crt.is_dir() {
782            return Err(Error::Cross {
783                why: format!(
784                    "{} has no {}, so it is not a tree for {tuple} to link against. `rucc \
785                     --fetch {tuple}` lays one out and prints where",
786                    sysroot.root().display(),
787                    crt.display(),
788                    tuple = tuple.to_canonical_string()
789                ),
790            });
791        }
792    }
793    cross_line(target, opts, &[], "a.exe", &sysroot)?;
794    Ok(())
795}
796
797/// Writes the stub libraries a glibc cross link reads, into [`Sysroot::stubs`].
798///
799/// `spec/cross-compile/09-libc-stubs.md` section 9.1: the stubs are generated on demand rather than
800/// shipped, out of the description `rucc-stub` carries, cut at the release the tuple names or at
801/// the bundled one when it names none. So there is nothing to fetch for them and nothing to go
802/// stale, and a pin is a different directory rather than a different download.
803///
804/// A file is written only when its bytes differ from what is there, and then through a temporary
805/// name and a rename, because two builds for one target run side by side all the time and a
806/// linker must never read a half written `libc.so`. The bytes are the same on every host, so two
807/// processes racing to write them race to write the same thing.
808///
809/// Nothing for a link against this machine, a `--sysroot` the user named, or a target that is not
810/// glibc. A glibc release newer than the bundled tree was already refused when the headers were
811/// chosen, so it is quietly nothing here too.
812///
813/// # Errors
814///
815/// [`Error::Cross`] when the stubs cannot be generated for this target or cannot be written.
816pub fn write_stubs(target: Triple, opts: &LinkOptions) -> Result<(), Error> {
817    let Some(sysroot) = cross_sysroot(target, opts) else { return Ok(()) };
818    let tuple = sysroot.target();
819    if rucc_stub::glibc::architecture(tuple).is_none() {
820        return Ok(());
821    }
822    let Ok(Some(minor)) = rucc_sysroot::bundled_glibc_minor(tuple) else { return Ok(()) };
823    let files = rucc_stub::glibc::stubs(tuple, minor).map_err(|why| Error::Cross {
824        why: format!("the glibc stubs for {}: {why}", tuple.to_canonical_string()),
825    })?;
826    let dir = sysroot.stubs();
827    let failed = |path: &Path, why: std::io::Error| Error::Cross {
828        why: format!("{} cannot be written: {why}", path.display()),
829    };
830    fs::create_dir_all(dir).map_err(|why| failed(dir, why))?;
831    for file in files {
832        let path = dir.join(&file.name);
833        if fs::read(&path).is_ok_and(|there| there == file.bytes) {
834            continue;
835        }
836        let temporary = dir.join(format!(".{}.{}", file.name, std::process::id()));
837        fs::write(&temporary, &file.bytes).map_err(|why| failed(&temporary, why))?;
838        fs::rename(&temporary, &path).map_err(|why| {
839            let _ = fs::remove_file(&temporary);
840            failed(&path, why)
841        })?;
842    }
843    Ok(())
844}
845
846/// The linker to use, looked for where a linker is.
847///
848/// `-B` prefixes first, since the point of one is to put a toolchain in front of the machine's,
849/// then the path. A name that contains a separator is a path and is taken as one, which is what
850/// gcc does with `-fuse-ld=/usr/bin/ld.gold` and what a build system relying on that expects.
851///
852/// # Errors
853///
854/// [`Error::Named`] when `-fuse-ld=` asked for one that is not here, and [`Error::NoLinker`] when
855/// nothing was, which name the candidates so that the message says what was looked for.
856pub fn find(target: Triple, opts: &LinkOptions) -> Result<Linker, Error> {
857    let tried = order(target, opts);
858    let places = lld_dirs(Path::new("/"), std::env::var_os("ProgramFiles").map(PathBuf::from));
859    // The first linker [`suitable`] turned down, which is the answer when nothing after it is any
860    // better. Ubuntu 24.04 has lld 18 on PATH and lld 19 under /usr/lib/llvm-19/bin once somebody
861    // installs `lld-19`, and the second is the one to use without asking them to change PATH.
862    let mut refused = None;
863    for name in &tried {
864        for path in linker_candidates(name, opts, &places) {
865            let linker = Linker { name: name.clone(), path };
866            match suitable(target, &linker) {
867                Ok(()) => return Ok(linker),
868                Err(why) => {
869                    refused.get_or_insert(why);
870                }
871            }
872        }
873    }
874    if let Some(why) = refused {
875        return Err(why);
876    }
877    match &opts.use_ld {
878        Some(name) => Err(Error::Named { name: name.clone() }),
879        None => Err(Error::NoLinker { tried }),
880    }
881}
882
883/// Every file a linker of this name could be, in the order they are asked.
884///
885/// A name with a separator in it is a path and is the only answer. Otherwise the `-B` prefixes,
886/// then every directory on `PATH` rather than the first hit, then the places an lld is installed
887/// without being put on `PATH`. All of them rather than the first, because the first may be an lld
888/// that [`suitable`] turns down and a later one may not be.
889fn linker_candidates(name: &str, opts: &LinkOptions, places: &[PathBuf]) -> Vec<PathBuf> {
890    if name.contains(std::path::MAIN_SEPARATOR) || name.contains('/') {
891        let path = PathBuf::from(name);
892        return if path.is_file() { vec![path] } else { Vec::new() };
893    }
894    let pathext = pathext();
895    let mut found: Vec<PathBuf> = opts
896        .prefixes
897        .iter()
898        .flat_map(|dir| spellings(&dir.join(name), &pathext))
899        .filter(|path| path.is_file())
900        .collect();
901    if let Some(path) = std::env::var_os("PATH") {
902        found.extend(
903            std::env::split_paths(&path)
904                .flat_map(|dir| spellings(&dir.join(name), &pathext))
905                .filter(|p| executable(p)),
906        );
907    }
908    // The same spellings as on PATH, so `ld.lld` here and `ld.lld.exe` on Windows. Only `.exe`
909    // was tried at first, which found nothing in /usr/lib/llvm-19/bin on the Linux machines this
910    // search was written for.
911    if is_lld(name) {
912        for dir in places {
913            for file in spellings(&dir.join(name), &pathext) {
914                if executable(&file) && !found.contains(&file) {
915                    found.push(file);
916                }
917            }
918        }
919    }
920    found
921}
922
923/// The extensions a program name is tried with, from `PATHEXT` on Windows and none elsewhere.
924///
925/// `ld.lld` on Windows is `ld.lld.exe`, and looking for the bare name finds nothing, which is how
926/// a Windows host with LLVM installed and on `PATH` used to be told there was no linker. `PATHEXT`
927/// is what `cmd.exe` uses for the same question, and when it is unset the list is the one
928/// Windows ships with.
929fn pathext() -> Vec<String> {
930    if !cfg!(windows) {
931        return Vec::new();
932    }
933    let list = std::env::var("PATHEXT").unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".to_owned());
934    list.split(';').filter(|ext| ext.starts_with('.')).map(str::to_ascii_lowercase).collect()
935}
936
937/// A path with each extension added, or as given when there are none or it already ends in one.
938///
939/// `ld.lld` ends in `.lld`, which is not an extension anybody runs, so the check is against the
940/// list rather than against whether there is a dot in the name at all. The bare name is left out
941/// when there is a list, because a file called `ld.lld` in a Git Bash directory on Windows is a
942/// shell script that `CreateProcess` cannot start.
943fn spellings(path: &Path, exts: &[String]) -> Vec<PathBuf> {
944    let has = path
945        .extension()
946        .and_then(|ext| ext.to_str())
947        .is_some_and(|ext| exts.iter().any(|e| e[1..].eq_ignore_ascii_case(ext)));
948    if exts.is_empty() || has {
949        return vec![path.to_path_buf()];
950    }
951    exts.iter()
952        .map(|ext| {
953            let mut name = path.as_os_str().to_owned();
954            name.push(ext);
955            PathBuf::from(name)
956        })
957        .collect()
958}
959
960/// Whether a name is one of the spellings lld is installed under.
961fn is_lld(name: &str) -> bool {
962    matches!(name, "ld.lld" | "ld64.lld" | "lld" | "lld-link")
963}
964
965/// Where lld is installed without being on `PATH`, newest first where a version is in the name.
966///
967/// Homebrew's `lld` and `llvm` formulae, whose `llvm` is keg-only and so never on `PATH` unless
968/// somebody put it there. Debian and Ubuntu's `lld-<N>` packages, which put the real program in
969/// `/usr/lib/llvm-<N>/bin` and only a versioned name in `/usr/bin`. Fedora's compatibility packages,
970/// which do the same under `/usr/lib64/llvm<N>/bin`. And the LLVM installer for Windows, which puts
971/// everything in `%ProgramFiles%\LLVM\bin` and leaves adding it to `PATH` as a checkbox that is off.
972///
973/// `root` is `/` except in the tests, which build the same tree somewhere they are allowed to.
974fn lld_dirs(root: &Path, program_files: Option<PathBuf>) -> Vec<PathBuf> {
975    let mut dirs: Vec<PathBuf> = [
976        "opt/homebrew/opt/lld/bin",
977        "opt/homebrew/opt/llvm/bin",
978        "usr/local/opt/lld/bin",
979        "usr/local/opt/llvm/bin",
980        "home/linuxbrew/.linuxbrew/opt/lld/bin",
981        "home/linuxbrew/.linuxbrew/opt/llvm/bin",
982    ]
983    .iter()
984    .map(|dir| root.join(dir))
985    .collect();
986    for (parent, prefix) in [("usr/lib", "llvm-"), ("usr/lib64", "llvm")] {
987        let mut versions: Vec<(u32, PathBuf)> = fs::read_dir(root.join(parent))
988            .into_iter()
989            .flatten()
990            .flatten()
991            .filter_map(|entry| {
992                let name = entry.file_name();
993                let version = name.to_str()?.strip_prefix(prefix)?.parse().ok()?;
994                Some((version, entry.path().join("bin")))
995            })
996            .collect();
997        versions.sort_by_key(|(version, _)| std::cmp::Reverse(*version));
998        dirs.extend(versions.into_iter().map(|(_, dir)| dir));
999    }
1000    if let Some(dir) = program_files {
1001        dirs.push(dir.join("LLVM").join("bin"));
1002    }
1003    dirs
1004}
1005
1006/// Where to get an lld on the machine this compiler is running on, as the end of a sentence.
1007///
1008/// One per host rather than a list of every package manager, because the person reading it has one
1009/// machine and wants the one command for it. Each names a place [`lld_dirs`] looks, so that doing
1010/// what it says is enough and nobody has to edit `PATH` afterwards.
1011fn lld_advice() -> &'static str {
1012    if cfg!(target_os = "macos") {
1013        "`brew install lld` installs one, and rucc finds it in Homebrew's directory without a \
1014         change to PATH"
1015    } else if cfg!(windows) {
1016        "the LLVM installer from https://github.com/llvm/llvm-project/releases, or `winget install \
1017         LLVM.LLVM`, installs one in %ProgramFiles%\\LLVM\\bin, where rucc finds it without a \
1018         change to PATH"
1019    } else {
1020        "on Debian and Ubuntu `apt install lld-19` installs one in /usr/lib/llvm-19/bin and on \
1021         Fedora `dnf install lld` installs one on PATH, and rucc finds either without a change to \
1022         PATH"
1023    }
1024}
1025
1026/// The first lld that reads `IMPORT_NAME_EXPORTAS`, which is what a windows-gnu link needs.
1027///
1028/// 18 does not read it and does not say so, so the number is not a convenience: below it the
1029/// answer is wrong rather than absent. tamnd/rucc#1515.
1030pub const LLD_EXPORTAS: u32 = 19;
1031
1032/// Whether a found linker can do this target's link, asked before it is handed anything.
1033///
1034/// Section 11.6's rule is that suitable is checked and not assumed, and this is the one check that
1035/// cannot be made by looking at a file. A windows-gnu link reads import libraries that mingw-w64's
1036/// `==` aliases compiled into `IMPORT_NAME_EXPORTAS` records, which lld reads from
1037/// [`LLD_EXPORTAS`] on. An older lld writes an import by ordinal zero instead, without a warning
1038/// and with a successful exit, so nothing later in the toolchain has anything to notice: the
1039/// program is wrong at startup and the link that made it said nothing. Ubuntu 24.04 is the current
1040/// LTS and ships 18, so the machine this happens on is an ordinary one.
1041///
1042/// Every other target is left alone, and so is anything that is not an lld, because this is the one
1043/// version of the one linker that is known to answer wrongly rather than not at all.
1044///
1045/// A linker that will not run or whose version cannot be read is allowed through. What the check
1046/// can establish is that a specific old lld is here, and it should not turn every unusual linker
1047/// into a refusal on the strength of failing to recognise it.
1048///
1049/// # Errors
1050///
1051/// [`Error::TooOld`] when the linker is an lld older than [`LLD_EXPORTAS`] and the target is
1052/// windows-gnu.
1053pub fn suitable(target: Triple, linker: &Linker) -> Result<(), Error> {
1054    if (target.os, target.env) != (Os::Windows, Env::Gnu) {
1055        return Ok(());
1056    }
1057    let Some(found) = lld_major(&reported_version(&linker.path)) else { return Ok(()) };
1058    if found >= LLD_EXPORTAS {
1059        return Ok(());
1060    }
1061    Err(Error::TooOld {
1062        name: linker.name.clone(),
1063        found,
1064        target: target.tuple().to_canonical_string(),
1065    })
1066}
1067
1068/// What `<linker> --version` prints, or an empty string when it will not say.
1069///
1070/// A linker that cannot be started is not this function's problem to report, because the link is
1071/// about to start it again and say so properly. What this returns for such a one is nothing to
1072/// read, which is the same as a linker that ran and said something unrecognisable.
1073fn reported_version(path: &Path) -> String {
1074    let Ok(out) = Command::new(path).arg("--version").output() else { return String::new() };
1075    String::from_utf8_lossy(&out.stdout).into_owned()
1076}
1077
1078/// The major version in an lld's `--version`, when the program that printed it was an lld.
1079///
1080/// What lld prints is `LLD 18.1.8 (compatible with GNU linkers)`, with a distribution's own prefix
1081/// in front of it often enough that the word is looked for rather than the line starting with it:
1082/// Ubuntu's says `Ubuntu LLD 18.1.3`. Binutils prints `GNU ld (GNU Binutils for Ubuntu) 2.42` and
1083/// mold prints its own name, and neither has the word, so both come back as [`None`] and are left
1084/// alone.
1085fn lld_major(text: &str) -> Option<u32> {
1086    let mut words = text.split_whitespace();
1087    words.find(|word| *word == "LLD")?;
1088    words.next()?.split('.').next()?.parse().ok()
1089}
1090
1091/// Whether a path is a file this process could run.
1092#[cfg(unix)]
1093fn executable(path: &Path) -> bool {
1094    use std::os::unix::fs::PermissionsExt as _;
1095    path.metadata().is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
1096}
1097
1098/// Whether a path is a file this process could run.
1099///
1100/// Windows has no executable bit and decides by extension, and the names looked for above carry
1101/// theirs, so being a file is the whole of the question here.
1102#[cfg(not(unix))]
1103fn executable(path: &Path) -> bool {
1104    path.is_file()
1105}
1106
1107/// What the linker is told, in order, not counting the linker itself.
1108///
1109/// Two lines and [`cross_sysroot`] picks which: the one above for this machine, and
1110/// [`rucc_sysroot::argv`]'s for any other. Nothing about the machine is read on the second path, so
1111/// `-###` prints the same line on every host and prints it whether the sysroot has been built or
1112/// not, which is what makes it worth printing.
1113///
1114/// # Errors
1115///
1116/// [`Error::Target`] for a platform there is no native line for yet, which is every one but Linux,
1117/// and [`Error::Cross`] for a cross link that cannot be produced at all.
1118pub fn line(
1119    target: Triple,
1120    opts: &LinkOptions,
1121    items: &[Item],
1122    output: &str,
1123) -> Result<Vec<String>, Error> {
1124    if opts.relocatable {
1125        return relocatable_line(target, opts, items, output);
1126    }
1127    if target.os == Os::Darwin {
1128        return darwin_line(target, opts, items, output);
1129    }
1130    if is_msvc(target) {
1131        return cross_line(target, opts, items, output, &msvc_sysroot(target, opts)?);
1132    }
1133    if let Some(sysroot) = cross_sysroot(target, opts) {
1134        return cross_line(target, opts, items, output, &sysroot);
1135    }
1136    if target.os != Os::Linux {
1137        return Err(Error::Target { triple: target.to_string() });
1138    }
1139    let machine = emulation(target);
1140    let root = opts.sysroot.as_deref();
1141    // A distribution's cross tree is linked by the same line, with its two directories in place of
1142    // this machine's, because it is laid out the way this machine's own library is.
1143    let distro = distro_cross(target, opts);
1144    let dirs = match &distro {
1145        Some(distro) => vec![distro.lib()],
1146        None => library_dirs(target, root),
1147    };
1148    // Where a gcc on this machine keeps its own runtime, which is a different place from where
1149    // the C library keeps its own, and where our runtime is if it was built for this target.
1150    let runtime = match distro {
1151        Some(distro) => distro.gcc,
1152        None => runtime_dirs(target, root),
1153    };
1154    let ours = if opts.no_builtins_lib { None } else { builtins_archive(target, &opts.prefixes) };
1155    let mut args = vec![
1156        "-o".to_owned(),
1157        output.to_owned(),
1158        // Which of the several formats one `ld` can write is meant. A linker built for more than
1159        // one machine guesses from its first input otherwise, and a link of no objects at all has
1160        // nothing to guess from.
1161        "-m".to_owned(),
1162        machine.to_owned(),
1163        // The table a program unwinds through, which a C program with no exceptions in it still
1164        // needs because `backtrace` and every crash handler read it.
1165        "--eh-frame-hdr".to_owned(),
1166        // The symbol hash a dynamic loader from this century reads. The old one is still written
1167        // alongside by default on some distributions, and asking for this one is what stops a link
1168        // from carrying a table nothing has needed since 2006.
1169        "--hash-style=gnu".to_owned(),
1170    ];
1171
1172    let pie = opts.pie.unwrap_or(!opts.is_static && !opts.shared);
1173    if opts.shared {
1174        args.push("-shared".to_owned());
1175    } else if opts.is_static {
1176        args.push("-static".to_owned());
1177    } else if pie {
1178        args.push("-pie".to_owned());
1179    } else {
1180        args.push("-no-pie".to_owned());
1181    }
1182    if !opts.is_static && !opts.shared {
1183        args.push("-dynamic-linker".to_owned());
1184        args.push(target_path(root, loader(target)));
1185    }
1186    if opts.export_dynamic {
1187        args.push("--export-dynamic".to_owned());
1188    }
1189    if opts.strip {
1190        args.push("-s".to_owned());
1191    }
1192
1193    if opts.wants_startfiles() {
1194        for name in startfile(opts, pie).into_iter().chain(["crti.o"]) {
1195            if let Some(path) = find_file(&dirs, name) {
1196                args.push(path.display().to_string());
1197            }
1198        }
1199        // The compiler's own startup file, which runs the static constructors. Three spellings
1200        // of the same thing, and which one is right is about how the code in it refers to
1201        // itself: `S` for a position independent result, `T` for a static one, plain for the
1202        // rest. Skipped when there is no gcc on the machine to take it from, because a program
1203        // with no constructor in it does not miss it.
1204        let begin = if opts.shared || pie {
1205            "crtbeginS.o"
1206        } else if opts.is_static {
1207            "crtbeginT.o"
1208        } else {
1209            "crtbegin.o"
1210        };
1211        if let Some(path) = find_file(&runtime, begin).or_else(|| find_file(&runtime, "crtbegin.o"))
1212        {
1213            args.push(path.display().to_string());
1214        }
1215    }
1216
1217    for dir in &opts.search {
1218        args.push(format!("-L{}", dir.display()));
1219    }
1220    for dir in &dirs {
1221        args.push(format!("-L{}", dir.display()));
1222    }
1223    // Where `libgcc.a` and `libgcc_eh.a` are, which is not where the C library is. Nothing is
1224    // added when there is no gcc on the machine, and then the `-l` names below are left off too.
1225    for dir in &runtime {
1226        args.push(format!("-L{}", dir.display()));
1227    }
1228
1229    for item in items {
1230        match item {
1231            Item::File(path) => args.push(path.clone()),
1232            Item::Library(name) => args.push(format!("-l{name}")),
1233            Item::Linker(arg) => args.push(arg.clone()),
1234        }
1235    }
1236    // After the objects, because a static archive is searched for what is undefined at the point
1237    // it is reached and a library named before the object that needs it contributes nothing.
1238    args.extend(runtime_items(opts, &runtime, ours.as_deref()));
1239
1240    if opts.wants_startfiles() {
1241        // The other end of `crtbegin`, and it goes before `crtn.o` for the same reason `crti.o`
1242        // goes before `crtbegin`: the four are two nested pairs and not four separate files.
1243        // The fast math constructor, ahead of `crtend` where gcc puts it. Skipped when there is
1244        // no gcc to take it from, as `crtbegin` is.
1245        if opts.wants_fastmath() {
1246            if let Some(path) = find_file(&runtime, "crtfastmath.o") {
1247                args.push(path.display().to_string());
1248            }
1249        }
1250        let end = if opts.shared || pie { "crtendS.o" } else { "crtend.o" };
1251        if let Some(path) = find_file(&runtime, end).or_else(|| find_file(&runtime, "crtend.o")) {
1252            args.push(path.display().to_string());
1253        }
1254        if let Some(path) = find_file(&dirs, "crtn.o") {
1255            args.push(path.display().to_string());
1256        }
1257    }
1258
1259    Ok(args)
1260}
1261
1262/// The line for `-r`, which is the objects and the machine and nothing else.
1263///
1264/// No sysroot is read, because a relocatable link takes nothing from the C library, so this is the
1265/// same line for this machine and for any other Linux target. The `-L` directories the command
1266/// line gave are kept for a `-l` written on it, which the linker still resolves against archives.
1267fn relocatable_line(
1268    target: Triple,
1269    opts: &LinkOptions,
1270    items: &[Item],
1271    output: &str,
1272) -> Result<Vec<String>, Error> {
1273    if target.os == Os::Darwin {
1274        // `ld64` joins objects with `-r` too, and needs only to be told the architecture, since
1275        // nothing from the SDK goes into an object that is still going to be linked.
1276        let mut args = vec![
1277            "-r".to_owned(),
1278            "-arch".to_owned(),
1279            darwin_arch(target)?.to_owned(),
1280            "-o".to_owned(),
1281            output.to_owned(),
1282        ];
1283        for dir in &opts.search {
1284            args.push(format!("-L{}", dir.display()));
1285        }
1286        push_items(&mut args, items);
1287        return Ok(args);
1288    }
1289    if target.os != Os::Linux {
1290        return Err(Error::Target { triple: target.to_string() });
1291    }
1292    let mut args = vec![
1293        "-o".to_owned(),
1294        output.to_owned(),
1295        "-m".to_owned(),
1296        emulation(target).to_owned(),
1297        "-r".to_owned(),
1298    ];
1299    if opts.strip {
1300        args.push("-s".to_owned());
1301    }
1302    for dir in &opts.search {
1303        args.push(format!("-L{}", dir.display()));
1304    }
1305    for item in items {
1306        match item {
1307            Item::File(path) => args.push(path.clone()),
1308            Item::Library(name) => args.push(format!("-l{name}")),
1309            Item::Linker(arg) => args.push(arg.clone()),
1310        }
1311    }
1312    Ok(args)
1313}
1314
1315/// The line `ld64` takes, which is Apple's `ld` or lld's Mach-O flavour.
1316///
1317/// Shorter than the ELF one, because the SDK carries everything a program starts with. There are no
1318/// start files to find: `libSystem` has the C library, the startup code and the runtime in it, and
1319/// `dyld` calls `main` itself. What the linker has to be told is the architecture, the platform with
1320/// the oldest release the program runs on and the SDK it was built against, and where the SDK is,
1321/// which `-syslibroot` puts in front of every library it looks for.
1322///
1323/// `-pie` and `-no-pie` are not passed on. Every arm64 program on a Mac is position independent and
1324/// `ld64` rejects `-no_pie` there, so the flag has nothing to change. `-static` is refused, because
1325/// Apple ships no static C library and a static executable is a kernel's business.
1326///
1327/// # Errors
1328///
1329/// [`Error::Cross`] when there is no SDK to link against, or for a static link, or an architecture
1330/// that has no Mach-O name.
1331pub fn darwin_line(
1332    target: Triple,
1333    opts: &LinkOptions,
1334    items: &[Item],
1335    output: &str,
1336) -> Result<Vec<String>, Error> {
1337    let arch = darwin_arch(target)?;
1338    if opts.is_static && !opts.shared {
1339        return Err(Error::Cross {
1340            why: "there is no static C library for Apple platforms, so -static cannot make a \
1341                  program for one"
1342                .to_owned(),
1343        });
1344    }
1345    let Some(sdk) = crate::library::sdk(opts.sysroot.as_deref()) else {
1346        return Err(Error::Cross {
1347            why: format!(
1348                "no SDK was found to link {} against. Install the command line tools with \
1349                 `xcode-select --install`, or name one with -isysroot <dir> or SDKROOT",
1350                target_tuple(target, opts).to_canonical_string()
1351            ),
1352        });
1353    };
1354    let tuple = target_tuple(target, opts);
1355    let (platform, default) = match (tuple.os(), tuple.env()) {
1356        (rucc_tuple::Os::IOs, rucc_tuple::Env::Simulator) => ("ios-simulator", "14.0"),
1357        (rucc_tuple::Os::IOs, rucc_tuple::Env::MacAbi) => ("mac-catalyst", "14.0"),
1358        (rucc_tuple::Os::IOs, _) => ("ios", "14.0"),
1359        _ => ("macos", "11.0"),
1360    };
1361    let minimum = opts
1362        .os_version
1363        .or_else(|| tuple.os_version())
1364        .map_or_else(|| default.to_owned(), |version| version.to_string());
1365    // What the SDK says it is, which `ld64` records so the loader can tell which behaviours the
1366    // program was built to expect. The deployment target when the SDK does not say, which is the
1367    // answer that asks for no behaviour newer than the program claims to run on.
1368    let version = sdk_version(&sdk).unwrap_or_else(|| minimum.clone());
1369
1370    let mut args = vec![
1371        "-arch".to_owned(),
1372        arch.to_owned(),
1373        "-platform_version".to_owned(),
1374        platform.to_owned(),
1375        minimum,
1376        version,
1377        "-syslibroot".to_owned(),
1378        sdk.display().to_string(),
1379        "-o".to_owned(),
1380        output.to_owned(),
1381    ];
1382    if opts.shared {
1383        args.push("-dylib".to_owned());
1384    }
1385    if opts.export_dynamic {
1386        args.push("-export_dynamic".to_owned());
1387    }
1388    if opts.strip {
1389        // The debug map and the local symbols, which between them are what `-s` leaves out of an
1390        // ELF program. `ld64` has no one flag for it.
1391        args.push("-S".to_owned());
1392        args.push("-x".to_owned());
1393    }
1394    for dir in &opts.search {
1395        args.push(format!("-L{}", dir.display()));
1396    }
1397    push_items(&mut args, items);
1398    if opts.wants_runtime() && !opts.no_builtins_lib {
1399        if let Some(ours) = builtins_archive(target, &opts.prefixes) {
1400            args.push(ours.display().to_string());
1401        }
1402    }
1403    if opts.wants_defaultlibs() {
1404        args.push("-lSystem".to_owned());
1405    }
1406    Ok(args)
1407}
1408
1409/// What `ld64` calls the architecture, which is not what the triple does.
1410fn darwin_arch(target: Triple) -> Result<&'static str, Error> {
1411    match target.arch {
1412        Arch::Aarch64 => Ok("arm64"),
1413        Arch::X86_64 => Ok("x86_64"),
1414        _ => Err(Error::Target { triple: target.to_string() }),
1415    }
1416}
1417
1418/// The version an SDK says it is, from the `SDKSettings.json` every SDK since Xcode 7 has at its
1419/// root, or from its directory name, which is `MacOSX15.2.sdk` when it is not the unversioned link.
1420///
1421/// Read by hand rather than parsed, because the one field wanted is a quoted string at the top
1422/// level and a JSON parser would be a dependency for one line.
1423fn sdk_version(sdk: &Path) -> Option<String> {
1424    let valid = |text: &str| {
1425        !text.is_empty()
1426            && text
1427                .split('.')
1428                .all(|part| !part.is_empty() && part.bytes().all(|b| b.is_ascii_digit()))
1429    };
1430    if let Ok(text) = fs::read_to_string(sdk.join("SDKSettings.json")) {
1431        if let Some(at) = text.find("\"Version\"") {
1432            let rest = &text[at + "\"Version\"".len()..];
1433            let rest = rest.trim_start().strip_prefix(':')?.trim_start().strip_prefix('"')?;
1434            let version = &rest[..rest.find('"')?];
1435            if valid(version) {
1436                return Some(version.to_owned());
1437            }
1438        }
1439    }
1440    let name = sdk.file_name()?.to_str()?.strip_suffix(".sdk")?;
1441    let version = name.trim_start_matches(|c: char| c.is_ascii_alphabetic());
1442    valid(version).then(|| version.to_owned())
1443}
1444
1445/// The objects, libraries and linker words, in the order they were written.
1446fn push_items(args: &mut Vec<String>, items: &[Item]) {
1447    for item in items {
1448        match item {
1449            Item::File(path) => args.push(path.clone()),
1450            Item::Library(name) => args.push(format!("-l{name}")),
1451            Item::Linker(arg) => args.push(arg.clone()),
1452        }
1453    }
1454}
1455
1456/// The startup file the C library brings, or `None` for a link that calls nothing.
1457///
1458/// This is what calls `main` and what passes it the arguments, so a shared object takes none of
1459/// them: nothing starts one and it has no `main` to be started at. `Scrt1.o` rather than `crt1.o`
1460/// when the result moves, because the two differ in whether the reference to `main` in them is one
1461/// a loader may relocate.
1462///
1463/// A profiled program gets a different one again, which does all of that and starts and stops the
1464/// counting around it. There are two of those rather than three: the one that relocates itself is
1465/// only needed by a static position independent link, and every other link takes the plain one,
1466/// which is what gcc does with the same flag.
1467fn startfile(opts: &LinkOptions, pie: bool) -> Option<&'static str> {
1468    if opts.shared {
1469        None
1470    } else if opts.profile {
1471        Some(if pie && opts.is_static { "grcrt1.o" } else { "gcrt1.o" })
1472    } else if pie {
1473        Some("Scrt1.o")
1474    } else {
1475        Some("crt1.o")
1476    }
1477}
1478
1479/// The libraries the compiler's own runtime contributes, in the order the linker wants them.
1480///
1481/// The C library first, then ours, then the machine's `libgcc`. Order inside this list is not
1482/// about whether a symbol resolves, it is about which archive supplies one that more than one of
1483/// them defines, and the two places that happens both have a right answer.
1484///
1485/// `memcpy` and its three neighbours are in the C library on a hosted target and in ours only for
1486/// a freestanding one, which is what `spec/12-abi-and-runtime.md` section 12.8 says they are for.
1487/// glibc's are written in assembly per microarchitecture and ours is a word at a time loop, so a
1488/// link that took ours over glibc's would be slower at the one routine every program reaches.
1489///
1490/// The wide arithmetic is in ours and in `libgcc` both, and the two are ABI-identical on purpose,
1491/// so which one answers is not a correctness question. Ours comes first because it is ours, and
1492/// `-fno-builtins-lib` leaves it off for somebody who would rather it were not.
1493///
1494/// A static link puts the whole list inside `--start-group`. `libc.a` refers to `_Unwind_Resume`,
1495/// and the unwinder refers back into `libc.a`, so a linker walking the list once resolves
1496/// whichever it reaches first and reports the other as undefined. That is exactly the failure
1497/// issue #277 describes and the group is the fix for it.
1498///
1499/// A dynamic link needs no group, because the shared `libc` resolves its own references inside
1500/// itself. `libgcc_s` is asked for `--as-needed` there, the way gcc asks for it, so a program that
1501/// never unwinds does not acquire a dependency on it.
1502fn runtime_items(opts: &LinkOptions, runtime: &[PathBuf], ours: Option<&Path>) -> Vec<String> {
1503    let mut args = Vec::new();
1504    if !opts.wants_defaultlibs() && !opts.wants_runtime() {
1505        return args;
1506    }
1507    // Only when there is a gcc to take them from. On a machine without one the names would be an
1508    // error about a library that was never going to be there, and a program that needs neither
1509    // the unwinder nor a wide divide links and runs without them.
1510    let has_gcc = find_file(runtime, "libgcc.a").is_some();
1511
1512    if opts.is_static {
1513        args.push("--start-group".to_owned());
1514    }
1515    if opts.wants_defaultlibs() {
1516        args.push("-lc".to_owned());
1517    }
1518    if opts.wants_runtime() {
1519        if let Some(path) = ours {
1520            args.push(path.display().to_string());
1521        }
1522        if has_gcc {
1523            args.push("-lgcc".to_owned());
1524            if opts.is_static {
1525                args.push("-lgcc_eh".to_owned());
1526            }
1527        }
1528    }
1529    if opts.is_static {
1530        args.push("--end-group".to_owned());
1531    } else if opts.wants_runtime() && has_gcc {
1532        // The shared half, and only if something still wants it after everything above.
1533        args.push("--as-needed".to_owned());
1534        args.push("-lgcc_s".to_owned());
1535        args.push("--no-as-needed".to_owned());
1536    }
1537    args
1538}
1539
1540/// Where a gcc on this machine keeps `crtbegin.o`, `crtend.o` and `libgcc.a`, newest first.
1541///
1542/// This is not where the C library's files are. A distribution puts them under a directory named
1543/// for the gcc version, and there may be several, so the answer is every one that exists with the
1544/// highest version in front. Newest first because a newer `libgcc` is a superset of an older one
1545/// and because that is the one the C library on the same machine was built against.
1546#[must_use]
1547pub fn runtime_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
1548    let libc = match target.env {
1549        Env::Musl => "musl",
1550        Env::None | Env::Gnu | Env::Msvc => "gnu",
1551    };
1552    let arch = target.arch.as_str();
1553    // The spellings the distributions use for the same triple. Debian and Ubuntu drop the vendor
1554    // field, the source builds and Arch keep `pc`, and Red Hat and SUSE write their own name in
1555    // it, so all of them are looked for and the ones that are there are taken.
1556    let names = [
1557        format!("{arch}-linux-{libc}"),
1558        format!("{arch}-pc-linux-{libc}"),
1559        format!("{arch}-redhat-linux"),
1560        format!("{arch}-suse-linux"),
1561        format!("{arch}-alpine-linux-{libc}"),
1562    ];
1563    let mut found = Vec::new();
1564    for base in ["/usr/lib/gcc", "/usr/lib64/gcc", "/usr/local/lib/gcc"] {
1565        for name in &names {
1566            found.extend(newest_first(&under(sysroot, &format!("{base}/{name}"))));
1567        }
1568    }
1569    found
1570}
1571
1572/// The version directories under one of gcc's, highest version first.
1573fn newest_first(dir: &Path) -> Vec<PathBuf> {
1574    let Ok(entries) = fs::read_dir(dir) else { return Vec::new() };
1575    let mut versions: Vec<(Vec<u64>, PathBuf)> = entries
1576        .flatten()
1577        .map(|e| e.path())
1578        .filter(|p| p.is_dir())
1579        .map(|p| (version_key(&p), p))
1580        .collect();
1581    // Descending, so the highest version is the first place `find_file` looks. Ties keep the order
1582    // the directory gave, which is arbitrary and does not matter because two directories that sort
1583    // the same hold the same version.
1584    versions.sort_by(|a, b| b.0.cmp(&a.0));
1585    versions.into_iter().map(|(_, path)| path).collect()
1586}
1587
1588/// A directory name read as a version, so that `13` sorts above `9` and `10.2` above `10`.
1589///
1590/// A name that is not a version at all sorts below every name that is, rather than being left
1591/// out, because a directory holding a `libgcc.a` is worth looking in whatever it is called.
1592fn version_key(dir: &Path) -> Vec<u64> {
1593    let name = dir.file_name().unwrap_or_default().to_string_lossy();
1594    name.split('.').map(|part| part.parse::<u64>().unwrap_or(0)).collect()
1595}
1596
1597/// Our own runtime library for this target, if it was built.
1598///
1599/// Looked for beside the compiler rather than at a path decided when the compiler was built, for
1600/// the same reason everything else here is looked for: one binary runs wherever it is copied. A
1601/// `-B` prefix is asked first, because that is what a `-B` prefix is for.
1602#[must_use]
1603pub fn builtins_archive(target: Triple, prefixes: &[PathBuf]) -> Option<PathBuf> {
1604    // The name from the crate that puts it on a line, rather than a second spelling of it here,
1605    // which is what that constant asks of anybody who needs the name.
1606    const NAME: &str = rucc_sysroot::link::BUILTINS;
1607    // The four field triple first and then the tuple the sysroots are named by, because `cargo
1608    // xtask builtins --target=aarch64-linux-musl` names its directory after what it was given, and
1609    // that shorter spelling is the one people type.
1610    let spellings = [target.to_string(), target.tuple().to_string()];
1611    let mut places: Vec<PathBuf> = Vec::new();
1612    for prefix in prefixes {
1613        for triple in &spellings {
1614            places.push(prefix.join(triple).join(NAME));
1615        }
1616        places.push(prefix.join(NAME));
1617    }
1618    if let Some(dir) =
1619        std::env::current_exe().ok().and_then(|exe| exe.parent().map(Path::to_path_buf))
1620    {
1621        // A release archive: the compiler at the top of the unpacked directory and the runtime
1622        // beside it in `lib/rucc/<triple>`, which is how `.github/package.sh` lays one out.
1623        for triple in &spellings {
1624            places.push(dir.join("lib").join("rucc").join(triple).join(NAME));
1625        }
1626        if let Some(up) = dir.parent() {
1627            for triple in &spellings {
1628                // An install: the compiler in `bin` and its runtime in `lib/rucc/<triple>`.
1629                places.push(up.join("lib").join("rucc").join(triple).join(NAME));
1630                // A build tree: the compiler in `target/release` and the runtime, which is built
1631                // for the target and not the host, in `target/<triple>/release`.
1632                for profile in ["release", "debug"] {
1633                    places.push(up.join(triple).join(profile).join(NAME));
1634                }
1635            }
1636        }
1637        places.push(dir.join(NAME));
1638    }
1639    places.into_iter().find(|path| path.is_file())
1640}
1641
1642/// Which output format this `ld` should write, in the name `ld` knows it by.
1643fn emulation(target: Triple) -> &'static str {
1644    match target.arch {
1645        Arch::X86_64 => "elf_x86_64",
1646        Arch::Aarch64 => "aarch64linux",
1647        Arch::Riscv64 => "elf64lriscv",
1648    }
1649}
1650
1651/// The program that starts a dynamically linked program, whose path is part of the file.
1652///
1653/// It is a per-target constant rather than something to look for, because the name is fixed by
1654/// the platform's ABI and a program naming a different one does not start.
1655fn loader(target: Triple) -> &'static str {
1656    match (target.arch, target.env) {
1657        (Arch::X86_64, Env::Musl) => "/lib/ld-musl-x86_64.so.1",
1658        (Arch::X86_64, _) => "/lib64/ld-linux-x86-64.so.2",
1659        (Arch::Aarch64, Env::Musl) => "/lib/ld-musl-aarch64.so.1",
1660        (Arch::Aarch64, _) => "/lib/ld-linux-aarch64.so.1",
1661        (Arch::Riscv64, Env::Musl) => "/lib/ld-musl-riscv64.so.1",
1662        (Arch::Riscv64, _) => "/lib/ld-linux-riscv64-lp64d.so.1",
1663    }
1664}
1665
1666/// Where the library's own files might be, in search order.
1667///
1668/// The multiarch directory first for the reason it comes first in the header search: it is where
1669/// a distribution that can hold two architectures at once puts the one being asked for, and a
1670/// distribution that cannot simply does not have it. `lib64` after it, which is what the
1671/// distributions that split by word size use instead, and `lib` last, which is every other one.
1672#[must_use]
1673pub fn candidates(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
1674    let multiarch = multiarch(target);
1675    [
1676        format!("/usr/lib/{multiarch}"),
1677        format!("/lib/{multiarch}"),
1678        "/usr/lib64".to_owned(),
1679        "/lib64".to_owned(),
1680        "/usr/lib".to_owned(),
1681        "/lib".to_owned(),
1682    ]
1683    .into_iter()
1684    .map(|dir| under(sysroot, &dir))
1685    .collect()
1686}
1687
1688/// The name a distribution that holds two architectures at once files this target under.
1689///
1690/// `x86_64-linux-gnu` and its friends, which is what `gcc -print-multiarch` prints and what a
1691/// build system pastes into a path when it is looking for a library itself.
1692#[must_use]
1693pub fn multiarch(target: Triple) -> String {
1694    let libc = match target.env {
1695        Env::Musl => "musl",
1696        Env::None | Env::Gnu | Env::Msvc => "gnu",
1697    };
1698    format!("{}-linux-{libc}", target.arch.as_str())
1699}
1700
1701/// The candidates that are there.
1702fn library_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
1703    candidates(target, sysroot).into_iter().filter(|dir| dir.is_dir()).collect()
1704}
1705
1706/// Where a library is looked for, in the order it is looked for in.
1707///
1708/// The command line first and the target's own after it, which is the order the linker is handed
1709/// and therefore the order `-print-search-dirs` has to print.
1710#[must_use]
1711pub fn search_dirs(link: &LinkOptions, target: Triple) -> Vec<PathBuf> {
1712    let mut dirs = link.search.clone();
1713    // A cross link searches the sysroot's directory and, for a libc that is a stub, the one its
1714    // stubs are written to, so this is those and not the machine's. What `-print-search-dirs` says is what a build
1715    // system pastes into a link line of its own, and an answer that named `/usr/lib` for a target
1716    // whose link line never goes near it would be worse than no answer at all.
1717    if let Some(sysroot) = cross_sysroot(target, link) {
1718        dirs.push(sysroot.lib());
1719        if rucc_sysroot::link::libc(sysroot.target()) == rucc_sysroot::link::Libc::Stub {
1720            dirs.push(sysroot.stubs().to_path_buf());
1721        }
1722        return dirs;
1723    }
1724    if let Some(distro) = distro_cross(target, link) {
1725        dirs.push(distro.lib());
1726        return dirs;
1727    }
1728    dirs.extend(candidates(target, link.sysroot.as_deref()));
1729    dirs
1730}
1731
1732/// The full path of a file with that name, when one of the search directories holds it.
1733///
1734/// What `-print-file-name=` answers. GCC prints the name back unchanged when it finds nothing,
1735/// which is what makes the flag safe to paste into a link line either way.
1736#[must_use]
1737pub fn find_in_search(link: &LinkOptions, target: Triple, name: &str) -> Option<PathBuf> {
1738    find_file(&search_dirs(link, target), name)
1739}
1740
1741/// The first of those directories holding a file of that name.
1742fn find_file(dirs: &[PathBuf], name: &str) -> Option<PathBuf> {
1743    dirs.iter().map(|dir| dir.join(name)).find(|path| path.is_file())
1744}
1745
1746/// A path under the sysroot, when there is one.
1747fn under(sysroot: Option<&Path>, path: &str) -> PathBuf {
1748    match sysroot {
1749        // `strip_prefix` because joining an absolute path replaces the root rather than extending
1750        // it, which would make every entry the unprefixed one.
1751        Some(root) => root.join(path.strip_prefix('/').unwrap_or(path)),
1752        None => PathBuf::from(path),
1753    }
1754}
1755
1756/// A path on the machine that will run the program, rather than on the one compiling it.
1757///
1758/// Written with the separator of the target and not of the host, which matters for the one path
1759/// that is not looked at here but stored in the file and read by something else later: the loader
1760/// a dynamic program names. A Windows host joining it would put a backslash in the middle of a
1761/// name that a Linux loader has to find, and the program would not start.
1762fn target_path(sysroot: Option<&Path>, path: &str) -> String {
1763    match sysroot {
1764        Some(root) => {
1765            let root = root.display().to_string();
1766            format!("{}/{}", root.trim_end_matches(['/', '\\']), path.trim_start_matches('/'))
1767        }
1768        None => path.to_owned(),
1769    }
1770}
1771
1772/// The whole invocation as one line, quoted the way `-###` prints it.
1773#[must_use]
1774pub fn render(linker: &Linker, args: &[String]) -> String {
1775    let mut out = linker.path.display().to_string();
1776    for arg in args {
1777        out.push(' ');
1778        if arg.is_empty() || arg.contains(char::is_whitespace) {
1779            out.push('"');
1780            out.push_str(arg);
1781            out.push('"');
1782        } else {
1783            out.push_str(arg);
1784        }
1785    }
1786    out
1787}
1788
1789/// How long a command line may be on Windows before the arguments go in a file instead.
1790///
1791/// `CreateProcess` takes 32767 characters, the program's own path and the quoting included, and
1792/// what is left for the arguments is not worth computing to the character.
1793const WINDOWS_LINE: usize = 30_000;
1794
1795/// The same for every other host, where the limit is a megabyte or more shared with the
1796/// environment, and a link this long is Kbuild's `-r` of a whole subsystem.
1797const UNIX_LINE: usize = 256 * 1024;
1798
1799/// Whether these arguments, joined with a space and a pair of quotes each, are over `limit`.
1800fn too_long(args: &[String], limit: usize) -> bool {
1801    args.iter().map(|arg| arg.len() + 3).sum::<usize>() > limit
1802}
1803
1804/// The arguments that stay on the command line and the ones that go in the file.
1805///
1806/// `-m` and its value stay, because they are what makes `ld.lld` pick its MinGW driver over its
1807/// ELF one, and it is better not to depend on that choice looking inside the file.
1808fn split_for_file(args: &[String]) -> (Vec<String>, Vec<String>) {
1809    let mut front = Vec::new();
1810    let mut rest = Vec::with_capacity(args.len());
1811    let mut words = args.iter();
1812    while let Some(arg) = words.next() {
1813        if arg == "-m" {
1814            front.push(arg.clone());
1815            front.extend(words.next().cloned());
1816        } else {
1817            rest.push(arg.clone());
1818        }
1819    }
1820    (front, rest)
1821}
1822
1823/// Whether this linker reads a response file with Windows quoting, where a backslash is an
1824/// ordinary character unless it comes before a quote.
1825///
1826/// lld does on a Windows host, both its ELF driver and its MinGW one, and the MinGW one has no
1827/// option to say otherwise. `lld-link` is the same program and reads the same way, and Microsoft's
1828/// `link.exe` only runs on Windows and has never read any other. GNU ld reads the GNU way on every
1829/// host, MSYS2's included.
1830fn windows_quoting(linker: &Linker) -> bool {
1831    let file = linker.path.file_stem().and_then(|stem| stem.to_str()).unwrap_or_default();
1832    let microsoft = linker.name == "link.exe" || file.eq_ignore_ascii_case("link");
1833    cfg!(windows)
1834        && (is_lld(&linker.name)
1835            || file.starts_with("ld.lld")
1836            || file.starts_with("lld")
1837            || microsoft)
1838}
1839
1840/// The words of a response file, one to a line, quoted so that the linker reads them back as
1841/// they were.
1842///
1843/// The GNU way is a backslash before every quote and every backslash. The Windows way leaves a
1844/// backslash alone unless a run of them ends at a quote, and then doubles the run and escapes the
1845/// quote, which is the rule `CommandLineToArgvW` reads with and so the rule LLVM reads with too.
1846fn response_text(args: &[String], windows: bool) -> String {
1847    let mut text = String::new();
1848    for arg in args {
1849        text.push('"');
1850        let mut slashes = 0;
1851        for c in arg.chars() {
1852            match c {
1853                '\\' if windows => slashes += 1,
1854                '"' if windows => {
1855                    text.extend(std::iter::repeat_n('\\', slashes * 2 + 1));
1856                    text.push('"');
1857                    slashes = 0;
1858                }
1859                _ if windows => {
1860                    text.extend(std::iter::repeat_n('\\', slashes));
1861                    text.push(c);
1862                    slashes = 0;
1863                }
1864                '"' | '\\' => {
1865                    text.push('\\');
1866                    text.push(c);
1867                }
1868                _ => text.push(c),
1869            }
1870        }
1871        text.extend(std::iter::repeat_n('\\', slashes * 2));
1872        text.push_str("\"\n");
1873    }
1874    text
1875}
1876
1877/// Runs the linker and waits for it.
1878///
1879/// A line too long for the host goes to the linker as a response file, which is what a Windows
1880/// build of a large program needs: a few hundred objects in a deep directory are past what
1881/// `CreateProcess` takes.
1882///
1883/// # Errors
1884///
1885/// [`Error::Spawn`] when it could not be started, which is a machine problem, and
1886/// [`Error::Refused`] when it ran and said no, which is a program problem and one the linker has
1887/// already explained on its own error output.
1888pub fn run(linker: &Linker, args: &[String]) -> Result<(), Error> {
1889    let spawn = |why: std::io::Error| Error::Spawn {
1890        path: linker.path.display().to_string(),
1891        why: why.to_string(),
1892    };
1893    let mut command = Command::new(&linker.path);
1894    let mut written = None;
1895    if too_long(args, if cfg!(windows) { WINDOWS_LINE } else { UNIX_LINE }) {
1896        let (front, rest) = split_for_file(args);
1897        let path = std::env::temp_dir().join(format!("rucc-link-{}.rsp", std::process::id()));
1898        fs::write(&path, response_text(&rest, windows_quoting(linker))).map_err(spawn)?;
1899        let mut at = OsString::from("@");
1900        at.push(&path);
1901        command.args(front).arg(at);
1902        written = Some(path);
1903    } else {
1904        command.args(args.iter().map(OsString::from));
1905    }
1906    let status = command.status();
1907    if let Some(path) = written {
1908        let _ = fs::remove_file(path);
1909    }
1910    let status = status.map_err(spawn)?;
1911    if status.success() {
1912        return Ok(());
1913    }
1914    // Nothing is added to what the linker printed. It has already named the symbol or the file,
1915    // and a second message from here saying that linking failed would only push the first one
1916    // further up the screen.
1917    Err(Error::Refused {
1918        status: match status.code() {
1919            Some(code) => format!("exited with status {code}"),
1920            None => "was killed before it finished".to_owned(),
1921        },
1922    })
1923}
1924
1925#[cfg(test)]
1926mod tests {
1927    use super::*;
1928
1929    fn linux() -> Triple {
1930        Triple::new(Arch::X86_64, Os::Linux, Env::Gnu)
1931    }
1932
1933    fn one(name: &str) -> Vec<Item> {
1934        vec![Item::File(name.to_owned())]
1935    }
1936
1937    #[test]
1938    fn the_fast_one_is_looked_for_first_and_the_platforms_own_last() {
1939        let names = order(linux(), &LinkOptions::default());
1940        assert_eq!(names.first().map(String::as_str), Some("ld.mold"));
1941        assert_eq!(names.last().map(String::as_str), Some("ld"));
1942    }
1943
1944    #[test]
1945    fn naming_one_is_the_whole_of_the_order() {
1946        let opts = LinkOptions { use_ld: Some("gold".to_owned()), ..LinkOptions::default() };
1947        assert_eq!(order(linux(), &opts), ["ld.gold", "gold"]);
1948    }
1949
1950    #[test]
1951    fn a_dynamic_program_names_the_loader_that_will_start_it() {
1952        let args = line(linux(), &LinkOptions::default(), &one("a.o"), "a.out").expect("a line");
1953        let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
1954        assert!(args[at + 1].ends_with("/lib64/ld-linux-x86-64.so.2"), "{args:?}");
1955    }
1956
1957    /// Kbuild's `built-in.o`, which is objects joined into an object and is linked again later.
1958    #[test]
1959    fn a_relocatable_link_is_the_objects_and_nothing_a_program_needs() {
1960        let opts = LinkOptions { relocatable: true, ..LinkOptions::default() };
1961        let args = line(linux(), &opts, &one("a.o"), "built-in.o").expect("a line");
1962        assert_eq!(args, ["-o", "built-in.o", "-m", "elf_x86_64", "-r", "a.o"]);
1963    }
1964
1965    #[test]
1966    fn a_static_program_names_no_loader_because_nothing_will_start_it() {
1967        let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
1968        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
1969        assert!(args.contains(&"-static".to_owned()), "{args:?}");
1970        assert!(!args.contains(&"-dynamic-linker".to_owned()), "{args:?}");
1971    }
1972
1973    #[test]
1974    fn the_startup_file_of_a_program_that_moves_is_not_the_one_of_a_program_that_does_not() {
1975        let moving = LinkOptions { pie: Some(true), ..LinkOptions::default() };
1976        let fixed = LinkOptions { pie: Some(false), ..LinkOptions::default() };
1977        let named = |opts: &LinkOptions| {
1978            line(linux(), opts, &one("a.o"), "a.out")
1979                .expect("a line")
1980                .iter()
1981                .filter_map(|a| Path::new(a).file_name().map(|n| n.to_string_lossy().into_owned()))
1982                .find(|n| n.ends_with("crt1.o"))
1983        };
1984        // Only when the machine running this has them, which is what makes this two assertions
1985        // rather than one: a machine with no glibc development files has neither to find.
1986        if let Some(name) = named(&moving) {
1987            assert_eq!(name, "Scrt1.o");
1988            assert_eq!(named(&fixed).as_deref(), Some("crt1.o"));
1989        }
1990    }
1991
1992    /// A profiled program is started by a startup file of its own.
1993    ///
1994    /// The counts it keeps have to be started before `main` runs and written out after it returns,
1995    /// and what does both is this file rather than anything the compiler wrote. So a build that
1996    /// compiles with the flag and links without it produces a program that calls the hook on every
1997    /// function and never writes a profile, which is the failure this is here to keep out.
1998    ///
1999    /// A shared object takes none of them either way, since nothing starts one.
2000    #[test]
2001    fn a_profiled_program_is_started_by_the_startup_file_that_counts() {
2002        let profile = LinkOptions { profile: true, ..LinkOptions::default() };
2003        assert_eq!(startfile(&profile, false), Some("gcrt1.o"));
2004        assert_eq!(startfile(&profile, true), Some("gcrt1.o"));
2005        let still = LinkOptions { is_static: true, ..profile.clone() };
2006        assert_eq!(startfile(&still, true), Some("grcrt1.o"));
2007        assert_eq!(startfile(&still, false), Some("gcrt1.o"));
2008        let shared = LinkOptions { shared: true, ..profile };
2009        assert_eq!(startfile(&shared, false), None);
2010    }
2011
2012    /// And a program that is not profiled is started by the one it always was.
2013    #[test]
2014    fn a_program_that_is_not_profiled_is_started_by_the_usual_one() {
2015        let plain = LinkOptions::default();
2016        assert_eq!(startfile(&plain, false), Some("crt1.o"));
2017        assert_eq!(startfile(&plain, true), Some("Scrt1.o"));
2018    }
2019
2020    #[test]
2021    fn asking_for_no_startup_files_leaves_out_both_ends_of_them() {
2022        let opts = LinkOptions { no_startfiles: true, ..LinkOptions::default() };
2023        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
2024        assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
2025        assert!(!args.iter().any(|a| a.ends_with("crtn.o")), "{args:?}");
2026        // And still links against the library, because that is the other flag.
2027        assert!(args.contains(&"-lc".to_owned()), "{args:?}");
2028    }
2029
2030    #[test]
2031    fn asking_for_no_library_at_all_leaves_out_the_startup_files_too() {
2032        let opts = LinkOptions { no_stdlib: true, ..LinkOptions::default() };
2033        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
2034        assert!(!args.contains(&"-lc".to_owned()), "{args:?}");
2035        assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
2036    }
2037
2038    #[test]
2039    fn the_library_comes_after_the_objects_that_need_it() {
2040        let items = vec![Item::File("a.o".to_owned()), Item::Library("m".to_owned())];
2041        let args = line(linux(), &LinkOptions::default(), &items, "a.out").expect("a line");
2042        let obj = args.iter().position(|a| a == "a.o").expect("the object");
2043        let m = args.iter().position(|a| a == "-lm").expect("the library");
2044        let c = args.iter().position(|a| a == "-lc").expect("the library");
2045        assert!(obj < m && m < c, "{args:?}");
2046    }
2047
2048    #[test]
2049    fn what_the_user_told_the_linker_stays_where_the_user_wrote_it() {
2050        // The pair libtool writes around a set of convenience archives, which is what found this.
2051        // Both words are about the files between them, so a line that collects them and puts them
2052        // at the end has two options that do nothing and an archive whose members were all dropped.
2053        let items = vec![
2054            Item::File("a.o".to_owned()),
2055            Item::Linker("--whole-archive".to_owned()),
2056            Item::File("libaesni.a".to_owned()),
2057            Item::Linker("--no-whole-archive".to_owned()),
2058            Item::Library("m".to_owned()),
2059        ];
2060        let args = line(linux(), &LinkOptions::default(), &items, "a.out").expect("a line");
2061        let at = |what: &str| args.iter().position(|a| a == what).expect(what);
2062        assert!(at("a.o") < at("--whole-archive"), "{args:?}");
2063        assert!(at("--whole-archive") < at("libaesni.a"), "{args:?}");
2064        assert!(at("libaesni.a") < at("--no-whole-archive"), "{args:?}");
2065        assert!(at("--no-whole-archive") < at("-lm"), "{args:?}");
2066        assert!(at("-lm") < at("-lc"), "{args:?}");
2067    }
2068
2069    #[test]
2070    fn a_sysroot_moves_every_path_this_decided_and_none_the_user_wrote() {
2071        let opts = LinkOptions {
2072            sysroot: Some(PathBuf::from("/nowhere-at-all")),
2073            search: vec![PathBuf::from("/opt/mine")],
2074            ..LinkOptions::default()
2075        };
2076        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
2077        let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
2078        assert_eq!(args[at + 1], "/nowhere-at-all/lib64/ld-linux-x86-64.so.2");
2079        assert!(args.contains(&"-L/opt/mine".to_owned()), "{args:?}");
2080    }
2081
2082    #[test]
2083    fn a_platform_with_no_link_line_is_said_so_rather_than_linked_wrongly() {
2084        let triple = Triple::new(Arch::X86_64, Os::None, Env::None);
2085        let error = line(triple, &LinkOptions::default(), &one("a.o"), "a.out")
2086            .expect_err("no line for it");
2087        assert!(matches!(error, Error::Target { .. }), "{error:?}");
2088    }
2089
2090    /// A made up SDK with the one file the line reads out of it, so that nothing about this machine
2091    /// decides what the line says.
2092    fn an_sdk(name: &str, settings: Option<&str>) -> PathBuf {
2093        let dir = std::env::temp_dir()
2094            .join(format!("rucc-sdk-{}-{}", std::process::id(), name.replace('.', "-")))
2095            .join(name);
2096        fs::create_dir_all(&dir).expect("a temporary directory");
2097        if let Some(settings) = settings {
2098            fs::write(dir.join("SDKSettings.json"), settings).expect("a settings file");
2099        }
2100        dir
2101    }
2102
2103    fn mac() -> Triple {
2104        Triple::new(Arch::Aarch64, Os::Darwin, Env::None)
2105    }
2106
2107    #[test]
2108    fn a_mac_link_names_the_platform_the_sdk_and_libsystem() {
2109        let sdk = an_sdk(
2110            "MacOSX.sdk",
2111            Some(
2112                "{\"CanonicalName\": \"macosx15.2\", \"Version\" : \"15.2\", \"MaximumDeploymentTarget\": \"15.2.99\"}",
2113            ),
2114        );
2115        let opts = LinkOptions {
2116            sysroot: Some(sdk.clone()),
2117            search: vec![PathBuf::from("/opt/mine")],
2118            no_builtins_lib: true,
2119            ..LinkOptions::default()
2120        };
2121        let items = vec![Item::File("a.o".to_owned()), Item::Library("m".to_owned())];
2122        let args = line(mac(), &opts, &items, "a.out").expect("a line");
2123        let sdk = sdk.display().to_string();
2124        let want: Vec<&str> = vec![
2125            "-arch",
2126            "arm64",
2127            "-platform_version",
2128            "macos",
2129            "11.0",
2130            "15.2",
2131            "-syslibroot",
2132            &sdk,
2133            "-o",
2134            "a.out",
2135            "-L/opt/mine",
2136            "a.o",
2137            "-lm",
2138            "-lSystem",
2139        ];
2140        assert_eq!(args, want);
2141
2142        // The deployment target from the command line, and a shared library.
2143        let opts =
2144            LinkOptions { os_version: Some(rucc_tuple::Version::new(13, 4)), shared: true, ..opts };
2145        let args = line(mac(), &opts, &one("a.o"), "liba.dylib").expect("a line");
2146        assert_eq!(args[3..6], ["macos", "13.4", "15.2"]);
2147        assert!(args.contains(&"-dylib".to_owned()), "{args:?}");
2148        assert!(!args.iter().any(|arg| arg.contains("pie")), "{args:?}");
2149    }
2150
2151    #[test]
2152    fn the_sdk_version_comes_from_its_name_when_it_has_no_settings() {
2153        let sdk = an_sdk("MacOSX14.5.sdk", None);
2154        let opts =
2155            LinkOptions { sysroot: Some(sdk), no_builtins_lib: true, ..LinkOptions::default() };
2156        let args = line(mac(), &opts, &one("a.o"), "a.out").expect("a line");
2157        assert_eq!(args[3..6], ["macos", "11.0", "14.5"]);
2158        // And the deployment target when neither says anything.
2159        let sdk = an_sdk("Somewhere", None);
2160        let opts = LinkOptions {
2161            sysroot: Some(sdk),
2162            os_version: Some(rucc_tuple::Version::major(12)),
2163            ..opts
2164        };
2165        let args = line(mac(), &opts, &one("a.o"), "a.out").expect("a line");
2166        assert_eq!(args[3..6], ["macos", "12", "12"]);
2167    }
2168
2169    #[test]
2170    fn a_mac_link_looks_for_a_mach_o_linker_and_nothing_else() {
2171        assert_eq!(order(mac(), &LinkOptions::default()), ["ld", "ld64.lld"]);
2172    }
2173
2174    #[test]
2175    fn a_static_mac_program_is_refused_and_a_relocatable_one_is_not() {
2176        let sdk = an_sdk("MacOSX.sdk", None);
2177        let opts = LinkOptions { sysroot: Some(sdk), is_static: true, ..LinkOptions::default() };
2178        let error = line(mac(), &opts, &one("a.o"), "a.out").expect_err("no static line");
2179        let Error::Cross { why } = &error else { panic!("{error:?}") };
2180        assert!(why.contains("-static"), "{why}");
2181        let args = relocatable_line(mac(), &LinkOptions::default(), &one("a.o"), "b.o")
2182            .expect("ld64 takes -r");
2183        assert_eq!(args, ["-r", "-arch", "arm64", "-o", "b.o", "a.o"]);
2184    }
2185
2186    #[test]
2187    fn the_line_is_printed_the_way_it_would_be_typed() {
2188        let linker = Linker { name: "ld".to_owned(), path: PathBuf::from("/usr/bin/ld") };
2189        let args = ["-o".to_owned(), "a b".to_owned()];
2190        assert_eq!(render(&linker, &args), "/usr/bin/ld -o \"a b\"");
2191    }
2192
2193    #[test]
2194    fn a_linker_that_is_not_there_is_said_by_name() {
2195        let opts = LinkOptions {
2196            use_ld: Some("a-linker-nobody-has".to_owned()),
2197            ..LinkOptions::default()
2198        };
2199        let error = find(linux(), &opts).expect_err("not on this machine");
2200        assert_eq!(error, Error::Named { name: "a-linker-nobody-has".to_owned() });
2201    }
2202    /// A directory with a `libgcc.a` in it, so a test can say what a machine with a gcc on it
2203    /// looks like without needing one.
2204    fn a_gcc_dir(name: &str) -> PathBuf {
2205        let dir = std::env::temp_dir().join(format!("rucc-link-{name}-{}", std::process::id()));
2206        fs::create_dir_all(&dir).expect("a temporary directory");
2207        fs::write(dir.join("libgcc.a"), b"not really an archive").expect("a file in it");
2208        dir
2209    }
2210
2211    #[test]
2212    fn the_c_library_supplies_the_block_routines_and_our_runtime_does_not_displace_them() {
2213        let gcc = a_gcc_dir("order");
2214        let ours = PathBuf::from("/somewhere/librucc_builtins.a");
2215        let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
2216        let at_libc = args.iter().position(|a| a == "-lc").expect("libc");
2217        let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
2218        // glibc's `memcpy` is assembly per microarchitecture and ours is a word at a time loop,
2219        // so on a target that has one, its is the one that should answer.
2220        assert!(at_libc < at_ours, "{args:?}");
2221    }
2222
2223    #[test]
2224    fn a_static_link_puts_them_in_a_group_because_two_of_them_refer_to_each_other() {
2225        let gcc = a_gcc_dir("group");
2226        let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
2227        let args = runtime_items(&opts, &[gcc], None);
2228        assert_eq!(args.first().map(String::as_str), Some("--start-group"), "{args:?}");
2229        assert_eq!(args.last().map(String::as_str), Some("--end-group"), "{args:?}");
2230        // The unwinder, which is what `libc.a` refers to and what a static link fails on without
2231        // it. Issue #277.
2232        assert!(args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
2233    }
2234
2235    #[test]
2236    fn a_dynamic_link_needs_no_group_and_asks_for_the_shared_half_only_if_something_wants_it() {
2237        let gcc = a_gcc_dir("dynamic");
2238        let args = runtime_items(&LinkOptions::default(), &[gcc], None);
2239        assert!(!args.contains(&"--start-group".to_owned()), "{args:?}");
2240        assert!(!args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
2241        let at = args.iter().position(|a| a == "-lgcc_s").expect("the shared half");
2242        assert_eq!(args[at - 1], "--as-needed", "{args:?}");
2243        assert_eq!(args[at + 1], "--no-as-needed", "{args:?}");
2244    }
2245
2246    #[test]
2247    fn our_own_runtime_comes_before_the_machines_because_the_two_are_interchangeable() {
2248        let gcc = a_gcc_dir("ours");
2249        let ours = PathBuf::from("/somewhere/librucc_builtins.a");
2250        let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
2251        let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
2252        let at_gcc = args.iter().position(|a| a == "-lgcc").expect("libgcc");
2253        assert!(at_ours < at_gcc, "{args:?}");
2254    }
2255
2256    #[test]
2257    fn no_builtins_lib_leaves_ours_off_and_keeps_the_machines() {
2258        let gcc = a_gcc_dir("theirs");
2259        let opts = LinkOptions { no_builtins_lib: true, ..LinkOptions::default() };
2260        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
2261        assert!(!args.iter().any(|a| a.ends_with("librucc_builtins.a")), "{args:?}");
2262        // And the machine's half is still decided the same way it was, from the directories
2263        // that are there, which on the machine running this test may be none.
2264        assert!(runtime_items(&opts, &[gcc], None).contains(&"-lgcc".to_owned()));
2265    }
2266
2267    #[test]
2268    fn nodefaultlibs_leaves_the_whole_runtime_off_and_not_only_the_c_library() {
2269        let gcc = a_gcc_dir("none");
2270        let opts = LinkOptions { no_defaultlibs: true, ..LinkOptions::default() };
2271        assert!(runtime_items(&opts, &[gcc], None).is_empty());
2272    }
2273
2274    #[test]
2275    fn a_machine_with_no_gcc_on_it_gets_no_names_for_libraries_that_are_not_there() {
2276        let empty = std::env::temp_dir().join("rucc-link-empty-not-a-gcc");
2277        let args = runtime_items(&LinkOptions::default(), &[empty], None);
2278        assert_eq!(args, ["-lc"], "{args:?}");
2279    }
2280
2281    #[test]
2282    fn a_gcc_version_directory_is_read_as_a_version_and_not_as_a_word() {
2283        assert!(version_key(Path::new("/usr/lib/gcc/x/13")) > version_key(Path::new("/x/9")));
2284        assert!(version_key(Path::new("/x/10.2")) > version_key(Path::new("/x/10")));
2285        // Something that is not a version at all still sorts, and sorts below one that is.
2286        assert!(version_key(Path::new("/x/snapshot")) < version_key(Path::new("/x/1")));
2287    }
2288
2289    /// A command line that has a cache to find generated sysroots in, which a real one always has.
2290    ///
2291    /// And a `-B` prefix with our runtime in it, because a cross link refuses without one and
2292    /// every machine that does this for real has the archive `cargo xtask builtins` wrote. What
2293    /// happens when it is missing is its own test below.
2294    /// The fast math startup file follows gcc's end file spec: the family puts it in anything
2295    /// that is not a shared object, and `-mdaz-ftz` decides it outright either way.
2296    #[test]
2297    fn the_fast_math_startup_file_is_wanted_where_gccs_spec_wants_it() {
2298        let fast = LinkOptions { fast_math: true, ..LinkOptions::default() };
2299        assert!(!LinkOptions::default().wants_fastmath());
2300        assert!(fast.wants_fastmath());
2301        assert!(!LinkOptions { shared: true, ..fast.clone() }.wants_fastmath());
2302        assert!(!LinkOptions { daz_ftz: Some(false), ..fast.clone() }.wants_fastmath());
2303        let forced = LinkOptions { shared: true, daz_ftz: Some(true), ..LinkOptions::default() };
2304        assert!(forced.wants_fastmath());
2305    }
2306
2307    fn cached() -> LinkOptions {
2308        LinkOptions {
2309            cache: Some(PathBuf::from("/cache")),
2310            prefixes: vec![a_builtins_dir()],
2311            ..LinkOptions::default()
2312        }
2313    }
2314
2315    /// A directory with our runtime archive in it, so that a test can say what a machine where the
2316    /// runtime was built looks like without building one.
2317    ///
2318    /// One directory for every test rather than one each, since none of them writes to it and the
2319    /// name of the file is the whole of what they read.
2320    fn a_builtins_dir() -> PathBuf {
2321        let dir = std::env::temp_dir().join(format!("rucc-link-ours-{}", std::process::id()));
2322        fs::create_dir_all(&dir).expect("a temporary directory");
2323        fs::write(dir.join("librucc_builtins.a"), b"not really an archive").expect("a file in it");
2324        dir
2325    }
2326
2327    /// An archive in a directory named by the short tuple is found, which is where `cargo xtask
2328    /// builtins --target=aarch64-linux-musl` puts it.
2329    #[test]
2330    fn the_runtime_is_found_under_the_tuple_as_well_as_the_triple() {
2331        let dir = std::env::temp_dir().join(format!("rucc-link-tuple-{}", std::process::id()));
2332        let target: Triple = "aarch64-linux-musl".parse().expect("a triple");
2333        let under = dir.join(target.tuple().to_string());
2334        fs::create_dir_all(&under).expect("a temporary directory");
2335        fs::write(under.join("librucc_builtins.a"), b"not really an archive")
2336            .expect("a file in it");
2337        let found = builtins_archive(target, std::slice::from_ref(&dir));
2338        assert_eq!(found, Some(under.join("librucc_builtins.a")));
2339        let _ = fs::remove_dir_all(&dir);
2340    }
2341
2342    /// Where that cache would keep this target's sysroot.
2343    fn a_sysroot(target: Triple) -> Sysroot {
2344        Sysroot::in_cache(Path::new("/cache"), target.tuple())
2345    }
2346
2347    /// A target that is not the machine running this test, whatever machine that is.
2348    ///
2349    /// A freestanding one, because [`Triple::host`] answers Linux, Darwin or Windows and never
2350    /// `Os::None`. Every other triple is somebody's host, so a test that wants the cross path out of
2351    /// [`line`] itself has to use this one and the rest go through [`cross_line`].
2352    fn foreign() -> Triple {
2353        Triple::new(Arch::X86_64, Os::None, Env::None)
2354    }
2355
2356    #[test]
2357    fn a_cross_link_reads_the_targets_own_sysroot_and_nothing_of_this_machine() {
2358        let target = Triple::new(Arch::Aarch64, Os::Linux, Env::Musl);
2359        let sysroot = a_sysroot(target);
2360        // The paths as this host spells them, because what is being checked is which directory the
2361        // files are in and a Windows separator is a backslash.
2362        let root = sysroot.root().display().to_string();
2363        let lib = sysroot.lib();
2364        let args = cross_line(target, &cached(), &one("a.o"), "a.out", &sysroot).expect("a line");
2365        assert!(args.contains(&format!("--sysroot={root}")), "{args:?}");
2366        assert!(args.contains(&format!("-L{}", lib.display())), "{args:?}");
2367        assert!(args.contains(&lib.join("libc.a").display().to_string()), "{args:?}");
2368        let at = args.iter().position(|a| a == "-dynamic-linker").expect("the loader");
2369        assert_eq!(args[at + 1], "/lib/ld-musl-aarch64.so.1", "{args:?}");
2370        // The whole point of the other path not being taken: not one directory of this machine is
2371        // on the line, so the line is the same on every host and the recorded ones describe it.
2372        for arg in &args {
2373            assert!(!arg.contains("/usr/lib"), "{arg} in {args:?}");
2374            assert!(!arg.contains("/lib64"), "{arg} in {args:?}");
2375        }
2376    }
2377
2378    #[test]
2379    fn a_freestanding_target_links_against_our_runtime_instead_of_being_refused() {
2380        let args = line(foreign(), &cached(), &one("a.o"), "a.out").expect("a line");
2381        assert!(args.iter().any(|a| a.ends_with("librucc_builtins.a")), "{args:?}");
2382        // No libc, because there is not one, and no start file either: what runs before `main` on a
2383        // freestanding target comes from whatever is being built.
2384        assert!(!args.iter().any(|a| a.ends_with("libc.a")), "{args:?}");
2385        assert!(!args.contains(&"-lc".to_owned()), "{args:?}");
2386        assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
2387    }
2388
2389    /// And with nothing to find sysroots in it is refused, which is what it was before this.
2390    #[test]
2391    fn a_driver_with_no_cache_to_look_in_says_so_rather_than_guessing() {
2392        let error = line(foreign(), &LinkOptions::default(), &one("a.o"), "a.out")
2393            .expect_err("no line for it");
2394        assert!(matches!(error, Error::Target { .. }), "{error:?}");
2395    }
2396
2397    #[test]
2398    fn a_static_link_against_a_libc_that_is_a_stub_is_refused_rather_than_attempted() {
2399        let target = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
2400        let opts = LinkOptions { is_static: true, ..cached() };
2401        let error = cross_line(target, &opts, &one("a.o"), "a.out", &a_sysroot(target))
2402            .expect_err("there is no libc.a in a stub sysroot");
2403        let Error::Cross { why } = &error else { panic!("{error:?}") };
2404        // Because a stub carries the names a library exports and none of the bodies, which is
2405        // everything a dynamic link reads and nothing a static one does.
2406        assert!(why.contains("stub"), "{why}");
2407    }
2408
2409    #[test]
2410    fn a_target_whose_linker_wants_a_different_line_is_refused_by_name() {
2411        let target = Triple::new(Arch::Aarch64, Os::Darwin, Env::None);
2412        let error = cross_line(target, &cached(), &one("a.o"), "a.out", &a_sysroot(target))
2413            .expect_err("no line for that format");
2414        let Error::Cross { why } = &error else { panic!("{error:?}") };
2415        assert!(why.contains(&target.tuple().to_canonical_string()), "{why}");
2416    }
2417
2418    #[test]
2419    fn an_msvc_link_is_against_the_tree_sysroot_names_and_says_what_to_run_without_one() {
2420        let target = Triple::new(Arch::X86_64, Os::Windows, Env::Msvc);
2421        // Not the cache, even with one to look in, because nothing of ours is ever there for it.
2422        let error = line(target, &cached(), &one("a.obj"), "a.exe").expect_err("no tree named");
2423        let Error::Cross { why } = &error else { panic!("{error:?}") };
2424        assert!(why.contains("rucc --fetch x86_64-windows-msvc"), "{why}");
2425
2426        let tree = PathBuf::from("/trees/msvc");
2427        let named = LinkOptions { sysroot: Some(tree.clone()), ..cached() };
2428        let args = line(target, &named, &one("a.obj"), "a.exe").expect("a line");
2429        let crt = format!("-libpath:{}", tree.join("crt/lib").join("x86_64").display());
2430        assert!(args.contains(&crt), "{args:?}");
2431        assert!(args.contains(&"-out:a.exe".to_owned()), "{args:?}");
2432        assert!(args.contains(&"libcmt.lib".to_owned()), "{args:?}");
2433        assert!(args.iter().any(|arg| arg.ends_with("librucc_builtins.a")), "{args:?}");
2434        let dll = LinkOptions { crt: Crt::Dll, ..named };
2435        let args = line(target, &dll, &one("a.obj"), "a.exe").expect("a line");
2436        assert!(args.contains(&"msvcrt.lib".to_owned()), "{args:?}");
2437        assert!(!args.contains(&"libcmt.lib".to_owned()), "{args:?}");
2438
2439        // And the linker that reads that line, whether or not there is a cache.
2440        assert_eq!(order(target, &cached()).first().map(String::as_str), Some("lld-link"));
2441        assert_eq!(order(target, &LinkOptions::default())[0], "lld-link");
2442    }
2443
2444    #[test]
2445    fn an_msvc_tree_without_the_crt_for_this_architecture_is_said_before_anything_is_compiled() {
2446        let target = Triple::new(Arch::X86_64, Os::Windows, Env::Msvc);
2447        let tree = std::env::temp_dir().join(format!("rucc-msvc-tree-{}", std::process::id()));
2448        fs::create_dir_all(tree.join("crt/lib/aarch64")).expect("a temporary directory");
2449        let named = LinkOptions { sysroot: Some(tree.clone()), ..cached() };
2450        let error = preflight(target, &named).expect_err("a tree for the other architecture");
2451        let Error::Cross { why } = &error else { panic!("{error:?}") };
2452        assert!(why.contains("rucc --fetch x86_64-windows-msvc"), "{why}");
2453        fs::create_dir_all(tree.join("crt/lib/x86_64")).expect("the right one");
2454        preflight(target, &named).expect("a tree for this one");
2455        let _ = fs::remove_dir_all(&tree);
2456    }
2457
2458    #[test]
2459    fn a_mingw_target_links_and_looks_for_a_linker_that_can_write_a_pe_image() {
2460        let target = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2461        let args = cross_line(target, &cached(), &one("a.o"), "a.exe", &a_sysroot(target))
2462            .expect("a line for mingw-w64");
2463        let at = |flag: &str| args.iter().position(|arg| arg == flag).expect(flag);
2464        assert_eq!(args[at("-m") + 1], "i386pep");
2465        assert_eq!(args[at("--subsystem") + 1], "console");
2466        assert!(args.iter().any(|arg| arg.ends_with("libmsvcrt.a")), "{args:?}");
2467        // And the prefixed name a distribution files its mingw binutils under, which is not the
2468        // multiarch one.
2469        let names = cross_order(target);
2470        assert_eq!(names.first().map(String::as_str), Some("ld.lld"));
2471        assert!(names.contains(&"x86_64-w64-mingw32-ld".to_owned()), "{names:?}");
2472    }
2473
2474    #[test]
2475    fn our_runtime_comes_from_beside_the_compiler_rather_than_from_inside_the_sysroot() {
2476        // The two halves of tamnd/rucc#1514. The line used to name it under the sysroot's `lib`,
2477        // where nothing ever put it: it is this compiler's output for the target and a sysroot
2478        // fetched from a release holds the platform's files and not ours. So the path on the line
2479        // is the one the driver found, and the only `librucc_builtins.a` on the line is that one.
2480        let target = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2481        let sysroot = a_sysroot(target);
2482        let opts = cached();
2483        let args = cross_line(target, &opts, &one("a.o"), "a.exe", &sysroot).expect("a line");
2484        let ours: Vec<&String> =
2485            args.iter().filter(|arg| arg.ends_with("librucc_builtins.a")).collect();
2486        assert_eq!(ours.len(), 1, "{args:?}");
2487        assert_eq!(ours[0], &opts.prefixes[0].join("librucc_builtins.a").display().to_string());
2488        assert!(!ours[0].starts_with(&sysroot.lib().display().to_string()), "{args:?}");
2489        // And it is still last, after everything that calls into it.
2490        assert_eq!(args.last(), Some(ours[0]), "{args:?}");
2491    }
2492
2493    #[test]
2494    fn a_cross_link_with_no_runtime_to_find_says_which_command_writes_one() {
2495        // What the linker would say instead is that `___chkstk_ms` is undefined, referenced from
2496        // mingw-w64's own objects, which is tamnd/rucc#1513 and is nobody's first guess at a
2497        // missing archive.
2498        let target = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2499        let opts = LinkOptions { prefixes: Vec::new(), ..cached() };
2500        let error = cross_line(target, &opts, &one("a.o"), "a.exe", &a_sysroot(target))
2501            .expect_err("there is no runtime for it to find");
2502        let Error::Cross { why } = &error else { panic!("{error:?}") };
2503        assert!(why.contains("cargo xtask builtins"), "{why}");
2504        assert!(why.contains("-fno-builtins-lib"), "{why}");
2505
2506        // And that flag is the way through it, for somebody who meant to link without ours.
2507        let without = LinkOptions { no_builtins_lib: true, ..opts };
2508        let args = cross_line(target, &without, &one("a.o"), "a.exe", &a_sysroot(target))
2509            .expect("a line without ours on it");
2510        assert!(!args.iter().any(|arg| arg.ends_with("librucc_builtins.a")), "{args:?}");
2511    }
2512
2513    #[test]
2514    fn the_version_an_lld_prints_is_read_and_nothing_elses_is() {
2515        // What each of these programs actually prints, because the word being in the line is the
2516        // whole of how one is told from another.
2517        assert_eq!(lld_major("LLD 18.1.8 (compatible with GNU linkers)\n"), Some(18));
2518        assert_eq!(lld_major("Ubuntu LLD 18.1.3 (compatible with GNU linkers)\n"), Some(18));
2519        assert_eq!(lld_major("LLD 20.1.2 (compatible with GNU linkers)\n"), Some(20));
2520
2521        // Binutils and mold do not have it, and neither of them has this problem, so the answer
2522        // for both is that this check has nothing to say about them.
2523        assert_eq!(lld_major("GNU ld (GNU Binutils for Ubuntu) 2.42\n"), None);
2524        assert_eq!(lld_major("mold 2.4.1 (compatible with GNU ld)\n"), None);
2525        assert_eq!(lld_major(""), None);
2526    }
2527
2528    /// A program that prints `text` and exits, which is as much of a linker as this check reads.
2529    ///
2530    /// Named after what it says, so that two of them in one test are two files.
2531    #[cfg(unix)]
2532    fn a_linker_that_says(tag: &str, text: &str) -> Linker {
2533        use std::os::unix::fs::PermissionsExt as _;
2534        let dir = std::env::temp_dir().join(format!("rucc-link-ld-{}", std::process::id()));
2535        fs::create_dir_all(&dir).expect("a temporary directory");
2536        let path = dir.join(format!("ld.lld-{tag}"));
2537        fs::write(&path, format!("#!/bin/sh\necho '{text}'\n")).expect("a script");
2538        fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).expect("an executable one");
2539        Linker { name: "ld.lld".to_owned(), path }
2540    }
2541
2542    #[test]
2543    #[cfg(unix)]
2544    fn an_lld_too_old_to_read_exportas_is_refused_for_windows_gnu_and_nowhere_else() {
2545        // The failure this replaces has no diagnostic at all: 18 writes an import by ordinal zero,
2546        // exits successfully, and the program dies at startup under wine. tamnd/rucc#1515.
2547        let windows = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2548        let old = a_linker_that_says("18", "LLD 18.1.8 (compatible with GNU linkers)");
2549        let error = suitable(windows, &old).expect_err("18 cannot link this");
2550        let Error::TooOld { name, found, target } = &error else { panic!("{error:?}") };
2551        assert_eq!((name.as_str(), *found, target.as_str()), ("ld.lld", 18, "x86_64-windows-gnu"));
2552        assert!(error.to_string().contains("IMPORT_NAME_EXPORTAS"), "{error}");
2553
2554        // The same linker for a target whose import libraries have no such records in them, which
2555        // is every other target, since this is one encoding in one format.
2556        let linux = Triple::new(Arch::X86_64, Os::Linux, Env::Musl);
2557        assert_eq!(suitable(linux, &old), Ok(()));
2558
2559        // And the first one that reads them.
2560        let new = a_linker_that_says("19", "LLD 19.1.0 (compatible with GNU linkers)");
2561        assert_eq!(suitable(windows, &new), Ok(()));
2562    }
2563
2564    #[test]
2565    #[cfg(unix)]
2566    fn an_old_lld_first_in_line_is_passed_over_for_a_newer_one_behind_it() {
2567        // Ubuntu 24.04 after `apt install lld-19`: 18 is the one on PATH and 19 is somewhere else.
2568        // Two -B prefixes stand in for the two places, since the search asks them in order too. The
2569        // name is one nothing on this machine is called, so a real lld on PATH does not answer.
2570        use std::os::unix::fs::PermissionsExt as _;
2571        let root = std::env::temp_dir().join(format!("rucc-link-two-lld-{}", std::process::id()));
2572        let mut prefixes = Vec::new();
2573        for (dir, version) in [("old", "18.1.3"), ("new", "19.1.7")] {
2574            let dir = root.join(dir);
2575            fs::create_dir_all(&dir).expect("a temporary directory");
2576            let path = dir.join("ld.rucc-test-lld");
2577            fs::write(&path, format!("#!/bin/sh\necho 'Ubuntu LLD {version}'\n"))
2578                .expect("a script");
2579            fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).expect("executable");
2580            prefixes.push(dir);
2581        }
2582        let windows = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2583        let opts = LinkOptions {
2584            use_ld: Some("rucc-test-lld".to_owned()),
2585            prefixes,
2586            ..LinkOptions::default()
2587        };
2588        let found = find(windows, &opts).expect("the newer one");
2589        assert_eq!(found.path, root.join("new").join("ld.rucc-test-lld"));
2590
2591        // With only the old one there, the answer is the refusal that names it.
2592        let opts = LinkOptions { prefixes: vec![root.join("old")], ..opts };
2593        let error = find(windows, &opts).expect_err("only 18 is here");
2594        assert!(matches!(error, Error::TooOld { found: 18, .. }), "{error:?}");
2595        let _ = fs::remove_dir_all(&root);
2596    }
2597
2598    #[test]
2599    fn a_long_line_goes_in_a_response_file_that_reads_back_as_it_was() {
2600        let args: Vec<String> =
2601            ["-o", r"C:\Users\a b\out.exe", "-m", "i386pep", r#"say "hi""#, "", "plain.o"]
2602                .map(str::to_owned)
2603                .to_vec();
2604        let (front, rest) = split_for_file(&args);
2605        assert_eq!(front, ["-m", "i386pep"]);
2606        assert_eq!(
2607            crate::response_words(&response_text(&rest, false)),
2608            [&args[..2], &args[4..]].concat()
2609        );
2610        // And the Windows way, which leaves the backslashes in a path alone.
2611        assert_eq!(
2612            response_text(&rest, true),
2613            "\"-o\"\n\"C:\\Users\\a b\\out.exe\"\n\"say \\\"hi\\\"\"\n\"\"\n\"plain.o\"\n"
2614        );
2615        assert_eq!(response_text(&[r"C:\dir\".to_owned()], true), "\"C:\\dir\\\\\"\n");
2616        assert!(!too_long(&args, 1000));
2617        assert!(too_long(&vec!["x".repeat(100); 400], WINDOWS_LINE));
2618    }
2619
2620    #[test]
2621    fn a_program_name_is_tried_with_each_pathext_extension_it_does_not_already_have() {
2622        let exts = vec![".com".to_owned(), ".exe".to_owned()];
2623        let dir = Path::new("bin");
2624        assert_eq!(
2625            spellings(&dir.join("ld.lld"), &exts),
2626            [dir.join("ld.lld.com"), dir.join("ld.lld.exe")]
2627        );
2628        assert_eq!(spellings(&dir.join("lld-link.EXE"), &exts), [dir.join("lld-link.EXE")]);
2629        assert_eq!(spellings(&dir.join("ld.lld"), &[]), [dir.join("ld.lld")]);
2630    }
2631
2632    #[test]
2633    #[cfg(unix)]
2634    fn an_lld_off_path_is_found_under_its_own_name() {
2635        use std::os::unix::fs::PermissionsExt as _;
2636        let dir = std::env::temp_dir().join(format!("rucc-link-off-path-{}", std::process::id()));
2637        fs::create_dir_all(&dir).expect("a temporary directory");
2638        let lld = dir.join("ld.lld");
2639        fs::write(&lld, "#!/bin/sh\n").expect("a file");
2640        fs::set_permissions(&lld, fs::Permissions::from_mode(0o755)).expect("permissions");
2641        let found =
2642            linker_candidates("ld.lld", &LinkOptions::default(), std::slice::from_ref(&dir));
2643        let _ = fs::remove_dir_all(&dir);
2644        assert!(found.contains(&lld), "{found:?}");
2645    }
2646
2647    #[test]
2648    fn lld_is_looked_for_where_package_managers_put_it_newest_version_first() {
2649        let root = std::env::temp_dir().join(format!("rucc-link-lld-dirs-{}", std::process::id()));
2650        for dir in ["usr/lib/llvm-18/bin", "usr/lib/llvm-19/bin", "usr/lib/llvm-9/bin", "usr/lib/x"]
2651        {
2652            fs::create_dir_all(root.join(dir)).expect("a temporary directory");
2653        }
2654        let dirs = lld_dirs(&root, Some(PathBuf::from("C:/Program Files")));
2655        let under_lib: Vec<_> =
2656            dirs.iter().filter(|dir| dir.starts_with(root.join("usr/lib"))).collect();
2657        assert_eq!(
2658            under_lib,
2659            [
2660                &root.join("usr/lib/llvm-19/bin"),
2661                &root.join("usr/lib/llvm-18/bin"),
2662                &root.join("usr/lib/llvm-9/bin")
2663            ]
2664        );
2665        assert!(dirs.contains(&root.join("opt/homebrew/opt/lld/bin")), "{dirs:?}");
2666        assert_eq!(dirs.last(), Some(&PathBuf::from("C:/Program Files").join("LLVM").join("bin")));
2667        let _ = fs::remove_dir_all(&root);
2668    }
2669
2670    #[test]
2671    fn no_linker_says_where_to_get_lld_only_when_lld_was_looked_for() {
2672        let cross = Error::NoLinker { tried: vec!["ld.lld".to_owned(), "lld".to_owned()] };
2673        assert!(cross.to_string().contains("lld 19 or newer"), "{cross}");
2674        let native = Error::NoLinker { tried: vec!["ld".to_owned()] };
2675        assert_eq!(native.to_string(), "no linker was found; tried ld");
2676    }
2677
2678    #[test]
2679    #[cfg(unix)]
2680    fn a_linker_that_will_not_say_what_it_is_is_left_alone() {
2681        // Every linker that is not an lld reaches this check too, and what it can establish is
2682        // that a specific old lld is here rather than that anything else is fit. Turning "I did
2683        // not recognise this" into a refusal would break machines this problem never touched.
2684        let windows = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2685        let quiet = a_linker_that_says("gnu", "GNU ld (GNU Binutils for Ubuntu) 2.42");
2686        assert_eq!(suitable(windows, &quiet), Ok(()));
2687
2688        let missing = Linker { name: "ld.lld".to_owned(), path: PathBuf::from("/no/such/linker") };
2689        assert_eq!(suitable(windows, &missing), Ok(()));
2690    }
2691
2692    #[test]
2693    fn profiling_a_cross_link_is_refused_because_the_startup_file_is_compiled_code() {
2694        let target = Triple::new(Arch::X86_64, Os::Linux, Env::Musl);
2695        let opts = LinkOptions { profile: true, ..cached() };
2696        let error = cross_line(target, &opts, &one("a.o"), "a.out", &a_sysroot(target))
2697            .expect_err("there is no gcrt1.o in a generated sysroot");
2698        let Error::Cross { why } = &error else { panic!("{error:?}") };
2699        assert!(why.contains("gcrt1.o"), "{why}");
2700    }
2701
2702    #[test]
2703    fn a_distributions_cross_tree_is_used_when_there_is_no_sysroot_of_ours() {
2704        let usr = std::env::temp_dir().join(format!("rucc-link-usr-{}", std::process::id()));
2705        let root = usr.join("aarch64-linux-gnu");
2706        for dir in ["include", "lib"] {
2707            fs::create_dir_all(root.join(dir)).expect("a scratch tree");
2708        }
2709        for version in ["9", "13"] {
2710            fs::create_dir_all(usr.join("lib/gcc-cross/aarch64-linux-gnu").join(version))
2711                .expect("a scratch gcc");
2712        }
2713        let host = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
2714        let arm = Triple::new(Arch::Aarch64, Os::Linux, Env::Gnu);
2715        let opts = LinkOptions { usr: Some(usr.clone()), ..cached() };
2716        let distro = distro_for(arm, &opts, Some(host)).expect("the packages are there");
2717        assert_eq!(distro.include(), root.join("include"));
2718        assert_eq!(distro.lib(), root.join("lib"));
2719        assert_eq!(
2720            distro.gcc,
2721            [13, 9].map(|v| usr.join("lib/gcc-cross/aarch64-linux-gnu").join(v.to_string()))
2722        );
2723        // And then it is not a link against a sysroot of ours, which is what decides the line.
2724        assert!(cross_for(arm, &opts, Some(host)).is_none());
2725        // The host itself, a target with no tree, a named tree and a pinned release all leave it.
2726        assert!(distro_for(host, &opts, Some(host)).is_none());
2727        let riscv = Triple::new(Arch::Riscv64, Os::Linux, Env::Gnu);
2728        assert!(distro_for(riscv, &opts, Some(host)).is_none());
2729        let named = LinkOptions { sysroot: Some(PathBuf::from("/opt/root")), ..opts.clone() };
2730        assert!(distro_for(arm, &named, Some(host)).is_none());
2731        let pinned = LinkOptions {
2732            pinned: Some("aarch64-linux-gnu.2.28".parse::<TargetTuple>().expect("a release")),
2733            ..opts.clone()
2734        };
2735        assert!(distro_for(arm, &pinned, Some(host)).is_none());
2736        // A sysroot of ours in the cache wins over the packages, because it is the one pinned.
2737        let cache = usr.join("cache");
2738        fs::create_dir_all(Sysroot::in_cache(&cache, arm.tuple()).lib()).expect("a sysroot");
2739        let fetched = LinkOptions { cache: Some(cache), ..opts };
2740        assert!(distro_for(arm, &fetched, Some(host)).is_none());
2741        let _ = fs::remove_dir_all(&usr);
2742    }
2743
2744    #[test]
2745    fn the_host_takes_the_host_line_and_a_tree_the_user_named_takes_it_too() {
2746        let host = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
2747        let other = Triple::new(Arch::Riscv64, Os::Linux, Env::Musl);
2748        assert!(cross_for(host, &cached(), Some(host)).is_none());
2749        assert!(cross_for(other, &cached(), Some(host)).is_some());
2750        // A tree somebody assembled and named is what `--sysroot` has always meant here, and the
2751        // native line prefixes every path it decides with it.
2752        let named = LinkOptions { sysroot: Some(PathBuf::from("/opt/root")), ..cached() };
2753        assert!(cross_for(other, &named, Some(host)).is_none());
2754        // A host this compiler cannot name is a host whose directories it should not be guessing at.
2755        assert!(cross_for(other, &cached(), None).is_some());
2756    }
2757
2758    #[test]
2759    fn a_windows_host_compiles_for_itself_against_the_fetched_tree() {
2760        // There is no `/usr/include` on Windows, so the host line would find no `stdio.h` at all.
2761        let host = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2762        let at =
2763            cross_for(host, &cached(), Some(host)).expect("the cache is the only tree there is");
2764        assert!(at.root().ends_with("x86_64-windows-gnu"), "{:?}", at.root());
2765    }
2766
2767    #[test]
2768    fn a_pinned_release_on_this_machines_own_target_is_a_cross_compile() {
2769        // The case that used to be dropped on the floor. `--target=x86_64-linux-gnu.2.28` on an
2770        // x86-64 glibc machine read that machine's headers and linked that machine's libc, and the
2771        // release reached nothing, so what came out was a binary for whatever release the build
2772        // machine happened to have. A pin is the one thing a person writes to say otherwise.
2773        let host = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
2774        let pinned = LinkOptions {
2775            pinned: Some(
2776                "x86_64-linux-gnu.2.28".parse::<TargetTuple>().expect("a spelling with a release"),
2777            ),
2778            ..cached()
2779        };
2780        let at = cross_for(host, &pinned, Some(host)).expect("a pin is a cross compile");
2781        // And against the release's own directory, because the release is in the cache key: a tree
2782        // produced for 2.28 and a tree produced for 2.44 are two trees and the path has to say which.
2783        assert!(at.root().ends_with("x86_64-linux-gnu.2.28"), "{:?}", at.root());
2784        // The release is the whole of the difference. The same command line without it is this
2785        // machine, which is what every native compile has always been.
2786        let bare = LinkOptions { pinned: None, ..cached() };
2787        assert!(cross_for(host, &bare, Some(host)).is_none());
2788    }
2789
2790    #[test]
2791    fn a_glibc_cross_link_writes_its_stubs_beside_the_sysroot_once() {
2792        let cache = std::env::temp_dir().join(format!("rucc-link-stubs-{}", std::process::id()));
2793        let _ = fs::remove_dir_all(&cache);
2794        let target = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
2795        let pinned = LinkOptions {
2796            cache: Some(cache.clone()),
2797            pinned: Some("x86_64-linux-gnu.2.28".parse().expect("a spelling with a release")),
2798            ..LinkOptions::default()
2799        };
2800        write_stubs(target, &pinned).expect("x86_64 glibc has a description");
2801        let dir = cache.join("stubs").join("x86_64-linux-gnu.2.28");
2802        let libc = dir.join("libc.so");
2803        let bytes = fs::read(&libc).expect("libc.so was written");
2804        assert!(bytes.starts_with(b"\x7fELF"));
2805        assert!(dir.join("libm.so").is_file());
2806        // 2.28 is before the release that emptied libpthread, so there is no empty one to write.
2807        assert!(!dir.join("libpthread.so").exists());
2808        // The second time finds the same bytes and leaves the file alone, which is what keeps a
2809        // linker in another build from ever reading one that is being replaced.
2810        let before = fs::metadata(&libc).and_then(|m| m.modified()).expect("a time");
2811        write_stubs(target, &pinned).expect("again");
2812        let after = fs::metadata(&libc).and_then(|m| m.modified()).expect("a time");
2813        assert_eq!(before, after);
2814        // And nothing for a libc that is not glibc, whose sysroot has a real one in it.
2815        let musl = LinkOptions {
2816            cache: Some(cache.clone()),
2817            pinned: Some("x86_64-linux-musl".parse().expect("musl")),
2818            ..LinkOptions::default()
2819        };
2820        write_stubs(Triple::new(Arch::X86_64, Os::Linux, Env::Musl), &musl).expect("nothing");
2821        assert!(!cache.join("stubs").join("x86_64-linux-musl").exists());
2822        let _ = fs::remove_dir_all(&cache);
2823    }
2824
2825    #[test]
2826    fn what_a_cross_link_searches_is_the_sysroot_and_not_this_machine() {
2827        let dirs = search_dirs(&cached(), foreign());
2828        // One directory, because that is what the line has, and the same one the line has, because
2829        // `-print-search-dirs` is what a build system reads to write a link line of its own.
2830        assert_eq!(dirs.len(), 1, "{dirs:?}");
2831        assert!(dirs[0].starts_with("/cache"), "{dirs:?}");
2832        assert!(dirs[0].ends_with("lib"), "{dirs:?}");
2833        // And what the user wrote still comes first, the way it does on the line itself.
2834        let mine = LinkOptions { search: vec![PathBuf::from("/opt/mine")], ..cached() };
2835        assert_eq!(search_dirs(&mine, foreign())[0], PathBuf::from("/opt/mine"));
2836    }
2837
2838    #[test]
2839    fn gnu_ld_is_not_looked_for_against_the_fetched_mingw_sysroot() {
2840        let windows = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2841        assert_eq!(order(windows, &cached()), ["ld.lld", "lld"]);
2842    }
2843
2844    #[test]
2845    fn the_linker_looked_for_on_a_cross_link_is_one_that_can_cross() {
2846        let names = cross_order(Triple::new(Arch::Aarch64, Os::Linux, Env::Gnu));
2847        assert_eq!(names.first().map(String::as_str), Some("ld.lld"));
2848        assert!(names.contains(&"aarch64-linux-gnu-ld".to_owned()), "{names:?}");
2849        // mold links for the machine it is running on, and so does a distribution's own `ld`, so
2850        // neither is a default here. `-fuse-ld=` is still there for somebody whose is different.
2851        assert!(!names.iter().any(|name| name.contains("mold")), "{names:?}");
2852        assert!(!names.contains(&"ld".to_owned()), "{names:?}");
2853        // And the lookup the driver really does for a target that is not this machine.
2854        assert_eq!(order(foreign(), &cached()), ["ld.lld", "lld"]);
2855    }
2856
2857    #[test]
2858    fn the_four_flags_become_the_five_modes_they_describe() {
2859        let plain = LinkOptions::default();
2860        assert_eq!(mode(&plain), LinkMode::Dynamic);
2861        assert_eq!(
2862            mode(&LinkOptions { pie: Some(false), ..plain.clone() }),
2863            LinkMode::DynamicNoPie
2864        );
2865        assert_eq!(mode(&LinkOptions { is_static: true, ..plain.clone() }), LinkMode::Static);
2866        let both = LinkOptions { is_static: true, pie: Some(true), ..plain.clone() };
2867        assert_eq!(mode(&both), LinkMode::StaticPie);
2868        assert_eq!(mode(&LinkOptions { shared: true, ..plain }), LinkMode::Shared);
2869    }
2870
2871    #[test]
2872    fn a_sysroot_that_has_not_been_built_is_named_before_anything_is_compiled() {
2873        let opts = LinkOptions {
2874            cache: Some(std::env::temp_dir().join("rucc-a-cache-nobody-filled")),
2875            ..LinkOptions::default()
2876        };
2877        let error = preflight(foreign(), &opts).expect_err("nothing has built one");
2878        let Error::Sysroot { dir, pinned, .. } = &error else { panic!("{error:?}") };
2879        assert!(dir.ends_with("x86_64-none"), "{dir}");
2880        // Nothing is pinned for that target, or for any target yet, so the message says that rather
2881        // than naming a command that would not work.
2882        assert!(!pinned, "nothing should be pinned for a bare metal target");
2883        let said = error.to_string();
2884        assert!(said.contains("pins none for it to fetch"), "{said}");
2885    }
2886
2887    /// The other half of the same message, which is what a target this release does pin an artifact
2888    /// for is told. Built by hand rather than through `preflight`, because what is being checked is
2889    /// the message and not which targets `rucc_sysroot::artifact` happens to pin this release.
2890    #[test]
2891    fn a_sysroot_that_could_be_fetched_is_told_what_to_run() {
2892        let said = Error::Sysroot {
2893            target: "x86_64-linux-musl".to_owned(),
2894            dir: "/somewhere/sysroots/x86_64-linux-musl".to_owned(),
2895            pinned: true,
2896        }
2897        .to_string();
2898        assert!(said.contains("`rucc --fetch x86_64-linux-musl`"), "{said}");
2899        // And the other way out of it, because a person who has a tree already does not want a
2900        // download.
2901        assert!(said.contains("--sysroot=<dir>"), "{said}");
2902    }
2903
2904    #[test]
2905    fn a_link_against_this_machine_has_nothing_to_check_before_it_starts() {
2906        // Its directories are looked for as the line is built, and one that is not there is simply
2907        // one that is not offered, so there is no question to answer early.
2908        assert!(preflight(linux(), &LinkOptions::default()).is_ok());
2909    }
2910
2911    #[test]
2912    fn a_runtime_directory_that_is_not_on_this_machine_is_not_offered() {
2913        let dirs = runtime_dirs(linux(), Some(Path::new("/definitely/not/a/sysroot")));
2914        assert!(dirs.is_empty(), "{dirs:?}");
2915    }
2916}