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