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 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//! PE in Microsoft's environment has a line of its own, `msvc`, because `lld-link` and
37//! `link.exe` take a different command line rather than a different set of flags. Its libraries
38//! come out of a tree `--fetch` lays out once the person asking has accepted Microsoft's
39//! licence, which is the one sysroot here that is not ours to produce, so the line names the tree's
40//! directories and the libraries by name and leaves the finding to the linker.
41//!
42//! wasm has a line of its own, `wasm`, for `wasm-ld`. It reads the GNU syntax for the inputs, but
43//! a wasm module has no loader, no sections to bracket and no position to choose, so almost none
44//! of the ELF flags apply, and the start file and the entry point are what is left to decide.
45//!
46//! Mach-O is refused rather than approximated. `ld64` wants a platform version load command and a
47//! `-syslibroot`, which is not a different spelling of what is below. [`Unsupported`] says so by
48//! name, which is a better answer than a line that looks plausible and produces nothing that runs.
49
50use std::fmt;
51use std::path::{Path, PathBuf};
52
53use rucc_tuple::{Arch, DataModel, Endian, Env, ObjectFormat, Os, TargetTuple};
54
55use crate::layout::Sysroot;
56use crate::link::{BUILTINS, Crt, Libc, LinkLine, LinkMode, libc, loader};
57use crate::msvc::Chip;
58
59/// One input to the link, in the position the user wrote it.
60///
61/// Link order is semantic: an archive is searched for what is undefined at the moment the linker
62/// reaches it, so a library named before the object that needs it contributes nothing. That is why
63/// this is one ordered list rather than a list of objects and a list of libraries, which is a shape
64/// that cannot represent what the user typed.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub enum Item {
67    /// A file, which is an object this compilation produced or one named on the command line.
68    File(PathBuf),
69    /// `-l<name>`, which the linker resolves against the search path.
70    Library(String),
71    /// One word from `-Wl,` or `-Xlinker`, handed to the linker where the user wrote it.
72    ///
73    /// Here for the reason the other two are. A great many of the linker's options are a bracket
74    /// around the files after them, so an option moved away from what it brackets means something
75    /// else or nothing at all: `--whole-archive` takes every member of every archive after it
76    /// whether anything referenced it or not, `--start-group` searches the archives after it again
77    /// until nothing more comes out, and `-Bstatic` picks which half of a library that ships both
78    /// is wanted.
79    Linker(String),
80}
81
82/// What the driver knows that the line needs, beyond the target and the sysroot.
83///
84/// A struct because most of it is empty in the common case, and because a function with nine
85/// positional parameters of which seven are usually a default is a function somebody calls wrong.
86#[derive(Debug, Clone, Default)]
87pub struct Invocation<'a> {
88    /// The objects and libraries, in the order they were written.
89    pub inputs: &'a [Item],
90    /// `-o`. Empty means the linker's own default, which is what a caller testing a line wants.
91    pub output: Option<&'a Path>,
92    /// How the program is linked, which decides the start file and four of the flags.
93    pub mode: LinkMode,
94    /// `-L`, in the order given. The user's own, and they come before ours, because somebody who
95    /// passed `-L` meant it to win.
96    pub search: &'a [PathBuf],
97    /// `-nostartfiles`, which leaves `crt1.o`, `crti.o` and `crtn.o` off.
98    pub no_startfiles: bool,
99    /// `-nodefaultlibs`, which leaves the libc and our runtime off.
100    pub no_defaultlibs: bool,
101    /// `-fno-builtins-lib`, which leaves our own runtime off and keeps the libc.
102    ///
103    /// It means something narrower here than it does on a native link. There it leaves ours off so
104    /// that the machine's `libgcc` answers for the wide arithmetic instead, and there is no `libgcc`
105    /// in a generated sysroot, so here it leaves those names undefined. Which is what somebody
106    /// passing it with a `-l` of their own is asking for, and the link says so by name if they are
107    /// not.
108    pub no_builtins_lib: bool,
109    /// Our own runtime archive for this target, if it is on the machine.
110    ///
111    /// A path from the caller rather than a name this crate joins onto the sysroot, because it is
112    /// the compiler's own output for the target and not the platform's, and a fetched sysroot will
113    /// never hold it. The driver is what looks for it, in the `-B` prefixes and then beside the
114    /// compiler, and [`None`] is what it says when there is none: the line goes without it and
115    /// whatever wanted a wide divide is undefined. See [`crate::link::BUILTINS`].
116    pub builtins: Option<&'a Path>,
117    /// `-rdynamic`, which puts every symbol in the dynamic table so a program can look itself up.
118    pub export_dynamic: bool,
119    /// `-s`, which drops the symbol table.
120    pub strip: bool,
121    /// `-mwindows`, which makes a Windows program a GUI one: the `windows` subsystem, so no console
122    /// is opened for it, and GDI and the common dialogs on the line the way gcc puts them there.
123    /// Nothing on any other target.
124    pub gui: bool,
125    /// `-municode`, which starts a Windows program at `wmain` or `wWinMain` through `crt2u.o`
126    /// instead of `crt2.o`. Nothing for a DLL or on any other target.
127    pub unicode: bool,
128    /// `-fms-runtime-lib=`, which is `/MT` or `/MD`. Only a line for the MSVC environment reads it.
129    pub crt: Crt,
130    /// `-mexec-model=reactor`, which makes a WASI module a reactor: `crt1-reactor.o` and the entry
131    /// `_initialize` in place of `crt1-command.o` and `_start`. Nothing on any other target.
132    pub reactor: bool,
133    /// gcc's runtime directory for a mingw-w64 target, when the tree the line is against has one.
134    ///
135    /// That is a tree somebody named rather than one this compiler fetched, laid out the way MSYS2
136    /// lays out `/ucrt64`, with gcc's own files under `lib/gcc/<arch>-w64-mingw32/<version>`. Its
137    /// `crtbegin.o` goes after the start file and its `crtend.o` at the very end, the places gcc
138    /// puts them, and the directory is searched after the tree's `lib` so that a `-lgcc` finds the
139    /// one beside them. [`LinkLine::mingw`] leaves both files off a fetched tree, which has no gcc
140    /// in it, and nothing on any other target reads this. tamnd/rucc#2573.
141    pub gcc: Option<&'a Path>,
142    /// gcc's `libgcc.a` for the target, which goes on after ours.
143    ///
144    /// For what we have not written, which on a mingw-w64 target is the decimal floating point
145    /// arithmetic, `__bid_adddd3` and the rest of it. After ours so that ours answers for anything
146    /// both have, and only what is still undefined by then is taken from it. The driver finds it in
147    /// the named tree's gcc directory or in a mingw-w64 gcc installed on this machine, and when
148    /// there is neither a program using `_Decimal64` fails to link by those names. tamnd/rucc#2145.
149    pub libgcc: Option<&'a Path>,
150}
151
152/// A target, or a combination of a target and a mode, that has no line here.
153///
154/// The variants are different kinds of answer. A format is not supported yet and will be. The MSVC
155/// ABI is waiting on something that is not code. A static glibc link is not a thing this scheme can
156/// produce at all. A shared wasm library and a WASI component are later steps of the wasm plan. The distinction matters to somebody reading the message, because only
157/// some of them are worth waiting for.
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub enum Unsupported {
160    /// The target's object format is neither ELF nor PE, and the linker for it wants a different
161    /// line rather than a different spelling of this one.
162    Format {
163        /// The target that was asked for.
164        target: String,
165        /// Its object format, in the spelling `--print-config` uses.
166        format: &'static str,
167    },
168    /// A Windows target in Microsoft's ABI whose architecture Microsoft ships no C library for.
169    ///
170    /// ARM64EC, which is the one row this is. It is tier 4 in
171    /// `spec/cross-compile/04-target-matrix.md`, nothing in this compiler emits code for it, and
172    /// what the installer manifest has for it is a few kilobytes of thunks rather than a CRT, so
173    /// [`Chip::of`] has no answer for it and there are no directories to name. x86-64 and AArch64
174    /// in the same environment have a line, which is `msvc`.
175    MsvcAbi {
176        /// The target that was asked for.
177        target: String,
178    },
179    /// A PE target whose architecture has no machine type among the ones a PE linker writes.
180    ///
181    /// Unreachable through the target table, which has three mingw-w64 rows and an ARM64EC one that
182    /// the MSVC ABI refuses first. It is a variant rather than a panic because the table is data and
183    /// a row added to it should produce a sentence rather than a crash.
184    Machine {
185        /// The target that was asked for.
186        target: String,
187    },
188    /// A static link against a libc that is a stub.
189    ///
190    /// `spec/cross-compile/09-libc-stubs.md` section 9.1 is the reason: a stub carries the names a
191    /// library exports and none of the code behind them, which is everything a dynamic link needs
192    /// and nothing a static one does. glibc's own `libc.a` is several megabytes of objects that
193    /// cannot be synthesized from a description of an interface, so this combination is refused
194    /// here rather than failing later with several thousand undefined symbols.
195    ///
196    /// Every [`crate::link::Libc::Stub`] target, which is glibc and also bionic, the BSDs and
197    /// illumos. musl is the exception rather than the rule here, because musl is the one whose libc
198    /// we build from source.
199    StaticStub {
200        /// The target that was asked for.
201        target: String,
202    },
203    /// A shared library on wasm, which is a module with a `dylink.0` section, code that is
204    /// position independent, and a loader in the host to put it together with the program. None of
205    /// the three is written yet.
206    WasmShared {
207        /// The target that was asked for.
208        target: String,
209    },
210}
211
212impl fmt::Display for Unsupported {
213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214        match self {
215            Unsupported::Format { target, format } => write!(
216                f,
217                "there is no cross link line for {target} yet, because its object format is \
218                 {format} and that linker takes a different line rather than a different spelling \
219                 of this one"
220            ),
221            Unsupported::MsvcAbi { target } => write!(
222                f,
223                "there is no link line for {target}, because Microsoft ships no C runtime for that \
224                 architecture to link it against. x86_64-windows-msvc and aarch64-windows-msvc \
225                 have one, and the mingw-w64 environment needs nothing installed"
226            ),
227            Unsupported::Machine { target } => write!(
228                f,
229                "there is no PE machine type for {target}, so there is nothing to write after -m \
230                 and a linker would guess the machine from the first object it read"
231            ),
232            Unsupported::StaticStub { target } => write!(
233                f,
234                "{target} cannot be linked statically against a generated sysroot, because its \
235                 libc there is a stub: it carries the names the platform's libc exports and none of \
236                 the code behind them, which is what a dynamic link reads and not what a static one \
237                 needs. Link it dynamically, or use a musl target, which ships a real libc.a"
238            ),
239            Unsupported::WasmShared { target } => write!(
240                f,
241                "shared libraries on wasm (dylink.0) are not supported yet, so {target} cannot \
242                 be linked with -shared. A module that the host calls into is -mexec-model=reactor"
243            ),
244        }
245    }
246}
247
248impl std::error::Error for Unsupported {}
249
250/// The whole linker command line for this target, not counting the linker itself.
251///
252/// Section 11.3's eight items, in the order a linker wants them:
253///
254/// 1. `-m`, the output format, because a linker built for more than one machine guesses from its
255///    first input otherwise and a link of no objects has nothing to guess from.
256/// 2. `--sysroot`, and every `-L` rooted inside it.
257/// 3. `-dynamic-linker`, the one string on the line that describes the target's filesystem rather
258///    than ours.
259/// 4. The start files, by absolute path, in the order the two nested pairs need.
260/// 5. The default libraries, which are ours rather than the host's.
261/// 6. `librucc_builtins.a` for the target, which [`LinkLine`] puts after the libc.
262/// 7. No host paths at all.
263/// 8. The format's own extras, which on ELF is the hardening and reproducibility set below and on
264///    PE is the subsystem, the address space layout flags and the header timestamp.
265///
266/// Two formats reach a line here and the difference between them is the flags rather than the shape.
267/// Both are written in the GNU style, which is what `ld`, `ld.lld` and `ld.lld` in its MinGW mode all
268/// read, so the inputs and the `-L` directories are assembled once for both rather than twice. The
269/// MSVC line and the wasm line are their own, which is the module documentation.
270///
271/// # Errors
272///
273/// [`Unsupported::Format`] for a target whose object format is not ELF, PE or wasm,
274/// [`Unsupported::MsvcAbi`] for a Windows target in Microsoft's ABI with no CRT to link against,
275/// [`Unsupported::Machine`] for a PE target with no machine type,
276/// [`Unsupported::StaticStub`] for a static link against a libc that is a stub, and
277/// [`Unsupported::WasmShared`] for `-shared` on wasm.
278pub fn argv(
279    target: TargetTuple,
280    sysroot: &Sysroot,
281    options: &Invocation<'_>,
282) -> Result<Vec<String>, Unsupported> {
283    let format = target.object_format();
284    match format {
285        ObjectFormat::Elf => elf(target, sysroot, options),
286        // mingw-w64 in the GNU style and Microsoft's environment in its own, because the MSVC ABI
287        // is linked by a different linker with a different argument syntax.
288        ObjectFormat::Coff if target.env() == Env::Gnu => coff(target, sysroot, options),
289        ObjectFormat::Coff => msvc(target, sysroot, options),
290        ObjectFormat::Wasm => wasm(target, sysroot, options),
291        _ => Err(Unsupported::Format {
292            target: target.to_canonical_string(),
293            format: format.as_str(),
294        }),
295    }
296}
297
298/// The line for an ELF target.
299fn elf(
300    target: TargetTuple,
301    sysroot: &Sysroot,
302    options: &Invocation<'_>,
303) -> Result<Vec<String>, Unsupported> {
304    let statically = matches!(options.mode, LinkMode::Static | LinkMode::StaticPie);
305    if statically && libc(target) == Libc::Stub {
306        return Err(Unsupported::StaticStub { target: target.to_canonical_string() });
307    }
308
309    let mut args = output(options);
310    if let Some(name) = emulation(target) {
311        args.push("-m".to_owned());
312        args.push(name.to_owned());
313    }
314    args.push(sysroot_flag(sysroot));
315
316    args.extend(mode_flags(target, options.mode));
317    args.extend(hardening(target));
318    if options.export_dynamic {
319        args.push("--export-dynamic".to_owned());
320    }
321    if options.strip {
322        args.push("-s".to_owned());
323    }
324
325    args.extend(body(sysroot, options));
326    Ok(args)
327}
328
329/// The line for a mingw-w64 target.
330///
331/// The same eight items as the ELF line and four of them answered differently. The emulation is a PE
332/// one. There is no dynamic linker, because a PE image names no interpreter: the loader is part of
333/// the operating system and finds a DLL by name at load time rather than by a path written into the
334/// program. There is no `-pie` and no `-no-pie`, because every PE image carries a relocation table
335/// and may be placed anywhere, so the question the two flags answer does not exist here and what is
336/// left of it is whether the loader is asked to use that freedom, which is `--dynamicbase`. And
337/// `-static` says something narrower than it does on ELF, which is the note on
338/// [`crate::link::Libc::Import`].
339///
340/// So the five modes are three lines here, and the recorded file shows two pairs of identical
341/// blocks. That is the answer rather than a gap: a position independent executable and one that is
342/// not are the same image on this format, so a build system that passes `-static-pie` or `-no-pie`
343/// gets what it asked for and loses nothing by the flag having nowhere to go.
344///
345/// The subsystem is named rather than left to the linker. Both linkers default it from the entry
346/// point they find, which means a program with a `WinMain` in it silently becomes a GUI program, and
347/// a cross link deciding anything from what it happens to find in the inputs is the failure mode
348/// section 11.3 is about. A user who wants the other one passes `-Wl,--subsystem,windows`, which
349/// goes on last and wins.
350fn coff(
351    target: TargetTuple,
352    sysroot: &Sysroot,
353    options: &Invocation<'_>,
354) -> Result<Vec<String>, Unsupported> {
355    let Some(machine) = pe_machine(target) else {
356        return Err(Unsupported::Machine { target: target.to_canonical_string() });
357    };
358
359    let mut args = output(options);
360    args.push("-m".to_owned());
361    args.push(machine.to_owned());
362    args.push(sysroot_flag(sysroot));
363
364    if options.mode == LinkMode::Shared {
365        args.push("-shared".to_owned());
366        // The entry point of a DLL is named the way gcc names it. lld before 20 does not pick it
367        // for a MinGW DLL and falls back to MSVC's `_DllMainCRTStartup`, which `dllcrt2.o` does
368        // not define. On i686 the name is decorated, and both linkers drop the leading underscore.
369        args.push("-e".to_owned());
370        let entry = if machine == "i386pe" { "_DllMainCRTStartup@12" } else { "DllMainCRTStartup" };
371        args.push(entry.to_owned());
372    } else {
373        args.push("--subsystem".to_owned());
374        args.push(if options.gui { "windows" } else { "console" }.to_owned());
375    }
376    if matches!(options.mode, LinkMode::Static | LinkMode::StaticPie) {
377        args.push("-static".to_owned());
378    }
379    args.extend(pe_hardening(target));
380    // GNU ld reserves two megabytes of stack for a PE program and lld's MinGW driver reserves one,
381    // so the same program built by gcc and by us had a stack half the size, and a frame of one
382    // megabyte ran out of it. Named here so both linkers agree with gcc. A `-Wl,--stack,N` of
383    // the user's own comes later on the line and wins.
384    if options.mode != LinkMode::Shared {
385        args.push("--stack".to_owned());
386        args.push("2097152".to_owned());
387    }
388    // The PE counterpart of `--export-dynamic`, and a different word rather than a different
389    // default: a Windows image exports what its own export table names, and `-rdynamic` asks for
390    // every symbol to be in there so that a program can look itself up.
391    if options.export_dynamic {
392        args.push("--export-all-symbols".to_owned());
393    }
394    if options.strip {
395        args.push("-s".to_owned());
396    }
397
398    args.extend(body(sysroot, options));
399    Ok(args)
400}
401
402/// The line for a program in Microsoft's environment, which is `lld-link`'s and `link.exe`'s.
403///
404/// Every option starts with a dash rather than a slash. Both linkers take either, and a slash is
405/// what Microsoft's documentation writes, but on a host where an absolute path starts with a slash
406/// `/out:` and `/home/me/main.obj` are the same shape, and a dash is how clang writes this line
407/// for the same reason.
408///
409/// Where it differs from the mingw-w64 line, item by item:
410///
411/// - The machine is `-machine:`, in Microsoft's names, rather than a GNU emulation.
412/// - The libraries are found on `-libpath:` rather than named by path, which is [`LinkLine::msvc`].
413///   The tree is `xwin`'s shape: the CRT in `crt/lib/<arch>`, and the universal CRT and the Win32
414///   import libraries in `sdk/lib/ucrt/<arch>` and `sdk/lib/um/<arch>`.
415/// - There is no start file and no entry point. The CRT has both, `mainCRTStartup` in `libcmt.lib`
416///   or `msvcrt.lib`, and the linker picks it because the program defines `main`, or `wmain` for
417///   `wmainCRTStartup`, which is why `-municode` changes nothing here.
418/// - The five modes are two lines: a program and a DLL. `-static` is about the C runtime on the
419///   other two formats and here that is `-fms-runtime-lib=`, which is [`Invocation::crt`], so the
420///   four program modes are one line here.
421/// - `-Brepro` is the reproducibility flag, which leaves the header's timestamp as a hash of the
422///   output rather than the second it was linked, and is the `--no-insert-timestamp` of this
423///   linker. The stack reservation is left at Microsoft's megabyte, which is what `cl.exe` gives.
424///
425/// `-nodefaultlibs` also writes `-nodefaultlib`, because an object built by `cl.exe` names its CRT
426/// in a directive, and leaving the CRT off the line would otherwise put it back. `-nostartfiles`
427/// changes nothing, since there is no start file to leave off that is not also the C runtime.
428/// Neither `-rdynamic` nor `-s` has anything to say: a PE image exports what its export table
429/// names, and this line writes no symbol table to strip.
430fn msvc(
431    target: TargetTuple,
432    sysroot: &Sysroot,
433    options: &Invocation<'_>,
434) -> Result<Vec<String>, Unsupported> {
435    let (Some(chip), Some(machine)) = (Chip::of(target), link_machine(target)) else {
436        return Err(Unsupported::MsvcAbi { target: target.to_canonical_string() });
437    };
438
439    let mut args = Vec::new();
440    if let Some(path) = options.output {
441        args.push(format!("-out:{}", path.display()));
442    }
443    args.push("-nologo".to_owned());
444    args.push(format!("-machine:{machine}"));
445    if options.mode == LinkMode::Shared {
446        args.push("-dll".to_owned());
447    } else {
448        args.push(format!("-subsystem:{}", if options.gui { "windows" } else { "console" }));
449    }
450    args.push("-dynamicbase".to_owned());
451    args.push("-nxcompat".to_owned());
452    if target.pointer_width() == 64 {
453        args.push("-highentropyva".to_owned());
454    }
455    args.push("-Brepro".to_owned());
456    if options.no_defaultlibs {
457        args.push("-nodefaultlib".to_owned());
458    }
459
460    for dir in options.search {
461        args.push(format!("-libpath:{}", dir.display()));
462    }
463    let arch = chip.in_tree();
464    for dir in ["crt/lib", "sdk/lib/ucrt", "sdk/lib/um"] {
465        args.push(format!("-libpath:{}", sysroot.root().join(dir).join(arch).display()));
466    }
467
468    for input in options.inputs {
469        match input {
470            Item::File(path) => args.push(path.display().to_string()),
471            // What clang makes of `-lfoo` for this environment, which is the library's own name
472            // with the extension every Windows library has.
473            Item::Library(name) if name.to_ascii_lowercase().ends_with(".lib") => {
474                args.push(name.clone());
475            }
476            Item::Library(name) => args.push(format!("{name}.lib")),
477            Item::Linker(arg) => args.push(arg.clone()),
478        }
479    }
480
481    if !options.no_defaultlibs {
482        let line = LinkLine::msvc(options.crt, options.builtins);
483        args.extend(shown(&libraries(&line, options)));
484    }
485    Ok(args)
486}
487
488/// Which machine `lld-link` is to write for, in the name `-machine:` knows it by.
489///
490/// Microsoft's names, which are neither the architecture's nor GNU ld's. [`None`] for a target that
491/// is not in Microsoft's environment, whose linker reads [`pe_machine`] instead.
492#[must_use]
493pub fn link_machine(target: TargetTuple) -> Option<&'static str> {
494    if target.object_format() != ObjectFormat::Coff || target.env() != Env::Msvc {
495        return None;
496    }
497    Some(match target.arch() {
498        Arch::X86_64 => "x64",
499        Arch::X86 => "x86",
500        Arch::Aarch64 => "arm64",
501        Arch::Arm64Ec => "arm64ec",
502        Arch::Arm => "arm",
503        _ => return None,
504    })
505}
506
507/// `-o`, or nothing, which is what a caller testing a line wants.
508fn output(options: &Invocation<'_>) -> Vec<String> {
509    match options.output {
510        Some(path) => vec!["-o".to_owned(), path.display().to_string()],
511        None => Vec::new(),
512    }
513}
514
515/// `--sysroot`.
516///
517/// Not because anything below needs it, since every path this function writes is absolute and
518/// complete, but because a linker script inside the sysroot resolves the names in it against this.
519/// On a real distribution `libc.so` is such a script, and without this the names in one found under
520/// a sysroot are looked for on the host.
521fn sysroot_flag(sysroot: &Sysroot) -> String {
522    format!("--sysroot={}", sysroot.root().display())
523}
524
525/// Everything after the flags: the start files, the search directories, the inputs, the libraries
526/// and whatever the user told the linker directly.
527///
528/// One function for both formats, because none of this differs between them. What has to be linked
529/// is [`LinkLine`]'s answer and it is already a per target one, and `-L`, `-l` and everything a user
530/// hands the linker directly are spelled the same by every linker that reads a GNU command line.
531///
532/// What the user said goes where the user wrote it, in among the inputs, rather than at the end. An
533/// option that brackets the files after it means nothing once it is moved behind them, which is what
534/// [`Item::Linker`] is about.
535fn body(sysroot: &Sysroot, options: &Invocation<'_>) -> Vec<String> {
536    let mut args = Vec::new();
537    let mut line = LinkLine::for_target(sysroot, options.mode, options.builtins);
538    let gcc = options.gcc.filter(|_| is_mingw(sysroot.target()));
539    if libc(sysroot.target()) == Libc::Import {
540        windows_flags(&mut line, options);
541    }
542    if let Some(gcc) = gcc {
543        line.start.push(gcc.join("crtbegin.o"));
544        line.end.push(gcc.join("crtend.o"));
545    }
546    if !options.no_startfiles {
547        args.extend(shown(&line.start));
548    }
549
550    // The user's search directories first and ours second, which is the order they are written in a
551    // native link too, so that `-L` in front of a sysroot behaves the way somebody passing it
552    // expects. And then nothing else: step 7 is that there is no host directory here at all.
553    for dir in options.search {
554        args.push(format!("-L{}", dir.display()));
555    }
556    args.push(format!("-L{}", sysroot.lib().display()));
557    if let Some(gcc) = gcc {
558        args.push(format!("-L{}", gcc.display()));
559    }
560    // And the stubs after the sysroot's own files, so that `-lm` finds the one the driver wrote.
561    // Only for a libc that is a stub and only where they are somewhere else, which is a sysroot in
562    // the cache: a tree the user named keeps its libraries in one place and a second `-L` to it
563    // would be noise.
564    if libc(sysroot.target()) == Libc::Stub && sysroot.stubs() != sysroot.lib() {
565        args.push(format!("-L{}", sysroot.stubs().display()));
566    }
567
568    for input in options.inputs {
569        match input {
570            Item::File(path) => args.push(path.display().to_string()),
571            Item::Library(name) => args.push(format!("-l{name}")),
572            Item::Linker(arg) => args.push(arg.clone()),
573        }
574    }
575
576    if !options.no_defaultlibs {
577        args.extend(shown(&libraries(&line, options)));
578    }
579    if !options.no_startfiles {
580        args.extend(shown(&line.end));
581    }
582
583    args
584}
585
586/// The line for a wasm target, which `wasm-ld` reads.
587///
588/// It is the line clang 23 of wasi-sdk 34 writes for the same row, so that a link that fails for
589/// one compiler fails for the other in the same way. Two differences: our runtime takes the place
590/// of `libclang_rt.builtins.a`, and `libc.a` is named by its path as on every other line here,
591/// where clang writes `-lc` with the same directory on the line.
592///
593/// The ELF flags are not here, because each of them is about a thing a wasm module does not have:
594/// a loader to name, an address to place the image at, a `.eh_frame` to index, or pages to mark.
595/// There is no `--sysroot` either, because wasm-ld reads no linker scripts that would resolve
596/// names against it. The five modes give one line, which is the note on [`LinkLine::wasi`], with
597/// the exception of `-shared`, which is refused.
598///
599/// A WASI command needs nothing more. A reactor gets `--entry _initialize` after its start file,
600/// which is what clang writes, because wasm-ld looks for `_start` by default. wasm32-none has no
601/// libc and no start file, so it has no `_start` either, and gets `--no-entry` unless the user
602/// named an entry with `-Wl,`. Its library directory is not on the line, because there is no
603/// sysroot for a target with no libc to put one in.
604///
605/// wasm32-wasip2 and wasm32-wasip3 have the same line, and the driver gives it to
606/// `wasm-component-ld`, which takes the flags of `wasm-ld`, runs it, and makes a component of the
607/// module. wasm32-wasip3 has cooperative threads in wasi-libc, so its line has
608/// `--cooperative-threading` after the inputs and `libpthread.a` in front of the libraries, which
609/// is where clang writes `--cooperative-threading -lpthread`. The flag makes wasm-ld keep the stack
610/// pointer and the TLS base in the context slots.
611fn wasm(
612    target: TargetTuple,
613    sysroot: &Sysroot,
614    options: &Invocation<'_>,
615) -> Result<Vec<String>, Unsupported> {
616    if options.mode == LinkMode::Shared {
617        return Err(Unsupported::WasmShared { target: target.to_canonical_string() });
618    }
619    let hosted = libc(target) != Libc::None;
620    let threads = hosted && target.os_version().is_some_and(|version| version.major_part() == 3);
621    let line = if hosted {
622        LinkLine::wasi(sysroot, options.reactor, options.builtins)
623    } else {
624        LinkLine::freestanding(options.builtins)
625    };
626
627    let mut args = output(options);
628    args.push("-m".to_owned());
629    args.push("wasm32".to_owned());
630    if !options.no_startfiles {
631        args.extend(shown(&line.start));
632    }
633    if hosted && options.reactor {
634        args.push("--entry".to_owned());
635        args.push("_initialize".to_owned());
636    }
637    let entry = options.inputs.iter().any(
638        |input| matches!(input, Item::Linker(arg) if arg == "-e" || arg.starts_with("--entry")),
639    );
640    if !hosted && !entry {
641        args.push("--no-entry".to_owned());
642    }
643    if options.export_dynamic {
644        args.push("--export-dynamic".to_owned());
645    }
646    if options.strip {
647        args.push("--strip-all".to_owned());
648    }
649    for dir in options.search {
650        args.push(format!("-L{}", dir.display()));
651    }
652    if hosted {
653        args.push(format!("-L{}", sysroot.lib().display()));
654    }
655    for input in options.inputs {
656        match input {
657            Item::File(path) => args.push(path.display().to_string()),
658            Item::Library(name) => args.push(format!("-l{name}")),
659            Item::Linker(arg) => args.push(arg.clone()),
660        }
661    }
662    if threads {
663        args.push("--cooperative-threading".to_owned());
664    }
665    if !options.no_defaultlibs {
666        if threads {
667            args.push(sysroot.lib().join("libpthread.a").display().to_string());
668        }
669        args.extend(shown(&libraries(&line, options)));
670    }
671    Ok(args)
672}
673
674/// Whether this is a mingw-w64 target, the only one whose line takes gcc's runtime directory.
675fn is_mingw(target: TargetTuple) -> bool {
676    target.object_format() == ObjectFormat::Coff && target.env() == Env::Gnu
677}
678
679/// What `-municode` and `-mwindows` change about a mingw-w64 line.
680///
681/// The start file and two libraries, and where the libraries go is gcc's: before `libadvapi32.a`,
682/// which is where its spec writes `-lgdi32 -lcomdlg32` for the second flag.
683fn windows_flags(line: &mut LinkLine, options: &Invocation<'_>) {
684    if options.unicode && options.mode != LinkMode::Shared {
685        for file in &mut line.start {
686            if file.file_name().is_some_and(|name| name == "crt2.o") {
687                file.set_file_name("crt2u.o");
688            }
689        }
690    }
691    if options.gui {
692        let at = line
693            .libraries
694            .iter()
695            .position(|path| path.file_name().is_some_and(|name| name == "libadvapi32.a"))
696            .unwrap_or(line.libraries.len());
697        let dir = line.libraries.first().and_then(|path| path.parent()).map(Path::to_path_buf);
698        let dir = dir.unwrap_or_default();
699        for (i, name) in ["libgdi32.a", "libcomdlg32.a"].into_iter().enumerate() {
700            line.libraries.insert(at + i, dir.join(name));
701        }
702    }
703}
704
705/// The libraries, with ours left off if that is what was asked for.
706///
707/// By the one name in [`BUILTINS`] rather than by position, because the position is
708/// [`LinkLine`]'s business and a caller that knew it would be a second place to fix the day the
709/// order changes.
710fn libraries(line: &LinkLine, options: &Invocation<'_>) -> Vec<PathBuf> {
711    let mut libraries = line.libraries.clone();
712    if options.no_builtins_lib {
713        libraries.retain(|path| path.file_name().is_none_or(|name| name != BUILTINS));
714    }
715    libraries.extend(options.libgcc.map(Path::to_path_buf));
716    libraries
717}
718
719/// The flags that say how the result is linked, and the loader when there is one.
720///
721/// `-static-pie` is not among them and that is the point of this function being separate. It is a
722/// compiler driver flag, and the three linkers this line has to suit take three flags instead: the
723/// link is static, the result is position independent, and there is explicitly no interpreter,
724/// because a static binary that names one gets one mapped and then relocates itself twice.
725fn mode_flags(target: TargetTuple, mode: LinkMode) -> Vec<String> {
726    let mut args = Vec::new();
727    match mode {
728        LinkMode::Static => args.push("-static".to_owned()),
729        LinkMode::StaticPie => {
730            args.push("-static".to_owned());
731            args.push("-pie".to_owned());
732            args.push("--no-dynamic-linker".to_owned());
733        }
734        LinkMode::Dynamic => args.push("-pie".to_owned()),
735        LinkMode::DynamicNoPie => args.push("-no-pie".to_owned()),
736        LinkMode::Shared => args.push("-shared".to_owned()),
737    }
738    // A shared object is started by whatever loads it, so it names no interpreter even though it is
739    // linked dynamically. That is the one place `is_dynamic` is not the condition.
740    if matches!(mode, LinkMode::Dynamic | LinkMode::DynamicNoPie) {
741        if let Some(path) = loader(target) {
742            args.push("-dynamic-linker".to_owned());
743            args.push(path.to_owned());
744        }
745    }
746    args
747}
748
749/// The flags that are on every ELF line, whatever the mode.
750///
751/// Five answers to defaults nobody wants. An executable stack is a target default several linkers
752/// still assume when no input object says otherwise. `relro` and `now` make the relocation tables
753/// read only before `main` runs, which is the cheapest hardening there is. The unwind table header
754/// is needed by every crash handler and by `backtrace`, in a C program with no exceptions in it.
755/// The GNU hash table is the one a loader from this century reads.
756///
757/// `--build-id=none` is the reproducibility one and it is the interesting one.
758/// `spec/cross-compile/11-linking.md` section 11.4 wants byte identical output from two hosts, and a
759/// build id computed over the inputs carries their absolute paths into the binary. A deterministic
760/// one would also do, and it is a linker's own idea of deterministic rather than ours, so the
761/// absence of one is the answer that holds on all three linkers.
762///
763/// A GNU or musl Linux row is different. Its line is the same as the native line, so that a native
764/// link and a cross link for one row give the same program. Each distribution GCC passes
765/// `--build-id`, and `debuginfod`, `systemd-coredump` and the Debian and Fedora debug packages read
766/// the note. The default build ID is a SHA-1 of the output, so it is the same on two hosts when the
767/// output is. `-z now` is not on the line, because upstream GCC does not pass it. `-fhardened` and
768/// `-Wl,-z,now` add it on both lines.
769fn hardening(target: TargetTuple) -> Vec<String> {
770    let linux = target.os() == Os::Linux && matches!(target.env(), Env::Gnu | Env::Musl);
771    let mut flags = vec!["--eh-frame-hdr", "--hash-style=gnu", "-z", "relro"];
772    if !linux {
773        flags.extend(["-z", "now"]);
774    }
775    flags.extend(["-z", "noexecstack"]);
776    flags.push(if linux { "--build-id" } else { "--build-id=none" });
777    flags.into_iter().map(str::to_owned).collect()
778}
779
780/// The flags that are on every PE line, whatever the target and whatever the mode.
781///
782/// The same job as [`hardening`] above and a different list, because the two formats protect
783/// themselves with different mechanisms. `--dynamicbase` is the PE counterpart of a position
784/// independent executable: the image carries a relocation table either way, and this is the bit in
785/// the header that tells the loader it may use it rather than placing the image where it asks. It is
786/// not a default in GNU ld's PE port, which is the reason it is written here.
787/// `--high-entropy-va` goes with it on a 64-bit target, where it widens the address space the loader
788/// picks from, and means nothing on a 32-bit one. `--nxcompat` is the `noexecstack` of this format.
789///
790/// `--no-insert-timestamp` is the reproducibility one and it is this format's version of
791/// `--build-id=none`. A PE header carries the time it was linked, `spec/cross-compile/11-linking.md`
792/// section 11.4 names it as one of the four ways byte identical output is lost, and a link that
793/// stamps the current second produces a different file every time it runs on one machine, let alone
794/// on two.
795fn pe_hardening(target: TargetTuple) -> Vec<String> {
796    let mut args = vec!["--dynamicbase".to_owned(), "--nxcompat".to_owned()];
797    if target.pointer_width() == 64 {
798        args.push("--high-entropy-va".to_owned());
799    }
800    args.push("--no-insert-timestamp".to_owned());
801    args
802}
803
804/// Which machine a PE linker is to write for, in the name `-m` knows it by.
805///
806/// A different table from [`emulation`] and a much shorter one, because PE has four machine types
807/// that matter against ELF's dozen formats: there is no byte order to spell, since every Windows port
808/// is little endian, and no data model to spell either, since each machine type fixes one.
809///
810/// The names are GNU ld's PE emulations, which `ld.lld` accepts in its MinGW mode for exactly this
811/// reason. `i386pep` is the 64-bit x86 one and `i386pe` the 32-bit one, and the `p` that tells them
812/// apart is PE32+ rather than anything about the architecture, which is a piece of 1990s naming that
813/// nothing can be done about now.
814///
815/// [`None`] for a Windows target in Microsoft's ABI, which is not a gap. These names are GNU ld's
816/// and `lld-link` has never read one: it takes `/MACHINE:X64`, in an argument syntax where the rest
817/// of the line is different too, so there is nothing for a shared table to hold. ARM64EC is
818/// [`None`] for that reason first and for a second one:
819/// `spec/cross-compile/09-libc-stubs.md` refuses its import libraries as well, because an export in
820/// that ABI is a mangled name and a library written the way the others are written links and then
821/// fails to load.
822#[must_use]
823pub fn pe_machine(target: TargetTuple) -> Option<&'static str> {
824    if target.object_format() != ObjectFormat::Coff || target.env() != Env::Gnu {
825        return None;
826    }
827    Some(match target.arch() {
828        Arch::X86_64 => "i386pep",
829        Arch::X86 => "i386pe",
830        Arch::Aarch64 => "arm64pe",
831        Arch::Arm => "thumb2pe",
832        _ => return None,
833    })
834}
835
836/// Paths as the line carries them.
837fn shown(paths: &[PathBuf]) -> Vec<String> {
838    paths.iter().map(|path| path.display().to_string()).collect()
839}
840
841/// Which of the formats one linker can write is meant, in the name `-m` knows it by.
842///
843/// The same names in `ld`, `ld.lld` and `mold`, which is why this is one table rather than one per
844/// linker. They are not derivable from the architecture: three of them spell the byte order into
845/// the name, two spell the data model, and the narrow modes of a 64-bit architecture are a different
846/// format rather than a flag on one.
847///
848/// [`None`] for a target whose format is not ELF, which is every one of them rather than wasm alone.
849/// An emulation is an ELF idea: `ld64` takes an architecture and a platform version, and the COFF
850/// linkers take a machine, so a Mach-O target that answered `aarch64linux` here would be answering a
851/// question nobody asked it in a word its linker does not know. Nothing reaches this through
852/// [`argv`], which refuses a non-ELF target before asking, and the answer still has to be right for
853/// the recorded files and for anybody calling it directly.
854#[must_use]
855pub fn emulation(target: TargetTuple) -> Option<&'static str> {
856    if target.object_format() != ObjectFormat::Elf {
857        return None;
858    }
859    let narrow = target.data_model() == DataModel::Ilp32On64;
860    let little = target.endian() == Endian::Little;
861    Some(match target.arch() {
862        Arch::X86_64 if narrow => "elf32_x86_64",
863        Arch::X86_64 => "elf_x86_64",
864        Arch::X86 => "elf_i386",
865        Arch::Aarch64 | Arch::Arm64Ec => match (little, narrow) {
866            (true, false) => "aarch64linux",
867            (true, true) => "aarch64linux32",
868            (false, false) => "aarch64linuxb",
869            (false, true) => "aarch64linux32b",
870        },
871        Arch::Arm if little => "armelf_linux_eabi",
872        Arch::Arm => "armelfb_linux_eabi",
873        Arch::Riscv64 if little => "elf64lriscv",
874        Arch::Riscv64 => "elf64briscv",
875        Arch::Riscv32 if little => "elf32lriscv",
876        Arch::Riscv32 => "elf32briscv",
877        // 32-bit z/Architecture is `elf32_s390` and is not a target here, so there is one row.
878        Arch::S390x => "elf64_s390",
879        Arch::PowerPc64 if little => "elf64lppc",
880        Arch::PowerPc64 => "elf64ppc",
881        Arch::LoongArch64 => "elf64loongarch",
882        // Unreachable, because a wasm target's format is wasm and the check above has already
883        // returned. It is here because the match is exhaustive and a wasm emulation name does not
884        // exist to write in it.
885        Arch::Wasm32 => return None,
886    })
887}
888
889#[cfg(test)]
890mod tests {
891    use std::path::{Path, PathBuf};
892
893    use rucc_tuple::TargetTuple;
894
895    use super::{Invocation, Item, Unsupported, argv, emulation, link_machine, pe_machine};
896    use crate::layout::Sysroot;
897    use crate::link::{Crt, LinkMode};
898
899    fn target(spelling: &str) -> TargetTuple {
900        spelling.parse().expect("a tuple the table knows")
901    }
902
903    fn sysroot(spelling: &str) -> Sysroot {
904        Sysroot::in_cache(Path::new("/cache"), target(spelling))
905    }
906
907    /// Where our own runtime is, which is beside the compiler on a real machine and therefore
908    /// nowhere near the sysroot. The driver finds it and hands the path in.
909    fn builtins() -> PathBuf {
910        PathBuf::from("/beside/the/compiler/librucc_builtins.a")
911    }
912
913    fn line(spelling: &str, mode: LinkMode) -> Vec<String> {
914        let one = [Item::File(Path::new("main.o").to_path_buf())];
915        let ours = builtins();
916        let options = Invocation {
917            inputs: &one,
918            output: Some(Path::new("main")),
919            mode,
920            builtins: Some(&ours),
921            ..Invocation::default()
922        };
923        argv(target(spelling), &sysroot(spelling), &options).expect("a line")
924    }
925
926    #[test]
927    fn nothing_on_the_line_comes_from_the_host() {
928        // The mirror of the header search rule, and the property `spec/cross-compile/02-the-goal.md`
929        // claim 5 rests on. Every path is under the sysroot or is what the caller wrote.
930        for spelling in ["aarch64-linux-musl", "x86_64-linux-gnu", "riscv64-linux-musl"] {
931            for mode in [LinkMode::Dynamic, LinkMode::DynamicNoPie, LinkMode::Shared] {
932                for arg in line(spelling, mode) {
933                    let host = ["/usr/lib", "/usr/local", "/lib64/", "/lib/x86_64"]
934                        .iter()
935                        .any(|bad| arg.starts_with(bad));
936                    // The loader is the one absolute path that is not a path on this machine. It is
937                    // read by the kernel on the target, which is why it is written in full.
938                    let is_loader = arg.contains("ld-musl") || arg.contains("ld-linux");
939                    assert!(!host || is_loader, "{spelling} {mode:?} {arg}");
940                }
941            }
942        }
943    }
944
945    #[test]
946    fn every_file_of_ours_is_under_the_sysroot_except_the_runtime_the_caller_named() {
947        let spelling = "aarch64-linux-musl";
948        // The prefix as this host spells it rather than as a literal, because the question is which
949        // directory these files are in and a Windows separator is a backslash.
950        let root = sysroot(spelling).root().display().to_string();
951        let ours = builtins().display().to_string();
952        for arg in line(spelling, LinkMode::Static) {
953            // The caller's own `main.o` is relative and is theirs. Our runtime is absolute and is
954            // also theirs, because it is the compiler's output for the target and the caller is
955            // what knows where it put it. Everything else absolute is under the sysroot.
956            let named = arg.starts_with('/') && (arg.ends_with(".o") || arg.ends_with(".a"));
957            assert!(!named || arg == ours || arg.starts_with(&root), "{arg}");
958        }
959    }
960
961    #[test]
962    fn the_static_line_names_no_loader_because_nothing_will_start_it() {
963        let args = line("aarch64-linux-musl", LinkMode::Static);
964        assert!(args.contains(&"-static".to_owned()), "{args:?}");
965        assert!(!args.contains(&"-dynamic-linker".to_owned()), "{args:?}");
966    }
967
968    #[test]
969    fn a_static_position_independent_link_is_three_flags_and_not_the_driver_one() {
970        // `-static-pie` is a gcc flag and none of the three linkers has it, which is the whole
971        // reason the mode is spelled out rather than passed through.
972        let args = line("x86_64-linux-musl", LinkMode::StaticPie);
973        assert!(!args.iter().any(|arg| arg == "-static-pie"), "{args:?}");
974        for flag in ["-static", "-pie", "--no-dynamic-linker"] {
975            assert!(args.contains(&flag.to_owned()), "{flag} missing from {args:?}");
976        }
977    }
978
979    #[test]
980    fn a_dynamic_program_names_the_loader_that_will_start_it_and_a_shared_object_does_not() {
981        let program = line("x86_64-linux-gnu", LinkMode::Dynamic);
982        let at = program.iter().position(|arg| arg == "-dynamic-linker").expect("the flag");
983        assert_eq!(program[at + 1], "/lib64/ld-linux-x86-64.so.2");
984        let library = line("x86_64-linux-gnu", LinkMode::Shared);
985        assert!(!library.contains(&"-dynamic-linker".to_owned()), "{library:?}");
986        assert!(library.contains(&"-shared".to_owned()), "{library:?}");
987    }
988
989    #[test]
990    fn the_start_file_of_a_program_that_moves_is_not_the_one_of_a_program_that_does_not() {
991        let named = |mode| {
992            line("x86_64-linux-gnu", mode)
993                .iter()
994                .filter_map(|arg| {
995                    Path::new(arg).file_name().map(|n| n.to_string_lossy().into_owned())
996                })
997                .find(|name| name.ends_with("crt1.o"))
998        };
999        assert_eq!(named(LinkMode::Dynamic).as_deref(), Some("Scrt1.o"));
1000        assert_eq!(named(LinkMode::DynamicNoPie).as_deref(), Some("crt1.o"));
1001        assert_eq!(named(LinkMode::Shared), None);
1002    }
1003
1004    #[test]
1005    fn the_library_comes_after_the_objects_that_need_it() {
1006        let inputs = [Item::File(Path::new("main.o").to_path_buf()), Item::Library("m".to_owned())];
1007        let options =
1008            Invocation { inputs: &inputs, mode: LinkMode::Static, ..Invocation::default() };
1009        let args = argv(target("x86_64-linux-musl"), &sysroot("x86_64-linux-musl"), &options)
1010            .expect("a line");
1011        let object = args.iter().position(|arg| arg == "main.o").expect("the object");
1012        let asked = args.iter().position(|arg| arg == "-lm").expect("the library");
1013        let libc = args.iter().position(|arg| arg.ends_with("libc.a")).expect("the libc");
1014        let end = args.iter().position(|arg| arg.ends_with("crtn.o")).expect("the end file");
1015        assert!(object < asked && asked < libc && libc < end, "{args:?}");
1016    }
1017
1018    #[test]
1019    fn a_static_glibc_link_is_refused_by_name_rather_than_attempted() {
1020        // A stub has no code in it, so there is nothing for a static link to take. Saying that is
1021        // the whole value here: the alternative is a line that produces several thousand undefined
1022        // symbols and a user reading the first forty of them.
1023        let options = Invocation { mode: LinkMode::Static, ..Invocation::default() };
1024        let error = argv(target("x86_64-linux-gnu"), &sysroot("x86_64-linux-gnu"), &options)
1025            .expect_err("refused");
1026        assert!(matches!(error, Unsupported::StaticStub { .. }), "{error:?}");
1027        assert!(error.to_string().contains("musl"), "the way out is not in the message");
1028        // And a musl target links statically, which is the exit criterion of #618.
1029        assert!(argv(target("x86_64-linux-musl"), &sysroot("x86_64-linux-musl"), &options).is_ok());
1030    }
1031
1032    #[test]
1033    fn a_format_with_no_line_of_its_own_is_refused_by_name_rather_than_approximated() {
1034        let spelling = "aarch64-macos";
1035        let options = Invocation { mode: LinkMode::Dynamic, ..Invocation::default() };
1036        let error =
1037            argv(target(spelling), &sysroot(spelling), &options).expect_err("no line for it");
1038        assert!(matches!(error, Unsupported::Format { .. }), "{spelling} {error:?}");
1039    }
1040
1041    #[test]
1042    fn an_msvc_line_is_lld_link_s_with_the_static_crt_unless_asked_otherwise() {
1043        let args = line("x86_64-windows-msvc", LinkMode::Dynamic);
1044        let at = |what: &str| {
1045            args.iter().position(|arg| arg.ends_with(what)).unwrap_or_else(|| panic!("{what}"))
1046        };
1047        for flag in ["-out:main", "-machine:x64", "-subsystem:console", "-Brepro"] {
1048            assert!(args.contains(&flag.to_owned()), "{flag} in {args:?}");
1049        }
1050        // No entry point, because the CRT has one and the linker picks it from `main`.
1051        assert!(!args.iter().any(|arg| arg.starts_with("-entry")), "{args:?}");
1052        // The three directories of the tree, for this architecture.
1053        let root = sysroot("x86_64-windows-msvc").root().to_path_buf();
1054        for dir in ["crt/lib", "sdk/lib/ucrt", "sdk/lib/um"] {
1055            let flag = format!("-libpath:{}", root.join(dir).join("x86_64").display());
1056            assert!(args.contains(&flag), "{flag} in {args:?}");
1057        }
1058        // `/MT`, which is the default, and then ours after everything Microsoft's has.
1059        assert!(at("main.o") < at("libcmt.lib"), "{args:?}");
1060        assert!(at("libcmt.lib") < at("libucrt.lib"), "{args:?}");
1061        assert!(at("libucrt.lib") < at("libvcruntime.lib"), "{args:?}");
1062        assert!(at("oldnames.lib") < at("librucc_builtins.a"), "{args:?}");
1063        assert!(!args.iter().any(|arg| arg == "msvcrt.lib"), "{args:?}");
1064        // And nothing in the GNU style, which this linker would take for an input file.
1065        assert!(!args.iter().any(|arg| arg.starts_with("--") || arg == "-o"), "{args:?}");
1066    }
1067
1068    #[test]
1069    fn the_dll_runtime_is_the_other_three_libraries() {
1070        let one = [Item::File(Path::new("main.obj").to_path_buf())];
1071        let options = Invocation {
1072            inputs: &one,
1073            mode: LinkMode::Dynamic,
1074            crt: Crt::Dll,
1075            ..Invocation::default()
1076        };
1077        let spelling = "aarch64-windows-msvc";
1078        let args = argv(target(spelling), &sysroot(spelling), &options).expect("a line");
1079        assert!(args.contains(&"-machine:arm64".to_owned()), "{args:?}");
1080        for name in ["msvcrt.lib", "ucrt.lib", "vcruntime.lib", "kernel32.lib"] {
1081            assert!(args.contains(&name.to_owned()), "{name} in {args:?}");
1082        }
1083        for name in ["libcmt.lib", "libucrt.lib", "libvcruntime.lib"] {
1084            assert!(!args.contains(&name.to_owned()), "{name} in {args:?}");
1085        }
1086        assert!(args.iter().any(|arg| arg.ends_with("aarch64")), "{args:?}");
1087    }
1088
1089    #[test]
1090    fn an_msvc_dll_is_one_flag_and_takes_its_entry_from_the_crt_too() {
1091        let args = line("x86_64-windows-msvc", LinkMode::Shared);
1092        assert!(args.contains(&"-dll".to_owned()), "{args:?}");
1093        assert!(!args.iter().any(|arg| arg.starts_with("-subsystem")), "{args:?}");
1094        assert!(!args.iter().any(|arg| arg.starts_with("-entry")), "{args:?}");
1095        // The four program modes are one line, because none of them changes anything here.
1096        let program = line("x86_64-windows-msvc", LinkMode::Dynamic);
1097        for mode in [LinkMode::Static, LinkMode::StaticPie, LinkMode::DynamicNoPie] {
1098            assert_eq!(line("x86_64-windows-msvc", mode), program, "{mode:?}");
1099        }
1100    }
1101
1102    #[test]
1103    fn an_msvc_library_is_named_the_way_windows_names_one_and_nodefaultlibs_means_all_of_them() {
1104        let inputs = [
1105            Item::File(Path::new("main.obj").to_path_buf()),
1106            Item::Library("user32".to_owned()),
1107            Item::Library("gdi32.lib".to_owned()),
1108        ];
1109        let options = Invocation {
1110            inputs: &inputs,
1111            no_defaultlibs: true,
1112            gui: true,
1113            ..Invocation::default()
1114        };
1115        let spelling = "x86_64-windows-msvc";
1116        let args = argv(target(spelling), &sysroot(spelling), &options).expect("a line");
1117        assert!(args.contains(&"user32.lib".to_owned()), "{args:?}");
1118        assert!(args.contains(&"gdi32.lib".to_owned()), "{args:?}");
1119        assert!(args.contains(&"-subsystem:windows".to_owned()), "{args:?}");
1120        // The flag too, so that a directive in somebody's object cannot put the CRT back.
1121        assert!(args.contains(&"-nodefaultlib".to_owned()), "{args:?}");
1122        assert!(!args.iter().any(|arg| arg.ends_with("libcmt.lib")), "{args:?}");
1123    }
1124
1125    #[test]
1126    fn arm64ec_is_refused_because_there_is_no_crt_for_it_to_link_against() {
1127        let spelling = "arm64ec-windows-msvc";
1128        let options = Invocation { mode: LinkMode::Dynamic, ..Invocation::default() };
1129        let error = argv(target(spelling), &sysroot(spelling), &options).expect_err("refused");
1130        assert!(matches!(error, Unsupported::MsvcAbi { .. }), "{error:?}");
1131        assert!(error.to_string().contains("mingw-w64"), "{error}");
1132    }
1133
1134    #[test]
1135    fn what_the_user_told_the_linker_stays_where_the_user_wrote_it() {
1136        // The pair libtool writes around a set of convenience archives. Both words bracket the files
1137        // between them, so a line that collects them and appends them to the end has two options
1138        // that say nothing and an archive that went in empty. Written in the middle here for that
1139        // reason: what is checked is the position rather than the presence.
1140        let inputs = [
1141            Item::File(Path::new("main.o").to_path_buf()),
1142            Item::Linker("--whole-archive".to_owned()),
1143            Item::File(Path::new("libaesni.a").to_path_buf()),
1144            Item::Linker("--no-whole-archive".to_owned()),
1145            Item::Library("m".to_owned()),
1146        ];
1147        let options =
1148            Invocation { mode: LinkMode::Dynamic, inputs: &inputs, ..Invocation::default() };
1149        let args = argv(target("x86_64-linux-gnu"), &sysroot("x86_64-linux-gnu"), &options)
1150            .expect("a line");
1151        let at = |what: &str| args.iter().position(|arg| arg == what).expect(what);
1152        assert!(at("main.o") < at("--whole-archive"), "{args:?}");
1153        assert!(at("--whole-archive") < at("libaesni.a"), "{args:?}");
1154        assert!(at("libaesni.a") < at("--no-whole-archive"), "{args:?}");
1155        assert!(at("--no-whole-archive") < at("-lm"), "{args:?}");
1156        // And still in front of the libc and the end start files, which are ours and go after every
1157        // input whatever kind each one turned out to be.
1158        assert!(args.iter().position(|arg| arg.ends_with("crtn.o")).expect("crtn") > at("-lm"));
1159    }
1160
1161    #[test]
1162    fn asking_for_no_start_files_leaves_out_both_ends_of_them() {
1163        let options =
1164            Invocation { mode: LinkMode::Dynamic, no_startfiles: true, ..Invocation::default() };
1165        let args = argv(target("x86_64-linux-gnu"), &sysroot("x86_64-linux-gnu"), &options)
1166            .expect("a line");
1167        assert!(!args.iter().any(|arg| arg.ends_with("crt1.o")), "{args:?}");
1168        assert!(!args.iter().any(|arg| arg.ends_with("crtn.o")), "{args:?}");
1169        // And still links against the libc, because that is the other flag.
1170        assert!(args.iter().any(|arg| arg.ends_with("libc.so")), "{args:?}");
1171    }
1172
1173    #[test]
1174    fn a_narrow_mode_of_a_wide_architecture_is_a_different_output_format() {
1175        // The row that proves the data model belongs in the tuple. Linking x32 as `elf_x86_64`
1176        // produces 64-bit pointers for a target whose pointers are 32 bits.
1177        assert_eq!(emulation(target("x86_64-linux-gnux32")), Some("elf32_x86_64"));
1178        assert_eq!(emulation(target("x86_64-linux-gnu")), Some("elf_x86_64"));
1179    }
1180
1181    #[test]
1182    fn byte_order_is_in_the_output_format_name() {
1183        assert_eq!(emulation(target("s390x-linux-gnu")), Some("elf64_s390"));
1184        assert_eq!(emulation(target("powerpc64le-linux-gnu")), Some("elf64lppc"));
1185        assert_eq!(emulation(target("riscv64-linux-musl")), Some("elf64lriscv"));
1186    }
1187
1188    /// The two flags that are about the line rather than about the target.
1189    ///
1190    /// `-rdynamic` is a flag the linker has and `-fno-builtins-lib` is one it does not, so one of
1191    /// them appears and the other one takes a path away, and both are here because a flag the cross
1192    /// line ignored would be a flag that works natively and stops working the moment the target is
1193    /// somebody else's.
1194    #[test]
1195    fn rdynamic_reaches_the_linker_and_no_builtins_lib_takes_our_runtime_off() {
1196        let one = [Item::File(Path::new("main.o").to_path_buf())];
1197        let ours = builtins();
1198        let both = Invocation {
1199            inputs: &one,
1200            output: Some(Path::new("main")),
1201            mode: LinkMode::Dynamic,
1202            export_dynamic: true,
1203            no_builtins_lib: true,
1204            // Found on the machine and still left off, which is what the flag is. A line built
1205            // with no runtime to name would pass this test without the flag doing anything.
1206            builtins: Some(&ours),
1207            ..Invocation::default()
1208        };
1209        let spelling = "x86_64-linux-musl";
1210        let args = argv(target(spelling), &sysroot(spelling), &both).expect("a line");
1211        assert!(args.contains(&"--export-dynamic".to_owned()), "{args:?}");
1212        assert!(!args.iter().any(|arg| arg.ends_with("librucc_builtins.a")), "{args:?}");
1213        // And the libc it was asked to keep is still there, because that is the other flag.
1214        assert!(args.iter().any(|arg| arg.ends_with("libc.a")), "{args:?}");
1215    }
1216
1217    #[test]
1218    fn a_mingw_line_names_the_pe_machine_and_the_subsystem_and_no_loader() {
1219        let args = line("x86_64-windows-gnu", LinkMode::Dynamic);
1220        let at = args.iter().position(|arg| arg == "-m").expect("the machine flag");
1221        assert_eq!(args[at + 1], "i386pep");
1222        let at = args.iter().position(|arg| arg == "--subsystem").expect("the subsystem flag");
1223        assert_eq!(args[at + 1], "console");
1224        // A PE image names no interpreter and carries a relocation table whatever it is linked as,
1225        // so the two flags that answer those questions on ELF have nothing to say here.
1226        for absent in ["-dynamic-linker", "-pie", "-no-pie", "--eh-frame-hdr"] {
1227            assert!(!args.contains(&absent.to_owned()), "{absent} in {args:?}");
1228        }
1229    }
1230
1231    #[test]
1232    fn mwindows_and_municode_change_the_subsystem_the_start_file_and_two_libraries() {
1233        let one = [Item::File(Path::new("main.o").to_path_buf())];
1234        let options = Invocation {
1235            inputs: &one,
1236            mode: LinkMode::Dynamic,
1237            gui: true,
1238            unicode: true,
1239            ..Invocation::default()
1240        };
1241        let spelling = "x86_64-windows-gnu";
1242        let args = argv(target(spelling), &sysroot(spelling), &options).expect("a line");
1243        let at = |name: &str| {
1244            args.iter().position(|arg| arg.ends_with(name)).unwrap_or_else(|| panic!("{name}"))
1245        };
1246        let subsystem = args.iter().position(|arg| arg == "--subsystem").expect("the flag");
1247        assert_eq!(args[subsystem + 1], "windows");
1248        assert!(at("crt2u.o") < at("main.o"), "{args:?}");
1249        assert!(!args.iter().any(|arg| arg.ends_with("/crt2.o")), "{args:?}");
1250        assert!(at("libmsvcrt.a") < at("libgdi32.a"), "{args:?}");
1251        assert!(at("libcomdlg32.a") < at("libadvapi32.a"), "{args:?}");
1252
1253        // And neither says anything on an ELF line.
1254        let linux = "x86_64-linux-musl";
1255        let args = argv(target(linux), &sysroot(linux), &options).expect("a line");
1256        assert!(!args.iter().any(|arg| arg.contains("gdi32") || arg.contains("crt2u")));
1257    }
1258
1259    #[test]
1260    fn a_mingw_line_carries_the_crt_and_the_win32_libraries_in_single_pass_order() {
1261        let args = line("x86_64-windows-gnu", LinkMode::Dynamic);
1262        let at = |name: &str| {
1263            args.iter().position(|arg| arg.ends_with(name)).unwrap_or_else(|| panic!("{name}"))
1264        };
1265        // One start file and no end file, because PE has no `.init` and `.fini` for a pair of them
1266        // to open and close.
1267        assert!(at("crt2.o") < at("main.o"), "{args:?}");
1268        assert!(!args.iter().any(|arg| arg.ends_with("crtn.o")), "{args:?}");
1269        // Then a library after everything that calls into it, which is what GNU ld's PE port needs
1270        // and what lld's COFF linker does not care about.
1271        assert!(at("main.o") < at("libmingw32.a"), "{args:?}");
1272        assert!(at("libmingwex.a") < at("libmsvcrt.a"), "{args:?}");
1273        assert!(at("libmsvcrt.a") < at("libkernel32.a"), "{args:?}");
1274        assert!(at("libkernel32.a") < at("librucc_builtins.a"), "{args:?}");
1275    }
1276
1277    #[test]
1278    fn a_dll_takes_the_other_start_file_and_no_subsystem() {
1279        let args = line("x86_64-windows-gnu", LinkMode::Shared);
1280        assert!(args.contains(&"-shared".to_owned()), "{args:?}");
1281        // By file name rather than by suffix, since `dllcrt2.o` ends with the other one's name.
1282        let named = |name: &str| {
1283            args.iter().any(|arg| Path::new(arg).file_name().is_some_and(|file| file == name))
1284        };
1285        assert!(named("dllcrt2.o"), "{args:?}");
1286        assert!(!named("crt2.o"), "{args:?}");
1287        assert!(!args.contains(&"--subsystem".to_owned()), "{args:?}");
1288        let entry = args.iter().position(|arg| arg == "-e").expect("an entry point");
1289        assert_eq!(args[entry + 1], "DllMainCRTStartup", "{args:?}");
1290    }
1291
1292    /// What Postgres's `meson.build` hands the linker on MinGW, behind `-Wl,`, for `postgres.exe`
1293    /// and for a DLL: lld's MinGW driver reads every one of these words itself, so the line passes
1294    /// them through as written, in the order written and after the objects. Ours go first, so the
1295    /// `--stack` Postgres asks for is the last one on the line and the one lld keeps.
1296    /// tamnd/rucc#1993.
1297    #[test]
1298    fn the_mingw_link_flags_postgres_writes_reach_the_linker_as_written_and_last() {
1299        let words = [
1300            "--allow-multiple-definition",
1301            "--disable-auto-import",
1302            "--stack",
1303            "4194304",
1304            "--export-all-symbols",
1305            "--out-implib=libpostgres.exe.a",
1306        ];
1307        let mut inputs = vec![Item::File(Path::new("main.o").to_path_buf())];
1308        inputs.extend(words.iter().map(|word| Item::Linker((*word).to_owned())));
1309        for mode in [LinkMode::Dynamic, LinkMode::Shared] {
1310            let options = Invocation {
1311                inputs: &inputs,
1312                output: Some(Path::new("postgres.exe")),
1313                mode,
1314                ..Invocation::default()
1315            };
1316            let spelling = "x86_64-windows-gnu";
1317            let args = argv(target(spelling), &sysroot(spelling), &options).expect("a line");
1318            let at = args
1319                .windows(words.len())
1320                .position(|run| run == &words[..])
1321                .unwrap_or_else(|| panic!("{mode:?} {args:?}"));
1322            let object = args.iter().position(|arg| arg == "main.o").expect("the object");
1323            assert!(object < at, "{mode:?} {args:?}");
1324            let last = args.iter().rposition(|arg| arg == "--stack").expect("a stack size");
1325            assert_eq!(args[last + 1], "4194304", "{mode:?} {args:?}");
1326        }
1327    }
1328
1329    #[test]
1330    fn a_static_windows_link_is_not_refused_because_the_crt_there_is_a_dll_on_every_machine() {
1331        // The difference between an import library and a stub shared object that shows up on the
1332        // line. `-static` on Windows is a statement about our libraries rather than about the CRT,
1333        // and the program it produces runs, which is why the refusal is about `Libc::Stub` by name.
1334        let args = line("x86_64-windows-gnu", LinkMode::Static);
1335        assert!(args.contains(&"-static".to_owned()), "{args:?}");
1336        assert!(args.iter().any(|arg| arg.ends_with("libmsvcrt.a")), "{args:?}");
1337    }
1338
1339    #[test]
1340    fn the_pe_header_carries_no_timestamp_so_that_two_links_produce_one_file() {
1341        // Section 11.4's second cause of a host reaching a binary, and the PE counterpart of
1342        // `--build-id=none`. A stamped header differs between two runs on one machine.
1343        for spelling in ["x86_64-windows-gnu", "i686-windows-gnu", "aarch64-windows-gnu"] {
1344            let args = line(spelling, LinkMode::Dynamic);
1345            assert!(args.contains(&"--no-insert-timestamp".to_owned()), "{spelling} {args:?}");
1346            assert!(args.contains(&"--dynamicbase".to_owned()), "{spelling} {args:?}");
1347            // The wide address space is a 64-bit idea and i686 has no room for it.
1348            let wide = args.contains(&"--high-entropy-va".to_owned());
1349            assert_eq!(wide, spelling != "i686-windows-gnu", "{spelling} {args:?}");
1350        }
1351    }
1352
1353    #[test]
1354    fn the_pe_machine_is_the_one_the_linker_knows_and_not_the_one_the_architecture_is_called() {
1355        assert_eq!(pe_machine(target("x86_64-windows-gnu")), Some("i386pep"));
1356        assert_eq!(pe_machine(target("i686-windows-gnu")), Some("i386pe"));
1357        assert_eq!(pe_machine(target("aarch64-windows-gnu")), Some("arm64pe"));
1358        // An ELF target has no PE machine, the same way a PE target has no ELF emulation. And
1359        // neither has the MSVC ABI, whose linker takes `/MACHINE:X64` and reads none of these names.
1360        assert_eq!(pe_machine(target("x86_64-linux-gnu")), None);
1361        assert_eq!(pe_machine(target("x86_64-windows-msvc")), None);
1362        assert_eq!(emulation(target("x86_64-windows-gnu")), None);
1363        // Which is the other table, and the other way round.
1364        assert_eq!(link_machine(target("x86_64-windows-msvc")), Some("x64"));
1365        assert_eq!(link_machine(target("aarch64-windows-msvc")), Some("arm64"));
1366        assert_eq!(link_machine(target("x86_64-windows-gnu")), None);
1367    }
1368
1369    #[test]
1370    fn a_format_with_no_emulation_names_none_rather_than_its_architecture_s() {
1371        // An emulation is an ELF idea. A Mach-O target whose architecture is also an ELF one would
1372        // otherwise answer `aarch64linux` here, which is a word `ld64` has never heard and exactly
1373        // the almost-right answer `spec/cross-compile/06-abis.md` opens by warning about.
1374        for spelling in
1375            ["aarch64-macos", "x86_64-windows-gnu", "x86_64-windows-msvc", "wasm32-wasi"]
1376        {
1377            assert_eq!(emulation(target(spelling)), None, "{spelling}");
1378        }
1379    }
1380
1381    #[test]
1382    fn a_freestanding_link_has_no_libc_and_no_start_files_and_still_has_our_runtime() {
1383        // Section 8.2's first row is nine headers and no link inputs, so there is no `crt1.o` to
1384        // name and no `libc.a` either. The builtins stay, because a 32-bit target doing 64-bit
1385        // arithmetic reaches them whether a libc exists or not.
1386        let args = line("armv7m-none-eabi", LinkMode::Static);
1387        assert!(!args.iter().any(|arg| arg.ends_with("crt1.o")), "{args:?}");
1388        assert!(!args.iter().any(|arg| arg.ends_with("crti.o")), "{args:?}");
1389        assert!(!args.iter().any(|arg| arg.ends_with("crtn.o")), "{args:?}");
1390        assert!(!args.iter().any(|arg| arg.ends_with("libc.a")), "{args:?}");
1391        assert!(args.iter().any(|arg| arg.ends_with("librucc_builtins.a")), "{args:?}");
1392        // And it is a static link with nothing to interpret it, which is what a bare metal target is.
1393        assert!(args.contains(&"-static".to_owned()), "{args:?}");
1394        assert!(!args.contains(&"-dynamic-linker".to_owned()), "{args:?}");
1395    }
1396
1397    #[test]
1398    fn a_wasi_command_is_crt1_command_the_objects_libc_and_our_runtime_and_no_elf_flags() {
1399        let args = line("wasm32-wasip1", LinkMode::Dynamic);
1400        let lib = sysroot("wasm32-wasip1").lib();
1401        let want = [
1402            "-o".to_owned(),
1403            "main".to_owned(),
1404            "-m".to_owned(),
1405            "wasm32".to_owned(),
1406            lib.join("crt1-command.o").display().to_string(),
1407            format!("-L{}", lib.display()),
1408            "main.o".to_owned(),
1409            lib.join("libsetjmp.a").display().to_string(),
1410            lib.join("libc.a").display().to_string(),
1411            builtins().display().to_string(),
1412        ];
1413        assert_eq!(args, want);
1414        // Every mode but a shared one is the same module, because wasm has one kind of image.
1415        for mode in [LinkMode::Static, LinkMode::StaticPie, LinkMode::DynamicNoPie] {
1416            assert_eq!(line("wasm32-wasip1", mode), want, "{mode:?}");
1417        }
1418    }
1419
1420    #[test]
1421    fn a_wasi_reactor_starts_at_initialize_and_wasm32_none_has_no_entry() {
1422        let one = [Item::File(Path::new("main.o").to_path_buf())];
1423        let reactor = Invocation { inputs: &one, reactor: true, ..Invocation::default() };
1424        let args = argv(target("wasm32-wasip1"), &sysroot("wasm32-wasip1"), &reactor).unwrap();
1425        let crt = sysroot("wasm32-wasip1").lib().join("crt1-reactor.o").display().to_string();
1426        let at = args.iter().position(|arg| *arg == crt).expect("crt1-reactor.o");
1427        assert_eq!(args[at + 1..at + 3], ["--entry", "_initialize"]);
1428        assert!(!args.iter().any(|arg| arg.ends_with("crt1-command.o")), "{args:?}");
1429
1430        // No libc, so no start file, no `_start`, and no directory of a sysroot nobody fetched.
1431        let args = line("wasm32-none", LinkMode::Dynamic);
1432        assert!(args.contains(&"--no-entry".to_owned()), "{args:?}");
1433        assert!(
1434            !args.iter().any(|arg| arg.starts_with("-L") || arg.ends_with(".o") && arg != "main.o")
1435        );
1436        assert!(args.iter().any(|arg| arg.ends_with("librucc_builtins.a")), "{args:?}");
1437        // An entry the user named wins, and `--no-entry` would take it away.
1438        let named = [Item::Linker("--entry=go".to_owned())];
1439        let options = Invocation { inputs: &named, ..Invocation::default() };
1440        let args = argv(target("wasm32-none"), &sysroot("wasm32-none"), &options).unwrap();
1441        assert!(!args.contains(&"--no-entry".to_owned()), "{args:?}");
1442    }
1443
1444    #[test]
1445    fn a_shared_wasm_library_is_refused_by_name() {
1446        let shared = Invocation { mode: LinkMode::Shared, ..Invocation::default() };
1447        let error = argv(target("wasm32-wasip1"), &sysroot("wasm32-wasip1"), &shared).unwrap_err();
1448        assert!(matches!(error, Unsupported::WasmShared { .. }), "{error:?}");
1449        assert!(error.to_string().contains("dylink.0"), "{error}");
1450    }
1451
1452    /// The line of clang 23 for each preview, for `wasm-component-ld` on the two that make a
1453    /// component. wasip3 adds the cooperative threads and their library, and a reactor has the
1454    /// same entry on every preview.
1455    #[test]
1456    fn a_wasi_component_has_the_line_of_its_preview() {
1457        let p2 = sysroot("wasm32-wasip2").lib();
1458        let want = [
1459            "-o".to_owned(),
1460            "main".to_owned(),
1461            "-m".to_owned(),
1462            "wasm32".to_owned(),
1463            p2.join("crt1-command.o").display().to_string(),
1464            format!("-L{}", p2.display()),
1465            "main.o".to_owned(),
1466            p2.join("libsetjmp.a").display().to_string(),
1467            p2.join("libc.a").display().to_string(),
1468            builtins().display().to_string(),
1469        ];
1470        assert_eq!(line("wasm32-wasip2", LinkMode::Dynamic), want);
1471
1472        let p3 = sysroot("wasm32-wasip3").lib();
1473        let want = [
1474            "-o".to_owned(),
1475            "main".to_owned(),
1476            "-m".to_owned(),
1477            "wasm32".to_owned(),
1478            p3.join("crt1-command.o").display().to_string(),
1479            format!("-L{}", p3.display()),
1480            "main.o".to_owned(),
1481            "--cooperative-threading".to_owned(),
1482            p3.join("libpthread.a").display().to_string(),
1483            p3.join("libsetjmp.a").display().to_string(),
1484            p3.join("libc.a").display().to_string(),
1485            builtins().display().to_string(),
1486        ];
1487        assert_eq!(line("wasm32-wasip3", LinkMode::Dynamic), want);
1488
1489        let one = [Item::File(Path::new("main.o").to_path_buf())];
1490        let options = Invocation { inputs: &one, no_defaultlibs: true, ..Invocation::default() };
1491        let args = argv(target("wasm32-wasip3"), &sysroot("wasm32-wasip3"), &options).unwrap();
1492        assert_eq!(args.last().map(String::as_str), Some("--cooperative-threading"), "{args:?}");
1493        let reactor = Invocation { inputs: &one, reactor: true, ..Invocation::default() };
1494        let args = argv(target("wasm32-wasip2"), &sysroot("wasm32-wasip2"), &reactor).unwrap();
1495        let crt = p2.join("crt1-reactor.o").display().to_string();
1496        let at = args.iter().position(|arg| *arg == crt).expect("crt1-reactor.o");
1497        assert_eq!(args[at + 1..at + 3], ["--entry", "_initialize"]);
1498    }
1499
1500    #[test]
1501    fn the_platforms_whose_libc_we_stub_refuse_a_static_link_too_and_not_only_glibc() {
1502        // The refusal follows from the sysroot holding a stub rather than from the target being a
1503        // glibc one. bionic and the BSDs are in the same position for the same reason, and a line
1504        // that pretended otherwise would fail in the linker instead of here.
1505        for spelling in ["aarch64-linux-android", "x86_64-freebsd", "x86_64-illumos"] {
1506            let options = Invocation { mode: LinkMode::Static, ..Invocation::default() };
1507            let error = argv(target(spelling), &sysroot(spelling), &options).expect_err("refused");
1508            assert!(matches!(error, Unsupported::StaticStub { .. }), "{spelling} {error:?}");
1509        }
1510    }
1511
1512    #[test]
1513    fn the_same_line_comes_out_every_time_it_is_asked_for() {
1514        // Claim 5 in the smallest form it has: the function reads nothing but its arguments, so
1515        // two calls agree and so do two hosts.
1516        for mode in [LinkMode::Dynamic, LinkMode::Shared] {
1517            assert_eq!(line("aarch64-linux-gnu", mode), line("aarch64-linux-gnu", mode));
1518        }
1519    }
1520}