1use std::ffi::OsString;
34use std::path::{Path, PathBuf};
35use std::process::Command;
36
37use rucc_target::{Arch, Env, Os, Triple};
38
39#[derive(Debug, Default, Clone, PartialEq, Eq)]
45pub struct LinkOptions {
46 pub use_ld: Option<String>,
48 pub search: Vec<PathBuf>,
50 pub passthrough: Vec<String>,
52 pub prefixes: Vec<PathBuf>,
54 pub sysroot: Option<PathBuf>,
56 pub is_static: bool,
58 pub shared: bool,
60 pub pie: Option<bool>,
62 pub no_stdlib: bool,
64 pub no_startfiles: bool,
66 pub no_defaultlibs: bool,
68 pub export_dynamic: bool,
70 pub strip: bool,
72}
73
74impl LinkOptions {
75 fn wants_startfiles(&self) -> bool {
77 !self.no_stdlib && !self.no_startfiles
78 }
79
80 fn wants_defaultlibs(&self) -> bool {
82 !self.no_stdlib && !self.no_defaultlibs
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
92pub enum Item {
93 File(String),
95 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#[derive(Debug, Clone, PartialEq, Eq)]
110pub enum Error {
111 NoLinker {
113 tried: Vec<String>,
115 },
116 Named {
118 name: String,
120 },
121 Target {
123 triple: String,
125 },
126 Spawn {
128 path: String,
130 why: String,
132 },
133 Refused {
135 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#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct Linker {
163 pub name: String,
165 pub path: PathBuf,
167}
168
169#[must_use]
176pub fn order(target: Triple, opts: &LinkOptions) -> Vec<String> {
177 if let Some(named) = &opts.use_ld {
178 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
193pub 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
229fn 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#[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#[cfg(not(unix))]
250fn executable(path: &Path) -> bool {
251 path.is_file()
252}
253
254pub 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 "-m".to_owned(),
278 machine.to_owned(),
279 "--eh-frame-hdr".to_owned(),
282 "--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 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 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 args.extend(opts.passthrough.iter().cloned());
354 Ok(args)
355}
356
357fn 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
366fn 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#[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
407fn library_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
409 candidates(target, sysroot).into_iter().filter(|dir| dir.is_dir()).collect()
410}
411
412fn find_file(dirs: &[PathBuf], name: &str) -> Option<PathBuf> {
414 dirs.iter().map(|dir| dir.join(name)).find(|path| path.is_file())
415}
416
417fn under(sysroot: Option<&Path>, path: &str) -> PathBuf {
419 match sysroot {
420 Some(root) => root.join(path.strip_prefix('/').unwrap_or(path)),
423 None => PathBuf::from(path),
424 }
425}
426
427fn 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#[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
460pub 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 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 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 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}