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