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 libc = match target.env {
586 Env::Musl => "musl",
587 Env::None | Env::Gnu | Env::Msvc => "gnu",
588 };
589 let multiarch = format!("{}-linux-{libc}", target.arch.as_str());
590 [
591 format!("/usr/lib/{multiarch}"),
592 format!("/lib/{multiarch}"),
593 "/usr/lib64".to_owned(),
594 "/lib64".to_owned(),
595 "/usr/lib".to_owned(),
596 "/lib".to_owned(),
597 ]
598 .into_iter()
599 .map(|dir| under(sysroot, &dir))
600 .collect()
601}
602
603fn library_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
605 candidates(target, sysroot).into_iter().filter(|dir| dir.is_dir()).collect()
606}
607
608fn find_file(dirs: &[PathBuf], name: &str) -> Option<PathBuf> {
610 dirs.iter().map(|dir| dir.join(name)).find(|path| path.is_file())
611}
612
613fn under(sysroot: Option<&Path>, path: &str) -> PathBuf {
615 match sysroot {
616 Some(root) => root.join(path.strip_prefix('/').unwrap_or(path)),
619 None => PathBuf::from(path),
620 }
621}
622
623fn target_path(sysroot: Option<&Path>, path: &str) -> String {
630 match sysroot {
631 Some(root) => {
632 let root = root.display().to_string();
633 format!("{}/{}", root.trim_end_matches(['/', '\\']), path.trim_start_matches('/'))
634 }
635 None => path.to_owned(),
636 }
637}
638
639#[must_use]
641pub fn render(linker: &Linker, args: &[String]) -> String {
642 let mut out = linker.path.display().to_string();
643 for arg in args {
644 out.push(' ');
645 if arg.is_empty() || arg.contains(char::is_whitespace) {
646 out.push('"');
647 out.push_str(arg);
648 out.push('"');
649 } else {
650 out.push_str(arg);
651 }
652 }
653 out
654}
655
656pub fn run(linker: &Linker, args: &[String]) -> Result<(), Error> {
664 let args: Vec<OsString> = args.iter().map(OsString::from).collect();
665 let status = Command::new(&linker.path).args(&args).status().map_err(|why| Error::Spawn {
666 path: linker.path.display().to_string(),
667 why: why.to_string(),
668 })?;
669 if status.success() {
670 return Ok(());
671 }
672 Err(Error::Refused {
676 status: match status.code() {
677 Some(code) => format!("exited with status {code}"),
678 None => "was killed before it finished".to_owned(),
679 },
680 })
681}
682
683#[cfg(test)]
684mod tests {
685 use super::*;
686
687 fn linux() -> Triple {
688 Triple::new(Arch::X86_64, Os::Linux, Env::Gnu)
689 }
690
691 fn one(name: &str) -> Vec<Item> {
692 vec![Item::File(name.to_owned())]
693 }
694
695 #[test]
696 fn the_fast_one_is_looked_for_first_and_the_platforms_own_last() {
697 let names = order(linux(), &LinkOptions::default());
698 assert_eq!(names.first().map(String::as_str), Some("ld.mold"));
699 assert_eq!(names.last().map(String::as_str), Some("ld"));
700 }
701
702 #[test]
703 fn naming_one_is_the_whole_of_the_order() {
704 let opts = LinkOptions { use_ld: Some("gold".to_owned()), ..LinkOptions::default() };
705 assert_eq!(order(linux(), &opts), ["ld.gold", "gold"]);
706 }
707
708 #[test]
709 fn a_dynamic_program_names_the_loader_that_will_start_it() {
710 let args = line(linux(), &LinkOptions::default(), &one("a.o"), "a.out").expect("a line");
711 let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
712 assert!(args[at + 1].ends_with("/lib64/ld-linux-x86-64.so.2"), "{args:?}");
713 }
714
715 #[test]
716 fn a_static_program_names_no_loader_because_nothing_will_start_it() {
717 let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
718 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
719 assert!(args.contains(&"-static".to_owned()), "{args:?}");
720 assert!(!args.contains(&"-dynamic-linker".to_owned()), "{args:?}");
721 }
722
723 #[test]
724 fn the_startup_file_of_a_program_that_moves_is_not_the_one_of_a_program_that_does_not() {
725 let moving = LinkOptions { pie: Some(true), ..LinkOptions::default() };
726 let fixed = LinkOptions { pie: Some(false), ..LinkOptions::default() };
727 let named = |opts: &LinkOptions| {
728 line(linux(), opts, &one("a.o"), "a.out")
729 .expect("a line")
730 .iter()
731 .filter_map(|a| Path::new(a).file_name().map(|n| n.to_string_lossy().into_owned()))
732 .find(|n| n.ends_with("crt1.o"))
733 };
734 if let Some(name) = named(&moving) {
737 assert_eq!(name, "Scrt1.o");
738 assert_eq!(named(&fixed).as_deref(), Some("crt1.o"));
739 }
740 }
741
742 #[test]
743 fn asking_for_no_startup_files_leaves_out_both_ends_of_them() {
744 let opts = LinkOptions { no_startfiles: true, ..LinkOptions::default() };
745 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
746 assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
747 assert!(!args.iter().any(|a| a.ends_with("crtn.o")), "{args:?}");
748 assert!(args.contains(&"-lc".to_owned()), "{args:?}");
750 }
751
752 #[test]
753 fn asking_for_no_library_at_all_leaves_out_the_startup_files_too() {
754 let opts = LinkOptions { no_stdlib: true, ..LinkOptions::default() };
755 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
756 assert!(!args.contains(&"-lc".to_owned()), "{args:?}");
757 assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
758 }
759
760 #[test]
761 fn the_library_comes_after_the_objects_that_need_it() {
762 let items = vec![Item::File("a.o".to_owned()), Item::Library("m".to_owned())];
763 let args = line(linux(), &LinkOptions::default(), &items, "a.out").expect("a line");
764 let obj = args.iter().position(|a| a == "a.o").expect("the object");
765 let m = args.iter().position(|a| a == "-lm").expect("the library");
766 let c = args.iter().position(|a| a == "-lc").expect("the library");
767 assert!(obj < m && m < c, "{args:?}");
768 }
769
770 #[test]
771 fn what_the_user_told_the_linker_comes_after_what_this_told_it() {
772 let opts = LinkOptions {
773 passthrough: vec!["--no-eh-frame-hdr".to_owned()],
774 ..LinkOptions::default()
775 };
776 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
777 assert_eq!(args.last().map(String::as_str), Some("--no-eh-frame-hdr"));
778 }
779
780 #[test]
781 fn a_sysroot_moves_every_path_this_decided_and_none_the_user_wrote() {
782 let opts = LinkOptions {
783 sysroot: Some(PathBuf::from("/nowhere-at-all")),
784 search: vec![PathBuf::from("/opt/mine")],
785 ..LinkOptions::default()
786 };
787 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
788 let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
789 assert_eq!(args[at + 1], "/nowhere-at-all/lib64/ld-linux-x86-64.so.2");
790 assert!(args.contains(&"-L/opt/mine".to_owned()), "{args:?}");
791 }
792
793 #[test]
794 fn a_platform_with_no_link_line_is_said_so_rather_than_linked_wrongly() {
795 for triple in [
796 Triple::new(Arch::X86_64, Os::Darwin, Env::Gnu),
797 Triple::new(Arch::X86_64, Os::Windows, Env::Msvc),
798 ] {
799 let error = line(triple, &LinkOptions::default(), &one("a.o"), "a.out")
800 .expect_err("no line for it");
801 assert!(matches!(error, Error::Target { .. }), "{error:?}");
802 }
803 }
804
805 #[test]
806 fn the_line_is_printed_the_way_it_would_be_typed() {
807 let linker = Linker { name: "ld".to_owned(), path: PathBuf::from("/usr/bin/ld") };
808 let args = ["-o".to_owned(), "a b".to_owned()];
809 assert_eq!(render(&linker, &args), "/usr/bin/ld -o \"a b\"");
810 }
811
812 #[test]
813 fn a_linker_that_is_not_there_is_said_by_name() {
814 let opts = LinkOptions {
815 use_ld: Some("a-linker-nobody-has".to_owned()),
816 ..LinkOptions::default()
817 };
818 let error = find(linux(), &opts).expect_err("not on this machine");
819 assert_eq!(error, Error::Named { name: "a-linker-nobody-has".to_owned() });
820 }
821 fn a_gcc_dir(name: &str) -> PathBuf {
824 let dir = std::env::temp_dir().join(format!("rucc-link-{name}-{}", std::process::id()));
825 fs::create_dir_all(&dir).expect("a temporary directory");
826 fs::write(dir.join("libgcc.a"), b"not really an archive").expect("a file in it");
827 dir
828 }
829
830 #[test]
831 fn the_c_library_supplies_the_block_routines_and_our_runtime_does_not_displace_them() {
832 let gcc = a_gcc_dir("order");
833 let ours = PathBuf::from("/somewhere/librucc_builtins.a");
834 let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
835 let at_libc = args.iter().position(|a| a == "-lc").expect("libc");
836 let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
837 assert!(at_libc < at_ours, "{args:?}");
840 }
841
842 #[test]
843 fn a_static_link_puts_them_in_a_group_because_two_of_them_refer_to_each_other() {
844 let gcc = a_gcc_dir("group");
845 let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
846 let args = runtime_items(&opts, &[gcc], None);
847 assert_eq!(args.first().map(String::as_str), Some("--start-group"), "{args:?}");
848 assert_eq!(args.last().map(String::as_str), Some("--end-group"), "{args:?}");
849 assert!(args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
852 }
853
854 #[test]
855 fn a_dynamic_link_needs_no_group_and_asks_for_the_shared_half_only_if_something_wants_it() {
856 let gcc = a_gcc_dir("dynamic");
857 let args = runtime_items(&LinkOptions::default(), &[gcc], None);
858 assert!(!args.contains(&"--start-group".to_owned()), "{args:?}");
859 assert!(!args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
860 let at = args.iter().position(|a| a == "-lgcc_s").expect("the shared half");
861 assert_eq!(args[at - 1], "--as-needed", "{args:?}");
862 assert_eq!(args[at + 1], "--no-as-needed", "{args:?}");
863 }
864
865 #[test]
866 fn our_own_runtime_comes_before_the_machines_because_the_two_are_interchangeable() {
867 let gcc = a_gcc_dir("ours");
868 let ours = PathBuf::from("/somewhere/librucc_builtins.a");
869 let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
870 let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
871 let at_gcc = args.iter().position(|a| a == "-lgcc").expect("libgcc");
872 assert!(at_ours < at_gcc, "{args:?}");
873 }
874
875 #[test]
876 fn no_builtins_lib_leaves_ours_off_and_keeps_the_machines() {
877 let gcc = a_gcc_dir("theirs");
878 let opts = LinkOptions { no_builtins_lib: true, ..LinkOptions::default() };
879 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
880 assert!(!args.iter().any(|a| a.ends_with("librucc_builtins.a")), "{args:?}");
881 assert!(runtime_items(&opts, &[gcc], None).contains(&"-lgcc".to_owned()));
884 }
885
886 #[test]
887 fn nodefaultlibs_leaves_the_whole_runtime_off_and_not_only_the_c_library() {
888 let gcc = a_gcc_dir("none");
889 let opts = LinkOptions { no_defaultlibs: true, ..LinkOptions::default() };
890 assert!(runtime_items(&opts, &[gcc], None).is_empty());
891 }
892
893 #[test]
894 fn a_machine_with_no_gcc_on_it_gets_no_names_for_libraries_that_are_not_there() {
895 let empty = std::env::temp_dir().join("rucc-link-empty-not-a-gcc");
896 let args = runtime_items(&LinkOptions::default(), &[empty], None);
897 assert_eq!(args, ["-lc"], "{args:?}");
898 }
899
900 #[test]
901 fn a_gcc_version_directory_is_read_as_a_version_and_not_as_a_word() {
902 assert!(version_key(Path::new("/usr/lib/gcc/x/13")) > version_key(Path::new("/x/9")));
903 assert!(version_key(Path::new("/x/10.2")) > version_key(Path::new("/x/10")));
904 assert!(version_key(Path::new("/x/snapshot")) < version_key(Path::new("/x/1")));
906 }
907
908 #[test]
909 fn a_runtime_directory_that_is_not_on_this_machine_is_not_offered() {
910 let dirs = runtime_dirs(linux(), Some(Path::new("/definitely/not/a/sysroot")));
911 assert!(dirs.is_empty(), "{dirs:?}");
912 }
913}