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//! # What is not here yet
36//!
37//! Darwin and Windows. `ld64` wants a different line, a platform version load command and a
38//! different set of default libraries, and `link.exe` wants another one again. Each arrives with
39//! the target that needs it.
40
41use std::ffi::OsString;
42use std::fs;
43use std::path::{Path, PathBuf};
44use std::process::Command;
45
46use rucc_target::{Arch, Env, Os, Triple};
47
48/// What the command line said about linking.
49///
50/// Kept apart from `Options` because none of it reaches the compilation. A flag here changes what
51/// the linker is told and changes nothing about the object files handed to it, which is why `-lm`
52/// on a `-c` line is a note rather than an error.
53#[derive(Debug, Default, Clone, PartialEq, Eq)]
54pub struct LinkOptions {
55    /// `-fuse-ld=<name>`, which names a linker rather than a path to one.
56    pub use_ld: Option<String>,
57    /// `-L<dir>`, in order, because the linker takes the first library it finds.
58    pub search: Vec<PathBuf>,
59    /// `-Wl,<arg>` and `-Xlinker <arg>`, in order, passed through untouched.
60    pub passthrough: Vec<String>,
61    /// `-B<prefix>`, which is where to look for the linker before looking on the path.
62    pub prefixes: Vec<PathBuf>,
63    /// `--sysroot=<dir>`, which prefixes the directories this looks in.
64    pub sysroot: Option<PathBuf>,
65    /// `-static`.
66    pub is_static: bool,
67    /// `-shared`.
68    pub shared: bool,
69    /// `-pie` or `-no-pie`, and the platform's default when neither was written.
70    pub pie: Option<bool>,
71    /// `-nostdlib`, which is `-nostartfiles` and `-nodefaultlibs` together.
72    pub no_stdlib: bool,
73    /// `-nostartfiles`.
74    pub no_startfiles: bool,
75    /// `-nodefaultlibs`.
76    pub no_defaultlibs: bool,
77    /// `-rdynamic`, which puts every symbol in the dynamic table so a program can look itself up.
78    pub export_dynamic: bool,
79    /// `-s`, which drops the symbol table.
80    pub strip: bool,
81    /// `-fno-builtins-lib`, which leaves our own runtime off the line so that the machine's
82    /// libgcc answers for everything instead.
83    pub no_builtins_lib: bool,
84}
85
86impl LinkOptions {
87    /// Whether the startup files go on the line.
88    fn wants_startfiles(&self) -> bool {
89        !self.no_stdlib && !self.no_startfiles
90    }
91
92    /// Whether the library the program was written against goes on the line.
93    fn wants_defaultlibs(&self) -> bool {
94        !self.no_stdlib && !self.no_defaultlibs
95    }
96
97    /// Whether the compiler's own runtime goes on the line.
98    ///
99    /// The same switch as the C library, because `-nodefaultlibs` in GCC means the compiler's
100    /// runtime too, and a link that keeps `libgcc` while dropping `libc` is not a thing anyone
101    /// asks for on purpose.
102    fn wants_runtime(&self) -> bool {
103        !self.no_stdlib && !self.no_defaultlibs
104    }
105}
106
107/// One item on the link line, in the order it was written, because link order is semantic.
108///
109/// A library named before the object that needs it is not found on a static link, which is the
110/// oldest surprise in the toolchain and the reason this is one ordered list rather than a list of
111/// files and a list of libraries.
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub enum Item {
114    /// A file: an object this compilation produced, or one named on the command line.
115    File(String),
116    /// `-l<name>`, which the linker resolves against its search path.
117    Library(String),
118}
119
120impl std::fmt::Display for Item {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        match self {
123            Item::File(path) => f.write_str(path),
124            Item::Library(name) => write!(f, "-l{name}"),
125        }
126    }
127}
128
129/// Why a link could not be run.
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub enum Error {
132    /// No linker was found, after looking everywhere there was to look.
133    NoLinker {
134        /// The names that were tried, in the order they were tried.
135        tried: Vec<String>,
136    },
137    /// `-fuse-ld=` named one that is not on this machine.
138    Named {
139        /// What it named.
140        name: String,
141    },
142    /// A target this does not know how to build a link line for.
143    Target {
144        /// The triple that was asked for.
145        triple: String,
146    },
147    /// The linker was found and could not be started.
148    Spawn {
149        /// Where it was.
150        path: String,
151        /// What the operating system said.
152        why: String,
153    },
154    /// The linker ran and said no.
155    Refused {
156        /// What it exited with, or a description when it was killed instead.
157        status: String,
158    },
159}
160
161impl std::fmt::Display for Error {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        match self {
164            Error::NoLinker { tried } => {
165                write!(f, "no linker was found; tried {}", tried.join(", "))
166            }
167            Error::Named { name } => {
168                write!(f, "-fuse-ld={name} asks for a linker that is not on this machine")
169            }
170            Error::Target { triple } => {
171                write!(f, "there is no link line for {triple} in this compiler yet")
172            }
173            Error::Spawn { path, why } => write!(f, "could not run the linker at {path}: {why}"),
174            Error::Refused { status } => write!(f, "the linker {status}"),
175        }
176    }
177}
178
179impl std::error::Error for Error {}
180
181/// A linker, found.
182#[derive(Debug, Clone, PartialEq, Eq)]
183pub struct Linker {
184    /// The name it is known by, which is what `--print-config` reports.
185    pub name: String,
186    /// Where it is, which is what gets spawned.
187    pub path: PathBuf,
188}
189
190/// The names to look for, in the order section 4.9 gives.
191///
192/// `mold` first because it is dramatically faster, and a compiler that is twice the speed of
193/// another one while the link takes twelve seconds has not helped anybody. Then `lld`, then the
194/// platform's own. Each is looked for under both the bare name and the `ld.` prefix, because a
195/// distribution installs `mold` under its own name and `ld.mold` for exactly this lookup.
196#[must_use]
197pub fn order(target: Triple, opts: &LinkOptions) -> Vec<String> {
198    if let Some(named) = &opts.use_ld {
199        // A name rather than a path, so `-fuse-ld=mold` finds a `mold` that is not `ld.mold`.
200        return vec![format!("ld.{named}"), named.clone()];
201    }
202    match target.os {
203        Os::Windows => vec!["lld-link".to_owned(), "link.exe".to_owned()],
204        _ => vec![
205            "ld.mold".to_owned(),
206            "mold".to_owned(),
207            "ld.lld".to_owned(),
208            "lld".to_owned(),
209            "ld".to_owned(),
210        ],
211    }
212}
213
214/// The linker to use, looked for where a linker is.
215///
216/// `-B` prefixes first, since the point of one is to put a toolchain in front of the machine's,
217/// then the path. A name that contains a separator is a path and is taken as one, which is what
218/// gcc does with `-fuse-ld=/usr/bin/ld.gold` and what a build system relying on that expects.
219///
220/// # Errors
221///
222/// [`Error::Named`] when `-fuse-ld=` asked for one that is not here, and [`Error::NoLinker`] when
223/// nothing was, which name the candidates so that the message says what was looked for.
224pub fn find(target: Triple, opts: &LinkOptions) -> Result<Linker, Error> {
225    let tried = order(target, opts);
226    for name in &tried {
227        if name.contains(std::path::MAIN_SEPARATOR) || name.contains('/') {
228            let path = PathBuf::from(name);
229            if path.is_file() {
230                return Ok(Linker { name: name.clone(), path });
231            }
232            continue;
233        }
234        for dir in &opts.prefixes {
235            let path = dir.join(name);
236            if path.is_file() {
237                return Ok(Linker { name: name.clone(), path });
238            }
239        }
240        if let Some(path) = on_path(name) {
241            return Ok(Linker { name: name.clone(), path });
242        }
243    }
244    match &opts.use_ld {
245        Some(name) => Err(Error::Named { name: name.clone() }),
246        None => Err(Error::NoLinker { tried }),
247    }
248}
249
250/// The first executable of that name on `PATH`.
251///
252/// Executability is checked rather than assumed, because a directory of that name on `PATH` is
253/// not a thing to try to run and neither is a file nobody may execute.
254fn on_path(name: &str) -> Option<PathBuf> {
255    let path = std::env::var_os("PATH")?;
256    std::env::split_paths(&path).map(|dir| dir.join(name)).find(|p| executable(p))
257}
258
259/// Whether a path is a file this process could run.
260#[cfg(unix)]
261fn executable(path: &Path) -> bool {
262    use std::os::unix::fs::PermissionsExt as _;
263    path.metadata().is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
264}
265
266/// Whether a path is a file this process could run.
267///
268/// Windows has no executable bit and decides by extension, and the names looked for above carry
269/// theirs, so being a file is the whole of the question here.
270#[cfg(not(unix))]
271fn executable(path: &Path) -> bool {
272    path.is_file()
273}
274
275/// What the linker is told, in order, not counting the linker itself.
276///
277/// # Errors
278///
279/// [`Error::Target`] for a platform there is no line for yet, which is every one but Linux.
280pub fn line(
281    target: Triple,
282    opts: &LinkOptions,
283    items: &[Item],
284    output: &str,
285) -> Result<Vec<String>, Error> {
286    if target.os != Os::Linux {
287        return Err(Error::Target { triple: target.to_string() });
288    }
289    let machine = emulation(target);
290    let root = opts.sysroot.as_deref();
291    let dirs = library_dirs(target, root);
292    // Where a gcc on this machine keeps its own runtime, which is a different place from where
293    // the C library keeps its own, and where our runtime is if it was built for this target.
294    let runtime = runtime_dirs(target, root);
295    let ours = if opts.no_builtins_lib { None } else { builtins_archive(target, &opts.prefixes) };
296    let mut args = vec![
297        "-o".to_owned(),
298        output.to_owned(),
299        // Which of the several formats one `ld` can write is meant. A linker built for more than
300        // one machine guesses from its first input otherwise, and a link of no objects at all has
301        // nothing to guess from.
302        "-m".to_owned(),
303        machine.to_owned(),
304        // The table a program unwinds through, which a C program with no exceptions in it still
305        // needs because `backtrace` and every crash handler read it.
306        "--eh-frame-hdr".to_owned(),
307        // The symbol hash a dynamic loader from this century reads. The old one is still written
308        // alongside by default on some distributions, and asking for this one is what stops a link
309        // from carrying a table nothing has needed since 2006.
310        "--hash-style=gnu".to_owned(),
311    ];
312
313    let pie = opts.pie.unwrap_or(!opts.is_static && !opts.shared);
314    if opts.shared {
315        args.push("-shared".to_owned());
316    } else if opts.is_static {
317        args.push("-static".to_owned());
318    } else if pie {
319        args.push("-pie".to_owned());
320    } else {
321        args.push("-no-pie".to_owned());
322    }
323    if !opts.is_static && !opts.shared {
324        args.push("-dynamic-linker".to_owned());
325        args.push(target_path(root, loader(target)));
326    }
327    if opts.export_dynamic {
328        args.push("--export-dynamic".to_owned());
329    }
330    if opts.strip {
331        args.push("-s".to_owned());
332    }
333
334    // The startup file the C library brings, which is what calls `main` and what passes it the
335    // arguments. `Scrt1.o` rather than `crt1.o` when the result moves, because the two differ in
336    // whether the reference to `main` in them is one a loader may relocate.
337    if opts.wants_startfiles() {
338        let first = if opts.shared {
339            None
340        } else if pie {
341            Some("Scrt1.o")
342        } else {
343            Some("crt1.o")
344        };
345        for name in first.into_iter().chain(["crti.o"]) {
346            if let Some(path) = find_file(&dirs, name) {
347                args.push(path.display().to_string());
348            }
349        }
350        // The compiler's own startup file, which runs the static constructors. Three spellings
351        // of the same thing, and which one is right is about how the code in it refers to
352        // itself: `S` for a position independent result, `T` for a static one, plain for the
353        // rest. Skipped when there is no gcc on the machine to take it from, because a program
354        // with no constructor in it does not miss it.
355        let begin = if opts.shared || pie {
356            "crtbeginS.o"
357        } else if opts.is_static {
358            "crtbeginT.o"
359        } else {
360            "crtbegin.o"
361        };
362        if let Some(path) = find_file(&runtime, begin).or_else(|| find_file(&runtime, "crtbegin.o"))
363        {
364            args.push(path.display().to_string());
365        }
366    }
367
368    for dir in &opts.search {
369        args.push(format!("-L{}", dir.display()));
370    }
371    for dir in &dirs {
372        args.push(format!("-L{}", dir.display()));
373    }
374    // Where `libgcc.a` and `libgcc_eh.a` are, which is not where the C library is. Nothing is
375    // added when there is no gcc on the machine, and then the `-l` names below are left off too.
376    for dir in &runtime {
377        args.push(format!("-L{}", dir.display()));
378    }
379
380    for item in items {
381        match item {
382            Item::File(path) => args.push(path.clone()),
383            Item::Library(name) => args.push(format!("-l{name}")),
384        }
385    }
386    // After the objects, because a static archive is searched for what is undefined at the point
387    // it is reached and a library named before the object that needs it contributes nothing.
388    args.extend(runtime_items(opts, &runtime, ours.as_deref()));
389
390    if opts.wants_startfiles() {
391        // The other end of `crtbegin`, and it goes before `crtn.o` for the same reason `crti.o`
392        // goes before `crtbegin`: the four are two nested pairs and not four separate files.
393        let end = if opts.shared || pie { "crtendS.o" } else { "crtend.o" };
394        if let Some(path) = find_file(&runtime, end).or_else(|| find_file(&runtime, "crtend.o")) {
395            args.push(path.display().to_string());
396        }
397        if let Some(path) = find_file(&dirs, "crtn.o") {
398            args.push(path.display().to_string());
399        }
400    }
401
402    // Last, so that anything the user said wins over anything decided above, which is what
403    // `-Wl,` is for.
404    args.extend(opts.passthrough.iter().cloned());
405    Ok(args)
406}
407
408/// The libraries the compiler's own runtime contributes, in the order the linker wants them.
409///
410/// The C library first, then ours, then the machine's `libgcc`. Order inside this list is not
411/// about whether a symbol resolves, it is about which archive supplies one that more than one of
412/// them defines, and the two places that happens both have a right answer.
413///
414/// `memcpy` and its three neighbours are in the C library on a hosted target and in ours only for
415/// a freestanding one, which is what `spec/12-abi-and-runtime.md` section 12.8 says they are for.
416/// glibc's are written in assembly per microarchitecture and ours is a word at a time loop, so a
417/// link that took ours over glibc's would be slower at the one routine every program reaches.
418///
419/// The wide arithmetic is in ours and in `libgcc` both, and the two are ABI-identical on purpose,
420/// so which one answers is not a correctness question. Ours comes first because it is ours, and
421/// `-fno-builtins-lib` leaves it off for somebody who would rather it were not.
422///
423/// A static link puts the whole list inside `--start-group`. `libc.a` refers to `_Unwind_Resume`,
424/// and the unwinder refers back into `libc.a`, so a linker walking the list once resolves
425/// whichever it reaches first and reports the other as undefined. That is exactly the failure
426/// issue #277 describes and the group is the fix for it.
427///
428/// A dynamic link needs no group, because the shared `libc` resolves its own references inside
429/// itself. `libgcc_s` is asked for `--as-needed` there, the way gcc asks for it, so a program that
430/// never unwinds does not acquire a dependency on it.
431fn runtime_items(opts: &LinkOptions, runtime: &[PathBuf], ours: Option<&Path>) -> Vec<String> {
432    let mut args = Vec::new();
433    if !opts.wants_defaultlibs() && !opts.wants_runtime() {
434        return args;
435    }
436    // Only when there is a gcc to take them from. On a machine without one the names would be an
437    // error about a library that was never going to be there, and a program that needs neither
438    // the unwinder nor a wide divide links and runs without them.
439    let has_gcc = find_file(runtime, "libgcc.a").is_some();
440
441    if opts.is_static {
442        args.push("--start-group".to_owned());
443    }
444    if opts.wants_defaultlibs() {
445        args.push("-lc".to_owned());
446    }
447    if opts.wants_runtime() {
448        if let Some(path) = ours {
449            args.push(path.display().to_string());
450        }
451        if has_gcc {
452            args.push("-lgcc".to_owned());
453            if opts.is_static {
454                args.push("-lgcc_eh".to_owned());
455            }
456        }
457    }
458    if opts.is_static {
459        args.push("--end-group".to_owned());
460    } else if opts.wants_runtime() && has_gcc {
461        // The shared half, and only if something still wants it after everything above.
462        args.push("--as-needed".to_owned());
463        args.push("-lgcc_s".to_owned());
464        args.push("--no-as-needed".to_owned());
465    }
466    args
467}
468
469/// Where a gcc on this machine keeps `crtbegin.o`, `crtend.o` and `libgcc.a`, newest first.
470///
471/// This is not where the C library's files are. A distribution puts them under a directory named
472/// for the gcc version, and there may be several, so the answer is every one that exists with the
473/// highest version in front. Newest first because a newer `libgcc` is a superset of an older one
474/// and because that is the one the C library on the same machine was built against.
475#[must_use]
476pub fn runtime_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
477    let libc = match target.env {
478        Env::Musl => "musl",
479        Env::None | Env::Gnu | Env::Msvc => "gnu",
480    };
481    let arch = target.arch.as_str();
482    // The spellings the distributions use for the same triple. Debian and Ubuntu drop the vendor
483    // field, the source builds and Arch keep `pc`, and Red Hat and SUSE write their own name in
484    // it, so all of them are looked for and the ones that are there are taken.
485    let names = [
486        format!("{arch}-linux-{libc}"),
487        format!("{arch}-pc-linux-{libc}"),
488        format!("{arch}-redhat-linux"),
489        format!("{arch}-suse-linux"),
490        format!("{arch}-alpine-linux-{libc}"),
491    ];
492    let mut found = Vec::new();
493    for base in ["/usr/lib/gcc", "/usr/lib64/gcc", "/usr/local/lib/gcc"] {
494        for name in &names {
495            let dir = under(sysroot, &format!("{base}/{name}"));
496            let Ok(entries) = fs::read_dir(&dir) else { continue };
497            let mut versions: Vec<(Vec<u64>, PathBuf)> = entries
498                .flatten()
499                .map(|e| e.path())
500                .filter(|p| p.is_dir())
501                .map(|p| (version_key(&p), p))
502                .collect();
503            // Descending, so the highest version is the first place `find_file` looks. Ties keep
504            // the order the directory gave, which is arbitrary and does not matter because two
505            // directories that sort the same hold the same version.
506            versions.sort_by(|a, b| b.0.cmp(&a.0));
507            found.extend(versions.into_iter().map(|(_, path)| path));
508        }
509    }
510    found
511}
512
513/// A directory name read as a version, so that `13` sorts above `9` and `10.2` above `10`.
514///
515/// A name that is not a version at all sorts below every name that is, rather than being left
516/// out, because a directory holding a `libgcc.a` is worth looking in whatever it is called.
517fn version_key(dir: &Path) -> Vec<u64> {
518    let name = dir.file_name().unwrap_or_default().to_string_lossy();
519    name.split('.').map(|part| part.parse::<u64>().unwrap_or(0)).collect()
520}
521
522/// Our own runtime library for this target, if it was built.
523///
524/// Looked for beside the compiler rather than at a path decided when the compiler was built, for
525/// the same reason everything else here is looked for: one binary runs wherever it is copied. A
526/// `-B` prefix is asked first, because that is what a `-B` prefix is for.
527#[must_use]
528pub fn builtins_archive(target: Triple, prefixes: &[PathBuf]) -> Option<PathBuf> {
529    const NAME: &str = "librucc_builtins.a";
530    let triple = target.to_string();
531    let mut places: Vec<PathBuf> = Vec::new();
532    for prefix in prefixes {
533        places.push(prefix.join(&triple).join(NAME));
534        places.push(prefix.join(NAME));
535    }
536    if let Some(dir) =
537        std::env::current_exe().ok().and_then(|exe| exe.parent().map(Path::to_path_buf))
538    {
539        // An install: the compiler in `bin` and its runtime in `lib/rucc/<triple>`.
540        if let Some(up) = dir.parent() {
541            places.push(up.join("lib").join("rucc").join(&triple).join(NAME));
542            // A build tree: the compiler in `target/release` and the runtime, which is built for
543            // the target and not the host, in `target/<triple>/release`.
544            for profile in ["release", "debug"] {
545                places.push(up.join(&triple).join(profile).join(NAME));
546            }
547        }
548        places.push(dir.join(NAME));
549    }
550    places.into_iter().find(|path| path.is_file())
551}
552
553/// Which output format this `ld` should write, in the name `ld` knows it by.
554fn emulation(target: Triple) -> &'static str {
555    match target.arch {
556        Arch::X86_64 => "elf_x86_64",
557        Arch::Aarch64 => "aarch64linux",
558        Arch::Riscv64 => "elf64lriscv",
559    }
560}
561
562/// The program that starts a dynamically linked program, whose path is part of the file.
563///
564/// It is a per-target constant rather than something to look for, because the name is fixed by
565/// the platform's ABI and a program naming a different one does not start.
566fn loader(target: Triple) -> &'static str {
567    match (target.arch, target.env) {
568        (Arch::X86_64, Env::Musl) => "/lib/ld-musl-x86_64.so.1",
569        (Arch::X86_64, _) => "/lib64/ld-linux-x86-64.so.2",
570        (Arch::Aarch64, Env::Musl) => "/lib/ld-musl-aarch64.so.1",
571        (Arch::Aarch64, _) => "/lib/ld-linux-aarch64.so.1",
572        (Arch::Riscv64, Env::Musl) => "/lib/ld-musl-riscv64.so.1",
573        (Arch::Riscv64, _) => "/lib/ld-linux-riscv64-lp64d.so.1",
574    }
575}
576
577/// Where the library's own files might be, in search order.
578///
579/// The multiarch directory first for the reason it comes first in the header search: it is where
580/// a distribution that can hold two architectures at once puts the one being asked for, and a
581/// distribution that cannot simply does not have it. `lib64` after it, which is what the
582/// distributions that split by word size use instead, and `lib` last, which is every other one.
583#[must_use]
584pub fn candidates(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
585    let libc = match target.env {
586        Env::Musl => "musl",
587        Env::None | Env::Gnu | Env::Msvc => "gnu",
588    };
589    let multiarch = format!("{}-linux-{libc}", target.arch.as_str());
590    [
591        format!("/usr/lib/{multiarch}"),
592        format!("/lib/{multiarch}"),
593        "/usr/lib64".to_owned(),
594        "/lib64".to_owned(),
595        "/usr/lib".to_owned(),
596        "/lib".to_owned(),
597    ]
598    .into_iter()
599    .map(|dir| under(sysroot, &dir))
600    .collect()
601}
602
603/// The candidates that are there.
604fn library_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
605    candidates(target, sysroot).into_iter().filter(|dir| dir.is_dir()).collect()
606}
607
608/// The first of those directories holding a file of that name.
609fn find_file(dirs: &[PathBuf], name: &str) -> Option<PathBuf> {
610    dirs.iter().map(|dir| dir.join(name)).find(|path| path.is_file())
611}
612
613/// A path under the sysroot, when there is one.
614fn under(sysroot: Option<&Path>, path: &str) -> PathBuf {
615    match sysroot {
616        // `strip_prefix` because joining an absolute path replaces the root rather than extending
617        // it, which would make every entry the unprefixed one.
618        Some(root) => root.join(path.strip_prefix('/').unwrap_or(path)),
619        None => PathBuf::from(path),
620    }
621}
622
623/// A path on the machine that will run the program, rather than on the one compiling it.
624///
625/// Written with the separator of the target and not of the host, which matters for the one path
626/// that is not looked at here but stored in the file and read by something else later: the loader
627/// a dynamic program names. A Windows host joining it would put a backslash in the middle of a
628/// name that a Linux loader has to find, and the program would not start.
629fn target_path(sysroot: Option<&Path>, path: &str) -> String {
630    match sysroot {
631        Some(root) => {
632            let root = root.display().to_string();
633            format!("{}/{}", root.trim_end_matches(['/', '\\']), path.trim_start_matches('/'))
634        }
635        None => path.to_owned(),
636    }
637}
638
639/// The whole invocation as one line, quoted the way `-###` prints it.
640#[must_use]
641pub fn render(linker: &Linker, args: &[String]) -> String {
642    let mut out = linker.path.display().to_string();
643    for arg in args {
644        out.push(' ');
645        if arg.is_empty() || arg.contains(char::is_whitespace) {
646            out.push('"');
647            out.push_str(arg);
648            out.push('"');
649        } else {
650            out.push_str(arg);
651        }
652    }
653    out
654}
655
656/// Runs the linker and waits for it.
657///
658/// # Errors
659///
660/// [`Error::Spawn`] when it could not be started, which is a machine problem, and
661/// [`Error::Refused`] when it ran and said no, which is a program problem and one the linker has
662/// already explained on its own error output.
663pub fn run(linker: &Linker, args: &[String]) -> Result<(), Error> {
664    let args: Vec<OsString> = args.iter().map(OsString::from).collect();
665    let status = Command::new(&linker.path).args(&args).status().map_err(|why| Error::Spawn {
666        path: linker.path.display().to_string(),
667        why: why.to_string(),
668    })?;
669    if status.success() {
670        return Ok(());
671    }
672    // Nothing is added to what the linker printed. It has already named the symbol or the file,
673    // and a second message from here saying that linking failed would only push the first one
674    // further up the screen.
675    Err(Error::Refused {
676        status: match status.code() {
677            Some(code) => format!("exited with status {code}"),
678            None => "was killed before it finished".to_owned(),
679        },
680    })
681}
682
683#[cfg(test)]
684mod tests {
685    use super::*;
686
687    fn linux() -> Triple {
688        Triple::new(Arch::X86_64, Os::Linux, Env::Gnu)
689    }
690
691    fn one(name: &str) -> Vec<Item> {
692        vec![Item::File(name.to_owned())]
693    }
694
695    #[test]
696    fn the_fast_one_is_looked_for_first_and_the_platforms_own_last() {
697        let names = order(linux(), &LinkOptions::default());
698        assert_eq!(names.first().map(String::as_str), Some("ld.mold"));
699        assert_eq!(names.last().map(String::as_str), Some("ld"));
700    }
701
702    #[test]
703    fn naming_one_is_the_whole_of_the_order() {
704        let opts = LinkOptions { use_ld: Some("gold".to_owned()), ..LinkOptions::default() };
705        assert_eq!(order(linux(), &opts), ["ld.gold", "gold"]);
706    }
707
708    #[test]
709    fn a_dynamic_program_names_the_loader_that_will_start_it() {
710        let args = line(linux(), &LinkOptions::default(), &one("a.o"), "a.out").expect("a line");
711        let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
712        assert!(args[at + 1].ends_with("/lib64/ld-linux-x86-64.so.2"), "{args:?}");
713    }
714
715    #[test]
716    fn a_static_program_names_no_loader_because_nothing_will_start_it() {
717        let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
718        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
719        assert!(args.contains(&"-static".to_owned()), "{args:?}");
720        assert!(!args.contains(&"-dynamic-linker".to_owned()), "{args:?}");
721    }
722
723    #[test]
724    fn the_startup_file_of_a_program_that_moves_is_not_the_one_of_a_program_that_does_not() {
725        let moving = LinkOptions { pie: Some(true), ..LinkOptions::default() };
726        let fixed = LinkOptions { pie: Some(false), ..LinkOptions::default() };
727        let named = |opts: &LinkOptions| {
728            line(linux(), opts, &one("a.o"), "a.out")
729                .expect("a line")
730                .iter()
731                .filter_map(|a| Path::new(a).file_name().map(|n| n.to_string_lossy().into_owned()))
732                .find(|n| n.ends_with("crt1.o"))
733        };
734        // Only when the machine running this has them, which is what makes this two assertions
735        // rather than one: a machine with no glibc development files has neither to find.
736        if let Some(name) = named(&moving) {
737            assert_eq!(name, "Scrt1.o");
738            assert_eq!(named(&fixed).as_deref(), Some("crt1.o"));
739        }
740    }
741
742    #[test]
743    fn asking_for_no_startup_files_leaves_out_both_ends_of_them() {
744        let opts = LinkOptions { no_startfiles: true, ..LinkOptions::default() };
745        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
746        assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
747        assert!(!args.iter().any(|a| a.ends_with("crtn.o")), "{args:?}");
748        // And still links against the library, because that is the other flag.
749        assert!(args.contains(&"-lc".to_owned()), "{args:?}");
750    }
751
752    #[test]
753    fn asking_for_no_library_at_all_leaves_out_the_startup_files_too() {
754        let opts = LinkOptions { no_stdlib: true, ..LinkOptions::default() };
755        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
756        assert!(!args.contains(&"-lc".to_owned()), "{args:?}");
757        assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
758    }
759
760    #[test]
761    fn the_library_comes_after_the_objects_that_need_it() {
762        let items = vec![Item::File("a.o".to_owned()), Item::Library("m".to_owned())];
763        let args = line(linux(), &LinkOptions::default(), &items, "a.out").expect("a line");
764        let obj = args.iter().position(|a| a == "a.o").expect("the object");
765        let m = args.iter().position(|a| a == "-lm").expect("the library");
766        let c = args.iter().position(|a| a == "-lc").expect("the library");
767        assert!(obj < m && m < c, "{args:?}");
768    }
769
770    #[test]
771    fn what_the_user_told_the_linker_comes_after_what_this_told_it() {
772        let opts = LinkOptions {
773            passthrough: vec!["--no-eh-frame-hdr".to_owned()],
774            ..LinkOptions::default()
775        };
776        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
777        assert_eq!(args.last().map(String::as_str), Some("--no-eh-frame-hdr"));
778    }
779
780    #[test]
781    fn a_sysroot_moves_every_path_this_decided_and_none_the_user_wrote() {
782        let opts = LinkOptions {
783            sysroot: Some(PathBuf::from("/nowhere-at-all")),
784            search: vec![PathBuf::from("/opt/mine")],
785            ..LinkOptions::default()
786        };
787        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
788        let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
789        assert_eq!(args[at + 1], "/nowhere-at-all/lib64/ld-linux-x86-64.so.2");
790        assert!(args.contains(&"-L/opt/mine".to_owned()), "{args:?}");
791    }
792
793    #[test]
794    fn a_platform_with_no_link_line_is_said_so_rather_than_linked_wrongly() {
795        for triple in [
796            Triple::new(Arch::X86_64, Os::Darwin, Env::Gnu),
797            Triple::new(Arch::X86_64, Os::Windows, Env::Msvc),
798        ] {
799            let error = line(triple, &LinkOptions::default(), &one("a.o"), "a.out")
800                .expect_err("no line for it");
801            assert!(matches!(error, Error::Target { .. }), "{error:?}");
802        }
803    }
804
805    #[test]
806    fn the_line_is_printed_the_way_it_would_be_typed() {
807        let linker = Linker { name: "ld".to_owned(), path: PathBuf::from("/usr/bin/ld") };
808        let args = ["-o".to_owned(), "a b".to_owned()];
809        assert_eq!(render(&linker, &args), "/usr/bin/ld -o \"a b\"");
810    }
811
812    #[test]
813    fn a_linker_that_is_not_there_is_said_by_name() {
814        let opts = LinkOptions {
815            use_ld: Some("a-linker-nobody-has".to_owned()),
816            ..LinkOptions::default()
817        };
818        let error = find(linux(), &opts).expect_err("not on this machine");
819        assert_eq!(error, Error::Named { name: "a-linker-nobody-has".to_owned() });
820    }
821    /// A directory with a `libgcc.a` in it, so a test can say what a machine with a gcc on it
822    /// looks like without needing one.
823    fn a_gcc_dir(name: &str) -> PathBuf {
824        let dir = std::env::temp_dir().join(format!("rucc-link-{name}-{}", std::process::id()));
825        fs::create_dir_all(&dir).expect("a temporary directory");
826        fs::write(dir.join("libgcc.a"), b"not really an archive").expect("a file in it");
827        dir
828    }
829
830    #[test]
831    fn the_c_library_supplies_the_block_routines_and_our_runtime_does_not_displace_them() {
832        let gcc = a_gcc_dir("order");
833        let ours = PathBuf::from("/somewhere/librucc_builtins.a");
834        let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
835        let at_libc = args.iter().position(|a| a == "-lc").expect("libc");
836        let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
837        // glibc's `memcpy` is assembly per microarchitecture and ours is a word at a time loop,
838        // so on a target that has one, its is the one that should answer.
839        assert!(at_libc < at_ours, "{args:?}");
840    }
841
842    #[test]
843    fn a_static_link_puts_them_in_a_group_because_two_of_them_refer_to_each_other() {
844        let gcc = a_gcc_dir("group");
845        let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
846        let args = runtime_items(&opts, &[gcc], None);
847        assert_eq!(args.first().map(String::as_str), Some("--start-group"), "{args:?}");
848        assert_eq!(args.last().map(String::as_str), Some("--end-group"), "{args:?}");
849        // The unwinder, which is what `libc.a` refers to and what a static link fails on without
850        // it. Issue #277.
851        assert!(args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
852    }
853
854    #[test]
855    fn a_dynamic_link_needs_no_group_and_asks_for_the_shared_half_only_if_something_wants_it() {
856        let gcc = a_gcc_dir("dynamic");
857        let args = runtime_items(&LinkOptions::default(), &[gcc], None);
858        assert!(!args.contains(&"--start-group".to_owned()), "{args:?}");
859        assert!(!args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
860        let at = args.iter().position(|a| a == "-lgcc_s").expect("the shared half");
861        assert_eq!(args[at - 1], "--as-needed", "{args:?}");
862        assert_eq!(args[at + 1], "--no-as-needed", "{args:?}");
863    }
864
865    #[test]
866    fn our_own_runtime_comes_before_the_machines_because_the_two_are_interchangeable() {
867        let gcc = a_gcc_dir("ours");
868        let ours = PathBuf::from("/somewhere/librucc_builtins.a");
869        let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
870        let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
871        let at_gcc = args.iter().position(|a| a == "-lgcc").expect("libgcc");
872        assert!(at_ours < at_gcc, "{args:?}");
873    }
874
875    #[test]
876    fn no_builtins_lib_leaves_ours_off_and_keeps_the_machines() {
877        let gcc = a_gcc_dir("theirs");
878        let opts = LinkOptions { no_builtins_lib: true, ..LinkOptions::default() };
879        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
880        assert!(!args.iter().any(|a| a.ends_with("librucc_builtins.a")), "{args:?}");
881        // And the machine's half is still decided the same way it was, from the directories
882        // that are there, which on the machine running this test may be none.
883        assert!(runtime_items(&opts, &[gcc], None).contains(&"-lgcc".to_owned()));
884    }
885
886    #[test]
887    fn nodefaultlibs_leaves_the_whole_runtime_off_and_not_only_the_c_library() {
888        let gcc = a_gcc_dir("none");
889        let opts = LinkOptions { no_defaultlibs: true, ..LinkOptions::default() };
890        assert!(runtime_items(&opts, &[gcc], None).is_empty());
891    }
892
893    #[test]
894    fn a_machine_with_no_gcc_on_it_gets_no_names_for_libraries_that_are_not_there() {
895        let empty = std::env::temp_dir().join("rucc-link-empty-not-a-gcc");
896        let args = runtime_items(&LinkOptions::default(), &[empty], None);
897        assert_eq!(args, ["-lc"], "{args:?}");
898    }
899
900    #[test]
901    fn a_gcc_version_directory_is_read_as_a_version_and_not_as_a_word() {
902        assert!(version_key(Path::new("/usr/lib/gcc/x/13")) > version_key(Path::new("/x/9")));
903        assert!(version_key(Path::new("/x/10.2")) > version_key(Path::new("/x/10")));
904        // Something that is not a version at all still sorts, and sorts below one that is.
905        assert!(version_key(Path::new("/x/snapshot")) < version_key(Path::new("/x/1")));
906    }
907
908    #[test]
909    fn a_runtime_directory_that_is_not_on_this_machine_is_not_offered() {
910        let dirs = runtime_dirs(linux(), Some(Path::new("/definitely/not/a/sysroot")));
911        assert!(dirs.is_empty(), "{dirs:?}");
912    }
913}