Skip to main content

rucc_driver/
link.rs

1//! Finding a linker and telling it what to link.
2//!
3//! Design: `spec/04-driver-and-cli.md` section 4.9. There is no linker of our own before 1.0, so
4//! this finds one on the machine and builds the command line it wants.
5//!
6//! The linker is invoked directly rather than through the system compiler driver. Going through
7//! `cc` would be shorter to write and would borrow that compiler's idea of where everything is,
8//! and it would also mean this compiler cannot link on a machine that has no other compiler on
9//! it, which is most of the machines a compiler ends up on. It would also make `-###` output a
10//! line that does not say what happens, since the interesting half would be inside the program
11//! being spawned.
12//!
13//! # What is not decided here
14//!
15//! The startup files and the library directories are looked for rather than configured, for the
16//! same reason `library` looks for the headers: gcc settles this when it is built because a gcc
17//! is built for the machine it will run on, and this is one binary that runs wherever it is
18//! copied. So the shape of the answer is a list of candidates per platform of which the ones
19//! that exist are taken, and a cross build says where the rest is with `--sysroot`.
20//!
21//! # The compiler's own runtime
22//!
23//! `crtbegin`, `crtend` and the runtime libraries are found the same way, on the machine rather
24//! than by configuration. Ours is `librucc_builtins.a`, looked for beside the compiler, and the
25//! machine's `libgcc` goes on after it for the parts we have not written, which today is the
26//! unwinder and its personality routine. The C library goes in front of both, so that on a target
27//! that has one its `memcpy` is the one that answers rather than ours. `-fno-builtins-lib` leaves
28//! ours off, for somebody who wants libgcc to answer for everything.
29//!
30//! On a static link the three archives go inside `--start-group`, because `libc.a` refers to the
31//! unwinder and the unwinder refers back to `libc.a`, and a linker walking a list once resolves
32//! whichever of the two it reaches first and leaves the other undefined. That circularity is the
33//! whole reason `-static` failed before this, and it is issue #277.
34//!
35//! # Linking for a machine that is not this one
36//!
37//! Everything above describes a link against the machine running the compiler, and it is what runs
38//! when the target is that machine. A target that is not is a different problem: there is no
39//! `crt1.o` for it in `/usr/lib`, the `libc.so` there is the wrong architecture, and a line built
40//! out of what is lying around either fails at the first input or, worse, links. So a cross link
41//! does not look at this machine at all. It is built by [`rucc_sysroot::argv`] out of the target
42//! and a sysroot under the cache directory, and `spec/cross-compile/11-linking.md` section 11.3 is
43//! the design. [`cross_sysroot`] is the one place that decides which of the two it is.
44//!
45//! Two conditions keep that out of the way of everything that works today. The target has to differ
46//! from the host, and `--sysroot` must not have been given: somebody who assembled a tree and named
47//! it is asking for the line above with their own root in front of every path, which is what a
48//! cross compile with a real distribution tree in it has always been.
49//!
50//! That second condition is also the escape hatch for a machine which has a distribution's own cross
51//! files installed, where `/usr/lib/aarch64-linux-gnu` really does hold an AArch64 `crt1.o`.
52//! `--sysroot=/` takes the line above, and then every directory it decides is that machine's again.
53//!
54//! # What is not here yet
55//!
56//! Darwin, and Windows in Microsoft's ABI. `ld64` wants a platform version load command and a
57//! different set of default libraries, and `lld-link` wants a `/`-style command line and an import
58//! library set out of an SDK nobody may redistribute. Each arrives with the target that needs it,
59//! and a cross link to either is refused by name rather than approximated. A mingw-w64 target does
60//! have a line, because PE in that environment is written in the GNU style and the import libraries
61//! for it are ours to produce.
62//!
63//! The headers are the other half of a cross compile and [`crate::library::header_dirs`] is where
64//! they are decided. It asks [`cross_sysroot`] the same question this file asks it, which is the
65//! point: a compile that took its libc from the sysroot and its declarations from this machine would
66//! be wrong in the quietest way available, and one function answering for both is what stops that
67//! being possible.
68
69use std::ffi::OsString;
70use std::fs;
71use std::path::{Path, PathBuf};
72use std::process::Command;
73
74use rucc_sysroot::layout::{Kernel, Sysroot};
75use rucc_sysroot::{LinkMode, argv};
76use rucc_target::{Arch, Env, Os, Triple};
77
78/// What the command line said about linking.
79///
80/// Kept apart from `Options` because none of it reaches the compilation. A flag here changes what
81/// the linker is told and changes nothing about the object files handed to it, which is why `-lm`
82/// on a `-c` line is a note rather than an error.
83#[derive(Debug, Default, Clone, PartialEq, Eq)]
84pub struct LinkOptions {
85    /// `-fuse-ld=<name>`, which names a linker rather than a path to one.
86    pub use_ld: Option<String>,
87    /// `-L<dir>`, in order, because the linker takes the first library it finds.
88    pub search: Vec<PathBuf>,
89    /// `-Wl,<arg>` and `-Xlinker <arg>`, in order, passed through untouched.
90    pub passthrough: Vec<String>,
91    /// `-B<prefix>`, which is where to look for the linker before looking on the path.
92    pub prefixes: Vec<PathBuf>,
93    /// `--sysroot=<dir>`, which prefixes the directories this looks in.
94    pub sysroot: Option<PathBuf>,
95    /// Where the generated sysroots are, which is [`crate::cache::dir`] on a real command line.
96    ///
97    /// [`None`] is a caller that was not given one, which outside a test is nothing, and then there
98    /// is no cross link line and a foreign target is refused the way it was before there was one.
99    /// It is a field rather than a call inside this module because a link line that read the
100    /// environment could only be tested on a machine whose environment said the right thing.
101    pub cache: Option<PathBuf>,
102    /// `-static`.
103    pub is_static: bool,
104    /// `-shared`.
105    pub shared: bool,
106    /// `-pie` or `-no-pie`, and the platform's default when neither was written.
107    pub pie: Option<bool>,
108    /// `-nostdlib`, which is `-nostartfiles` and `-nodefaultlibs` together.
109    pub no_stdlib: bool,
110    /// `-nostartfiles`.
111    pub no_startfiles: bool,
112    /// `-nodefaultlibs`.
113    pub no_defaultlibs: bool,
114    /// `-rdynamic`, which puts every symbol in the dynamic table so a program can look itself up.
115    pub export_dynamic: bool,
116    /// `-s`, which drops the symbol table.
117    pub strip: bool,
118    /// `-fno-builtins-lib`, which leaves our own runtime off the line so that the machine's
119    /// libgcc answers for everything instead.
120    pub no_builtins_lib: bool,
121    /// `-pg`, which changes the link as well as the code.
122    ///
123    /// The counts a profiled program keeps have to be started before `main` runs and written out
124    /// after it returns, and what does both is a start file of its own. So a build that compiles
125    /// with the flag and links without it produces a program that calls the hook on every function
126    /// and never writes a profile.
127    pub profile: bool,
128}
129
130impl LinkOptions {
131    /// Whether the startup files go on the line.
132    fn wants_startfiles(&self) -> bool {
133        !self.no_stdlib && !self.no_startfiles
134    }
135
136    /// Whether the library the program was written against goes on the line.
137    fn wants_defaultlibs(&self) -> bool {
138        !self.no_stdlib && !self.no_defaultlibs
139    }
140
141    /// Whether the compiler's own runtime goes on the line.
142    ///
143    /// The same switch as the C library, because `-nodefaultlibs` in GCC means the compiler's
144    /// runtime too, and a link that keeps `libgcc` while dropping `libc` is not a thing anyone
145    /// asks for on purpose.
146    fn wants_runtime(&self) -> bool {
147        !self.no_stdlib && !self.no_defaultlibs
148    }
149}
150
151/// One item on the link line, in the order it was written, because link order is semantic.
152///
153/// A library named before the object that needs it is not found on a static link, which is the
154/// oldest surprise in the toolchain and the reason this is one ordered list rather than a list of
155/// files and a list of libraries.
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub enum Item {
158    /// A file: an object this compilation produced, or one named on the command line.
159    File(String),
160    /// `-l<name>`, which the linker resolves against its search path.
161    Library(String),
162}
163
164impl std::fmt::Display for Item {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        match self {
167            Item::File(path) => f.write_str(path),
168            Item::Library(name) => write!(f, "-l{name}"),
169        }
170    }
171}
172
173/// Why a link could not be run.
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub enum Error {
176    /// No linker was found, after looking everywhere there was to look.
177    NoLinker {
178        /// The names that were tried, in the order they were tried.
179        tried: Vec<String>,
180    },
181    /// `-fuse-ld=` named one that is not on this machine.
182    Named {
183        /// What it named.
184        name: String,
185    },
186    /// A target this does not know how to build a link line for.
187    Target {
188        /// The triple that was asked for.
189        triple: String,
190    },
191    /// A cross link this scheme cannot produce, which [`rucc_sysroot::argv`] has explained.
192    ///
193    /// The reason is carried as a sentence rather than as a variant per cause, because the causes
194    /// live in `rucc-sysroot` and a second enumeration here would be a second thing to keep in step
195    /// with them. What this adds is that the sentence came from a link rather than from a
196    /// compilation.
197    Cross {
198        /// Why, in full, ready to print.
199        why: String,
200    },
201    /// The sysroot a cross link needs is not on this machine.
202    Sysroot {
203        /// The target that was asked for.
204        target: String,
205        /// Where its sysroot would be.
206        dir: String,
207    },
208    /// The linker was found and could not be started.
209    Spawn {
210        /// Where it was.
211        path: String,
212        /// What the operating system said.
213        why: String,
214    },
215    /// The linker ran and said no.
216    Refused {
217        /// What it exited with, or a description when it was killed instead.
218        status: String,
219    },
220}
221
222impl std::fmt::Display for Error {
223    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224        match self {
225            Error::NoLinker { tried } => {
226                write!(f, "no linker was found; tried {}", tried.join(", "))
227            }
228            Error::Named { name } => {
229                write!(f, "-fuse-ld={name} asks for a linker that is not on this machine")
230            }
231            Error::Target { triple } => {
232                write!(f, "there is no link line for {triple} in this compiler yet")
233            }
234            Error::Cross { why } => f.write_str(why),
235            Error::Sysroot { target, dir } => write!(
236                f,
237                "there is no sysroot for {target} at {dir}, so there is nothing to link it \
238                 against. Pass --sysroot=<dir> to name a tree you have already, or see \
239                 spec/cross-compile/13-distribution.md section 13.2 for the cache that will hold \
240                 one"
241            ),
242            Error::Spawn { path, why } => write!(f, "could not run the linker at {path}: {why}"),
243            Error::Refused { status } => write!(f, "the linker {status}"),
244        }
245    }
246}
247
248impl std::error::Error for Error {}
249
250/// A linker, found.
251#[derive(Debug, Clone, PartialEq, Eq)]
252pub struct Linker {
253    /// The name it is known by, which is what `--print-config` reports.
254    pub name: String,
255    /// Where it is, which is what gets spawned.
256    pub path: PathBuf,
257}
258
259/// The names to look for, in the order section 4.9 gives.
260///
261/// `mold` first because it is dramatically faster, and a compiler that is twice the speed of
262/// another one while the link takes twelve seconds has not helped anybody. Then `lld`, then the
263/// platform's own. Each is looked for under both the bare name and the `ld.` prefix, because a
264/// distribution installs `mold` under its own name and `ld.mold` for exactly this lookup.
265#[must_use]
266pub fn order(target: Triple, opts: &LinkOptions) -> Vec<String> {
267    if let Some(named) = &opts.use_ld {
268        // A name rather than a path, so `-fuse-ld=mold` finds a `mold` that is not `ld.mold`.
269        return vec![format!("ld.{named}"), named.clone()];
270    }
271    if cross_sysroot(target, opts).is_some() {
272        return cross_order(target);
273    }
274    match target.os {
275        Os::Windows => vec!["lld-link".to_owned(), "link.exe".to_owned()],
276        _ => vec![
277            "ld.mold".to_owned(),
278            "mold".to_owned(),
279            "ld.lld".to_owned(),
280            "lld".to_owned(),
281            "ld".to_owned(),
282        ],
283    }
284}
285
286/// The names to look for when the target is not this machine.
287///
288/// A shorter list than the one above and a different one, because most of that list cannot do this.
289/// `spec/cross-compile/11-linking.md` section 11.2 settles it: `ld.lld` is the ELF cross linker,
290/// since one binary of it links for every architecture it was built with and that is all of them.
291/// mold is off the list because it links for the host and `wild` likewise, which is why section 11.2
292/// has them as `-fuse-ld=` choices for a native link rather than as defaults. The platform's own
293/// `ld` is off it for the same reason: a distribution's `/usr/bin/ld` is built for one architecture,
294/// and `-fuse-ld=` is still there for somebody whose is not.
295///
296/// A cross binutils under its prefixed name is last, because a machine that has
297/// `aarch64-linux-gnu-ld` installed has it on purpose. The prefix is a distribution convention and
298/// there are two of them: a Linux target is filed under its multiarch name and a mingw-w64 one under
299/// `<arch>-w64-mingw32`, which is what every distribution's mingw packages install. `ld.lld` is the
300/// same binary for both, because its MinGW mode is a mode of the one linker rather than a second one.
301fn cross_order(target: Triple) -> Vec<String> {
302    let mut names = vec!["ld.lld".to_owned(), "lld".to_owned()];
303    match (target.os, target.env) {
304        (Os::Linux, _) => names.push(format!("{}-ld", multiarch(target))),
305        (Os::Windows, Env::Gnu) => names.push(format!("{}-w64-mingw32-ld", target.arch.as_str())),
306        _ => {}
307    }
308    names
309}
310
311/// The sysroot a cross link would use, or [`None`] for a link against this machine.
312///
313/// The one place the two paths are told apart, so that the linker that is looked for and the line it
314/// is handed cannot disagree about which kind of link this is.
315///
316/// Three conditions, and two of them are about leaving working configurations alone. A target that
317/// is this machine is linked against this machine, which is what every native compile has always
318/// done and what the directories under `/usr/lib` are for. A `--sysroot` the user wrote is taken as
319/// the root of a tree they assembled, and the line above prefixes every path it decides with it,
320/// which is what cross compiling against a real distribution tree has always meant here. The third
321/// is that there has to be a cache directory to look in, which on a real command line there always
322/// is.
323///
324/// An unknown host counts as different from every target. A machine this compiler cannot name is a
325/// machine whose `/usr/lib` it should not be guessing at.
326#[must_use]
327pub fn cross_sysroot(target: Triple, opts: &LinkOptions) -> Option<Sysroot> {
328    cross_for(target, opts, Triple::host())
329}
330
331/// The same answer with the host as a parameter, so that both branches are testable on one machine.
332fn cross_for(target: Triple, opts: &LinkOptions, host: Option<Triple>) -> Option<Sysroot> {
333    if opts.sysroot.is_some() || host == Some(target) {
334        return None;
335    }
336    let cache = opts.cache.as_deref()?;
337    Some(Sysroot::in_cache(cache, target.tuple()))
338}
339
340/// The kernel headers that go with [`cross_sysroot`], for the targets that have any.
341///
342/// The same three conditions, asked through the same function, because the two halves of one
343/// target's system headers have to be decided together or a compile could read glibc's `sys/stat.h`
344/// against this machine's `asm/stat.h`. A `None` here on a Linux target where the sysroot is `Some`
345/// means only one thing, which is that the cache has no kernel tree for that architecture, and the
346/// directory is still named for the reason [`crate::library::header_dirs`] gives.
347///
348/// Not under the sysroot, because `linux/` and `asm-generic/` are the same nine megabytes for every
349/// target that shares an architecture, and a copy per target is eight copies of one thing.
350#[must_use]
351pub fn cross_kernel(target: Triple, opts: &LinkOptions) -> Option<Kernel> {
352    kernel_for(target, opts, Triple::host())
353}
354
355/// The same answer with the host as a parameter, for the same reason as [`cross_for`].
356fn kernel_for(target: Triple, opts: &LinkOptions, host: Option<Triple>) -> Option<Kernel> {
357    cross_for(target, opts, host)?;
358    Kernel::for_target(opts.cache.as_deref()?, target.tuple())
359}
360
361/// How the result is linked, as the five cases a sysroot link line is written over.
362///
363/// Four booleans reach here and five cases leave, because static and position independent are not
364/// independent of each other and the start file differs in four of the five. The default for `pie`
365/// is the one the native line above uses, so that a command line that says neither gets the same
366/// answer whichever path it takes.
367fn mode(opts: &LinkOptions) -> LinkMode {
368    let pie = opts.pie.unwrap_or(!opts.is_static && !opts.shared);
369    if opts.shared {
370        LinkMode::Shared
371    } else if opts.is_static {
372        if pie { LinkMode::StaticPie } else { LinkMode::Static }
373    } else if pie {
374        LinkMode::Dynamic
375    } else {
376        LinkMode::DynamicNoPie
377    }
378}
379
380/// The line for a machine that is not this one, from the target and the sysroot and nothing else.
381///
382/// Everything this knows is already in `opts`, and all it does is say it in the shape
383/// [`rucc_sysroot::argv`] is written over. There is deliberately no decision here: a second place
384/// that decided what goes on a cross link line would be a second place to get it wrong, and the
385/// recorded lines under `tests/link-lines` would stop describing what this compiler does.
386fn cross_line(
387    target: Triple,
388    opts: &LinkOptions,
389    items: &[Item],
390    output: &str,
391    sysroot: &Sysroot,
392) -> Result<Vec<String>, Error> {
393    if opts.profile {
394        // `gcrt1.o` is a compiled object out of the C library's own sources, and a generated sysroot
395        // has the names a libc exports rather than the bodies behind them. Said here rather than
396        // left to the linker, because what the linker would say is that `main` is undefined.
397        return Err(Error::Cross {
398            why: format!(
399                "-pg needs gcrt1.o, or gcrt2.o on Windows, the startup file that starts and stops \
400                 the counting, and a generated sysroot for {target} does not have one. Profile on \
401                 the host, or pass --sysroot=<dir> naming a tree that has it"
402            ),
403        });
404    }
405    let inputs: Vec<argv::Item> = items
406        .iter()
407        .map(|item| match item {
408            Item::File(path) => argv::Item::File(PathBuf::from(path)),
409            Item::Library(name) => argv::Item::Library(name.clone()),
410        })
411        .collect();
412    let output = PathBuf::from(output);
413    let invocation = argv::Invocation {
414        inputs: &inputs,
415        output: Some(&output),
416        mode: mode(opts),
417        search: &opts.search,
418        passthrough: &opts.passthrough,
419        no_startfiles: !opts.wants_startfiles(),
420        no_defaultlibs: !opts.wants_defaultlibs(),
421        no_builtins_lib: opts.no_builtins_lib,
422        export_dynamic: opts.export_dynamic,
423        strip: opts.strip,
424    };
425    argv::argv(target.tuple(), sysroot, &invocation)
426        .map_err(|why| Error::Cross { why: why.to_string() })
427}
428
429/// Whether this link can be run at all, asked before anything is compiled.
430///
431/// Two questions that have answers before the first object exists: whether there is a line for this
432/// target and mode at all, and whether the sysroot it would read is on the machine. Both are worth a
433/// second at the start rather than a message after a minute of compiling, which is the same reason
434/// the linker itself is looked for first.
435///
436/// The line is built rather than inspected, with no inputs and a name nothing will be written to,
437/// because the refusals belong to the one function that builds it. A link against this machine has
438/// nothing to answer here: its directories are looked for as the line is built and a missing one is
439/// simply a directory that is not offered.
440///
441/// # Errors
442///
443/// [`Error::Cross`] for a target or a mode that has no line, and [`Error::Sysroot`] when the sysroot
444/// it would be linked against is not there.
445pub fn preflight(target: Triple, opts: &LinkOptions) -> Result<(), Error> {
446    let Some(sysroot) = cross_sysroot(target, opts) else { return Ok(()) };
447    cross_line(target, opts, &[], "a.out", &sysroot)?;
448    // The library directory rather than the root, because the root of a cache directory that has
449    // been created and never populated is there and holds nothing. Section 11.6's rule is that
450    // suitable is checked and not assumed, and this is the cheapest form of that.
451    if !sysroot.lib().is_dir() {
452        return Err(Error::Sysroot {
453            target: target.tuple().to_canonical_string(),
454            dir: sysroot.root().display().to_string(),
455        });
456    }
457    Ok(())
458}
459
460/// The linker to use, looked for where a linker is.
461///
462/// `-B` prefixes first, since the point of one is to put a toolchain in front of the machine's,
463/// then the path. A name that contains a separator is a path and is taken as one, which is what
464/// gcc does with `-fuse-ld=/usr/bin/ld.gold` and what a build system relying on that expects.
465///
466/// # Errors
467///
468/// [`Error::Named`] when `-fuse-ld=` asked for one that is not here, and [`Error::NoLinker`] when
469/// nothing was, which name the candidates so that the message says what was looked for.
470pub fn find(target: Triple, opts: &LinkOptions) -> Result<Linker, Error> {
471    let tried = order(target, opts);
472    for name in &tried {
473        if name.contains(std::path::MAIN_SEPARATOR) || name.contains('/') {
474            let path = PathBuf::from(name);
475            if path.is_file() {
476                return Ok(Linker { name: name.clone(), path });
477            }
478            continue;
479        }
480        for dir in &opts.prefixes {
481            let path = dir.join(name);
482            if path.is_file() {
483                return Ok(Linker { name: name.clone(), path });
484            }
485        }
486        if let Some(path) = on_path(name) {
487            return Ok(Linker { name: name.clone(), path });
488        }
489    }
490    match &opts.use_ld {
491        Some(name) => Err(Error::Named { name: name.clone() }),
492        None => Err(Error::NoLinker { tried }),
493    }
494}
495
496/// The first executable of that name on `PATH`.
497///
498/// Executability is checked rather than assumed, because a directory of that name on `PATH` is
499/// not a thing to try to run and neither is a file nobody may execute.
500fn on_path(name: &str) -> Option<PathBuf> {
501    let path = std::env::var_os("PATH")?;
502    std::env::split_paths(&path).map(|dir| dir.join(name)).find(|p| executable(p))
503}
504
505/// Whether a path is a file this process could run.
506#[cfg(unix)]
507fn executable(path: &Path) -> bool {
508    use std::os::unix::fs::PermissionsExt as _;
509    path.metadata().is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
510}
511
512/// Whether a path is a file this process could run.
513///
514/// Windows has no executable bit and decides by extension, and the names looked for above carry
515/// theirs, so being a file is the whole of the question here.
516#[cfg(not(unix))]
517fn executable(path: &Path) -> bool {
518    path.is_file()
519}
520
521/// What the linker is told, in order, not counting the linker itself.
522///
523/// Two lines and [`cross_sysroot`] picks which: the one above for this machine, and
524/// [`rucc_sysroot::argv`]'s for any other. Nothing about the machine is read on the second path, so
525/// `-###` prints the same line on every host and prints it whether the sysroot has been built or
526/// not, which is what makes it worth printing.
527///
528/// # Errors
529///
530/// [`Error::Target`] for a platform there is no native line for yet, which is every one but Linux,
531/// and [`Error::Cross`] for a cross link that cannot be produced at all.
532pub fn line(
533    target: Triple,
534    opts: &LinkOptions,
535    items: &[Item],
536    output: &str,
537) -> Result<Vec<String>, Error> {
538    if let Some(sysroot) = cross_sysroot(target, opts) {
539        return cross_line(target, opts, items, output, &sysroot);
540    }
541    if target.os != Os::Linux {
542        return Err(Error::Target { triple: target.to_string() });
543    }
544    let machine = emulation(target);
545    let root = opts.sysroot.as_deref();
546    let dirs = library_dirs(target, root);
547    // Where a gcc on this machine keeps its own runtime, which is a different place from where
548    // the C library keeps its own, and where our runtime is if it was built for this target.
549    let runtime = runtime_dirs(target, root);
550    let ours = if opts.no_builtins_lib { None } else { builtins_archive(target, &opts.prefixes) };
551    let mut args = vec![
552        "-o".to_owned(),
553        output.to_owned(),
554        // Which of the several formats one `ld` can write is meant. A linker built for more than
555        // one machine guesses from its first input otherwise, and a link of no objects at all has
556        // nothing to guess from.
557        "-m".to_owned(),
558        machine.to_owned(),
559        // The table a program unwinds through, which a C program with no exceptions in it still
560        // needs because `backtrace` and every crash handler read it.
561        "--eh-frame-hdr".to_owned(),
562        // The symbol hash a dynamic loader from this century reads. The old one is still written
563        // alongside by default on some distributions, and asking for this one is what stops a link
564        // from carrying a table nothing has needed since 2006.
565        "--hash-style=gnu".to_owned(),
566    ];
567
568    let pie = opts.pie.unwrap_or(!opts.is_static && !opts.shared);
569    if opts.shared {
570        args.push("-shared".to_owned());
571    } else if opts.is_static {
572        args.push("-static".to_owned());
573    } else if pie {
574        args.push("-pie".to_owned());
575    } else {
576        args.push("-no-pie".to_owned());
577    }
578    if !opts.is_static && !opts.shared {
579        args.push("-dynamic-linker".to_owned());
580        args.push(target_path(root, loader(target)));
581    }
582    if opts.export_dynamic {
583        args.push("--export-dynamic".to_owned());
584    }
585    if opts.strip {
586        args.push("-s".to_owned());
587    }
588
589    if opts.wants_startfiles() {
590        for name in startfile(opts, pie).into_iter().chain(["crti.o"]) {
591            if let Some(path) = find_file(&dirs, name) {
592                args.push(path.display().to_string());
593            }
594        }
595        // The compiler's own startup file, which runs the static constructors. Three spellings
596        // of the same thing, and which one is right is about how the code in it refers to
597        // itself: `S` for a position independent result, `T` for a static one, plain for the
598        // rest. Skipped when there is no gcc on the machine to take it from, because a program
599        // with no constructor in it does not miss it.
600        let begin = if opts.shared || pie {
601            "crtbeginS.o"
602        } else if opts.is_static {
603            "crtbeginT.o"
604        } else {
605            "crtbegin.o"
606        };
607        if let Some(path) = find_file(&runtime, begin).or_else(|| find_file(&runtime, "crtbegin.o"))
608        {
609            args.push(path.display().to_string());
610        }
611    }
612
613    for dir in &opts.search {
614        args.push(format!("-L{}", dir.display()));
615    }
616    for dir in &dirs {
617        args.push(format!("-L{}", dir.display()));
618    }
619    // Where `libgcc.a` and `libgcc_eh.a` are, which is not where the C library is. Nothing is
620    // added when there is no gcc on the machine, and then the `-l` names below are left off too.
621    for dir in &runtime {
622        args.push(format!("-L{}", dir.display()));
623    }
624
625    for item in items {
626        match item {
627            Item::File(path) => args.push(path.clone()),
628            Item::Library(name) => args.push(format!("-l{name}")),
629        }
630    }
631    // After the objects, because a static archive is searched for what is undefined at the point
632    // it is reached and a library named before the object that needs it contributes nothing.
633    args.extend(runtime_items(opts, &runtime, ours.as_deref()));
634
635    if opts.wants_startfiles() {
636        // The other end of `crtbegin`, and it goes before `crtn.o` for the same reason `crti.o`
637        // goes before `crtbegin`: the four are two nested pairs and not four separate files.
638        let end = if opts.shared || pie { "crtendS.o" } else { "crtend.o" };
639        if let Some(path) = find_file(&runtime, end).or_else(|| find_file(&runtime, "crtend.o")) {
640            args.push(path.display().to_string());
641        }
642        if let Some(path) = find_file(&dirs, "crtn.o") {
643            args.push(path.display().to_string());
644        }
645    }
646
647    // Last, so that anything the user said wins over anything decided above, which is what
648    // `-Wl,` is for.
649    args.extend(opts.passthrough.iter().cloned());
650    Ok(args)
651}
652
653/// The startup file the C library brings, or `None` for a link that calls nothing.
654///
655/// This is what calls `main` and what passes it the arguments, so a shared object takes none of
656/// them: nothing starts one and it has no `main` to be started at. `Scrt1.o` rather than `crt1.o`
657/// when the result moves, because the two differ in whether the reference to `main` in them is one
658/// a loader may relocate.
659///
660/// A profiled program gets a different one again, which does all of that and starts and stops the
661/// counting around it. There are two of those rather than three: the one that relocates itself is
662/// only needed by a static position independent link, and every other link takes the plain one,
663/// which is what gcc does with the same flag.
664fn startfile(opts: &LinkOptions, pie: bool) -> Option<&'static str> {
665    if opts.shared {
666        None
667    } else if opts.profile {
668        Some(if pie && opts.is_static { "grcrt1.o" } else { "gcrt1.o" })
669    } else if pie {
670        Some("Scrt1.o")
671    } else {
672        Some("crt1.o")
673    }
674}
675
676/// The libraries the compiler's own runtime contributes, in the order the linker wants them.
677///
678/// The C library first, then ours, then the machine's `libgcc`. Order inside this list is not
679/// about whether a symbol resolves, it is about which archive supplies one that more than one of
680/// them defines, and the two places that happens both have a right answer.
681///
682/// `memcpy` and its three neighbours are in the C library on a hosted target and in ours only for
683/// a freestanding one, which is what `spec/12-abi-and-runtime.md` section 12.8 says they are for.
684/// glibc's are written in assembly per microarchitecture and ours is a word at a time loop, so a
685/// link that took ours over glibc's would be slower at the one routine every program reaches.
686///
687/// The wide arithmetic is in ours and in `libgcc` both, and the two are ABI-identical on purpose,
688/// so which one answers is not a correctness question. Ours comes first because it is ours, and
689/// `-fno-builtins-lib` leaves it off for somebody who would rather it were not.
690///
691/// A static link puts the whole list inside `--start-group`. `libc.a` refers to `_Unwind_Resume`,
692/// and the unwinder refers back into `libc.a`, so a linker walking the list once resolves
693/// whichever it reaches first and reports the other as undefined. That is exactly the failure
694/// issue #277 describes and the group is the fix for it.
695///
696/// A dynamic link needs no group, because the shared `libc` resolves its own references inside
697/// itself. `libgcc_s` is asked for `--as-needed` there, the way gcc asks for it, so a program that
698/// never unwinds does not acquire a dependency on it.
699fn runtime_items(opts: &LinkOptions, runtime: &[PathBuf], ours: Option<&Path>) -> Vec<String> {
700    let mut args = Vec::new();
701    if !opts.wants_defaultlibs() && !opts.wants_runtime() {
702        return args;
703    }
704    // Only when there is a gcc to take them from. On a machine without one the names would be an
705    // error about a library that was never going to be there, and a program that needs neither
706    // the unwinder nor a wide divide links and runs without them.
707    let has_gcc = find_file(runtime, "libgcc.a").is_some();
708
709    if opts.is_static {
710        args.push("--start-group".to_owned());
711    }
712    if opts.wants_defaultlibs() {
713        args.push("-lc".to_owned());
714    }
715    if opts.wants_runtime() {
716        if let Some(path) = ours {
717            args.push(path.display().to_string());
718        }
719        if has_gcc {
720            args.push("-lgcc".to_owned());
721            if opts.is_static {
722                args.push("-lgcc_eh".to_owned());
723            }
724        }
725    }
726    if opts.is_static {
727        args.push("--end-group".to_owned());
728    } else if opts.wants_runtime() && has_gcc {
729        // The shared half, and only if something still wants it after everything above.
730        args.push("--as-needed".to_owned());
731        args.push("-lgcc_s".to_owned());
732        args.push("--no-as-needed".to_owned());
733    }
734    args
735}
736
737/// Where a gcc on this machine keeps `crtbegin.o`, `crtend.o` and `libgcc.a`, newest first.
738///
739/// This is not where the C library's files are. A distribution puts them under a directory named
740/// for the gcc version, and there may be several, so the answer is every one that exists with the
741/// highest version in front. Newest first because a newer `libgcc` is a superset of an older one
742/// and because that is the one the C library on the same machine was built against.
743#[must_use]
744pub fn runtime_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
745    let libc = match target.env {
746        Env::Musl => "musl",
747        Env::None | Env::Gnu | Env::Msvc => "gnu",
748    };
749    let arch = target.arch.as_str();
750    // The spellings the distributions use for the same triple. Debian and Ubuntu drop the vendor
751    // field, the source builds and Arch keep `pc`, and Red Hat and SUSE write their own name in
752    // it, so all of them are looked for and the ones that are there are taken.
753    let names = [
754        format!("{arch}-linux-{libc}"),
755        format!("{arch}-pc-linux-{libc}"),
756        format!("{arch}-redhat-linux"),
757        format!("{arch}-suse-linux"),
758        format!("{arch}-alpine-linux-{libc}"),
759    ];
760    let mut found = Vec::new();
761    for base in ["/usr/lib/gcc", "/usr/lib64/gcc", "/usr/local/lib/gcc"] {
762        for name in &names {
763            let dir = under(sysroot, &format!("{base}/{name}"));
764            let Ok(entries) = fs::read_dir(&dir) else { continue };
765            let mut versions: Vec<(Vec<u64>, PathBuf)> = entries
766                .flatten()
767                .map(|e| e.path())
768                .filter(|p| p.is_dir())
769                .map(|p| (version_key(&p), p))
770                .collect();
771            // Descending, so the highest version is the first place `find_file` looks. Ties keep
772            // the order the directory gave, which is arbitrary and does not matter because two
773            // directories that sort the same hold the same version.
774            versions.sort_by(|a, b| b.0.cmp(&a.0));
775            found.extend(versions.into_iter().map(|(_, path)| path));
776        }
777    }
778    found
779}
780
781/// A directory name read as a version, so that `13` sorts above `9` and `10.2` above `10`.
782///
783/// A name that is not a version at all sorts below every name that is, rather than being left
784/// out, because a directory holding a `libgcc.a` is worth looking in whatever it is called.
785fn version_key(dir: &Path) -> Vec<u64> {
786    let name = dir.file_name().unwrap_or_default().to_string_lossy();
787    name.split('.').map(|part| part.parse::<u64>().unwrap_or(0)).collect()
788}
789
790/// Our own runtime library for this target, if it was built.
791///
792/// Looked for beside the compiler rather than at a path decided when the compiler was built, for
793/// the same reason everything else here is looked for: one binary runs wherever it is copied. A
794/// `-B` prefix is asked first, because that is what a `-B` prefix is for.
795#[must_use]
796pub fn builtins_archive(target: Triple, prefixes: &[PathBuf]) -> Option<PathBuf> {
797    const NAME: &str = "librucc_builtins.a";
798    let triple = target.to_string();
799    let mut places: Vec<PathBuf> = Vec::new();
800    for prefix in prefixes {
801        places.push(prefix.join(&triple).join(NAME));
802        places.push(prefix.join(NAME));
803    }
804    if let Some(dir) =
805        std::env::current_exe().ok().and_then(|exe| exe.parent().map(Path::to_path_buf))
806    {
807        // An install: the compiler in `bin` and its runtime in `lib/rucc/<triple>`.
808        if let Some(up) = dir.parent() {
809            places.push(up.join("lib").join("rucc").join(&triple).join(NAME));
810            // A build tree: the compiler in `target/release` and the runtime, which is built for
811            // the target and not the host, in `target/<triple>/release`.
812            for profile in ["release", "debug"] {
813                places.push(up.join(&triple).join(profile).join(NAME));
814            }
815        }
816        places.push(dir.join(NAME));
817    }
818    places.into_iter().find(|path| path.is_file())
819}
820
821/// Which output format this `ld` should write, in the name `ld` knows it by.
822fn emulation(target: Triple) -> &'static str {
823    match target.arch {
824        Arch::X86_64 => "elf_x86_64",
825        Arch::Aarch64 => "aarch64linux",
826        Arch::Riscv64 => "elf64lriscv",
827    }
828}
829
830/// The program that starts a dynamically linked program, whose path is part of the file.
831///
832/// It is a per-target constant rather than something to look for, because the name is fixed by
833/// the platform's ABI and a program naming a different one does not start.
834fn loader(target: Triple) -> &'static str {
835    match (target.arch, target.env) {
836        (Arch::X86_64, Env::Musl) => "/lib/ld-musl-x86_64.so.1",
837        (Arch::X86_64, _) => "/lib64/ld-linux-x86-64.so.2",
838        (Arch::Aarch64, Env::Musl) => "/lib/ld-musl-aarch64.so.1",
839        (Arch::Aarch64, _) => "/lib/ld-linux-aarch64.so.1",
840        (Arch::Riscv64, Env::Musl) => "/lib/ld-musl-riscv64.so.1",
841        (Arch::Riscv64, _) => "/lib/ld-linux-riscv64-lp64d.so.1",
842    }
843}
844
845/// Where the library's own files might be, in search order.
846///
847/// The multiarch directory first for the reason it comes first in the header search: it is where
848/// a distribution that can hold two architectures at once puts the one being asked for, and a
849/// distribution that cannot simply does not have it. `lib64` after it, which is what the
850/// distributions that split by word size use instead, and `lib` last, which is every other one.
851#[must_use]
852pub fn candidates(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
853    let multiarch = multiarch(target);
854    [
855        format!("/usr/lib/{multiarch}"),
856        format!("/lib/{multiarch}"),
857        "/usr/lib64".to_owned(),
858        "/lib64".to_owned(),
859        "/usr/lib".to_owned(),
860        "/lib".to_owned(),
861    ]
862    .into_iter()
863    .map(|dir| under(sysroot, &dir))
864    .collect()
865}
866
867/// The name a distribution that holds two architectures at once files this target under.
868///
869/// `x86_64-linux-gnu` and its friends, which is what `gcc -print-multiarch` prints and what a
870/// build system pastes into a path when it is looking for a library itself.
871#[must_use]
872pub fn multiarch(target: Triple) -> String {
873    let libc = match target.env {
874        Env::Musl => "musl",
875        Env::None | Env::Gnu | Env::Msvc => "gnu",
876    };
877    format!("{}-linux-{libc}", target.arch.as_str())
878}
879
880/// The candidates that are there.
881fn library_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
882    candidates(target, sysroot).into_iter().filter(|dir| dir.is_dir()).collect()
883}
884
885/// Where a library is looked for, in the order it is looked for in.
886///
887/// The command line first and the target's own after it, which is the order the linker is handed
888/// and therefore the order `-print-search-dirs` has to print.
889#[must_use]
890pub fn search_dirs(link: &LinkOptions, target: Triple) -> Vec<PathBuf> {
891    let mut dirs = link.search.clone();
892    // A cross link searches one directory and it is the sysroot's, so this is that and not the
893    // machine's. What `-print-search-dirs` says is what a build system pastes into a link line of its
894    // own, and an answer that named `/usr/lib` for a target whose link line never goes near it would
895    // be worse than no answer at all.
896    if let Some(sysroot) = cross_sysroot(target, link) {
897        dirs.push(sysroot.lib());
898        return dirs;
899    }
900    dirs.extend(candidates(target, link.sysroot.as_deref()));
901    dirs
902}
903
904/// The full path of a file with that name, when one of the search directories holds it.
905///
906/// What `-print-file-name=` answers. GCC prints the name back unchanged when it finds nothing,
907/// which is what makes the flag safe to paste into a link line either way.
908#[must_use]
909pub fn find_in_search(link: &LinkOptions, target: Triple, name: &str) -> Option<PathBuf> {
910    find_file(&search_dirs(link, target), name)
911}
912
913/// The first of those directories holding a file of that name.
914fn find_file(dirs: &[PathBuf], name: &str) -> Option<PathBuf> {
915    dirs.iter().map(|dir| dir.join(name)).find(|path| path.is_file())
916}
917
918/// A path under the sysroot, when there is one.
919fn under(sysroot: Option<&Path>, path: &str) -> PathBuf {
920    match sysroot {
921        // `strip_prefix` because joining an absolute path replaces the root rather than extending
922        // it, which would make every entry the unprefixed one.
923        Some(root) => root.join(path.strip_prefix('/').unwrap_or(path)),
924        None => PathBuf::from(path),
925    }
926}
927
928/// A path on the machine that will run the program, rather than on the one compiling it.
929///
930/// Written with the separator of the target and not of the host, which matters for the one path
931/// that is not looked at here but stored in the file and read by something else later: the loader
932/// a dynamic program names. A Windows host joining it would put a backslash in the middle of a
933/// name that a Linux loader has to find, and the program would not start.
934fn target_path(sysroot: Option<&Path>, path: &str) -> String {
935    match sysroot {
936        Some(root) => {
937            let root = root.display().to_string();
938            format!("{}/{}", root.trim_end_matches(['/', '\\']), path.trim_start_matches('/'))
939        }
940        None => path.to_owned(),
941    }
942}
943
944/// The whole invocation as one line, quoted the way `-###` prints it.
945#[must_use]
946pub fn render(linker: &Linker, args: &[String]) -> String {
947    let mut out = linker.path.display().to_string();
948    for arg in args {
949        out.push(' ');
950        if arg.is_empty() || arg.contains(char::is_whitespace) {
951            out.push('"');
952            out.push_str(arg);
953            out.push('"');
954        } else {
955            out.push_str(arg);
956        }
957    }
958    out
959}
960
961/// Runs the linker and waits for it.
962///
963/// # Errors
964///
965/// [`Error::Spawn`] when it could not be started, which is a machine problem, and
966/// [`Error::Refused`] when it ran and said no, which is a program problem and one the linker has
967/// already explained on its own error output.
968pub fn run(linker: &Linker, args: &[String]) -> Result<(), Error> {
969    let args: Vec<OsString> = args.iter().map(OsString::from).collect();
970    let status = Command::new(&linker.path).args(&args).status().map_err(|why| Error::Spawn {
971        path: linker.path.display().to_string(),
972        why: why.to_string(),
973    })?;
974    if status.success() {
975        return Ok(());
976    }
977    // Nothing is added to what the linker printed. It has already named the symbol or the file,
978    // and a second message from here saying that linking failed would only push the first one
979    // further up the screen.
980    Err(Error::Refused {
981        status: match status.code() {
982            Some(code) => format!("exited with status {code}"),
983            None => "was killed before it finished".to_owned(),
984        },
985    })
986}
987
988#[cfg(test)]
989mod tests {
990    use super::*;
991
992    fn linux() -> Triple {
993        Triple::new(Arch::X86_64, Os::Linux, Env::Gnu)
994    }
995
996    fn one(name: &str) -> Vec<Item> {
997        vec![Item::File(name.to_owned())]
998    }
999
1000    #[test]
1001    fn the_fast_one_is_looked_for_first_and_the_platforms_own_last() {
1002        let names = order(linux(), &LinkOptions::default());
1003        assert_eq!(names.first().map(String::as_str), Some("ld.mold"));
1004        assert_eq!(names.last().map(String::as_str), Some("ld"));
1005    }
1006
1007    #[test]
1008    fn naming_one_is_the_whole_of_the_order() {
1009        let opts = LinkOptions { use_ld: Some("gold".to_owned()), ..LinkOptions::default() };
1010        assert_eq!(order(linux(), &opts), ["ld.gold", "gold"]);
1011    }
1012
1013    #[test]
1014    fn a_dynamic_program_names_the_loader_that_will_start_it() {
1015        let args = line(linux(), &LinkOptions::default(), &one("a.o"), "a.out").expect("a line");
1016        let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
1017        assert!(args[at + 1].ends_with("/lib64/ld-linux-x86-64.so.2"), "{args:?}");
1018    }
1019
1020    #[test]
1021    fn a_static_program_names_no_loader_because_nothing_will_start_it() {
1022        let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
1023        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
1024        assert!(args.contains(&"-static".to_owned()), "{args:?}");
1025        assert!(!args.contains(&"-dynamic-linker".to_owned()), "{args:?}");
1026    }
1027
1028    #[test]
1029    fn the_startup_file_of_a_program_that_moves_is_not_the_one_of_a_program_that_does_not() {
1030        let moving = LinkOptions { pie: Some(true), ..LinkOptions::default() };
1031        let fixed = LinkOptions { pie: Some(false), ..LinkOptions::default() };
1032        let named = |opts: &LinkOptions| {
1033            line(linux(), opts, &one("a.o"), "a.out")
1034                .expect("a line")
1035                .iter()
1036                .filter_map(|a| Path::new(a).file_name().map(|n| n.to_string_lossy().into_owned()))
1037                .find(|n| n.ends_with("crt1.o"))
1038        };
1039        // Only when the machine running this has them, which is what makes this two assertions
1040        // rather than one: a machine with no glibc development files has neither to find.
1041        if let Some(name) = named(&moving) {
1042            assert_eq!(name, "Scrt1.o");
1043            assert_eq!(named(&fixed).as_deref(), Some("crt1.o"));
1044        }
1045    }
1046
1047    /// A profiled program is started by a startup file of its own.
1048    ///
1049    /// The counts it keeps have to be started before `main` runs and written out after it returns,
1050    /// and what does both is this file rather than anything the compiler wrote. So a build that
1051    /// compiles with the flag and links without it produces a program that calls the hook on every
1052    /// function and never writes a profile, which is the failure this is here to keep out.
1053    ///
1054    /// A shared object takes none of them either way, since nothing starts one.
1055    #[test]
1056    fn a_profiled_program_is_started_by_the_startup_file_that_counts() {
1057        let profile = LinkOptions { profile: true, ..LinkOptions::default() };
1058        assert_eq!(startfile(&profile, false), Some("gcrt1.o"));
1059        assert_eq!(startfile(&profile, true), Some("gcrt1.o"));
1060        let still = LinkOptions { is_static: true, ..profile.clone() };
1061        assert_eq!(startfile(&still, true), Some("grcrt1.o"));
1062        assert_eq!(startfile(&still, false), Some("gcrt1.o"));
1063        let shared = LinkOptions { shared: true, ..profile };
1064        assert_eq!(startfile(&shared, false), None);
1065    }
1066
1067    /// And a program that is not profiled is started by the one it always was.
1068    #[test]
1069    fn a_program_that_is_not_profiled_is_started_by_the_usual_one() {
1070        let plain = LinkOptions::default();
1071        assert_eq!(startfile(&plain, false), Some("crt1.o"));
1072        assert_eq!(startfile(&plain, true), Some("Scrt1.o"));
1073    }
1074
1075    #[test]
1076    fn asking_for_no_startup_files_leaves_out_both_ends_of_them() {
1077        let opts = LinkOptions { no_startfiles: true, ..LinkOptions::default() };
1078        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
1079        assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
1080        assert!(!args.iter().any(|a| a.ends_with("crtn.o")), "{args:?}");
1081        // And still links against the library, because that is the other flag.
1082        assert!(args.contains(&"-lc".to_owned()), "{args:?}");
1083    }
1084
1085    #[test]
1086    fn asking_for_no_library_at_all_leaves_out_the_startup_files_too() {
1087        let opts = LinkOptions { no_stdlib: true, ..LinkOptions::default() };
1088        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
1089        assert!(!args.contains(&"-lc".to_owned()), "{args:?}");
1090        assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
1091    }
1092
1093    #[test]
1094    fn the_library_comes_after_the_objects_that_need_it() {
1095        let items = vec![Item::File("a.o".to_owned()), Item::Library("m".to_owned())];
1096        let args = line(linux(), &LinkOptions::default(), &items, "a.out").expect("a line");
1097        let obj = args.iter().position(|a| a == "a.o").expect("the object");
1098        let m = args.iter().position(|a| a == "-lm").expect("the library");
1099        let c = args.iter().position(|a| a == "-lc").expect("the library");
1100        assert!(obj < m && m < c, "{args:?}");
1101    }
1102
1103    #[test]
1104    fn what_the_user_told_the_linker_comes_after_what_this_told_it() {
1105        let opts = LinkOptions {
1106            passthrough: vec!["--no-eh-frame-hdr".to_owned()],
1107            ..LinkOptions::default()
1108        };
1109        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
1110        assert_eq!(args.last().map(String::as_str), Some("--no-eh-frame-hdr"));
1111    }
1112
1113    #[test]
1114    fn a_sysroot_moves_every_path_this_decided_and_none_the_user_wrote() {
1115        let opts = LinkOptions {
1116            sysroot: Some(PathBuf::from("/nowhere-at-all")),
1117            search: vec![PathBuf::from("/opt/mine")],
1118            ..LinkOptions::default()
1119        };
1120        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
1121        let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
1122        assert_eq!(args[at + 1], "/nowhere-at-all/lib64/ld-linux-x86-64.so.2");
1123        assert!(args.contains(&"-L/opt/mine".to_owned()), "{args:?}");
1124    }
1125
1126    #[test]
1127    fn a_platform_with_no_link_line_is_said_so_rather_than_linked_wrongly() {
1128        for triple in [
1129            Triple::new(Arch::X86_64, Os::Darwin, Env::Gnu),
1130            Triple::new(Arch::X86_64, Os::Windows, Env::Msvc),
1131        ] {
1132            let error = line(triple, &LinkOptions::default(), &one("a.o"), "a.out")
1133                .expect_err("no line for it");
1134            assert!(matches!(error, Error::Target { .. }), "{error:?}");
1135        }
1136    }
1137
1138    #[test]
1139    fn the_line_is_printed_the_way_it_would_be_typed() {
1140        let linker = Linker { name: "ld".to_owned(), path: PathBuf::from("/usr/bin/ld") };
1141        let args = ["-o".to_owned(), "a b".to_owned()];
1142        assert_eq!(render(&linker, &args), "/usr/bin/ld -o \"a b\"");
1143    }
1144
1145    #[test]
1146    fn a_linker_that_is_not_there_is_said_by_name() {
1147        let opts = LinkOptions {
1148            use_ld: Some("a-linker-nobody-has".to_owned()),
1149            ..LinkOptions::default()
1150        };
1151        let error = find(linux(), &opts).expect_err("not on this machine");
1152        assert_eq!(error, Error::Named { name: "a-linker-nobody-has".to_owned() });
1153    }
1154    /// A directory with a `libgcc.a` in it, so a test can say what a machine with a gcc on it
1155    /// looks like without needing one.
1156    fn a_gcc_dir(name: &str) -> PathBuf {
1157        let dir = std::env::temp_dir().join(format!("rucc-link-{name}-{}", std::process::id()));
1158        fs::create_dir_all(&dir).expect("a temporary directory");
1159        fs::write(dir.join("libgcc.a"), b"not really an archive").expect("a file in it");
1160        dir
1161    }
1162
1163    #[test]
1164    fn the_c_library_supplies_the_block_routines_and_our_runtime_does_not_displace_them() {
1165        let gcc = a_gcc_dir("order");
1166        let ours = PathBuf::from("/somewhere/librucc_builtins.a");
1167        let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
1168        let at_libc = args.iter().position(|a| a == "-lc").expect("libc");
1169        let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
1170        // glibc's `memcpy` is assembly per microarchitecture and ours is a word at a time loop,
1171        // so on a target that has one, its is the one that should answer.
1172        assert!(at_libc < at_ours, "{args:?}");
1173    }
1174
1175    #[test]
1176    fn a_static_link_puts_them_in_a_group_because_two_of_them_refer_to_each_other() {
1177        let gcc = a_gcc_dir("group");
1178        let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
1179        let args = runtime_items(&opts, &[gcc], None);
1180        assert_eq!(args.first().map(String::as_str), Some("--start-group"), "{args:?}");
1181        assert_eq!(args.last().map(String::as_str), Some("--end-group"), "{args:?}");
1182        // The unwinder, which is what `libc.a` refers to and what a static link fails on without
1183        // it. Issue #277.
1184        assert!(args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
1185    }
1186
1187    #[test]
1188    fn a_dynamic_link_needs_no_group_and_asks_for_the_shared_half_only_if_something_wants_it() {
1189        let gcc = a_gcc_dir("dynamic");
1190        let args = runtime_items(&LinkOptions::default(), &[gcc], None);
1191        assert!(!args.contains(&"--start-group".to_owned()), "{args:?}");
1192        assert!(!args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
1193        let at = args.iter().position(|a| a == "-lgcc_s").expect("the shared half");
1194        assert_eq!(args[at - 1], "--as-needed", "{args:?}");
1195        assert_eq!(args[at + 1], "--no-as-needed", "{args:?}");
1196    }
1197
1198    #[test]
1199    fn our_own_runtime_comes_before_the_machines_because_the_two_are_interchangeable() {
1200        let gcc = a_gcc_dir("ours");
1201        let ours = PathBuf::from("/somewhere/librucc_builtins.a");
1202        let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
1203        let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
1204        let at_gcc = args.iter().position(|a| a == "-lgcc").expect("libgcc");
1205        assert!(at_ours < at_gcc, "{args:?}");
1206    }
1207
1208    #[test]
1209    fn no_builtins_lib_leaves_ours_off_and_keeps_the_machines() {
1210        let gcc = a_gcc_dir("theirs");
1211        let opts = LinkOptions { no_builtins_lib: true, ..LinkOptions::default() };
1212        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
1213        assert!(!args.iter().any(|a| a.ends_with("librucc_builtins.a")), "{args:?}");
1214        // And the machine's half is still decided the same way it was, from the directories
1215        // that are there, which on the machine running this test may be none.
1216        assert!(runtime_items(&opts, &[gcc], None).contains(&"-lgcc".to_owned()));
1217    }
1218
1219    #[test]
1220    fn nodefaultlibs_leaves_the_whole_runtime_off_and_not_only_the_c_library() {
1221        let gcc = a_gcc_dir("none");
1222        let opts = LinkOptions { no_defaultlibs: true, ..LinkOptions::default() };
1223        assert!(runtime_items(&opts, &[gcc], None).is_empty());
1224    }
1225
1226    #[test]
1227    fn a_machine_with_no_gcc_on_it_gets_no_names_for_libraries_that_are_not_there() {
1228        let empty = std::env::temp_dir().join("rucc-link-empty-not-a-gcc");
1229        let args = runtime_items(&LinkOptions::default(), &[empty], None);
1230        assert_eq!(args, ["-lc"], "{args:?}");
1231    }
1232
1233    #[test]
1234    fn a_gcc_version_directory_is_read_as_a_version_and_not_as_a_word() {
1235        assert!(version_key(Path::new("/usr/lib/gcc/x/13")) > version_key(Path::new("/x/9")));
1236        assert!(version_key(Path::new("/x/10.2")) > version_key(Path::new("/x/10")));
1237        // Something that is not a version at all still sorts, and sorts below one that is.
1238        assert!(version_key(Path::new("/x/snapshot")) < version_key(Path::new("/x/1")));
1239    }
1240
1241    /// A command line that has a cache to find generated sysroots in, which a real one always has.
1242    fn cached() -> LinkOptions {
1243        LinkOptions { cache: Some(PathBuf::from("/cache")), ..LinkOptions::default() }
1244    }
1245
1246    /// Where that cache would keep this target's sysroot.
1247    fn a_sysroot(target: Triple) -> Sysroot {
1248        Sysroot::in_cache(Path::new("/cache"), target.tuple())
1249    }
1250
1251    /// A target that is not the machine running this test, whatever machine that is.
1252    ///
1253    /// A freestanding one, because [`Triple::host`] answers Linux, Darwin or Windows and never
1254    /// `Os::None`. Every other triple is somebody's host, so a test that wants the cross path out of
1255    /// [`line`] itself has to use this one and the rest go through [`cross_line`].
1256    fn foreign() -> Triple {
1257        Triple::new(Arch::X86_64, Os::None, Env::None)
1258    }
1259
1260    #[test]
1261    fn a_cross_link_reads_the_targets_own_sysroot_and_nothing_of_this_machine() {
1262        let target = Triple::new(Arch::Aarch64, Os::Linux, Env::Musl);
1263        let sysroot = a_sysroot(target);
1264        // The paths as this host spells them, because what is being checked is which directory the
1265        // files are in and a Windows separator is a backslash.
1266        let root = sysroot.root().display().to_string();
1267        let lib = sysroot.lib();
1268        let args = cross_line(target, &cached(), &one("a.o"), "a.out", &sysroot).expect("a line");
1269        assert!(args.contains(&format!("--sysroot={root}")), "{args:?}");
1270        assert!(args.contains(&format!("-L{}", lib.display())), "{args:?}");
1271        assert!(args.contains(&lib.join("libc.a").display().to_string()), "{args:?}");
1272        let at = args.iter().position(|a| a == "-dynamic-linker").expect("the loader");
1273        assert_eq!(args[at + 1], "/lib/ld-musl-aarch64.so.1", "{args:?}");
1274        // The whole point of the other path not being taken: not one directory of this machine is
1275        // on the line, so the line is the same on every host and the recorded ones describe it.
1276        for arg in &args {
1277            assert!(!arg.contains("/usr/lib"), "{arg} in {args:?}");
1278            assert!(!arg.contains("/lib64"), "{arg} in {args:?}");
1279        }
1280    }
1281
1282    #[test]
1283    fn a_freestanding_target_links_against_our_runtime_instead_of_being_refused() {
1284        let args = line(foreign(), &cached(), &one("a.o"), "a.out").expect("a line");
1285        assert!(args.iter().any(|a| a.ends_with("librucc_builtins.a")), "{args:?}");
1286        // No libc, because there is not one, and no start file either: what runs before `main` on a
1287        // freestanding target comes from whatever is being built.
1288        assert!(!args.iter().any(|a| a.ends_with("libc.a")), "{args:?}");
1289        assert!(!args.contains(&"-lc".to_owned()), "{args:?}");
1290        assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
1291    }
1292
1293    /// And with nothing to find sysroots in it is refused, which is what it was before this.
1294    #[test]
1295    fn a_driver_with_no_cache_to_look_in_says_so_rather_than_guessing() {
1296        let error = line(foreign(), &LinkOptions::default(), &one("a.o"), "a.out")
1297            .expect_err("no line for it");
1298        assert!(matches!(error, Error::Target { .. }), "{error:?}");
1299    }
1300
1301    #[test]
1302    fn a_static_link_against_a_libc_that_is_a_stub_is_refused_rather_than_attempted() {
1303        let target = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
1304        let opts = LinkOptions { is_static: true, ..cached() };
1305        let error = cross_line(target, &opts, &one("a.o"), "a.out", &a_sysroot(target))
1306            .expect_err("there is no libc.a in a stub sysroot");
1307        let Error::Cross { why } = &error else { panic!("{error:?}") };
1308        // Because a stub carries the names a library exports and none of the bodies, which is
1309        // everything a dynamic link reads and nothing a static one does.
1310        assert!(why.contains("stub"), "{why}");
1311    }
1312
1313    #[test]
1314    fn a_target_whose_linker_wants_a_different_line_is_refused_by_name() {
1315        for target in [
1316            Triple::new(Arch::Aarch64, Os::Darwin, Env::None),
1317            Triple::new(Arch::X86_64, Os::Windows, Env::Msvc),
1318        ] {
1319            let error = cross_line(target, &cached(), &one("a.o"), "a.out", &a_sysroot(target))
1320                .expect_err("no line for that format");
1321            let Error::Cross { why } = &error else { panic!("{error:?}") };
1322            assert!(why.contains(&target.tuple().to_canonical_string()), "{why}");
1323        }
1324    }
1325
1326    #[test]
1327    fn a_mingw_target_links_and_looks_for_a_linker_that_can_write_a_pe_image() {
1328        let target = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
1329        let args = cross_line(target, &cached(), &one("a.o"), "a.exe", &a_sysroot(target))
1330            .expect("a line for mingw-w64");
1331        let at = |flag: &str| args.iter().position(|arg| arg == flag).expect(flag);
1332        assert_eq!(args[at("-m") + 1], "i386pep");
1333        assert_eq!(args[at("--subsystem") + 1], "console");
1334        assert!(args.iter().any(|arg| arg.ends_with("libmsvcrt.a")), "{args:?}");
1335        // And the prefixed name a distribution files its mingw binutils under, which is not the
1336        // multiarch one.
1337        let names = cross_order(target);
1338        assert_eq!(names.first().map(String::as_str), Some("ld.lld"));
1339        assert!(names.contains(&"x86_64-w64-mingw32-ld".to_owned()), "{names:?}");
1340    }
1341
1342    #[test]
1343    fn profiling_a_cross_link_is_refused_because_the_startup_file_is_compiled_code() {
1344        let target = Triple::new(Arch::X86_64, Os::Linux, Env::Musl);
1345        let opts = LinkOptions { profile: true, ..cached() };
1346        let error = cross_line(target, &opts, &one("a.o"), "a.out", &a_sysroot(target))
1347            .expect_err("there is no gcrt1.o in a generated sysroot");
1348        let Error::Cross { why } = &error else { panic!("{error:?}") };
1349        assert!(why.contains("gcrt1.o"), "{why}");
1350    }
1351
1352    #[test]
1353    fn the_host_takes_the_host_line_and_a_tree_the_user_named_takes_it_too() {
1354        let host = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
1355        let other = Triple::new(Arch::Riscv64, Os::Linux, Env::Musl);
1356        assert!(cross_for(host, &cached(), Some(host)).is_none());
1357        assert!(cross_for(other, &cached(), Some(host)).is_some());
1358        // A tree somebody assembled and named is what `--sysroot` has always meant here, and the
1359        // native line prefixes every path it decides with it.
1360        let named = LinkOptions { sysroot: Some(PathBuf::from("/opt/root")), ..cached() };
1361        assert!(cross_for(other, &named, Some(host)).is_none());
1362        // A host this compiler cannot name is a host whose directories it should not be guessing at.
1363        assert!(cross_for(other, &cached(), None).is_some());
1364    }
1365
1366    #[test]
1367    fn what_a_cross_link_searches_is_the_sysroot_and_not_this_machine() {
1368        let dirs = search_dirs(&cached(), foreign());
1369        // One directory, because that is what the line has, and the same one the line has, because
1370        // `-print-search-dirs` is what a build system reads to write a link line of its own.
1371        assert_eq!(dirs.len(), 1, "{dirs:?}");
1372        assert!(dirs[0].starts_with("/cache"), "{dirs:?}");
1373        assert!(dirs[0].ends_with("lib"), "{dirs:?}");
1374        // And what the user wrote still comes first, the way it does on the line itself.
1375        let mine = LinkOptions { search: vec![PathBuf::from("/opt/mine")], ..cached() };
1376        assert_eq!(search_dirs(&mine, foreign())[0], PathBuf::from("/opt/mine"));
1377    }
1378
1379    #[test]
1380    fn the_linker_looked_for_on_a_cross_link_is_one_that_can_cross() {
1381        let names = cross_order(Triple::new(Arch::Aarch64, Os::Linux, Env::Gnu));
1382        assert_eq!(names.first().map(String::as_str), Some("ld.lld"));
1383        assert!(names.contains(&"aarch64-linux-gnu-ld".to_owned()), "{names:?}");
1384        // mold links for the machine it is running on, and so does a distribution's own `ld`, so
1385        // neither is a default here. `-fuse-ld=` is still there for somebody whose is different.
1386        assert!(!names.iter().any(|name| name.contains("mold")), "{names:?}");
1387        assert!(!names.contains(&"ld".to_owned()), "{names:?}");
1388        // And the lookup the driver really does for a target that is not this machine.
1389        assert_eq!(order(foreign(), &cached()), ["ld.lld", "lld"]);
1390    }
1391
1392    #[test]
1393    fn the_four_flags_become_the_five_modes_they_describe() {
1394        let plain = LinkOptions::default();
1395        assert_eq!(mode(&plain), LinkMode::Dynamic);
1396        assert_eq!(
1397            mode(&LinkOptions { pie: Some(false), ..plain.clone() }),
1398            LinkMode::DynamicNoPie
1399        );
1400        assert_eq!(mode(&LinkOptions { is_static: true, ..plain.clone() }), LinkMode::Static);
1401        let both = LinkOptions { is_static: true, pie: Some(true), ..plain.clone() };
1402        assert_eq!(mode(&both), LinkMode::StaticPie);
1403        assert_eq!(mode(&LinkOptions { shared: true, ..plain }), LinkMode::Shared);
1404    }
1405
1406    #[test]
1407    fn a_sysroot_that_has_not_been_built_is_named_before_anything_is_compiled() {
1408        let opts = LinkOptions {
1409            cache: Some(std::env::temp_dir().join("rucc-a-cache-nobody-filled")),
1410            ..LinkOptions::default()
1411        };
1412        let error = preflight(foreign(), &opts).expect_err("nothing has built one");
1413        let Error::Sysroot { dir, .. } = &error else { panic!("{error:?}") };
1414        assert!(dir.ends_with("x86_64-none"), "{dir}");
1415    }
1416
1417    #[test]
1418    fn a_link_against_this_machine_has_nothing_to_check_before_it_starts() {
1419        // Its directories are looked for as the line is built, and one that is not there is simply
1420        // one that is not offered, so there is no question to answer early.
1421        assert!(preflight(linux(), &LinkOptions::default()).is_ok());
1422    }
1423
1424    #[test]
1425    fn a_runtime_directory_that_is_not_on_this_machine_is_not_offered() {
1426        let dirs = runtime_dirs(linux(), Some(Path::new("/definitely/not/a/sysroot")));
1427        assert!(dirs.is_empty(), "{dirs:?}");
1428    }
1429}