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}
85
86impl LinkOptions {
87 fn wants_startfiles(&self) -> bool {
89 !self.no_stdlib && !self.no_startfiles
90 }
91
92 fn wants_defaultlibs(&self) -> bool {
94 !self.no_stdlib && !self.no_defaultlibs
95 }
96
97 fn wants_runtime(&self) -> bool {
103 !self.no_stdlib && !self.no_defaultlibs
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
113pub enum Item {
114 File(String),
116 Library(String),
118}
119
120impl std::fmt::Display for Item {
121 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 match self {
123 Item::File(path) => f.write_str(path),
124 Item::Library(name) => write!(f, "-l{name}"),
125 }
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
131pub enum Error {
132 NoLinker {
134 tried: Vec<String>,
136 },
137 Named {
139 name: String,
141 },
142 Target {
144 triple: String,
146 },
147 Spawn {
149 path: String,
151 why: String,
153 },
154 Refused {
156 status: String,
158 },
159}
160
161impl std::fmt::Display for Error {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 match self {
164 Error::NoLinker { tried } => {
165 write!(f, "no linker was found; tried {}", tried.join(", "))
166 }
167 Error::Named { name } => {
168 write!(f, "-fuse-ld={name} asks for a linker that is not on this machine")
169 }
170 Error::Target { triple } => {
171 write!(f, "there is no link line for {triple} in this compiler yet")
172 }
173 Error::Spawn { path, why } => write!(f, "could not run the linker at {path}: {why}"),
174 Error::Refused { status } => write!(f, "the linker {status}"),
175 }
176 }
177}
178
179impl std::error::Error for Error {}
180
181#[derive(Debug, Clone, PartialEq, Eq)]
183pub struct Linker {
184 pub name: String,
186 pub path: PathBuf,
188}
189
190#[must_use]
197pub fn order(target: Triple, opts: &LinkOptions) -> Vec<String> {
198 if let Some(named) = &opts.use_ld {
199 return vec![format!("ld.{named}"), named.clone()];
201 }
202 match target.os {
203 Os::Windows => vec!["lld-link".to_owned(), "link.exe".to_owned()],
204 _ => vec![
205 "ld.mold".to_owned(),
206 "mold".to_owned(),
207 "ld.lld".to_owned(),
208 "lld".to_owned(),
209 "ld".to_owned(),
210 ],
211 }
212}
213
214pub fn find(target: Triple, opts: &LinkOptions) -> Result<Linker, Error> {
225 let tried = order(target, opts);
226 for name in &tried {
227 if name.contains(std::path::MAIN_SEPARATOR) || name.contains('/') {
228 let path = PathBuf::from(name);
229 if path.is_file() {
230 return Ok(Linker { name: name.clone(), path });
231 }
232 continue;
233 }
234 for dir in &opts.prefixes {
235 let path = dir.join(name);
236 if path.is_file() {
237 return Ok(Linker { name: name.clone(), path });
238 }
239 }
240 if let Some(path) = on_path(name) {
241 return Ok(Linker { name: name.clone(), path });
242 }
243 }
244 match &opts.use_ld {
245 Some(name) => Err(Error::Named { name: name.clone() }),
246 None => Err(Error::NoLinker { tried }),
247 }
248}
249
250fn on_path(name: &str) -> Option<PathBuf> {
255 let path = std::env::var_os("PATH")?;
256 std::env::split_paths(&path).map(|dir| dir.join(name)).find(|p| executable(p))
257}
258
259#[cfg(unix)]
261fn executable(path: &Path) -> bool {
262 use std::os::unix::fs::PermissionsExt as _;
263 path.metadata().is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
264}
265
266#[cfg(not(unix))]
271fn executable(path: &Path) -> bool {
272 path.is_file()
273}
274
275pub fn line(
281 target: Triple,
282 opts: &LinkOptions,
283 items: &[Item],
284 output: &str,
285) -> Result<Vec<String>, Error> {
286 if target.os != Os::Linux {
287 return Err(Error::Target { triple: target.to_string() });
288 }
289 let machine = emulation(target);
290 let root = opts.sysroot.as_deref();
291 let dirs = library_dirs(target, root);
292 let runtime = runtime_dirs(target, root);
295 let ours = if opts.no_builtins_lib { None } else { builtins_archive(target, &opts.prefixes) };
296 let mut args = vec![
297 "-o".to_owned(),
298 output.to_owned(),
299 "-m".to_owned(),
303 machine.to_owned(),
304 "--eh-frame-hdr".to_owned(),
307 "--hash-style=gnu".to_owned(),
311 ];
312
313 let pie = opts.pie.unwrap_or(!opts.is_static && !opts.shared);
314 if opts.shared {
315 args.push("-shared".to_owned());
316 } else if opts.is_static {
317 args.push("-static".to_owned());
318 } else if pie {
319 args.push("-pie".to_owned());
320 } else {
321 args.push("-no-pie".to_owned());
322 }
323 if !opts.is_static && !opts.shared {
324 args.push("-dynamic-linker".to_owned());
325 args.push(target_path(root, loader(target)));
326 }
327 if opts.export_dynamic {
328 args.push("--export-dynamic".to_owned());
329 }
330 if opts.strip {
331 args.push("-s".to_owned());
332 }
333
334 if opts.wants_startfiles() {
338 let first = if opts.shared {
339 None
340 } else if pie {
341 Some("Scrt1.o")
342 } else {
343 Some("crt1.o")
344 };
345 for name in first.into_iter().chain(["crti.o"]) {
346 if let Some(path) = find_file(&dirs, name) {
347 args.push(path.display().to_string());
348 }
349 }
350 let begin = if opts.shared || pie {
356 "crtbeginS.o"
357 } else if opts.is_static {
358 "crtbeginT.o"
359 } else {
360 "crtbegin.o"
361 };
362 if let Some(path) = find_file(&runtime, begin).or_else(|| find_file(&runtime, "crtbegin.o"))
363 {
364 args.push(path.display().to_string());
365 }
366 }
367
368 for dir in &opts.search {
369 args.push(format!("-L{}", dir.display()));
370 }
371 for dir in &dirs {
372 args.push(format!("-L{}", dir.display()));
373 }
374 for dir in &runtime {
377 args.push(format!("-L{}", dir.display()));
378 }
379
380 for item in items {
381 match item {
382 Item::File(path) => args.push(path.clone()),
383 Item::Library(name) => args.push(format!("-l{name}")),
384 }
385 }
386 args.extend(runtime_items(opts, &runtime, ours.as_deref()));
389
390 if opts.wants_startfiles() {
391 let end = if opts.shared || pie { "crtendS.o" } else { "crtend.o" };
394 if let Some(path) = find_file(&runtime, end).or_else(|| find_file(&runtime, "crtend.o")) {
395 args.push(path.display().to_string());
396 }
397 if let Some(path) = find_file(&dirs, "crtn.o") {
398 args.push(path.display().to_string());
399 }
400 }
401
402 args.extend(opts.passthrough.iter().cloned());
405 Ok(args)
406}
407
408fn runtime_items(opts: &LinkOptions, runtime: &[PathBuf], ours: Option<&Path>) -> Vec<String> {
432 let mut args = Vec::new();
433 if !opts.wants_defaultlibs() && !opts.wants_runtime() {
434 return args;
435 }
436 let has_gcc = find_file(runtime, "libgcc.a").is_some();
440
441 if opts.is_static {
442 args.push("--start-group".to_owned());
443 }
444 if opts.wants_defaultlibs() {
445 args.push("-lc".to_owned());
446 }
447 if opts.wants_runtime() {
448 if let Some(path) = ours {
449 args.push(path.display().to_string());
450 }
451 if has_gcc {
452 args.push("-lgcc".to_owned());
453 if opts.is_static {
454 args.push("-lgcc_eh".to_owned());
455 }
456 }
457 }
458 if opts.is_static {
459 args.push("--end-group".to_owned());
460 } else if opts.wants_runtime() && has_gcc {
461 args.push("--as-needed".to_owned());
463 args.push("-lgcc_s".to_owned());
464 args.push("--no-as-needed".to_owned());
465 }
466 args
467}
468
469#[must_use]
476pub fn runtime_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
477 let libc = match target.env {
478 Env::Musl => "musl",
479 Env::None | Env::Gnu | Env::Msvc => "gnu",
480 };
481 let arch = target.arch.as_str();
482 let names = [
486 format!("{arch}-linux-{libc}"),
487 format!("{arch}-pc-linux-{libc}"),
488 format!("{arch}-redhat-linux"),
489 format!("{arch}-suse-linux"),
490 format!("{arch}-alpine-linux-{libc}"),
491 ];
492 let mut found = Vec::new();
493 for base in ["/usr/lib/gcc", "/usr/lib64/gcc", "/usr/local/lib/gcc"] {
494 for name in &names {
495 let dir = under(sysroot, &format!("{base}/{name}"));
496 let Ok(entries) = fs::read_dir(&dir) else { continue };
497 let mut versions: Vec<(Vec<u64>, PathBuf)> = entries
498 .flatten()
499 .map(|e| e.path())
500 .filter(|p| p.is_dir())
501 .map(|p| (version_key(&p), p))
502 .collect();
503 versions.sort_by(|a, b| b.0.cmp(&a.0));
507 found.extend(versions.into_iter().map(|(_, path)| path));
508 }
509 }
510 found
511}
512
513fn version_key(dir: &Path) -> Vec<u64> {
518 let name = dir.file_name().unwrap_or_default().to_string_lossy();
519 name.split('.').map(|part| part.parse::<u64>().unwrap_or(0)).collect()
520}
521
522#[must_use]
528pub fn builtins_archive(target: Triple, prefixes: &[PathBuf]) -> Option<PathBuf> {
529 const NAME: &str = "librucc_builtins.a";
530 let triple = target.to_string();
531 let mut places: Vec<PathBuf> = Vec::new();
532 for prefix in prefixes {
533 places.push(prefix.join(&triple).join(NAME));
534 places.push(prefix.join(NAME));
535 }
536 if let Some(dir) =
537 std::env::current_exe().ok().and_then(|exe| exe.parent().map(Path::to_path_buf))
538 {
539 if let Some(up) = dir.parent() {
541 places.push(up.join("lib").join("rucc").join(&triple).join(NAME));
542 for profile in ["release", "debug"] {
545 places.push(up.join(&triple).join(profile).join(NAME));
546 }
547 }
548 places.push(dir.join(NAME));
549 }
550 places.into_iter().find(|path| path.is_file())
551}
552
553fn emulation(target: Triple) -> &'static str {
555 match target.arch {
556 Arch::X86_64 => "elf_x86_64",
557 Arch::Aarch64 => "aarch64linux",
558 Arch::Riscv64 => "elf64lriscv",
559 }
560}
561
562fn loader(target: Triple) -> &'static str {
567 match (target.arch, target.env) {
568 (Arch::X86_64, Env::Musl) => "/lib/ld-musl-x86_64.so.1",
569 (Arch::X86_64, _) => "/lib64/ld-linux-x86-64.so.2",
570 (Arch::Aarch64, Env::Musl) => "/lib/ld-musl-aarch64.so.1",
571 (Arch::Aarch64, _) => "/lib/ld-linux-aarch64.so.1",
572 (Arch::Riscv64, Env::Musl) => "/lib/ld-musl-riscv64.so.1",
573 (Arch::Riscv64, _) => "/lib/ld-linux-riscv64-lp64d.so.1",
574 }
575}
576
577#[must_use]
584pub fn candidates(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
585 let multiarch = multiarch(target);
586 [
587 format!("/usr/lib/{multiarch}"),
588 format!("/lib/{multiarch}"),
589 "/usr/lib64".to_owned(),
590 "/lib64".to_owned(),
591 "/usr/lib".to_owned(),
592 "/lib".to_owned(),
593 ]
594 .into_iter()
595 .map(|dir| under(sysroot, &dir))
596 .collect()
597}
598
599#[must_use]
604pub fn multiarch(target: Triple) -> String {
605 let libc = match target.env {
606 Env::Musl => "musl",
607 Env::None | Env::Gnu | Env::Msvc => "gnu",
608 };
609 format!("{}-linux-{libc}", target.arch.as_str())
610}
611
612fn library_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
614 candidates(target, sysroot).into_iter().filter(|dir| dir.is_dir()).collect()
615}
616
617#[must_use]
622pub fn search_dirs(link: &LinkOptions, target: Triple) -> Vec<PathBuf> {
623 let mut dirs = link.search.clone();
624 dirs.extend(candidates(target, link.sysroot.as_deref()));
625 dirs
626}
627
628#[must_use]
633pub fn find_in_search(link: &LinkOptions, target: Triple, name: &str) -> Option<PathBuf> {
634 find_file(&search_dirs(link, target), name)
635}
636
637fn find_file(dirs: &[PathBuf], name: &str) -> Option<PathBuf> {
639 dirs.iter().map(|dir| dir.join(name)).find(|path| path.is_file())
640}
641
642fn under(sysroot: Option<&Path>, path: &str) -> PathBuf {
644 match sysroot {
645 Some(root) => root.join(path.strip_prefix('/').unwrap_or(path)),
648 None => PathBuf::from(path),
649 }
650}
651
652fn target_path(sysroot: Option<&Path>, path: &str) -> String {
659 match sysroot {
660 Some(root) => {
661 let root = root.display().to_string();
662 format!("{}/{}", root.trim_end_matches(['/', '\\']), path.trim_start_matches('/'))
663 }
664 None => path.to_owned(),
665 }
666}
667
668#[must_use]
670pub fn render(linker: &Linker, args: &[String]) -> String {
671 let mut out = linker.path.display().to_string();
672 for arg in args {
673 out.push(' ');
674 if arg.is_empty() || arg.contains(char::is_whitespace) {
675 out.push('"');
676 out.push_str(arg);
677 out.push('"');
678 } else {
679 out.push_str(arg);
680 }
681 }
682 out
683}
684
685pub fn run(linker: &Linker, args: &[String]) -> Result<(), Error> {
693 let args: Vec<OsString> = args.iter().map(OsString::from).collect();
694 let status = Command::new(&linker.path).args(&args).status().map_err(|why| Error::Spawn {
695 path: linker.path.display().to_string(),
696 why: why.to_string(),
697 })?;
698 if status.success() {
699 return Ok(());
700 }
701 Err(Error::Refused {
705 status: match status.code() {
706 Some(code) => format!("exited with status {code}"),
707 None => "was killed before it finished".to_owned(),
708 },
709 })
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715
716 fn linux() -> Triple {
717 Triple::new(Arch::X86_64, Os::Linux, Env::Gnu)
718 }
719
720 fn one(name: &str) -> Vec<Item> {
721 vec![Item::File(name.to_owned())]
722 }
723
724 #[test]
725 fn the_fast_one_is_looked_for_first_and_the_platforms_own_last() {
726 let names = order(linux(), &LinkOptions::default());
727 assert_eq!(names.first().map(String::as_str), Some("ld.mold"));
728 assert_eq!(names.last().map(String::as_str), Some("ld"));
729 }
730
731 #[test]
732 fn naming_one_is_the_whole_of_the_order() {
733 let opts = LinkOptions { use_ld: Some("gold".to_owned()), ..LinkOptions::default() };
734 assert_eq!(order(linux(), &opts), ["ld.gold", "gold"]);
735 }
736
737 #[test]
738 fn a_dynamic_program_names_the_loader_that_will_start_it() {
739 let args = line(linux(), &LinkOptions::default(), &one("a.o"), "a.out").expect("a line");
740 let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
741 assert!(args[at + 1].ends_with("/lib64/ld-linux-x86-64.so.2"), "{args:?}");
742 }
743
744 #[test]
745 fn a_static_program_names_no_loader_because_nothing_will_start_it() {
746 let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
747 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
748 assert!(args.contains(&"-static".to_owned()), "{args:?}");
749 assert!(!args.contains(&"-dynamic-linker".to_owned()), "{args:?}");
750 }
751
752 #[test]
753 fn the_startup_file_of_a_program_that_moves_is_not_the_one_of_a_program_that_does_not() {
754 let moving = LinkOptions { pie: Some(true), ..LinkOptions::default() };
755 let fixed = LinkOptions { pie: Some(false), ..LinkOptions::default() };
756 let named = |opts: &LinkOptions| {
757 line(linux(), opts, &one("a.o"), "a.out")
758 .expect("a line")
759 .iter()
760 .filter_map(|a| Path::new(a).file_name().map(|n| n.to_string_lossy().into_owned()))
761 .find(|n| n.ends_with("crt1.o"))
762 };
763 if let Some(name) = named(&moving) {
766 assert_eq!(name, "Scrt1.o");
767 assert_eq!(named(&fixed).as_deref(), Some("crt1.o"));
768 }
769 }
770
771 #[test]
772 fn asking_for_no_startup_files_leaves_out_both_ends_of_them() {
773 let opts = LinkOptions { no_startfiles: true, ..LinkOptions::default() };
774 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
775 assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
776 assert!(!args.iter().any(|a| a.ends_with("crtn.o")), "{args:?}");
777 assert!(args.contains(&"-lc".to_owned()), "{args:?}");
779 }
780
781 #[test]
782 fn asking_for_no_library_at_all_leaves_out_the_startup_files_too() {
783 let opts = LinkOptions { no_stdlib: true, ..LinkOptions::default() };
784 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
785 assert!(!args.contains(&"-lc".to_owned()), "{args:?}");
786 assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
787 }
788
789 #[test]
790 fn the_library_comes_after_the_objects_that_need_it() {
791 let items = vec![Item::File("a.o".to_owned()), Item::Library("m".to_owned())];
792 let args = line(linux(), &LinkOptions::default(), &items, "a.out").expect("a line");
793 let obj = args.iter().position(|a| a == "a.o").expect("the object");
794 let m = args.iter().position(|a| a == "-lm").expect("the library");
795 let c = args.iter().position(|a| a == "-lc").expect("the library");
796 assert!(obj < m && m < c, "{args:?}");
797 }
798
799 #[test]
800 fn what_the_user_told_the_linker_comes_after_what_this_told_it() {
801 let opts = LinkOptions {
802 passthrough: vec!["--no-eh-frame-hdr".to_owned()],
803 ..LinkOptions::default()
804 };
805 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
806 assert_eq!(args.last().map(String::as_str), Some("--no-eh-frame-hdr"));
807 }
808
809 #[test]
810 fn a_sysroot_moves_every_path_this_decided_and_none_the_user_wrote() {
811 let opts = LinkOptions {
812 sysroot: Some(PathBuf::from("/nowhere-at-all")),
813 search: vec![PathBuf::from("/opt/mine")],
814 ..LinkOptions::default()
815 };
816 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
817 let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
818 assert_eq!(args[at + 1], "/nowhere-at-all/lib64/ld-linux-x86-64.so.2");
819 assert!(args.contains(&"-L/opt/mine".to_owned()), "{args:?}");
820 }
821
822 #[test]
823 fn a_platform_with_no_link_line_is_said_so_rather_than_linked_wrongly() {
824 for triple in [
825 Triple::new(Arch::X86_64, Os::Darwin, Env::Gnu),
826 Triple::new(Arch::X86_64, Os::Windows, Env::Msvc),
827 ] {
828 let error = line(triple, &LinkOptions::default(), &one("a.o"), "a.out")
829 .expect_err("no line for it");
830 assert!(matches!(error, Error::Target { .. }), "{error:?}");
831 }
832 }
833
834 #[test]
835 fn the_line_is_printed_the_way_it_would_be_typed() {
836 let linker = Linker { name: "ld".to_owned(), path: PathBuf::from("/usr/bin/ld") };
837 let args = ["-o".to_owned(), "a b".to_owned()];
838 assert_eq!(render(&linker, &args), "/usr/bin/ld -o \"a b\"");
839 }
840
841 #[test]
842 fn a_linker_that_is_not_there_is_said_by_name() {
843 let opts = LinkOptions {
844 use_ld: Some("a-linker-nobody-has".to_owned()),
845 ..LinkOptions::default()
846 };
847 let error = find(linux(), &opts).expect_err("not on this machine");
848 assert_eq!(error, Error::Named { name: "a-linker-nobody-has".to_owned() });
849 }
850 fn a_gcc_dir(name: &str) -> PathBuf {
853 let dir = std::env::temp_dir().join(format!("rucc-link-{name}-{}", std::process::id()));
854 fs::create_dir_all(&dir).expect("a temporary directory");
855 fs::write(dir.join("libgcc.a"), b"not really an archive").expect("a file in it");
856 dir
857 }
858
859 #[test]
860 fn the_c_library_supplies_the_block_routines_and_our_runtime_does_not_displace_them() {
861 let gcc = a_gcc_dir("order");
862 let ours = PathBuf::from("/somewhere/librucc_builtins.a");
863 let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
864 let at_libc = args.iter().position(|a| a == "-lc").expect("libc");
865 let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
866 assert!(at_libc < at_ours, "{args:?}");
869 }
870
871 #[test]
872 fn a_static_link_puts_them_in_a_group_because_two_of_them_refer_to_each_other() {
873 let gcc = a_gcc_dir("group");
874 let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
875 let args = runtime_items(&opts, &[gcc], None);
876 assert_eq!(args.first().map(String::as_str), Some("--start-group"), "{args:?}");
877 assert_eq!(args.last().map(String::as_str), Some("--end-group"), "{args:?}");
878 assert!(args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
881 }
882
883 #[test]
884 fn a_dynamic_link_needs_no_group_and_asks_for_the_shared_half_only_if_something_wants_it() {
885 let gcc = a_gcc_dir("dynamic");
886 let args = runtime_items(&LinkOptions::default(), &[gcc], None);
887 assert!(!args.contains(&"--start-group".to_owned()), "{args:?}");
888 assert!(!args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
889 let at = args.iter().position(|a| a == "-lgcc_s").expect("the shared half");
890 assert_eq!(args[at - 1], "--as-needed", "{args:?}");
891 assert_eq!(args[at + 1], "--no-as-needed", "{args:?}");
892 }
893
894 #[test]
895 fn our_own_runtime_comes_before_the_machines_because_the_two_are_interchangeable() {
896 let gcc = a_gcc_dir("ours");
897 let ours = PathBuf::from("/somewhere/librucc_builtins.a");
898 let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
899 let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
900 let at_gcc = args.iter().position(|a| a == "-lgcc").expect("libgcc");
901 assert!(at_ours < at_gcc, "{args:?}");
902 }
903
904 #[test]
905 fn no_builtins_lib_leaves_ours_off_and_keeps_the_machines() {
906 let gcc = a_gcc_dir("theirs");
907 let opts = LinkOptions { no_builtins_lib: true, ..LinkOptions::default() };
908 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
909 assert!(!args.iter().any(|a| a.ends_with("librucc_builtins.a")), "{args:?}");
910 assert!(runtime_items(&opts, &[gcc], None).contains(&"-lgcc".to_owned()));
913 }
914
915 #[test]
916 fn nodefaultlibs_leaves_the_whole_runtime_off_and_not_only_the_c_library() {
917 let gcc = a_gcc_dir("none");
918 let opts = LinkOptions { no_defaultlibs: true, ..LinkOptions::default() };
919 assert!(runtime_items(&opts, &[gcc], None).is_empty());
920 }
921
922 #[test]
923 fn a_machine_with_no_gcc_on_it_gets_no_names_for_libraries_that_are_not_there() {
924 let empty = std::env::temp_dir().join("rucc-link-empty-not-a-gcc");
925 let args = runtime_items(&LinkOptions::default(), &[empty], None);
926 assert_eq!(args, ["-lc"], "{args:?}");
927 }
928
929 #[test]
930 fn a_gcc_version_directory_is_read_as_a_version_and_not_as_a_word() {
931 assert!(version_key(Path::new("/usr/lib/gcc/x/13")) > version_key(Path::new("/x/9")));
932 assert!(version_key(Path::new("/x/10.2")) > version_key(Path::new("/x/10")));
933 assert!(version_key(Path::new("/x/snapshot")) < version_key(Path::new("/x/1")));
935 }
936
937 #[test]
938 fn a_runtime_directory_that_is_not_on_this_machine_is_not_offered() {
939 let dirs = runtime_dirs(linux(), Some(Path::new("/definitely/not/a/sysroot")));
940 assert!(dirs.is_empty(), "{dirs:?}");
941 }
942}