1use std::ffi::OsString;
42use std::fs;
43use std::path::{Path, PathBuf};
44use std::process::Command;
45
46use rucc_target::{Arch, Env, Os, Triple};
47
48#[derive(Debug, Default, Clone, PartialEq, Eq)]
54pub struct LinkOptions {
55 pub use_ld: Option<String>,
57 pub search: Vec<PathBuf>,
59 pub passthrough: Vec<String>,
61 pub prefixes: Vec<PathBuf>,
63 pub sysroot: Option<PathBuf>,
65 pub is_static: bool,
67 pub shared: bool,
69 pub pie: Option<bool>,
71 pub no_stdlib: bool,
73 pub no_startfiles: bool,
75 pub no_defaultlibs: bool,
77 pub export_dynamic: bool,
79 pub strip: bool,
81 pub no_builtins_lib: bool,
84 pub profile: bool,
91}
92
93impl LinkOptions {
94 fn wants_startfiles(&self) -> bool {
96 !self.no_stdlib && !self.no_startfiles
97 }
98
99 fn wants_defaultlibs(&self) -> bool {
101 !self.no_stdlib && !self.no_defaultlibs
102 }
103
104 fn wants_runtime(&self) -> bool {
110 !self.no_stdlib && !self.no_defaultlibs
111 }
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
120pub enum Item {
121 File(String),
123 Library(String),
125}
126
127impl std::fmt::Display for Item {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 match self {
130 Item::File(path) => f.write_str(path),
131 Item::Library(name) => write!(f, "-l{name}"),
132 }
133 }
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
138pub enum Error {
139 NoLinker {
141 tried: Vec<String>,
143 },
144 Named {
146 name: String,
148 },
149 Target {
151 triple: String,
153 },
154 Spawn {
156 path: String,
158 why: String,
160 },
161 Refused {
163 status: String,
165 },
166}
167
168impl std::fmt::Display for Error {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 match self {
171 Error::NoLinker { tried } => {
172 write!(f, "no linker was found; tried {}", tried.join(", "))
173 }
174 Error::Named { name } => {
175 write!(f, "-fuse-ld={name} asks for a linker that is not on this machine")
176 }
177 Error::Target { triple } => {
178 write!(f, "there is no link line for {triple} in this compiler yet")
179 }
180 Error::Spawn { path, why } => write!(f, "could not run the linker at {path}: {why}"),
181 Error::Refused { status } => write!(f, "the linker {status}"),
182 }
183 }
184}
185
186impl std::error::Error for Error {}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct Linker {
191 pub name: String,
193 pub path: PathBuf,
195}
196
197#[must_use]
204pub fn order(target: Triple, opts: &LinkOptions) -> Vec<String> {
205 if let Some(named) = &opts.use_ld {
206 return vec![format!("ld.{named}"), named.clone()];
208 }
209 match target.os {
210 Os::Windows => vec!["lld-link".to_owned(), "link.exe".to_owned()],
211 _ => vec![
212 "ld.mold".to_owned(),
213 "mold".to_owned(),
214 "ld.lld".to_owned(),
215 "lld".to_owned(),
216 "ld".to_owned(),
217 ],
218 }
219}
220
221pub fn find(target: Triple, opts: &LinkOptions) -> Result<Linker, Error> {
232 let tried = order(target, opts);
233 for name in &tried {
234 if name.contains(std::path::MAIN_SEPARATOR) || name.contains('/') {
235 let path = PathBuf::from(name);
236 if path.is_file() {
237 return Ok(Linker { name: name.clone(), path });
238 }
239 continue;
240 }
241 for dir in &opts.prefixes {
242 let path = dir.join(name);
243 if path.is_file() {
244 return Ok(Linker { name: name.clone(), path });
245 }
246 }
247 if let Some(path) = on_path(name) {
248 return Ok(Linker { name: name.clone(), path });
249 }
250 }
251 match &opts.use_ld {
252 Some(name) => Err(Error::Named { name: name.clone() }),
253 None => Err(Error::NoLinker { tried }),
254 }
255}
256
257fn on_path(name: &str) -> Option<PathBuf> {
262 let path = std::env::var_os("PATH")?;
263 std::env::split_paths(&path).map(|dir| dir.join(name)).find(|p| executable(p))
264}
265
266#[cfg(unix)]
268fn executable(path: &Path) -> bool {
269 use std::os::unix::fs::PermissionsExt as _;
270 path.metadata().is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
271}
272
273#[cfg(not(unix))]
278fn executable(path: &Path) -> bool {
279 path.is_file()
280}
281
282pub fn line(
288 target: Triple,
289 opts: &LinkOptions,
290 items: &[Item],
291 output: &str,
292) -> Result<Vec<String>, Error> {
293 if target.os != Os::Linux {
294 return Err(Error::Target { triple: target.to_string() });
295 }
296 let machine = emulation(target);
297 let root = opts.sysroot.as_deref();
298 let dirs = library_dirs(target, root);
299 let runtime = runtime_dirs(target, root);
302 let ours = if opts.no_builtins_lib { None } else { builtins_archive(target, &opts.prefixes) };
303 let mut args = vec![
304 "-o".to_owned(),
305 output.to_owned(),
306 "-m".to_owned(),
310 machine.to_owned(),
311 "--eh-frame-hdr".to_owned(),
314 "--hash-style=gnu".to_owned(),
318 ];
319
320 let pie = opts.pie.unwrap_or(!opts.is_static && !opts.shared);
321 if opts.shared {
322 args.push("-shared".to_owned());
323 } else if opts.is_static {
324 args.push("-static".to_owned());
325 } else if pie {
326 args.push("-pie".to_owned());
327 } else {
328 args.push("-no-pie".to_owned());
329 }
330 if !opts.is_static && !opts.shared {
331 args.push("-dynamic-linker".to_owned());
332 args.push(target_path(root, loader(target)));
333 }
334 if opts.export_dynamic {
335 args.push("--export-dynamic".to_owned());
336 }
337 if opts.strip {
338 args.push("-s".to_owned());
339 }
340
341 if opts.wants_startfiles() {
342 for name in startfile(opts, pie).into_iter().chain(["crti.o"]) {
343 if let Some(path) = find_file(&dirs, name) {
344 args.push(path.display().to_string());
345 }
346 }
347 let begin = if opts.shared || pie {
353 "crtbeginS.o"
354 } else if opts.is_static {
355 "crtbeginT.o"
356 } else {
357 "crtbegin.o"
358 };
359 if let Some(path) = find_file(&runtime, begin).or_else(|| find_file(&runtime, "crtbegin.o"))
360 {
361 args.push(path.display().to_string());
362 }
363 }
364
365 for dir in &opts.search {
366 args.push(format!("-L{}", dir.display()));
367 }
368 for dir in &dirs {
369 args.push(format!("-L{}", dir.display()));
370 }
371 for dir in &runtime {
374 args.push(format!("-L{}", dir.display()));
375 }
376
377 for item in items {
378 match item {
379 Item::File(path) => args.push(path.clone()),
380 Item::Library(name) => args.push(format!("-l{name}")),
381 }
382 }
383 args.extend(runtime_items(opts, &runtime, ours.as_deref()));
386
387 if opts.wants_startfiles() {
388 let end = if opts.shared || pie { "crtendS.o" } else { "crtend.o" };
391 if let Some(path) = find_file(&runtime, end).or_else(|| find_file(&runtime, "crtend.o")) {
392 args.push(path.display().to_string());
393 }
394 if let Some(path) = find_file(&dirs, "crtn.o") {
395 args.push(path.display().to_string());
396 }
397 }
398
399 args.extend(opts.passthrough.iter().cloned());
402 Ok(args)
403}
404
405fn startfile(opts: &LinkOptions, pie: bool) -> Option<&'static str> {
417 if opts.shared {
418 None
419 } else if opts.profile {
420 Some(if pie && opts.is_static { "grcrt1.o" } else { "gcrt1.o" })
421 } else if pie {
422 Some("Scrt1.o")
423 } else {
424 Some("crt1.o")
425 }
426}
427
428fn runtime_items(opts: &LinkOptions, runtime: &[PathBuf], ours: Option<&Path>) -> Vec<String> {
452 let mut args = Vec::new();
453 if !opts.wants_defaultlibs() && !opts.wants_runtime() {
454 return args;
455 }
456 let has_gcc = find_file(runtime, "libgcc.a").is_some();
460
461 if opts.is_static {
462 args.push("--start-group".to_owned());
463 }
464 if opts.wants_defaultlibs() {
465 args.push("-lc".to_owned());
466 }
467 if opts.wants_runtime() {
468 if let Some(path) = ours {
469 args.push(path.display().to_string());
470 }
471 if has_gcc {
472 args.push("-lgcc".to_owned());
473 if opts.is_static {
474 args.push("-lgcc_eh".to_owned());
475 }
476 }
477 }
478 if opts.is_static {
479 args.push("--end-group".to_owned());
480 } else if opts.wants_runtime() && has_gcc {
481 args.push("--as-needed".to_owned());
483 args.push("-lgcc_s".to_owned());
484 args.push("--no-as-needed".to_owned());
485 }
486 args
487}
488
489#[must_use]
496pub fn runtime_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
497 let libc = match target.env {
498 Env::Musl => "musl",
499 Env::None | Env::Gnu | Env::Msvc => "gnu",
500 };
501 let arch = target.arch.as_str();
502 let names = [
506 format!("{arch}-linux-{libc}"),
507 format!("{arch}-pc-linux-{libc}"),
508 format!("{arch}-redhat-linux"),
509 format!("{arch}-suse-linux"),
510 format!("{arch}-alpine-linux-{libc}"),
511 ];
512 let mut found = Vec::new();
513 for base in ["/usr/lib/gcc", "/usr/lib64/gcc", "/usr/local/lib/gcc"] {
514 for name in &names {
515 let dir = under(sysroot, &format!("{base}/{name}"));
516 let Ok(entries) = fs::read_dir(&dir) else { continue };
517 let mut versions: Vec<(Vec<u64>, PathBuf)> = entries
518 .flatten()
519 .map(|e| e.path())
520 .filter(|p| p.is_dir())
521 .map(|p| (version_key(&p), p))
522 .collect();
523 versions.sort_by(|a, b| b.0.cmp(&a.0));
527 found.extend(versions.into_iter().map(|(_, path)| path));
528 }
529 }
530 found
531}
532
533fn version_key(dir: &Path) -> Vec<u64> {
538 let name = dir.file_name().unwrap_or_default().to_string_lossy();
539 name.split('.').map(|part| part.parse::<u64>().unwrap_or(0)).collect()
540}
541
542#[must_use]
548pub fn builtins_archive(target: Triple, prefixes: &[PathBuf]) -> Option<PathBuf> {
549 const NAME: &str = "librucc_builtins.a";
550 let triple = target.to_string();
551 let mut places: Vec<PathBuf> = Vec::new();
552 for prefix in prefixes {
553 places.push(prefix.join(&triple).join(NAME));
554 places.push(prefix.join(NAME));
555 }
556 if let Some(dir) =
557 std::env::current_exe().ok().and_then(|exe| exe.parent().map(Path::to_path_buf))
558 {
559 if let Some(up) = dir.parent() {
561 places.push(up.join("lib").join("rucc").join(&triple).join(NAME));
562 for profile in ["release", "debug"] {
565 places.push(up.join(&triple).join(profile).join(NAME));
566 }
567 }
568 places.push(dir.join(NAME));
569 }
570 places.into_iter().find(|path| path.is_file())
571}
572
573fn emulation(target: Triple) -> &'static str {
575 match target.arch {
576 Arch::X86_64 => "elf_x86_64",
577 Arch::Aarch64 => "aarch64linux",
578 Arch::Riscv64 => "elf64lriscv",
579 }
580}
581
582fn loader(target: Triple) -> &'static str {
587 match (target.arch, target.env) {
588 (Arch::X86_64, Env::Musl) => "/lib/ld-musl-x86_64.so.1",
589 (Arch::X86_64, _) => "/lib64/ld-linux-x86-64.so.2",
590 (Arch::Aarch64, Env::Musl) => "/lib/ld-musl-aarch64.so.1",
591 (Arch::Aarch64, _) => "/lib/ld-linux-aarch64.so.1",
592 (Arch::Riscv64, Env::Musl) => "/lib/ld-musl-riscv64.so.1",
593 (Arch::Riscv64, _) => "/lib/ld-linux-riscv64-lp64d.so.1",
594 }
595}
596
597#[must_use]
604pub fn candidates(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
605 let multiarch = multiarch(target);
606 [
607 format!("/usr/lib/{multiarch}"),
608 format!("/lib/{multiarch}"),
609 "/usr/lib64".to_owned(),
610 "/lib64".to_owned(),
611 "/usr/lib".to_owned(),
612 "/lib".to_owned(),
613 ]
614 .into_iter()
615 .map(|dir| under(sysroot, &dir))
616 .collect()
617}
618
619#[must_use]
624pub fn multiarch(target: Triple) -> String {
625 let libc = match target.env {
626 Env::Musl => "musl",
627 Env::None | Env::Gnu | Env::Msvc => "gnu",
628 };
629 format!("{}-linux-{libc}", target.arch.as_str())
630}
631
632fn library_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
634 candidates(target, sysroot).into_iter().filter(|dir| dir.is_dir()).collect()
635}
636
637#[must_use]
642pub fn search_dirs(link: &LinkOptions, target: Triple) -> Vec<PathBuf> {
643 let mut dirs = link.search.clone();
644 dirs.extend(candidates(target, link.sysroot.as_deref()));
645 dirs
646}
647
648#[must_use]
653pub fn find_in_search(link: &LinkOptions, target: Triple, name: &str) -> Option<PathBuf> {
654 find_file(&search_dirs(link, target), name)
655}
656
657fn find_file(dirs: &[PathBuf], name: &str) -> Option<PathBuf> {
659 dirs.iter().map(|dir| dir.join(name)).find(|path| path.is_file())
660}
661
662fn under(sysroot: Option<&Path>, path: &str) -> PathBuf {
664 match sysroot {
665 Some(root) => root.join(path.strip_prefix('/').unwrap_or(path)),
668 None => PathBuf::from(path),
669 }
670}
671
672fn target_path(sysroot: Option<&Path>, path: &str) -> String {
679 match sysroot {
680 Some(root) => {
681 let root = root.display().to_string();
682 format!("{}/{}", root.trim_end_matches(['/', '\\']), path.trim_start_matches('/'))
683 }
684 None => path.to_owned(),
685 }
686}
687
688#[must_use]
690pub fn render(linker: &Linker, args: &[String]) -> String {
691 let mut out = linker.path.display().to_string();
692 for arg in args {
693 out.push(' ');
694 if arg.is_empty() || arg.contains(char::is_whitespace) {
695 out.push('"');
696 out.push_str(arg);
697 out.push('"');
698 } else {
699 out.push_str(arg);
700 }
701 }
702 out
703}
704
705pub fn run(linker: &Linker, args: &[String]) -> Result<(), Error> {
713 let args: Vec<OsString> = args.iter().map(OsString::from).collect();
714 let status = Command::new(&linker.path).args(&args).status().map_err(|why| Error::Spawn {
715 path: linker.path.display().to_string(),
716 why: why.to_string(),
717 })?;
718 if status.success() {
719 return Ok(());
720 }
721 Err(Error::Refused {
725 status: match status.code() {
726 Some(code) => format!("exited with status {code}"),
727 None => "was killed before it finished".to_owned(),
728 },
729 })
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735
736 fn linux() -> Triple {
737 Triple::new(Arch::X86_64, Os::Linux, Env::Gnu)
738 }
739
740 fn one(name: &str) -> Vec<Item> {
741 vec![Item::File(name.to_owned())]
742 }
743
744 #[test]
745 fn the_fast_one_is_looked_for_first_and_the_platforms_own_last() {
746 let names = order(linux(), &LinkOptions::default());
747 assert_eq!(names.first().map(String::as_str), Some("ld.mold"));
748 assert_eq!(names.last().map(String::as_str), Some("ld"));
749 }
750
751 #[test]
752 fn naming_one_is_the_whole_of_the_order() {
753 let opts = LinkOptions { use_ld: Some("gold".to_owned()), ..LinkOptions::default() };
754 assert_eq!(order(linux(), &opts), ["ld.gold", "gold"]);
755 }
756
757 #[test]
758 fn a_dynamic_program_names_the_loader_that_will_start_it() {
759 let args = line(linux(), &LinkOptions::default(), &one("a.o"), "a.out").expect("a line");
760 let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
761 assert!(args[at + 1].ends_with("/lib64/ld-linux-x86-64.so.2"), "{args:?}");
762 }
763
764 #[test]
765 fn a_static_program_names_no_loader_because_nothing_will_start_it() {
766 let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
767 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
768 assert!(args.contains(&"-static".to_owned()), "{args:?}");
769 assert!(!args.contains(&"-dynamic-linker".to_owned()), "{args:?}");
770 }
771
772 #[test]
773 fn the_startup_file_of_a_program_that_moves_is_not_the_one_of_a_program_that_does_not() {
774 let moving = LinkOptions { pie: Some(true), ..LinkOptions::default() };
775 let fixed = LinkOptions { pie: Some(false), ..LinkOptions::default() };
776 let named = |opts: &LinkOptions| {
777 line(linux(), opts, &one("a.o"), "a.out")
778 .expect("a line")
779 .iter()
780 .filter_map(|a| Path::new(a).file_name().map(|n| n.to_string_lossy().into_owned()))
781 .find(|n| n.ends_with("crt1.o"))
782 };
783 if let Some(name) = named(&moving) {
786 assert_eq!(name, "Scrt1.o");
787 assert_eq!(named(&fixed).as_deref(), Some("crt1.o"));
788 }
789 }
790
791 #[test]
800 fn a_profiled_program_is_started_by_the_startup_file_that_counts() {
801 let profile = LinkOptions { profile: true, ..LinkOptions::default() };
802 assert_eq!(startfile(&profile, false), Some("gcrt1.o"));
803 assert_eq!(startfile(&profile, true), Some("gcrt1.o"));
804 let still = LinkOptions { is_static: true, ..profile.clone() };
805 assert_eq!(startfile(&still, true), Some("grcrt1.o"));
806 assert_eq!(startfile(&still, false), Some("gcrt1.o"));
807 let shared = LinkOptions { shared: true, ..profile };
808 assert_eq!(startfile(&shared, false), None);
809 }
810
811 #[test]
813 fn a_program_that_is_not_profiled_is_started_by_the_usual_one() {
814 let plain = LinkOptions::default();
815 assert_eq!(startfile(&plain, false), Some("crt1.o"));
816 assert_eq!(startfile(&plain, true), Some("Scrt1.o"));
817 }
818
819 #[test]
820 fn asking_for_no_startup_files_leaves_out_both_ends_of_them() {
821 let opts = LinkOptions { no_startfiles: true, ..LinkOptions::default() };
822 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
823 assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
824 assert!(!args.iter().any(|a| a.ends_with("crtn.o")), "{args:?}");
825 assert!(args.contains(&"-lc".to_owned()), "{args:?}");
827 }
828
829 #[test]
830 fn asking_for_no_library_at_all_leaves_out_the_startup_files_too() {
831 let opts = LinkOptions { no_stdlib: true, ..LinkOptions::default() };
832 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
833 assert!(!args.contains(&"-lc".to_owned()), "{args:?}");
834 assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
835 }
836
837 #[test]
838 fn the_library_comes_after_the_objects_that_need_it() {
839 let items = vec![Item::File("a.o".to_owned()), Item::Library("m".to_owned())];
840 let args = line(linux(), &LinkOptions::default(), &items, "a.out").expect("a line");
841 let obj = args.iter().position(|a| a == "a.o").expect("the object");
842 let m = args.iter().position(|a| a == "-lm").expect("the library");
843 let c = args.iter().position(|a| a == "-lc").expect("the library");
844 assert!(obj < m && m < c, "{args:?}");
845 }
846
847 #[test]
848 fn what_the_user_told_the_linker_comes_after_what_this_told_it() {
849 let opts = LinkOptions {
850 passthrough: vec!["--no-eh-frame-hdr".to_owned()],
851 ..LinkOptions::default()
852 };
853 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
854 assert_eq!(args.last().map(String::as_str), Some("--no-eh-frame-hdr"));
855 }
856
857 #[test]
858 fn a_sysroot_moves_every_path_this_decided_and_none_the_user_wrote() {
859 let opts = LinkOptions {
860 sysroot: Some(PathBuf::from("/nowhere-at-all")),
861 search: vec![PathBuf::from("/opt/mine")],
862 ..LinkOptions::default()
863 };
864 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
865 let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
866 assert_eq!(args[at + 1], "/nowhere-at-all/lib64/ld-linux-x86-64.so.2");
867 assert!(args.contains(&"-L/opt/mine".to_owned()), "{args:?}");
868 }
869
870 #[test]
871 fn a_platform_with_no_link_line_is_said_so_rather_than_linked_wrongly() {
872 for triple in [
873 Triple::new(Arch::X86_64, Os::Darwin, Env::Gnu),
874 Triple::new(Arch::X86_64, Os::Windows, Env::Msvc),
875 ] {
876 let error = line(triple, &LinkOptions::default(), &one("a.o"), "a.out")
877 .expect_err("no line for it");
878 assert!(matches!(error, Error::Target { .. }), "{error:?}");
879 }
880 }
881
882 #[test]
883 fn the_line_is_printed_the_way_it_would_be_typed() {
884 let linker = Linker { name: "ld".to_owned(), path: PathBuf::from("/usr/bin/ld") };
885 let args = ["-o".to_owned(), "a b".to_owned()];
886 assert_eq!(render(&linker, &args), "/usr/bin/ld -o \"a b\"");
887 }
888
889 #[test]
890 fn a_linker_that_is_not_there_is_said_by_name() {
891 let opts = LinkOptions {
892 use_ld: Some("a-linker-nobody-has".to_owned()),
893 ..LinkOptions::default()
894 };
895 let error = find(linux(), &opts).expect_err("not on this machine");
896 assert_eq!(error, Error::Named { name: "a-linker-nobody-has".to_owned() });
897 }
898 fn a_gcc_dir(name: &str) -> PathBuf {
901 let dir = std::env::temp_dir().join(format!("rucc-link-{name}-{}", std::process::id()));
902 fs::create_dir_all(&dir).expect("a temporary directory");
903 fs::write(dir.join("libgcc.a"), b"not really an archive").expect("a file in it");
904 dir
905 }
906
907 #[test]
908 fn the_c_library_supplies_the_block_routines_and_our_runtime_does_not_displace_them() {
909 let gcc = a_gcc_dir("order");
910 let ours = PathBuf::from("/somewhere/librucc_builtins.a");
911 let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
912 let at_libc = args.iter().position(|a| a == "-lc").expect("libc");
913 let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
914 assert!(at_libc < at_ours, "{args:?}");
917 }
918
919 #[test]
920 fn a_static_link_puts_them_in_a_group_because_two_of_them_refer_to_each_other() {
921 let gcc = a_gcc_dir("group");
922 let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
923 let args = runtime_items(&opts, &[gcc], None);
924 assert_eq!(args.first().map(String::as_str), Some("--start-group"), "{args:?}");
925 assert_eq!(args.last().map(String::as_str), Some("--end-group"), "{args:?}");
926 assert!(args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
929 }
930
931 #[test]
932 fn a_dynamic_link_needs_no_group_and_asks_for_the_shared_half_only_if_something_wants_it() {
933 let gcc = a_gcc_dir("dynamic");
934 let args = runtime_items(&LinkOptions::default(), &[gcc], None);
935 assert!(!args.contains(&"--start-group".to_owned()), "{args:?}");
936 assert!(!args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
937 let at = args.iter().position(|a| a == "-lgcc_s").expect("the shared half");
938 assert_eq!(args[at - 1], "--as-needed", "{args:?}");
939 assert_eq!(args[at + 1], "--no-as-needed", "{args:?}");
940 }
941
942 #[test]
943 fn our_own_runtime_comes_before_the_machines_because_the_two_are_interchangeable() {
944 let gcc = a_gcc_dir("ours");
945 let ours = PathBuf::from("/somewhere/librucc_builtins.a");
946 let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
947 let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
948 let at_gcc = args.iter().position(|a| a == "-lgcc").expect("libgcc");
949 assert!(at_ours < at_gcc, "{args:?}");
950 }
951
952 #[test]
953 fn no_builtins_lib_leaves_ours_off_and_keeps_the_machines() {
954 let gcc = a_gcc_dir("theirs");
955 let opts = LinkOptions { no_builtins_lib: true, ..LinkOptions::default() };
956 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
957 assert!(!args.iter().any(|a| a.ends_with("librucc_builtins.a")), "{args:?}");
958 assert!(runtime_items(&opts, &[gcc], None).contains(&"-lgcc".to_owned()));
961 }
962
963 #[test]
964 fn nodefaultlibs_leaves_the_whole_runtime_off_and_not_only_the_c_library() {
965 let gcc = a_gcc_dir("none");
966 let opts = LinkOptions { no_defaultlibs: true, ..LinkOptions::default() };
967 assert!(runtime_items(&opts, &[gcc], None).is_empty());
968 }
969
970 #[test]
971 fn a_machine_with_no_gcc_on_it_gets_no_names_for_libraries_that_are_not_there() {
972 let empty = std::env::temp_dir().join("rucc-link-empty-not-a-gcc");
973 let args = runtime_items(&LinkOptions::default(), &[empty], None);
974 assert_eq!(args, ["-lc"], "{args:?}");
975 }
976
977 #[test]
978 fn a_gcc_version_directory_is_read_as_a_version_and_not_as_a_word() {
979 assert!(version_key(Path::new("/usr/lib/gcc/x/13")) > version_key(Path::new("/x/9")));
980 assert!(version_key(Path::new("/x/10.2")) > version_key(Path::new("/x/10")));
981 assert!(version_key(Path::new("/x/snapshot")) < version_key(Path::new("/x/1")));
983 }
984
985 #[test]
986 fn a_runtime_directory_that_is_not_on_this_machine_is_not_offered() {
987 let dirs = runtime_dirs(linux(), Some(Path::new("/definitely/not/a/sysroot")));
988 assert!(dirs.is_empty(), "{dirs:?}");
989 }
990}