Skip to main content

rucc_driver/
link.rs

1//! Finding a linker and telling it what to link.
2//!
3//! Design: `spec/04-driver-and-cli.md` section 4.9. There is no linker of our own before 1.0, so
4//! this finds one on the machine and builds the command line it wants.
5//!
6//! The linker is invoked directly rather than through the system compiler driver. Going through
7//! `cc` would be shorter to write and would borrow that compiler's idea of where everything is,
8//! and it would also mean this compiler cannot link on a machine that has no other compiler on
9//! it, which is most of the machines a compiler ends up on. It would also make `-###` output a
10//! line that does not say what happens, since the interesting half would be inside the program
11//! being spawned.
12//!
13//! # What is not decided here
14//!
15//! The startup files and the library directories are looked for rather than configured, for the
16//! same reason `library` looks for the headers: gcc settles this when it is built because a gcc
17//! is built for the machine it will run on, and this is one binary that runs wherever it is
18//! copied. So the shape of the answer is a list of candidates per platform of which the ones
19//! that exist are taken, and a cross build says where the rest is with `--sysroot`.
20//!
21//! # What is not here yet
22//!
23//! `crtbegin` and `crtend`, and `-lgcc`. Those are a compiler's own runtime rather than the
24//! library's, and this compiler's is `rucc-builtins`, which is not written. A program that needs
25//! neither links and runs without them, which is every program that does not divide a 128-bit
26//! integer or unwind through a frame, and a program that needs one gets an undefined symbol from
27//! the linker naming exactly what is missing rather than a wrong answer at run time.
28//!
29//! Darwin and Windows. `ld64` wants a different line, a platform version load command and a
30//! different set of default libraries, and `link.exe` wants another one again. Each arrives with
31//! the target that needs it.
32
33use std::ffi::OsString;
34use std::path::{Path, PathBuf};
35use std::process::Command;
36
37use rucc_target::{Arch, Env, Os, Triple};
38
39/// What the command line said about linking.
40///
41/// Kept apart from `Options` because none of it reaches the compilation. A flag here changes what
42/// the linker is told and changes nothing about the object files handed to it, which is why `-lm`
43/// on a `-c` line is a note rather than an error.
44#[derive(Debug, Default, Clone, PartialEq, Eq)]
45pub struct LinkOptions {
46    /// `-fuse-ld=<name>`, which names a linker rather than a path to one.
47    pub use_ld: Option<String>,
48    /// `-L<dir>`, in order, because the linker takes the first library it finds.
49    pub search: Vec<PathBuf>,
50    /// `-Wl,<arg>` and `-Xlinker <arg>`, in order, passed through untouched.
51    pub passthrough: Vec<String>,
52    /// `-B<prefix>`, which is where to look for the linker before looking on the path.
53    pub prefixes: Vec<PathBuf>,
54    /// `--sysroot=<dir>`, which prefixes the directories this looks in.
55    pub sysroot: Option<PathBuf>,
56    /// `-static`.
57    pub is_static: bool,
58    /// `-shared`.
59    pub shared: bool,
60    /// `-pie` or `-no-pie`, and the platform's default when neither was written.
61    pub pie: Option<bool>,
62    /// `-nostdlib`, which is `-nostartfiles` and `-nodefaultlibs` together.
63    pub no_stdlib: bool,
64    /// `-nostartfiles`.
65    pub no_startfiles: bool,
66    /// `-nodefaultlibs`.
67    pub no_defaultlibs: bool,
68    /// `-rdynamic`, which puts every symbol in the dynamic table so a program can look itself up.
69    pub export_dynamic: bool,
70    /// `-s`, which drops the symbol table.
71    pub strip: bool,
72}
73
74impl LinkOptions {
75    /// Whether the startup files go on the line.
76    fn wants_startfiles(&self) -> bool {
77        !self.no_stdlib && !self.no_startfiles
78    }
79
80    /// Whether the library the program was written against goes on the line.
81    fn wants_defaultlibs(&self) -> bool {
82        !self.no_stdlib && !self.no_defaultlibs
83    }
84}
85
86/// One item on the link line, in the order it was written, because link order is semantic.
87///
88/// A library named before the object that needs it is not found on a static link, which is the
89/// oldest surprise in the toolchain and the reason this is one ordered list rather than a list of
90/// files and a list of libraries.
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub enum Item {
93    /// A file: an object this compilation produced, or one named on the command line.
94    File(String),
95    /// `-l<name>`, which the linker resolves against its search path.
96    Library(String),
97}
98
99impl std::fmt::Display for Item {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        match self {
102            Item::File(path) => f.write_str(path),
103            Item::Library(name) => write!(f, "-l{name}"),
104        }
105    }
106}
107
108/// Why a link could not be run.
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub enum Error {
111    /// No linker was found, after looking everywhere there was to look.
112    NoLinker {
113        /// The names that were tried, in the order they were tried.
114        tried: Vec<String>,
115    },
116    /// `-fuse-ld=` named one that is not on this machine.
117    Named {
118        /// What it named.
119        name: String,
120    },
121    /// A target this does not know how to build a link line for.
122    Target {
123        /// The triple that was asked for.
124        triple: String,
125    },
126    /// The linker was found and could not be started.
127    Spawn {
128        /// Where it was.
129        path: String,
130        /// What the operating system said.
131        why: String,
132    },
133    /// The linker ran and said no.
134    Refused {
135        /// What it exited with, or a description when it was killed instead.
136        status: String,
137    },
138}
139
140impl std::fmt::Display for Error {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        match self {
143            Error::NoLinker { tried } => {
144                write!(f, "no linker was found; tried {}", tried.join(", "))
145            }
146            Error::Named { name } => {
147                write!(f, "-fuse-ld={name} asks for a linker that is not on this machine")
148            }
149            Error::Target { triple } => {
150                write!(f, "there is no link line for {triple} in this compiler yet")
151            }
152            Error::Spawn { path, why } => write!(f, "could not run the linker at {path}: {why}"),
153            Error::Refused { status } => write!(f, "the linker {status}"),
154        }
155    }
156}
157
158impl std::error::Error for Error {}
159
160/// A linker, found.
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct Linker {
163    /// The name it is known by, which is what `--print-config` reports.
164    pub name: String,
165    /// Where it is, which is what gets spawned.
166    pub path: PathBuf,
167}
168
169/// The names to look for, in the order section 4.9 gives.
170///
171/// `mold` first because it is dramatically faster, and a compiler that is twice the speed of
172/// another one while the link takes twelve seconds has not helped anybody. Then `lld`, then the
173/// platform's own. Each is looked for under both the bare name and the `ld.` prefix, because a
174/// distribution installs `mold` under its own name and `ld.mold` for exactly this lookup.
175#[must_use]
176pub fn order(target: Triple, opts: &LinkOptions) -> Vec<String> {
177    if let Some(named) = &opts.use_ld {
178        // A name rather than a path, so `-fuse-ld=mold` finds a `mold` that is not `ld.mold`.
179        return vec![format!("ld.{named}"), named.clone()];
180    }
181    match target.os {
182        Os::Windows => vec!["lld-link".to_owned(), "link.exe".to_owned()],
183        _ => vec![
184            "ld.mold".to_owned(),
185            "mold".to_owned(),
186            "ld.lld".to_owned(),
187            "lld".to_owned(),
188            "ld".to_owned(),
189        ],
190    }
191}
192
193/// The linker to use, looked for where a linker is.
194///
195/// `-B` prefixes first, since the point of one is to put a toolchain in front of the machine's,
196/// then the path. A name that contains a separator is a path and is taken as one, which is what
197/// gcc does with `-fuse-ld=/usr/bin/ld.gold` and what a build system relying on that expects.
198///
199/// # Errors
200///
201/// [`Error::Named`] when `-fuse-ld=` asked for one that is not here, and [`Error::NoLinker`] when
202/// nothing was, which name the candidates so that the message says what was looked for.
203pub fn find(target: Triple, opts: &LinkOptions) -> Result<Linker, Error> {
204    let tried = order(target, opts);
205    for name in &tried {
206        if name.contains(std::path::MAIN_SEPARATOR) || name.contains('/') {
207            let path = PathBuf::from(name);
208            if path.is_file() {
209                return Ok(Linker { name: name.clone(), path });
210            }
211            continue;
212        }
213        for dir in &opts.prefixes {
214            let path = dir.join(name);
215            if path.is_file() {
216                return Ok(Linker { name: name.clone(), path });
217            }
218        }
219        if let Some(path) = on_path(name) {
220            return Ok(Linker { name: name.clone(), path });
221        }
222    }
223    match &opts.use_ld {
224        Some(name) => Err(Error::Named { name: name.clone() }),
225        None => Err(Error::NoLinker { tried }),
226    }
227}
228
229/// The first executable of that name on `PATH`.
230///
231/// Executability is checked rather than assumed, because a directory of that name on `PATH` is
232/// not a thing to try to run and neither is a file nobody may execute.
233fn on_path(name: &str) -> Option<PathBuf> {
234    let path = std::env::var_os("PATH")?;
235    std::env::split_paths(&path).map(|dir| dir.join(name)).find(|p| executable(p))
236}
237
238/// Whether a path is a file this process could run.
239#[cfg(unix)]
240fn executable(path: &Path) -> bool {
241    use std::os::unix::fs::PermissionsExt as _;
242    path.metadata().is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
243}
244
245/// Whether a path is a file this process could run.
246///
247/// Windows has no executable bit and decides by extension, and the names looked for above carry
248/// theirs, so being a file is the whole of the question here.
249#[cfg(not(unix))]
250fn executable(path: &Path) -> bool {
251    path.is_file()
252}
253
254/// What the linker is told, in order, not counting the linker itself.
255///
256/// # Errors
257///
258/// [`Error::Target`] for a platform there is no line for yet, which is every one but Linux.
259pub fn line(
260    target: Triple,
261    opts: &LinkOptions,
262    items: &[Item],
263    output: &str,
264) -> Result<Vec<String>, Error> {
265    if target.os != Os::Linux {
266        return Err(Error::Target { triple: target.to_string() });
267    }
268    let machine = emulation(target);
269    let root = opts.sysroot.as_deref();
270    let dirs = library_dirs(target, root);
271    let mut args = vec![
272        "-o".to_owned(),
273        output.to_owned(),
274        // Which of the several formats one `ld` can write is meant. A linker built for more than
275        // one machine guesses from its first input otherwise, and a link of no objects at all has
276        // nothing to guess from.
277        "-m".to_owned(),
278        machine.to_owned(),
279        // The table a program unwinds through, which a C program with no exceptions in it still
280        // needs because `backtrace` and every crash handler read it.
281        "--eh-frame-hdr".to_owned(),
282        // The symbol hash a dynamic loader from this century reads. The old one is still written
283        // alongside by default on some distributions, and asking for this one is what stops a link
284        // from carrying a table nothing has needed since 2006.
285        "--hash-style=gnu".to_owned(),
286    ];
287
288    let pie = opts.pie.unwrap_or(!opts.is_static && !opts.shared);
289    if opts.shared {
290        args.push("-shared".to_owned());
291    } else if opts.is_static {
292        args.push("-static".to_owned());
293    } else if pie {
294        args.push("-pie".to_owned());
295    } else {
296        args.push("-no-pie".to_owned());
297    }
298    if !opts.is_static && !opts.shared {
299        args.push("-dynamic-linker".to_owned());
300        args.push(target_path(root, loader(target)));
301    }
302    if opts.export_dynamic {
303        args.push("--export-dynamic".to_owned());
304    }
305    if opts.strip {
306        args.push("-s".to_owned());
307    }
308
309    // The startup file the C library brings, which is what calls `main` and what passes it the
310    // arguments. `Scrt1.o` rather than `crt1.o` when the result moves, because the two differ in
311    // whether the reference to `main` in them is one a loader may relocate.
312    if opts.wants_startfiles() {
313        let first = if opts.shared {
314            None
315        } else if pie {
316            Some("Scrt1.o")
317        } else {
318            Some("crt1.o")
319        };
320        for name in first.into_iter().chain(["crti.o"]) {
321            if let Some(path) = find_file(&dirs, name) {
322                args.push(path.display().to_string());
323            }
324        }
325    }
326
327    for dir in &opts.search {
328        args.push(format!("-L{}", dir.display()));
329    }
330    for dir in &dirs {
331        args.push(format!("-L{}", dir.display()));
332    }
333
334    for item in items {
335        match item {
336            Item::File(path) => args.push(path.clone()),
337            Item::Library(name) => args.push(format!("-l{name}")),
338        }
339    }
340    // After the objects, because a static archive is searched for what is undefined at the point
341    // it is reached and a library named before the object that needs it contributes nothing.
342    if opts.wants_defaultlibs() {
343        args.push("-lc".to_owned());
344    }
345    if opts.wants_startfiles() {
346        if let Some(path) = find_file(&dirs, "crtn.o") {
347            args.push(path.display().to_string());
348        }
349    }
350
351    // Last, so that anything the user said wins over anything decided above, which is what
352    // `-Wl,` is for.
353    args.extend(opts.passthrough.iter().cloned());
354    Ok(args)
355}
356
357/// Which output format this `ld` should write, in the name `ld` knows it by.
358fn emulation(target: Triple) -> &'static str {
359    match target.arch {
360        Arch::X86_64 => "elf_x86_64",
361        Arch::Aarch64 => "aarch64linux",
362        Arch::Riscv64 => "elf64lriscv",
363    }
364}
365
366/// The program that starts a dynamically linked program, whose path is part of the file.
367///
368/// It is a per-target constant rather than something to look for, because the name is fixed by
369/// the platform's ABI and a program naming a different one does not start.
370fn loader(target: Triple) -> &'static str {
371    match (target.arch, target.env) {
372        (Arch::X86_64, Env::Musl) => "/lib/ld-musl-x86_64.so.1",
373        (Arch::X86_64, _) => "/lib64/ld-linux-x86-64.so.2",
374        (Arch::Aarch64, Env::Musl) => "/lib/ld-musl-aarch64.so.1",
375        (Arch::Aarch64, _) => "/lib/ld-linux-aarch64.so.1",
376        (Arch::Riscv64, Env::Musl) => "/lib/ld-musl-riscv64.so.1",
377        (Arch::Riscv64, _) => "/lib/ld-linux-riscv64-lp64d.so.1",
378    }
379}
380
381/// Where the library's own files might be, in search order.
382///
383/// The multiarch directory first for the reason it comes first in the header search: it is where
384/// a distribution that can hold two architectures at once puts the one being asked for, and a
385/// distribution that cannot simply does not have it. `lib64` after it, which is what the
386/// distributions that split by word size use instead, and `lib` last, which is every other one.
387#[must_use]
388pub fn candidates(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
389    let libc = match target.env {
390        Env::Musl => "musl",
391        Env::None | Env::Gnu | Env::Msvc => "gnu",
392    };
393    let multiarch = format!("{}-linux-{libc}", target.arch.as_str());
394    [
395        format!("/usr/lib/{multiarch}"),
396        format!("/lib/{multiarch}"),
397        "/usr/lib64".to_owned(),
398        "/lib64".to_owned(),
399        "/usr/lib".to_owned(),
400        "/lib".to_owned(),
401    ]
402    .into_iter()
403    .map(|dir| under(sysroot, &dir))
404    .collect()
405}
406
407/// The candidates that are there.
408fn library_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
409    candidates(target, sysroot).into_iter().filter(|dir| dir.is_dir()).collect()
410}
411
412/// The first of those directories holding a file of that name.
413fn find_file(dirs: &[PathBuf], name: &str) -> Option<PathBuf> {
414    dirs.iter().map(|dir| dir.join(name)).find(|path| path.is_file())
415}
416
417/// A path under the sysroot, when there is one.
418fn under(sysroot: Option<&Path>, path: &str) -> PathBuf {
419    match sysroot {
420        // `strip_prefix` because joining an absolute path replaces the root rather than extending
421        // it, which would make every entry the unprefixed one.
422        Some(root) => root.join(path.strip_prefix('/').unwrap_or(path)),
423        None => PathBuf::from(path),
424    }
425}
426
427/// A path on the machine that will run the program, rather than on the one compiling it.
428///
429/// Written with the separator of the target and not of the host, which matters for the one path
430/// that is not looked at here but stored in the file and read by something else later: the loader
431/// a dynamic program names. A Windows host joining it would put a backslash in the middle of a
432/// name that a Linux loader has to find, and the program would not start.
433fn target_path(sysroot: Option<&Path>, path: &str) -> String {
434    match sysroot {
435        Some(root) => {
436            let root = root.display().to_string();
437            format!("{}/{}", root.trim_end_matches(['/', '\\']), path.trim_start_matches('/'))
438        }
439        None => path.to_owned(),
440    }
441}
442
443/// The whole invocation as one line, quoted the way `-###` prints it.
444#[must_use]
445pub fn render(linker: &Linker, args: &[String]) -> String {
446    let mut out = linker.path.display().to_string();
447    for arg in args {
448        out.push(' ');
449        if arg.is_empty() || arg.contains(char::is_whitespace) {
450            out.push('"');
451            out.push_str(arg);
452            out.push('"');
453        } else {
454            out.push_str(arg);
455        }
456    }
457    out
458}
459
460/// Runs the linker and waits for it.
461///
462/// # Errors
463///
464/// [`Error::Spawn`] when it could not be started, which is a machine problem, and
465/// [`Error::Refused`] when it ran and said no, which is a program problem and one the linker has
466/// already explained on its own error output.
467pub fn run(linker: &Linker, args: &[String]) -> Result<(), Error> {
468    let args: Vec<OsString> = args.iter().map(OsString::from).collect();
469    let status = Command::new(&linker.path).args(&args).status().map_err(|why| Error::Spawn {
470        path: linker.path.display().to_string(),
471        why: why.to_string(),
472    })?;
473    if status.success() {
474        return Ok(());
475    }
476    // Nothing is added to what the linker printed. It has already named the symbol or the file,
477    // and a second message from here saying that linking failed would only push the first one
478    // further up the screen.
479    Err(Error::Refused {
480        status: match status.code() {
481            Some(code) => format!("exited with status {code}"),
482            None => "was killed before it finished".to_owned(),
483        },
484    })
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490
491    fn linux() -> Triple {
492        Triple::new(Arch::X86_64, Os::Linux, Env::Gnu)
493    }
494
495    fn one(name: &str) -> Vec<Item> {
496        vec![Item::File(name.to_owned())]
497    }
498
499    #[test]
500    fn the_fast_one_is_looked_for_first_and_the_platforms_own_last() {
501        let names = order(linux(), &LinkOptions::default());
502        assert_eq!(names.first().map(String::as_str), Some("ld.mold"));
503        assert_eq!(names.last().map(String::as_str), Some("ld"));
504    }
505
506    #[test]
507    fn naming_one_is_the_whole_of_the_order() {
508        let opts = LinkOptions { use_ld: Some("gold".to_owned()), ..LinkOptions::default() };
509        assert_eq!(order(linux(), &opts), ["ld.gold", "gold"]);
510    }
511
512    #[test]
513    fn a_dynamic_program_names_the_loader_that_will_start_it() {
514        let args = line(linux(), &LinkOptions::default(), &one("a.o"), "a.out").expect("a line");
515        let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
516        assert!(args[at + 1].ends_with("/lib64/ld-linux-x86-64.so.2"), "{args:?}");
517    }
518
519    #[test]
520    fn a_static_program_names_no_loader_because_nothing_will_start_it() {
521        let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
522        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
523        assert!(args.contains(&"-static".to_owned()), "{args:?}");
524        assert!(!args.contains(&"-dynamic-linker".to_owned()), "{args:?}");
525    }
526
527    #[test]
528    fn the_startup_file_of_a_program_that_moves_is_not_the_one_of_a_program_that_does_not() {
529        let moving = LinkOptions { pie: Some(true), ..LinkOptions::default() };
530        let fixed = LinkOptions { pie: Some(false), ..LinkOptions::default() };
531        let named = |opts: &LinkOptions| {
532            line(linux(), opts, &one("a.o"), "a.out")
533                .expect("a line")
534                .iter()
535                .filter_map(|a| Path::new(a).file_name().map(|n| n.to_string_lossy().into_owned()))
536                .find(|n| n.ends_with("crt1.o"))
537        };
538        // Only when the machine running this has them, which is what makes this two assertions
539        // rather than one: a machine with no glibc development files has neither to find.
540        if let Some(name) = named(&moving) {
541            assert_eq!(name, "Scrt1.o");
542            assert_eq!(named(&fixed).as_deref(), Some("crt1.o"));
543        }
544    }
545
546    #[test]
547    fn asking_for_no_startup_files_leaves_out_both_ends_of_them() {
548        let opts = LinkOptions { no_startfiles: true, ..LinkOptions::default() };
549        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
550        assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
551        assert!(!args.iter().any(|a| a.ends_with("crtn.o")), "{args:?}");
552        // And still links against the library, because that is the other flag.
553        assert!(args.contains(&"-lc".to_owned()), "{args:?}");
554    }
555
556    #[test]
557    fn asking_for_no_library_at_all_leaves_out_the_startup_files_too() {
558        let opts = LinkOptions { no_stdlib: true, ..LinkOptions::default() };
559        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
560        assert!(!args.contains(&"-lc".to_owned()), "{args:?}");
561        assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
562    }
563
564    #[test]
565    fn the_library_comes_after_the_objects_that_need_it() {
566        let items = vec![Item::File("a.o".to_owned()), Item::Library("m".to_owned())];
567        let args = line(linux(), &LinkOptions::default(), &items, "a.out").expect("a line");
568        let obj = args.iter().position(|a| a == "a.o").expect("the object");
569        let m = args.iter().position(|a| a == "-lm").expect("the library");
570        let c = args.iter().position(|a| a == "-lc").expect("the library");
571        assert!(obj < m && m < c, "{args:?}");
572    }
573
574    #[test]
575    fn what_the_user_told_the_linker_comes_after_what_this_told_it() {
576        let opts = LinkOptions {
577            passthrough: vec!["--no-eh-frame-hdr".to_owned()],
578            ..LinkOptions::default()
579        };
580        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
581        assert_eq!(args.last().map(String::as_str), Some("--no-eh-frame-hdr"));
582    }
583
584    #[test]
585    fn a_sysroot_moves_every_path_this_decided_and_none_the_user_wrote() {
586        let opts = LinkOptions {
587            sysroot: Some(PathBuf::from("/nowhere-at-all")),
588            search: vec![PathBuf::from("/opt/mine")],
589            ..LinkOptions::default()
590        };
591        let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
592        let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
593        assert_eq!(args[at + 1], "/nowhere-at-all/lib64/ld-linux-x86-64.so.2");
594        assert!(args.contains(&"-L/opt/mine".to_owned()), "{args:?}");
595    }
596
597    #[test]
598    fn a_platform_with_no_link_line_is_said_so_rather_than_linked_wrongly() {
599        for triple in [
600            Triple::new(Arch::X86_64, Os::Darwin, Env::Gnu),
601            Triple::new(Arch::X86_64, Os::Windows, Env::Msvc),
602        ] {
603            let error = line(triple, &LinkOptions::default(), &one("a.o"), "a.out")
604                .expect_err("no line for it");
605            assert!(matches!(error, Error::Target { .. }), "{error:?}");
606        }
607    }
608
609    #[test]
610    fn the_line_is_printed_the_way_it_would_be_typed() {
611        let linker = Linker { name: "ld".to_owned(), path: PathBuf::from("/usr/bin/ld") };
612        let args = ["-o".to_owned(), "a b".to_owned()];
613        assert_eq!(render(&linker, &args), "/usr/bin/ld -o \"a b\"");
614    }
615
616    #[test]
617    fn a_linker_that_is_not_there_is_said_by_name() {
618        let opts = LinkOptions {
619            use_ld: Some("a-linker-nobody-has".to_owned()),
620            ..LinkOptions::default()
621        };
622        let error = find(linux(), &opts).expect_err("not on this machine");
623        assert_eq!(error, Error::Named { name: "a-linker-nobody-has".to_owned() });
624    }
625}