Skip to main content

rucc_sysroot/
argv.rs

1//! The linker command line, as a function of the target and the sysroot and nothing else.
2//!
3//! Design: `spec/cross-compile/11-linking.md` section 11.3, which is a list of eight things a
4//! system linker gets right by default on the machine it came with and gets wrong when it is asked
5//! to link for another one.
6//!
7//! # Why this is a pure function
8//!
9//! Section 11.3 ends with the shape: `(tuple, sysroot, options) -> argv`, no environment reads, no
10//! filesystem probing. That is not tidiness, it is what makes the highest consequence code in the
11//! driver testable. A link line is the last thing that touches a binary and the first thing that
12//! can quietly ruin it, and a function that reads the machine it runs on can only be tested on the
13//! machine it runs on. This one is tested for every target in the table from any host, and
14//! `tests/link-lines` is what it produced for each of them when it was last changed.
15//!
16//! The mirror of that rule is the one [`crate::search`] enforces for headers: nothing from the host
17//! reaches the line. No `/usr/lib`, no `/lib64`, no `LIBRARY_PATH`, and no start file found by
18//! looking around. Every path here is either under the sysroot or something the user wrote on the
19//! command line themselves, and `nothing_on_the_line_comes_from_the_host` is that as a test.
20//!
21//! # What is not decided here
22//!
23//! Which linker runs. `spec/cross-compile/11-linking.md` section 11.2 picks one per format and the
24//! driver spawns it, and the arguments below are the ones `ld`, `ld.lld` and `mold` all read the
25//! same way. That is a real constraint rather than an aspiration: `-static-pie` is a compiler driver
26//! flag that none of the three linkers has, so the mode that means it is spelled out here as the
27//! three flags a linker does understand.
28//!
29//! # The two formats that have a line
30//!
31//! ELF, and PE in mingw-w64's environment. Both are written in the GNU style, which is the same
32//! syntax for the inputs and a different set of flags, so they share everything below that is about
33//! what has to be linked and differ in what is about the image. The PE line is GNU ld's PE port and
34//! `ld.lld` in its MinGW mode, which read each other's arguments for exactly this reason.
35//!
36//! Mach-O and the MSVC ABI are refused rather than approximated. `ld64` wants a platform version
37//! load command and a `-syslibroot`, `lld-link` wants `/MACHINE:` and a `/DEFAULTLIB:` set out of an
38//! SDK that cannot be redistributed, and neither is a different spelling of what is below.
39//! [`Unsupported`] says which by name, which is a better answer than a line that looks plausible and
40//! produces nothing that runs.
41
42use std::fmt;
43use std::path::{Path, PathBuf};
44
45use rucc_tuple::{Arch, DataModel, Endian, Env, ObjectFormat, TargetTuple};
46
47use crate::layout::Sysroot;
48use crate::link::{BUILTINS, Libc, LinkLine, LinkMode, libc, loader};
49
50/// One input to the link, in the position the user wrote it.
51///
52/// Link order is semantic: an archive is searched for what is undefined at the moment the linker
53/// reaches it, so a library named before the object that needs it contributes nothing. That is why
54/// this is one ordered list rather than a list of objects and a list of libraries, which is a shape
55/// that cannot represent what the user typed.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum Item {
58    /// A file, which is an object this compilation produced or one named on the command line.
59    File(PathBuf),
60    /// `-l<name>`, which the linker resolves against the search path.
61    Library(String),
62    /// One word from `-Wl,` or `-Xlinker`, handed to the linker where the user wrote it.
63    ///
64    /// Here for the reason the other two are. A great many of the linker's options are a bracket
65    /// around the files after them, so an option moved away from what it brackets means something
66    /// else or nothing at all: `--whole-archive` takes every member of every archive after it
67    /// whether anything referenced it or not, `--start-group` searches the archives after it again
68    /// until nothing more comes out, and `-Bstatic` picks which half of a library that ships both
69    /// is wanted.
70    Linker(String),
71}
72
73/// What the driver knows that the line needs, beyond the target and the sysroot.
74///
75/// A struct because most of it is empty in the common case, and because a function with nine
76/// positional parameters of which seven are usually a default is a function somebody calls wrong.
77#[derive(Debug, Clone, Default)]
78pub struct Invocation<'a> {
79    /// The objects and libraries, in the order they were written.
80    pub inputs: &'a [Item],
81    /// `-o`. Empty means the linker's own default, which is what a caller testing a line wants.
82    pub output: Option<&'a Path>,
83    /// How the program is linked, which decides the start file and four of the flags.
84    pub mode: LinkMode,
85    /// `-L`, in the order given. The user's own, and they come before ours, because somebody who
86    /// passed `-L` meant it to win.
87    pub search: &'a [PathBuf],
88    /// `-nostartfiles`, which leaves `crt1.o`, `crti.o` and `crtn.o` off.
89    pub no_startfiles: bool,
90    /// `-nodefaultlibs`, which leaves the libc and our runtime off.
91    pub no_defaultlibs: bool,
92    /// `-fno-builtins-lib`, which leaves our own runtime off and keeps the libc.
93    ///
94    /// It means something narrower here than it does on a native link. There it leaves ours off so
95    /// that the machine's `libgcc` answers for the wide arithmetic instead, and there is no `libgcc`
96    /// in a generated sysroot, so here it leaves those names undefined. Which is what somebody
97    /// passing it with a `-l` of their own is asking for, and the link says so by name if they are
98    /// not.
99    pub no_builtins_lib: bool,
100    /// Our own runtime archive for this target, if it is on the machine.
101    ///
102    /// A path from the caller rather than a name this crate joins onto the sysroot, because it is
103    /// the compiler's own output for the target and not the platform's, and a fetched sysroot will
104    /// never hold it. The driver is what looks for it, in the `-B` prefixes and then beside the
105    /// compiler, and [`None`] is what it says when there is none: the line goes without it and
106    /// whatever wanted a wide divide is undefined. See [`crate::link::BUILTINS`].
107    pub builtins: Option<&'a Path>,
108    /// `-rdynamic`, which puts every symbol in the dynamic table so a program can look itself up.
109    pub export_dynamic: bool,
110    /// `-s`, which drops the symbol table.
111    pub strip: bool,
112}
113
114/// A target, or a combination of a target and a mode, that has no line here.
115///
116/// Four variants and they are different kinds of answer. A format is not supported yet and will be.
117/// The MSVC ABI is waiting on something that is not code. A static glibc link is not a thing this
118/// scheme can produce at all. The distinction matters to somebody reading the message, because only
119/// some of them are worth waiting for.
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub enum Unsupported {
122    /// The target's object format is neither ELF nor PE, and the linker for it wants a different
123    /// line rather than a different spelling of this one.
124    Format {
125        /// The target that was asked for.
126        target: String,
127        /// Its object format, in the spelling `--print-config` uses.
128        format: &'static str,
129    },
130    /// A Windows target in Microsoft's ABI rather than mingw-w64's.
131    ///
132    /// Refused for two reasons and the second one is the one that matters. `lld-link` takes a
133    /// different command line rather than a different set of flags: `/MACHINE:`, `/SUBSYSTEM:`,
134    /// `/DEFAULTLIB:` and a response file, which is its own work. And the import libraries a program
135    /// in that ABI links against come from the Windows SDK and the universal CRT, which
136    /// `spec/cross-compile/08-sysroots.md` section 8.6 says cannot be redistributed, so there is
137    /// nothing to produce on this side and a user has to point at an installed one themselves.
138    MsvcAbi {
139        /// The target that was asked for.
140        target: String,
141    },
142    /// A PE target whose architecture has no machine type among the ones a PE linker writes.
143    ///
144    /// Unreachable through the target table, which has three mingw-w64 rows and an ARM64EC one that
145    /// the MSVC ABI refuses first. It is a variant rather than a panic because the table is data and
146    /// a row added to it should produce a sentence rather than a crash.
147    Machine {
148        /// The target that was asked for.
149        target: String,
150    },
151    /// A static link against a libc that is a stub.
152    ///
153    /// `spec/cross-compile/09-libc-stubs.md` section 9.1 is the reason: a stub carries the names a
154    /// library exports and none of the code behind them, which is everything a dynamic link needs
155    /// and nothing a static one does. glibc's own `libc.a` is several megabytes of objects that
156    /// cannot be synthesized from a description of an interface, so this combination is refused
157    /// here rather than failing later with several thousand undefined symbols.
158    ///
159    /// Every [`crate::link::Libc::Stub`] target, which is glibc and also bionic, the BSDs and
160    /// illumos. musl is the exception rather than the rule here, because musl is the one whose libc
161    /// we build from source.
162    StaticStub {
163        /// The target that was asked for.
164        target: String,
165    },
166}
167
168impl fmt::Display for Unsupported {
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170        match self {
171            Unsupported::Format { target, format } => write!(
172                f,
173                "there is no cross link line for {target} yet, because its object format is \
174                 {format} and that linker takes a different line rather than a different spelling \
175                 of this one"
176            ),
177            Unsupported::MsvcAbi { target } => write!(
178                f,
179                "there is no cross link line for {target}, because it is Microsoft's ABI: the \
180                 linker for it takes a different command line and the import libraries a program \
181                 there links against come from the Windows SDK, which cannot be redistributed. \
182                 Build for the mingw-w64 environment instead, which needs nothing installed, or \
183                 pass --sysroot=<dir> naming an SDK you have"
184            ),
185            Unsupported::Machine { target } => write!(
186                f,
187                "there is no PE machine type for {target}, so there is nothing to write after -m \
188                 and a linker would guess the machine from the first object it read"
189            ),
190            Unsupported::StaticStub { target } => write!(
191                f,
192                "{target} cannot be linked statically against a generated sysroot, because its \
193                 libc there is a stub: it carries the names the platform's libc exports and none of \
194                 the code behind them, which is what a dynamic link reads and not what a static one \
195                 needs. Link it dynamically, or use a musl target, which ships a real libc.a"
196            ),
197        }
198    }
199}
200
201impl std::error::Error for Unsupported {}
202
203/// The whole linker command line for this target, not counting the linker itself.
204///
205/// Section 11.3's eight items, in the order a linker wants them:
206///
207/// 1. `-m`, the output format, because a linker built for more than one machine guesses from its
208///    first input otherwise and a link of no objects has nothing to guess from.
209/// 2. `--sysroot`, and every `-L` rooted inside it.
210/// 3. `-dynamic-linker`, the one string on the line that describes the target's filesystem rather
211///    than ours.
212/// 4. The start files, by absolute path, in the order the two nested pairs need.
213/// 5. The default libraries, which are ours rather than the host's.
214/// 6. `librucc_builtins.a` for the target, which [`LinkLine`] puts after the libc.
215/// 7. No host paths at all.
216/// 8. The format's own extras, which on ELF is the hardening and reproducibility set below and on
217///    PE is the subsystem, the address space layout flags and the header timestamp.
218///
219/// Two formats reach a line here and the difference between them is the flags rather than the shape.
220/// Both are written in the GNU style, which is what `ld`, `ld.lld` and `ld.lld` in its MinGW mode all
221/// read, so the inputs and the `-L` directories are assembled once for both rather than twice.
222///
223/// # Errors
224///
225/// [`Unsupported::Format`] for a target whose object format is neither ELF nor PE,
226/// [`Unsupported::MsvcAbi`] for a Windows target in Microsoft's ABI,
227/// [`Unsupported::Machine`] for a PE target with no machine type, and
228/// [`Unsupported::StaticStub`] for a static link against a libc that is a stub.
229pub fn argv(
230    target: TargetTuple,
231    sysroot: &Sysroot,
232    options: &Invocation<'_>,
233) -> Result<Vec<String>, Unsupported> {
234    let format = target.object_format();
235    match format {
236        ObjectFormat::Elf => elf(target, sysroot, options),
237        // mingw-w64 rather than every COFF target, because the MSVC ABI is a different linker with a
238        // different argument syntax and an import library set we are not allowed to ship.
239        ObjectFormat::Coff if target.env() == Env::Gnu => coff(target, sysroot, options),
240        ObjectFormat::Coff => Err(Unsupported::MsvcAbi { target: target.to_canonical_string() }),
241        _ => Err(Unsupported::Format {
242            target: target.to_canonical_string(),
243            format: format.as_str(),
244        }),
245    }
246}
247
248/// The line for an ELF target.
249fn elf(
250    target: TargetTuple,
251    sysroot: &Sysroot,
252    options: &Invocation<'_>,
253) -> Result<Vec<String>, Unsupported> {
254    let statically = matches!(options.mode, LinkMode::Static | LinkMode::StaticPie);
255    if statically && libc(target) == Libc::Stub {
256        return Err(Unsupported::StaticStub { target: target.to_canonical_string() });
257    }
258
259    let mut args = output(options);
260    if let Some(name) = emulation(target) {
261        args.push("-m".to_owned());
262        args.push(name.to_owned());
263    }
264    args.push(sysroot_flag(sysroot));
265
266    args.extend(mode_flags(target, options.mode));
267    args.extend(hardening());
268    if options.export_dynamic {
269        args.push("--export-dynamic".to_owned());
270    }
271    if options.strip {
272        args.push("-s".to_owned());
273    }
274
275    args.extend(body(sysroot, options));
276    Ok(args)
277}
278
279/// The line for a mingw-w64 target.
280///
281/// The same eight items as the ELF line and four of them answered differently. The emulation is a PE
282/// one. There is no dynamic linker, because a PE image names no interpreter: the loader is part of
283/// the operating system and finds a DLL by name at load time rather than by a path written into the
284/// program. There is no `-pie` and no `-no-pie`, because every PE image carries a relocation table
285/// and may be placed anywhere, so the question the two flags answer does not exist here and what is
286/// left of it is whether the loader is asked to use that freedom, which is `--dynamicbase`. And
287/// `-static` says something narrower than it does on ELF, which is the note on
288/// [`crate::link::Libc::Import`].
289///
290/// So the five modes are three lines here, and the recorded file shows two pairs of identical
291/// blocks. That is the answer rather than a gap: a position independent executable and one that is
292/// not are the same image on this format, so a build system that passes `-static-pie` or `-no-pie`
293/// gets what it asked for and loses nothing by the flag having nowhere to go.
294///
295/// The subsystem is named rather than left to the linker. Both linkers default it from the entry
296/// point they find, which means a program with a `WinMain` in it silently becomes a GUI program, and
297/// a cross link deciding anything from what it happens to find in the inputs is the failure mode
298/// section 11.3 is about. A user who wants the other one passes `-Wl,--subsystem,windows`, which
299/// goes on last and wins.
300fn coff(
301    target: TargetTuple,
302    sysroot: &Sysroot,
303    options: &Invocation<'_>,
304) -> Result<Vec<String>, Unsupported> {
305    let Some(machine) = pe_machine(target) else {
306        return Err(Unsupported::Machine { target: target.to_canonical_string() });
307    };
308
309    let mut args = output(options);
310    args.push("-m".to_owned());
311    args.push(machine.to_owned());
312    args.push(sysroot_flag(sysroot));
313
314    if options.mode == LinkMode::Shared {
315        args.push("-shared".to_owned());
316    } else {
317        args.push("--subsystem".to_owned());
318        args.push("console".to_owned());
319    }
320    if matches!(options.mode, LinkMode::Static | LinkMode::StaticPie) {
321        args.push("-static".to_owned());
322    }
323    args.extend(pe_hardening(target));
324    // The PE counterpart of `--export-dynamic`, and a different word rather than a different
325    // default: a Windows image exports what its own export table names, and `-rdynamic` asks for
326    // every symbol to be in there so that a program can look itself up.
327    if options.export_dynamic {
328        args.push("--export-all-symbols".to_owned());
329    }
330    if options.strip {
331        args.push("-s".to_owned());
332    }
333
334    args.extend(body(sysroot, options));
335    Ok(args)
336}
337
338/// `-o`, or nothing, which is what a caller testing a line wants.
339fn output(options: &Invocation<'_>) -> Vec<String> {
340    match options.output {
341        Some(path) => vec!["-o".to_owned(), path.display().to_string()],
342        None => Vec::new(),
343    }
344}
345
346/// `--sysroot`.
347///
348/// Not because anything below needs it, since every path this function writes is absolute and
349/// complete, but because a linker script inside the sysroot resolves the names in it against this.
350/// On a real distribution `libc.so` is such a script, and without this the names in one found under
351/// a sysroot are looked for on the host.
352fn sysroot_flag(sysroot: &Sysroot) -> String {
353    format!("--sysroot={}", sysroot.root().display())
354}
355
356/// Everything after the flags: the start files, the search directories, the inputs, the libraries
357/// and whatever the user told the linker directly.
358///
359/// One function for both formats, because none of this differs between them. What has to be linked
360/// is [`LinkLine`]'s answer and it is already a per target one, and `-L`, `-l` and everything a user
361/// hands the linker directly are spelled the same by every linker that reads a GNU command line.
362///
363/// What the user said goes where the user wrote it, in among the inputs, rather than at the end. An
364/// option that brackets the files after it means nothing once it is moved behind them, which is what
365/// [`Item::Linker`] is about.
366fn body(sysroot: &Sysroot, options: &Invocation<'_>) -> Vec<String> {
367    let mut args = Vec::new();
368    let line = LinkLine::for_target(sysroot, options.mode, options.builtins);
369    if !options.no_startfiles {
370        args.extend(shown(&line.start));
371    }
372
373    // The user's search directories first and ours second, which is the order they are written in a
374    // native link too, so that `-L` in front of a sysroot behaves the way somebody passing it
375    // expects. And then nothing else: step 7 is that there is no host directory here at all.
376    for dir in options.search {
377        args.push(format!("-L{}", dir.display()));
378    }
379    args.push(format!("-L{}", sysroot.lib().display()));
380    // And the stubs after the sysroot's own files, so that `-lm` finds the one the driver wrote.
381    // Only for a libc that is a stub and only where they are somewhere else, which is a sysroot in
382    // the cache: a tree the user named keeps its libraries in one place and a second `-L` to it
383    // would be noise.
384    if libc(sysroot.target()) == Libc::Stub && sysroot.stubs() != sysroot.lib() {
385        args.push(format!("-L{}", sysroot.stubs().display()));
386    }
387
388    for input in options.inputs {
389        match input {
390            Item::File(path) => args.push(path.display().to_string()),
391            Item::Library(name) => args.push(format!("-l{name}")),
392            Item::Linker(arg) => args.push(arg.clone()),
393        }
394    }
395
396    if !options.no_defaultlibs {
397        args.extend(shown(&libraries(&line, options)));
398    }
399    if !options.no_startfiles {
400        args.extend(shown(&line.end));
401    }
402
403    args
404}
405
406/// The libraries, with ours left off if that is what was asked for.
407///
408/// By the one name in [`BUILTINS`] rather than by position, because the position is
409/// [`LinkLine`]'s business and a caller that knew it would be a second place to fix the day the
410/// order changes.
411fn libraries(line: &LinkLine, options: &Invocation<'_>) -> Vec<PathBuf> {
412    let mut libraries = line.libraries.clone();
413    if options.no_builtins_lib {
414        libraries.retain(|path| path.file_name().is_none_or(|name| name != BUILTINS));
415    }
416    libraries
417}
418
419/// The flags that say how the result is linked, and the loader when there is one.
420///
421/// `-static-pie` is not among them and that is the point of this function being separate. It is a
422/// compiler driver flag, and the three linkers this line has to suit take three flags instead: the
423/// link is static, the result is position independent, and there is explicitly no interpreter,
424/// because a static binary that names one gets one mapped and then relocates itself twice.
425fn mode_flags(target: TargetTuple, mode: LinkMode) -> Vec<String> {
426    let mut args = Vec::new();
427    match mode {
428        LinkMode::Static => args.push("-static".to_owned()),
429        LinkMode::StaticPie => {
430            args.push("-static".to_owned());
431            args.push("-pie".to_owned());
432            args.push("--no-dynamic-linker".to_owned());
433        }
434        LinkMode::Dynamic => args.push("-pie".to_owned()),
435        LinkMode::DynamicNoPie => args.push("-no-pie".to_owned()),
436        LinkMode::Shared => args.push("-shared".to_owned()),
437    }
438    // A shared object is started by whatever loads it, so it names no interpreter even though it is
439    // linked dynamically. That is the one place `is_dynamic` is not the condition.
440    if matches!(mode, LinkMode::Dynamic | LinkMode::DynamicNoPie) {
441        if let Some(path) = loader(target) {
442            args.push("-dynamic-linker".to_owned());
443            args.push(path.to_owned());
444        }
445    }
446    args
447}
448
449/// The flags that are on every ELF line, whatever the target and whatever the mode.
450///
451/// Five answers to defaults nobody wants. An executable stack is a target default several linkers
452/// still assume when no input object says otherwise. `relro` and `now` make the relocation tables
453/// read only before `main` runs, which is the cheapest hardening there is. The unwind table header
454/// is needed by every crash handler and by `backtrace`, in a C program with no exceptions in it.
455/// The GNU hash table is the one a loader from this century reads.
456///
457/// `--build-id=none` is the reproducibility one and it is the interesting one.
458/// `spec/cross-compile/11-linking.md` section 11.4 wants byte identical output from two hosts, and a
459/// build id computed over the inputs carries their absolute paths into the binary. A deterministic
460/// one would also do, and it is a linker's own idea of deterministic rather than ours, so the
461/// absence of one is the answer that holds on all three linkers.
462fn hardening() -> Vec<String> {
463    [
464        "--eh-frame-hdr",
465        "--hash-style=gnu",
466        "-z",
467        "relro",
468        "-z",
469        "now",
470        "-z",
471        "noexecstack",
472        "--build-id=none",
473    ]
474    .iter()
475    .map(|flag| (*flag).to_owned())
476    .collect()
477}
478
479/// The flags that are on every PE line, whatever the target and whatever the mode.
480///
481/// The same job as [`hardening`] above and a different list, because the two formats protect
482/// themselves with different mechanisms. `--dynamicbase` is the PE counterpart of a position
483/// independent executable: the image carries a relocation table either way, and this is the bit in
484/// the header that tells the loader it may use it rather than placing the image where it asks. It is
485/// not a default in GNU ld's PE port, which is the reason it is written here.
486/// `--high-entropy-va` goes with it on a 64-bit target, where it widens the address space the loader
487/// picks from, and means nothing on a 32-bit one. `--nxcompat` is the `noexecstack` of this format.
488///
489/// `--no-insert-timestamp` is the reproducibility one and it is this format's version of
490/// `--build-id=none`. A PE header carries the time it was linked, `spec/cross-compile/11-linking.md`
491/// section 11.4 names it as one of the four ways byte identical output is lost, and a link that
492/// stamps the current second produces a different file every time it runs on one machine, let alone
493/// on two.
494fn pe_hardening(target: TargetTuple) -> Vec<String> {
495    let mut args = vec!["--dynamicbase".to_owned(), "--nxcompat".to_owned()];
496    if target.pointer_width() == 64 {
497        args.push("--high-entropy-va".to_owned());
498    }
499    args.push("--no-insert-timestamp".to_owned());
500    args
501}
502
503/// Which machine a PE linker is to write for, in the name `-m` knows it by.
504///
505/// A different table from [`emulation`] and a much shorter one, because PE has four machine types
506/// that matter against ELF's dozen formats: there is no byte order to spell, since every Windows port
507/// is little endian, and no data model to spell either, since each machine type fixes one.
508///
509/// The names are GNU ld's PE emulations, which `ld.lld` accepts in its MinGW mode for exactly this
510/// reason. `i386pep` is the 64-bit x86 one and `i386pe` the 32-bit one, and the `p` that tells them
511/// apart is PE32+ rather than anything about the architecture, which is a piece of 1990s naming that
512/// nothing can be done about now.
513///
514/// [`None`] for a Windows target in Microsoft's ABI, which is not a gap. These names are GNU ld's
515/// and `lld-link` has never read one: it takes `/MACHINE:X64`, in an argument syntax where the rest
516/// of the line is different too, so there is nothing for a shared table to hold. ARM64EC is
517/// [`None`] for that reason first and for a second one:
518/// `spec/cross-compile/09-libc-stubs.md` refuses its import libraries as well, because an export in
519/// that ABI is a mangled name and a library written the way the others are written links and then
520/// fails to load.
521#[must_use]
522pub fn pe_machine(target: TargetTuple) -> Option<&'static str> {
523    if target.object_format() != ObjectFormat::Coff || target.env() != Env::Gnu {
524        return None;
525    }
526    Some(match target.arch() {
527        Arch::X86_64 => "i386pep",
528        Arch::X86 => "i386pe",
529        Arch::Aarch64 => "arm64pe",
530        Arch::Arm => "thumb2pe",
531        _ => return None,
532    })
533}
534
535/// Paths as the line carries them.
536fn shown(paths: &[PathBuf]) -> Vec<String> {
537    paths.iter().map(|path| path.display().to_string()).collect()
538}
539
540/// Which of the formats one linker can write is meant, in the name `-m` knows it by.
541///
542/// The same names in `ld`, `ld.lld` and `mold`, which is why this is one table rather than one per
543/// linker. They are not derivable from the architecture: three of them spell the byte order into
544/// the name, two spell the data model, and the narrow modes of a 64-bit architecture are a different
545/// format rather than a flag on one.
546///
547/// [`None`] for a target whose format is not ELF, which is every one of them rather than wasm alone.
548/// An emulation is an ELF idea: `ld64` takes an architecture and a platform version, and the COFF
549/// linkers take a machine, so a Mach-O target that answered `aarch64linux` here would be answering a
550/// question nobody asked it in a word its linker does not know. Nothing reaches this through
551/// [`argv`], which refuses a non-ELF target before asking, and the answer still has to be right for
552/// the recorded files and for anybody calling it directly.
553#[must_use]
554pub fn emulation(target: TargetTuple) -> Option<&'static str> {
555    if target.object_format() != ObjectFormat::Elf {
556        return None;
557    }
558    let narrow = target.data_model() == DataModel::Ilp32On64;
559    let little = target.endian() == Endian::Little;
560    Some(match target.arch() {
561        Arch::X86_64 if narrow => "elf32_x86_64",
562        Arch::X86_64 => "elf_x86_64",
563        Arch::X86 => "elf_i386",
564        Arch::Aarch64 | Arch::Arm64Ec => match (little, narrow) {
565            (true, false) => "aarch64linux",
566            (true, true) => "aarch64linux32",
567            (false, false) => "aarch64linuxb",
568            (false, true) => "aarch64linux32b",
569        },
570        Arch::Arm if little => "armelf_linux_eabi",
571        Arch::Arm => "armelfb_linux_eabi",
572        Arch::Riscv64 if little => "elf64lriscv",
573        Arch::Riscv64 => "elf64briscv",
574        Arch::Riscv32 if little => "elf32lriscv",
575        Arch::Riscv32 => "elf32briscv",
576        // 32-bit z/Architecture is `elf32_s390` and is not a target here, so there is one row.
577        Arch::S390x => "elf64_s390",
578        Arch::PowerPc64 if little => "elf64lppc",
579        Arch::PowerPc64 => "elf64ppc",
580        Arch::LoongArch64 => "elf64loongarch",
581        // Unreachable, because a wasm target's format is wasm and the check above has already
582        // returned. It is here because the match is exhaustive and a wasm emulation name does not
583        // exist to write in it.
584        Arch::Wasm32 => return None,
585    })
586}
587
588#[cfg(test)]
589mod tests {
590    use std::path::{Path, PathBuf};
591
592    use rucc_tuple::TargetTuple;
593
594    use super::{Invocation, Item, Unsupported, argv, emulation, pe_machine};
595    use crate::layout::Sysroot;
596    use crate::link::LinkMode;
597
598    fn target(spelling: &str) -> TargetTuple {
599        spelling.parse().expect("a tuple the table knows")
600    }
601
602    fn sysroot(spelling: &str) -> Sysroot {
603        Sysroot::in_cache(Path::new("/cache"), target(spelling))
604    }
605
606    /// Where our own runtime is, which is beside the compiler on a real machine and therefore
607    /// nowhere near the sysroot. The driver finds it and hands the path in.
608    fn builtins() -> PathBuf {
609        PathBuf::from("/beside/the/compiler/librucc_builtins.a")
610    }
611
612    fn line(spelling: &str, mode: LinkMode) -> Vec<String> {
613        let one = [Item::File(Path::new("main.o").to_path_buf())];
614        let ours = builtins();
615        let options = Invocation {
616            inputs: &one,
617            output: Some(Path::new("main")),
618            mode,
619            builtins: Some(&ours),
620            ..Invocation::default()
621        };
622        argv(target(spelling), &sysroot(spelling), &options).expect("a line")
623    }
624
625    #[test]
626    fn nothing_on_the_line_comes_from_the_host() {
627        // The mirror of the header search rule, and the property `spec/cross-compile/02-the-goal.md`
628        // claim 5 rests on. Every path is under the sysroot or is what the caller wrote.
629        for spelling in ["aarch64-linux-musl", "x86_64-linux-gnu", "riscv64-linux-musl"] {
630            for mode in [LinkMode::Dynamic, LinkMode::DynamicNoPie, LinkMode::Shared] {
631                for arg in line(spelling, mode) {
632                    let host = ["/usr/lib", "/usr/local", "/lib64/", "/lib/x86_64"]
633                        .iter()
634                        .any(|bad| arg.starts_with(bad));
635                    // The loader is the one absolute path that is not a path on this machine. It is
636                    // read by the kernel on the target, which is why it is written in full.
637                    let is_loader = arg.contains("ld-musl") || arg.contains("ld-linux");
638                    assert!(!host || is_loader, "{spelling} {mode:?} {arg}");
639                }
640            }
641        }
642    }
643
644    #[test]
645    fn every_file_of_ours_is_under_the_sysroot_except_the_runtime_the_caller_named() {
646        let spelling = "aarch64-linux-musl";
647        // The prefix as this host spells it rather than as a literal, because the question is which
648        // directory these files are in and a Windows separator is a backslash.
649        let root = sysroot(spelling).root().display().to_string();
650        let ours = builtins().display().to_string();
651        for arg in line(spelling, LinkMode::Static) {
652            // The caller's own `main.o` is relative and is theirs. Our runtime is absolute and is
653            // also theirs, because it is the compiler's output for the target and the caller is
654            // what knows where it put it. Everything else absolute is under the sysroot.
655            let named = arg.starts_with('/') && (arg.ends_with(".o") || arg.ends_with(".a"));
656            assert!(!named || arg == ours || arg.starts_with(&root), "{arg}");
657        }
658    }
659
660    #[test]
661    fn the_static_line_names_no_loader_because_nothing_will_start_it() {
662        let args = line("aarch64-linux-musl", LinkMode::Static);
663        assert!(args.contains(&"-static".to_owned()), "{args:?}");
664        assert!(!args.contains(&"-dynamic-linker".to_owned()), "{args:?}");
665    }
666
667    #[test]
668    fn a_static_position_independent_link_is_three_flags_and_not_the_driver_one() {
669        // `-static-pie` is a gcc flag and none of the three linkers has it, which is the whole
670        // reason the mode is spelled out rather than passed through.
671        let args = line("x86_64-linux-musl", LinkMode::StaticPie);
672        assert!(!args.iter().any(|arg| arg == "-static-pie"), "{args:?}");
673        for flag in ["-static", "-pie", "--no-dynamic-linker"] {
674            assert!(args.contains(&flag.to_owned()), "{flag} missing from {args:?}");
675        }
676    }
677
678    #[test]
679    fn a_dynamic_program_names_the_loader_that_will_start_it_and_a_shared_object_does_not() {
680        let program = line("x86_64-linux-gnu", LinkMode::Dynamic);
681        let at = program.iter().position(|arg| arg == "-dynamic-linker").expect("the flag");
682        assert_eq!(program[at + 1], "/lib64/ld-linux-x86-64.so.2");
683        let library = line("x86_64-linux-gnu", LinkMode::Shared);
684        assert!(!library.contains(&"-dynamic-linker".to_owned()), "{library:?}");
685        assert!(library.contains(&"-shared".to_owned()), "{library:?}");
686    }
687
688    #[test]
689    fn the_start_file_of_a_program_that_moves_is_not_the_one_of_a_program_that_does_not() {
690        let named = |mode| {
691            line("x86_64-linux-gnu", mode)
692                .iter()
693                .filter_map(|arg| {
694                    Path::new(arg).file_name().map(|n| n.to_string_lossy().into_owned())
695                })
696                .find(|name| name.ends_with("crt1.o"))
697        };
698        assert_eq!(named(LinkMode::Dynamic).as_deref(), Some("Scrt1.o"));
699        assert_eq!(named(LinkMode::DynamicNoPie).as_deref(), Some("crt1.o"));
700        assert_eq!(named(LinkMode::Shared), None);
701    }
702
703    #[test]
704    fn the_library_comes_after_the_objects_that_need_it() {
705        let inputs = [Item::File(Path::new("main.o").to_path_buf()), Item::Library("m".to_owned())];
706        let options =
707            Invocation { inputs: &inputs, mode: LinkMode::Static, ..Invocation::default() };
708        let args = argv(target("x86_64-linux-musl"), &sysroot("x86_64-linux-musl"), &options)
709            .expect("a line");
710        let object = args.iter().position(|arg| arg == "main.o").expect("the object");
711        let asked = args.iter().position(|arg| arg == "-lm").expect("the library");
712        let libc = args.iter().position(|arg| arg.ends_with("libc.a")).expect("the libc");
713        let end = args.iter().position(|arg| arg.ends_with("crtn.o")).expect("the end file");
714        assert!(object < asked && asked < libc && libc < end, "{args:?}");
715    }
716
717    #[test]
718    fn a_static_glibc_link_is_refused_by_name_rather_than_attempted() {
719        // A stub has no code in it, so there is nothing for a static link to take. Saying that is
720        // the whole value here: the alternative is a line that produces several thousand undefined
721        // symbols and a user reading the first forty of them.
722        let options = Invocation { mode: LinkMode::Static, ..Invocation::default() };
723        let error = argv(target("x86_64-linux-gnu"), &sysroot("x86_64-linux-gnu"), &options)
724            .expect_err("refused");
725        assert!(matches!(error, Unsupported::StaticStub { .. }), "{error:?}");
726        assert!(error.to_string().contains("musl"), "the way out is not in the message");
727        // And a musl target links statically, which is the exit criterion of #618.
728        assert!(argv(target("x86_64-linux-musl"), &sysroot("x86_64-linux-musl"), &options).is_ok());
729    }
730
731    #[test]
732    fn a_format_with_no_line_of_its_own_is_refused_by_name_rather_than_approximated() {
733        for spelling in ["aarch64-macos", "wasm32-wasi"] {
734            let options = Invocation { mode: LinkMode::Dynamic, ..Invocation::default() };
735            let error =
736                argv(target(spelling), &sysroot(spelling), &options).expect_err("no line for it");
737            assert!(matches!(error, Unsupported::Format { .. }), "{spelling} {error:?}");
738        }
739    }
740
741    #[test]
742    fn the_msvc_abi_is_refused_on_its_own_grounds_and_the_way_out_is_in_the_message() {
743        // Not the format, because mingw-w64 has a line and is the same format. What is missing is an
744        // SDK nobody may redistribute and a linker with a different command line, and the two are
745        // different kinds of missing, so the message names the environment that needs neither.
746        for spelling in ["x86_64-windows-msvc", "aarch64-windows-msvc", "arm64ec-windows-msvc"] {
747            let options = Invocation { mode: LinkMode::Dynamic, ..Invocation::default() };
748            let error = argv(target(spelling), &sysroot(spelling), &options).expect_err("refused");
749            assert!(matches!(error, Unsupported::MsvcAbi { .. }), "{spelling} {error:?}");
750            assert!(error.to_string().contains("mingw-w64"), "{spelling} {error}");
751        }
752    }
753
754    #[test]
755    fn what_the_user_told_the_linker_stays_where_the_user_wrote_it() {
756        // The pair libtool writes around a set of convenience archives. Both words bracket the files
757        // between them, so a line that collects them and appends them to the end has two options
758        // that say nothing and an archive that went in empty. Written in the middle here for that
759        // reason: what is checked is the position rather than the presence.
760        let inputs = [
761            Item::File(Path::new("main.o").to_path_buf()),
762            Item::Linker("--whole-archive".to_owned()),
763            Item::File(Path::new("libaesni.a").to_path_buf()),
764            Item::Linker("--no-whole-archive".to_owned()),
765            Item::Library("m".to_owned()),
766        ];
767        let options =
768            Invocation { mode: LinkMode::Dynamic, inputs: &inputs, ..Invocation::default() };
769        let args = argv(target("x86_64-linux-gnu"), &sysroot("x86_64-linux-gnu"), &options)
770            .expect("a line");
771        let at = |what: &str| args.iter().position(|arg| arg == what).expect(what);
772        assert!(at("main.o") < at("--whole-archive"), "{args:?}");
773        assert!(at("--whole-archive") < at("libaesni.a"), "{args:?}");
774        assert!(at("libaesni.a") < at("--no-whole-archive"), "{args:?}");
775        assert!(at("--no-whole-archive") < at("-lm"), "{args:?}");
776        // And still in front of the libc and the end start files, which are ours and go after every
777        // input whatever kind each one turned out to be.
778        assert!(args.iter().position(|arg| arg.ends_with("crtn.o")).expect("crtn") > at("-lm"));
779    }
780
781    #[test]
782    fn asking_for_no_start_files_leaves_out_both_ends_of_them() {
783        let options =
784            Invocation { mode: LinkMode::Dynamic, no_startfiles: true, ..Invocation::default() };
785        let args = argv(target("x86_64-linux-gnu"), &sysroot("x86_64-linux-gnu"), &options)
786            .expect("a line");
787        assert!(!args.iter().any(|arg| arg.ends_with("crt1.o")), "{args:?}");
788        assert!(!args.iter().any(|arg| arg.ends_with("crtn.o")), "{args:?}");
789        // And still links against the libc, because that is the other flag.
790        assert!(args.iter().any(|arg| arg.ends_with("libc.so")), "{args:?}");
791    }
792
793    #[test]
794    fn a_narrow_mode_of_a_wide_architecture_is_a_different_output_format() {
795        // The row that proves the data model belongs in the tuple. Linking x32 as `elf_x86_64`
796        // produces 64-bit pointers for a target whose pointers are 32 bits.
797        assert_eq!(emulation(target("x86_64-linux-gnux32")), Some("elf32_x86_64"));
798        assert_eq!(emulation(target("x86_64-linux-gnu")), Some("elf_x86_64"));
799    }
800
801    #[test]
802    fn byte_order_is_in_the_output_format_name() {
803        assert_eq!(emulation(target("s390x-linux-gnu")), Some("elf64_s390"));
804        assert_eq!(emulation(target("powerpc64le-linux-gnu")), Some("elf64lppc"));
805        assert_eq!(emulation(target("riscv64-linux-musl")), Some("elf64lriscv"));
806    }
807
808    /// The two flags that are about the line rather than about the target.
809    ///
810    /// `-rdynamic` is a flag the linker has and `-fno-builtins-lib` is one it does not, so one of
811    /// them appears and the other one takes a path away, and both are here because a flag the cross
812    /// line ignored would be a flag that works natively and stops working the moment the target is
813    /// somebody else's.
814    #[test]
815    fn rdynamic_reaches_the_linker_and_no_builtins_lib_takes_our_runtime_off() {
816        let one = [Item::File(Path::new("main.o").to_path_buf())];
817        let ours = builtins();
818        let both = Invocation {
819            inputs: &one,
820            output: Some(Path::new("main")),
821            mode: LinkMode::Dynamic,
822            export_dynamic: true,
823            no_builtins_lib: true,
824            // Found on the machine and still left off, which is what the flag is. A line built
825            // with no runtime to name would pass this test without the flag doing anything.
826            builtins: Some(&ours),
827            ..Invocation::default()
828        };
829        let spelling = "x86_64-linux-musl";
830        let args = argv(target(spelling), &sysroot(spelling), &both).expect("a line");
831        assert!(args.contains(&"--export-dynamic".to_owned()), "{args:?}");
832        assert!(!args.iter().any(|arg| arg.ends_with("librucc_builtins.a")), "{args:?}");
833        // And the libc it was asked to keep is still there, because that is the other flag.
834        assert!(args.iter().any(|arg| arg.ends_with("libc.a")), "{args:?}");
835    }
836
837    #[test]
838    fn a_mingw_line_names_the_pe_machine_and_the_subsystem_and_no_loader() {
839        let args = line("x86_64-windows-gnu", LinkMode::Dynamic);
840        let at = args.iter().position(|arg| arg == "-m").expect("the machine flag");
841        assert_eq!(args[at + 1], "i386pep");
842        let at = args.iter().position(|arg| arg == "--subsystem").expect("the subsystem flag");
843        assert_eq!(args[at + 1], "console");
844        // A PE image names no interpreter and carries a relocation table whatever it is linked as,
845        // so the two flags that answer those questions on ELF have nothing to say here.
846        for absent in ["-dynamic-linker", "-pie", "-no-pie", "--eh-frame-hdr"] {
847            assert!(!args.contains(&absent.to_owned()), "{absent} in {args:?}");
848        }
849    }
850
851    #[test]
852    fn a_mingw_line_carries_the_crt_and_the_win32_libraries_in_single_pass_order() {
853        let args = line("x86_64-windows-gnu", LinkMode::Dynamic);
854        let at = |name: &str| {
855            args.iter().position(|arg| arg.ends_with(name)).unwrap_or_else(|| panic!("{name}"))
856        };
857        // One start file and no end file, because PE has no `.init` and `.fini` for a pair of them
858        // to open and close.
859        assert!(at("crt2.o") < at("main.o"), "{args:?}");
860        assert!(!args.iter().any(|arg| arg.ends_with("crtn.o")), "{args:?}");
861        // Then a library after everything that calls into it, which is what GNU ld's PE port needs
862        // and what lld's COFF linker does not care about.
863        assert!(at("main.o") < at("libmingw32.a"), "{args:?}");
864        assert!(at("libmingwex.a") < at("libmsvcrt.a"), "{args:?}");
865        assert!(at("libmsvcrt.a") < at("libkernel32.a"), "{args:?}");
866        assert!(at("libkernel32.a") < at("librucc_builtins.a"), "{args:?}");
867    }
868
869    #[test]
870    fn a_dll_takes_the_other_start_file_and_no_subsystem() {
871        let args = line("x86_64-windows-gnu", LinkMode::Shared);
872        assert!(args.contains(&"-shared".to_owned()), "{args:?}");
873        // By file name rather than by suffix, since `dllcrt2.o` ends with the other one's name.
874        let named = |name: &str| {
875            args.iter().any(|arg| Path::new(arg).file_name().is_some_and(|file| file == name))
876        };
877        assert!(named("dllcrt2.o"), "{args:?}");
878        assert!(!named("crt2.o"), "{args:?}");
879        assert!(!args.contains(&"--subsystem".to_owned()), "{args:?}");
880    }
881
882    #[test]
883    fn a_static_windows_link_is_not_refused_because_the_crt_there_is_a_dll_on_every_machine() {
884        // The difference between an import library and a stub shared object that shows up on the
885        // line. `-static` on Windows is a statement about our libraries rather than about the CRT,
886        // and the program it produces runs, which is why the refusal is about `Libc::Stub` by name.
887        let args = line("x86_64-windows-gnu", LinkMode::Static);
888        assert!(args.contains(&"-static".to_owned()), "{args:?}");
889        assert!(args.iter().any(|arg| arg.ends_with("libmsvcrt.a")), "{args:?}");
890    }
891
892    #[test]
893    fn the_pe_header_carries_no_timestamp_so_that_two_links_produce_one_file() {
894        // Section 11.4's second cause of a host reaching a binary, and the PE counterpart of
895        // `--build-id=none`. A stamped header differs between two runs on one machine.
896        for spelling in ["x86_64-windows-gnu", "i686-windows-gnu", "aarch64-windows-gnu"] {
897            let args = line(spelling, LinkMode::Dynamic);
898            assert!(args.contains(&"--no-insert-timestamp".to_owned()), "{spelling} {args:?}");
899            assert!(args.contains(&"--dynamicbase".to_owned()), "{spelling} {args:?}");
900            // The wide address space is a 64-bit idea and i686 has no room for it.
901            let wide = args.contains(&"--high-entropy-va".to_owned());
902            assert_eq!(wide, spelling != "i686-windows-gnu", "{spelling} {args:?}");
903        }
904    }
905
906    #[test]
907    fn the_pe_machine_is_the_one_the_linker_knows_and_not_the_one_the_architecture_is_called() {
908        assert_eq!(pe_machine(target("x86_64-windows-gnu")), Some("i386pep"));
909        assert_eq!(pe_machine(target("i686-windows-gnu")), Some("i386pe"));
910        assert_eq!(pe_machine(target("aarch64-windows-gnu")), Some("arm64pe"));
911        // An ELF target has no PE machine, the same way a PE target has no ELF emulation. And
912        // neither has the MSVC ABI, whose linker takes `/MACHINE:X64` and reads none of these names.
913        assert_eq!(pe_machine(target("x86_64-linux-gnu")), None);
914        assert_eq!(pe_machine(target("x86_64-windows-msvc")), None);
915        assert_eq!(emulation(target("x86_64-windows-gnu")), None);
916    }
917
918    #[test]
919    fn a_format_with_no_emulation_names_none_rather_than_its_architecture_s() {
920        // An emulation is an ELF idea. A Mach-O target whose architecture is also an ELF one would
921        // otherwise answer `aarch64linux` here, which is a word `ld64` has never heard and exactly
922        // the almost-right answer `spec/cross-compile/06-abis.md` opens by warning about.
923        for spelling in
924            ["aarch64-macos", "x86_64-windows-gnu", "x86_64-windows-msvc", "wasm32-wasi"]
925        {
926            assert_eq!(emulation(target(spelling)), None, "{spelling}");
927        }
928    }
929
930    #[test]
931    fn a_freestanding_link_has_no_libc_and_no_start_files_and_still_has_our_runtime() {
932        // Section 8.2's first row is nine headers and no link inputs, so there is no `crt1.o` to
933        // name and no `libc.a` either. The builtins stay, because a 32-bit target doing 64-bit
934        // arithmetic reaches them whether a libc exists or not.
935        let args = line("armv7m-none-eabi", LinkMode::Static);
936        assert!(!args.iter().any(|arg| arg.ends_with("crt1.o")), "{args:?}");
937        assert!(!args.iter().any(|arg| arg.ends_with("crti.o")), "{args:?}");
938        assert!(!args.iter().any(|arg| arg.ends_with("crtn.o")), "{args:?}");
939        assert!(!args.iter().any(|arg| arg.ends_with("libc.a")), "{args:?}");
940        assert!(args.iter().any(|arg| arg.ends_with("librucc_builtins.a")), "{args:?}");
941        // And it is a static link with nothing to interpret it, which is what a bare metal target is.
942        assert!(args.contains(&"-static".to_owned()), "{args:?}");
943        assert!(!args.contains(&"-dynamic-linker".to_owned()), "{args:?}");
944    }
945
946    #[test]
947    fn the_platforms_whose_libc_we_stub_refuse_a_static_link_too_and_not_only_glibc() {
948        // The refusal follows from the sysroot holding a stub rather than from the target being a
949        // glibc one. bionic and the BSDs are in the same position for the same reason, and a line
950        // that pretended otherwise would fail in the linker instead of here.
951        for spelling in ["aarch64-linux-android", "x86_64-freebsd", "x86_64-illumos"] {
952            let options = Invocation { mode: LinkMode::Static, ..Invocation::default() };
953            let error = argv(target(spelling), &sysroot(spelling), &options).expect_err("refused");
954            assert!(matches!(error, Unsupported::StaticStub { .. }), "{spelling} {error:?}");
955        }
956    }
957
958    #[test]
959    fn the_same_line_comes_out_every_time_it_is_asked_for() {
960        // Claim 5 in the smallest form it has: the function reads nothing but its arguments, so
961        // two calls agree and so do two hosts.
962        for mode in [LinkMode::Dynamic, LinkMode::Shared] {
963            assert_eq!(line("aarch64-linux-gnu", mode), line("aarch64-linux-gnu", mode));
964        }
965    }
966}