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