1use std::ffi::OsString;
73use std::fs;
74use std::path::{Path, PathBuf};
75use std::process::Command;
76
77use rucc_sysroot::layout::{Kernel, Sysroot};
78use rucc_sysroot::{LinkMode, argv};
79use rucc_target::{Arch, Env, Os, Triple};
80use rucc_tuple::TargetTuple;
81
82#[derive(Debug, Default, Clone, PartialEq, Eq)]
88pub struct LinkOptions {
89 pub use_ld: Option<String>,
91 pub search: Vec<PathBuf>,
93 pub prefixes: Vec<PathBuf>,
95 pub sysroot: Option<PathBuf>,
97 pub cache: Option<PathBuf>,
104 pub usr: Option<PathBuf>,
112 pub is_static: bool,
114 pub shared: bool,
116 pub pie: Option<bool>,
118 pub no_stdlib: bool,
120 pub no_startfiles: bool,
122 pub no_defaultlibs: bool,
124 pub export_dynamic: bool,
126 pub strip: bool,
128 pub gui: bool,
130 pub unicode: bool,
132 pub no_builtins_lib: bool,
135 pub pinned: Option<TargetTuple>,
144 pub profile: bool,
151 pub fast_math: bool,
158 pub daz_ftz: Option<bool>,
161 pub relocatable: bool,
167 pub os_version: Option<rucc_tuple::Version>,
173}
174
175impl LinkOptions {
176 fn wants_fastmath(&self) -> bool {
178 self.daz_ftz.unwrap_or(self.fast_math && !self.shared)
179 }
180
181 fn wants_startfiles(&self) -> bool {
183 !self.no_stdlib && !self.no_startfiles && !self.relocatable
184 }
185
186 fn wants_defaultlibs(&self) -> bool {
188 !self.no_stdlib && !self.no_defaultlibs && !self.relocatable
189 }
190
191 fn wants_runtime(&self) -> bool {
197 !self.no_stdlib && !self.no_defaultlibs && !self.relocatable
198 }
199}
200
201#[derive(Debug, Clone, PartialEq, Eq)]
207pub enum Item {
208 File(String),
210 Library(String),
212 Linker(String),
222}
223
224impl std::fmt::Display for Item {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 match self {
227 Item::File(path) => f.write_str(path),
228 Item::Library(name) => write!(f, "-l{name}"),
229 Item::Linker(arg) => write!(f, "-Wl,{arg}"),
230 }
231 }
232}
233
234#[derive(Debug, Clone, PartialEq, Eq)]
236pub enum Error {
237 NoLinker {
239 tried: Vec<String>,
241 },
242 Named {
244 name: String,
246 },
247 Target {
249 triple: String,
251 },
252 Cross {
259 why: String,
261 },
262 Sysroot {
264 target: String,
266 dir: String,
268 pinned: bool,
271 },
272 TooOld {
278 name: String,
280 found: u32,
282 target: String,
284 },
285 Spawn {
287 path: String,
289 why: String,
291 },
292 Refused {
294 status: String,
296 },
297}
298
299impl std::fmt::Display for Error {
300 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301 match self {
302 Error::NoLinker { tried } => {
303 write!(f, "no linker was found; tried {}", tried.join(", "))?;
304 if tried.iter().any(|name| is_lld(name)) {
307 write!(
308 f,
309 ". lld {LLD_EXPORTAS} or newer links for every target, and {}",
310 lld_advice()
311 )?;
312 }
313 Ok(())
314 }
315 Error::Named { name } => {
316 write!(f, "-fuse-ld={name} asks for a linker that is not on this machine")
317 }
318 Error::Target { triple } => {
319 write!(f, "there is no link line for {triple} in this compiler yet")
320 }
321 Error::Cross { why } => f.write_str(why),
322 Error::Sysroot { target, dir, pinned: true } => write!(
328 f,
329 "there is no sysroot for {target} at {dir}, so there is nothing to link it \
330 against. `rucc --fetch {target}` gets the one this release pins, or pass \
331 --sysroot=<dir> to name a tree you have already"
332 ),
333 Error::Sysroot { target, dir, pinned: false } => write!(
334 f,
335 "there is no sysroot for {target} at {dir}, so there is nothing to link it \
336 against, and this release pins none for it to fetch. Pass --sysroot=<dir> to name \
337 a tree you have already, or see spec/cross-compile/13-distribution.md section \
338 13.2 for the cache that will hold one"
339 ),
340 Error::TooOld { name, found, target } => write!(
343 f,
344 "{name} is lld {found} and cannot link for {target}. mingw-w64 writes a few hundred \
345 of its aliases, `_crt_atexit == atexit` among them, as IMPORT_NAME_EXPORTAS \
346 records in its import libraries, which lld learned to read in {LLD_EXPORTAS}. An \
347 older one neither reads them nor says so: it writes an import by ordinal zero, the \
348 link succeeds, and the program dies at startup. No newer lld was found either. \
349 For lld {LLD_EXPORTAS} or newer, {}, or name one with -fuse-ld=",
350 lld_advice()
351 ),
352 Error::Spawn { path, why } => write!(f, "could not run the linker at {path}: {why}"),
353 Error::Refused { status } => write!(f, "the linker {status}"),
354 }
355 }
356}
357
358impl std::error::Error for Error {}
359
360#[derive(Debug, Clone, PartialEq, Eq)]
362pub struct Linker {
363 pub name: String,
365 pub path: PathBuf,
367}
368
369#[must_use]
376pub fn order(target: Triple, opts: &LinkOptions) -> Vec<String> {
377 if let Some(named) = &opts.use_ld {
378 return vec![format!("ld.{named}"), named.clone()];
380 }
381 if target.os == Os::Darwin {
385 return vec!["ld".to_owned(), "ld64.lld".to_owned()];
386 }
387 if cross_sysroot(target, opts).is_some() {
388 let mut names = cross_order(target);
389 if (target.os, target.env) == (Os::Windows, Env::Gnu) {
395 names.retain(|name| is_lld(name));
396 }
397 return names;
398 }
399 if distro_cross(target, opts).is_some() {
400 return cross_order(target);
401 }
402 match target.os {
403 Os::Windows => vec!["lld-link".to_owned(), "link.exe".to_owned()],
404 _ => vec![
405 "ld.mold".to_owned(),
406 "mold".to_owned(),
407 "ld.lld".to_owned(),
408 "lld".to_owned(),
409 "ld".to_owned(),
410 ],
411 }
412}
413
414fn cross_order(target: Triple) -> Vec<String> {
430 let mut names = vec!["ld.lld".to_owned(), "lld".to_owned()];
431 match (target.os, target.env) {
432 (Os::Linux, _) => names.push(format!("{}-ld", multiarch(target))),
433 (Os::Windows, Env::Gnu) => names.push(format!("{}-w64-mingw32-ld", target.arch.as_str())),
434 _ => {}
435 }
436 names
437}
438
439#[must_use]
466pub fn cross_sysroot(target: Triple, opts: &LinkOptions) -> Option<Sysroot> {
467 cross_for(target, opts, Triple::host())
468}
469
470fn cross_for(target: Triple, opts: &LinkOptions, host: Option<Triple>) -> Option<Sysroot> {
472 if opts.sysroot.is_some() {
473 return None;
474 }
475 let tuple = target_tuple(target, opts);
476 if host == Some(target) && target.os != Os::Windows && tuple.env_version().is_none() {
480 return None;
481 }
482 if distro_for(target, opts, host).is_some() {
483 return None;
484 }
485 let cache = opts.cache.as_deref()?;
486 Some(Sysroot::in_cache(cache, tuple))
487}
488
489#[derive(Debug, Clone, PartialEq, Eq)]
496pub struct Distro {
497 pub root: PathBuf,
499 pub gcc: Vec<PathBuf>,
502}
503
504impl Distro {
505 #[must_use]
507 pub fn include(&self) -> PathBuf {
508 self.root.join("include")
509 }
510
511 #[must_use]
513 pub fn lib(&self) -> PathBuf {
514 self.root.join("lib")
515 }
516}
517
518#[must_use]
526pub fn distro_cross(target: Triple, opts: &LinkOptions) -> Option<Distro> {
527 distro_for(target, opts, Triple::host())
528}
529
530fn distro_for(target: Triple, opts: &LinkOptions, host: Option<Triple>) -> Option<Distro> {
532 if opts.sysroot.is_some() || target.os != Os::Linux || host == Some(target) {
533 return None;
534 }
535 let tuple = target_tuple(target, opts);
536 if tuple.env_version().is_some() {
537 return None;
538 }
539 if opts.cache.as_deref().is_some_and(|cache| Sysroot::in_cache(cache, tuple).lib().is_dir()) {
540 return None;
541 }
542 let usr = opts.usr.as_deref()?;
543 let name = multiarch(target);
544 let root = usr.join(&name);
545 if !root.join("include").is_dir() || !root.join("lib").is_dir() {
546 return None;
547 }
548 let gcc = newest_first(&usr.join("lib/gcc-cross").join(&name));
549 Some(Distro { root, gcc })
550}
551
552fn target_tuple(target: Triple, opts: &LinkOptions) -> TargetTuple {
560 opts.pinned.unwrap_or_else(|| target.tuple())
561}
562
563#[must_use]
574pub fn cross_kernel(target: Triple, opts: &LinkOptions) -> Option<Kernel> {
575 kernel_for(target, opts, Triple::host())
576}
577
578fn kernel_for(target: Triple, opts: &LinkOptions, host: Option<Triple>) -> Option<Kernel> {
580 cross_for(target, opts, host)?;
581 Kernel::for_target(opts.cache.as_deref()?, target.tuple())
582}
583
584fn mode(opts: &LinkOptions) -> LinkMode {
591 let pie = opts.pie.unwrap_or(!opts.is_static && !opts.shared);
592 if opts.shared {
593 LinkMode::Shared
594 } else if opts.is_static {
595 if pie { LinkMode::StaticPie } else { LinkMode::Static }
596 } else if pie {
597 LinkMode::Dynamic
598 } else {
599 LinkMode::DynamicNoPie
600 }
601}
602
603fn cross_line(
610 target: Triple,
611 opts: &LinkOptions,
612 items: &[Item],
613 output: &str,
614 sysroot: &Sysroot,
615) -> Result<Vec<String>, Error> {
616 if opts.profile {
617 return Err(Error::Cross {
621 why: format!(
622 "-pg needs gcrt1.o, or gcrt2.o on Windows, the startup file that starts and stops \
623 the counting, and a generated sysroot for {target} does not have one. Profile on \
624 the host, or pass --sysroot=<dir> naming a tree that has it"
625 ),
626 });
627 }
628 let inputs: Vec<argv::Item> = items
629 .iter()
630 .map(|item| match item {
631 Item::File(path) => argv::Item::File(PathBuf::from(path)),
632 Item::Library(name) => argv::Item::Library(name.clone()),
633 Item::Linker(arg) => argv::Item::Linker(arg.clone()),
634 })
635 .collect();
636 let output = PathBuf::from(output);
637 let ours = builtins_archive(target, &opts.prefixes);
642 if ours.is_none() && opts.wants_runtime() && !opts.no_builtins_lib {
643 let tuple = target.tuple().to_canonical_string();
647 return Err(Error::Cross {
648 why: format!(
649 "a cross link ends with librucc_builtins.a, this compiler's own runtime for \
650 {tuple}, and there is none beside the compiler or under a -B prefix. A sysroot \
651 does not carry it, because it is our output rather than the platform's. Build it \
652 with `cargo xtask builtins --target={tuple}`, or pass -fno-builtins-lib to link \
653 without it"
654 ),
655 });
656 }
657 let invocation = argv::Invocation {
658 inputs: &inputs,
659 output: Some(&output),
660 mode: mode(opts),
661 search: &opts.search,
662 no_startfiles: !opts.wants_startfiles(),
663 no_defaultlibs: !opts.wants_defaultlibs(),
664 no_builtins_lib: opts.no_builtins_lib,
665 builtins: ours.as_deref(),
666 export_dynamic: opts.export_dynamic,
667 strip: opts.strip,
668 gui: opts.gui,
669 unicode: opts.unicode,
670 };
671 argv::argv(target.tuple(), sysroot, &invocation)
672 .map_err(|why| Error::Cross { why: why.to_string() })
673}
674
675pub fn preflight(target: Triple, opts: &LinkOptions) -> Result<(), Error> {
692 if opts.relocatable {
693 return relocatable_line(target, opts, &[], "a.out").map(drop);
694 }
695 if target.os == Os::Darwin {
698 return darwin_line(target, opts, &[], "a.out").map(drop);
699 }
700 let Some(sysroot) = cross_sysroot(target, opts) else { return Ok(()) };
701 let shape = LinkOptions { no_builtins_lib: true, ..opts.clone() };
705 cross_line(target, &shape, &[], "a.out", &sysroot)?;
706 if !sysroot.lib().is_dir() {
710 let tuple = target_tuple(target, opts).to_canonical_string();
711 return Err(Error::Sysroot {
712 dir: sysroot.root().display().to_string(),
713 pinned: rucc_sysroot::pinned_for_target(target_tuple(target, opts)).is_some(),
714 target: tuple,
715 });
716 }
717 cross_line(target, opts, &[], "a.out", &sysroot)?;
720 Ok(())
721}
722
723pub fn write_stubs(target: Triple, opts: &LinkOptions) -> Result<(), Error> {
743 let Some(sysroot) = cross_sysroot(target, opts) else { return Ok(()) };
744 let tuple = sysroot.target();
745 if rucc_stub::glibc::architecture(tuple).is_none() {
746 return Ok(());
747 }
748 let Ok(Some(minor)) = rucc_sysroot::bundled_glibc_minor(tuple) else { return Ok(()) };
749 let files = rucc_stub::glibc::stubs(tuple, minor).map_err(|why| Error::Cross {
750 why: format!("the glibc stubs for {}: {why}", tuple.to_canonical_string()),
751 })?;
752 let dir = sysroot.stubs();
753 let failed = |path: &Path, why: std::io::Error| Error::Cross {
754 why: format!("{} cannot be written: {why}", path.display()),
755 };
756 fs::create_dir_all(dir).map_err(|why| failed(dir, why))?;
757 for file in files {
758 let path = dir.join(&file.name);
759 if fs::read(&path).is_ok_and(|there| there == file.bytes) {
760 continue;
761 }
762 let temporary = dir.join(format!(".{}.{}", file.name, std::process::id()));
763 fs::write(&temporary, &file.bytes).map_err(|why| failed(&temporary, why))?;
764 fs::rename(&temporary, &path).map_err(|why| {
765 let _ = fs::remove_file(&temporary);
766 failed(&path, why)
767 })?;
768 }
769 Ok(())
770}
771
772pub fn find(target: Triple, opts: &LinkOptions) -> Result<Linker, Error> {
783 let tried = order(target, opts);
784 let places = lld_dirs(Path::new("/"), std::env::var_os("ProgramFiles").map(PathBuf::from));
785 let mut refused = None;
789 for name in &tried {
790 for path in linker_candidates(name, opts, &places) {
791 let linker = Linker { name: name.clone(), path };
792 match suitable(target, &linker) {
793 Ok(()) => return Ok(linker),
794 Err(why) => {
795 refused.get_or_insert(why);
796 }
797 }
798 }
799 }
800 if let Some(why) = refused {
801 return Err(why);
802 }
803 match &opts.use_ld {
804 Some(name) => Err(Error::Named { name: name.clone() }),
805 None => Err(Error::NoLinker { tried }),
806 }
807}
808
809fn linker_candidates(name: &str, opts: &LinkOptions, places: &[PathBuf]) -> Vec<PathBuf> {
816 if name.contains(std::path::MAIN_SEPARATOR) || name.contains('/') {
817 let path = PathBuf::from(name);
818 return if path.is_file() { vec![path] } else { Vec::new() };
819 }
820 let pathext = pathext();
821 let mut found: Vec<PathBuf> = opts
822 .prefixes
823 .iter()
824 .flat_map(|dir| spellings(&dir.join(name), &pathext))
825 .filter(|path| path.is_file())
826 .collect();
827 if let Some(path) = std::env::var_os("PATH") {
828 found.extend(
829 std::env::split_paths(&path)
830 .flat_map(|dir| spellings(&dir.join(name), &pathext))
831 .filter(|p| executable(p)),
832 );
833 }
834 if is_lld(name) {
838 for dir in places {
839 for file in spellings(&dir.join(name), &pathext) {
840 if executable(&file) && !found.contains(&file) {
841 found.push(file);
842 }
843 }
844 }
845 }
846 found
847}
848
849fn pathext() -> Vec<String> {
856 if !cfg!(windows) {
857 return Vec::new();
858 }
859 let list = std::env::var("PATHEXT").unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".to_owned());
860 list.split(';').filter(|ext| ext.starts_with('.')).map(str::to_ascii_lowercase).collect()
861}
862
863fn spellings(path: &Path, exts: &[String]) -> Vec<PathBuf> {
870 let has = path
871 .extension()
872 .and_then(|ext| ext.to_str())
873 .is_some_and(|ext| exts.iter().any(|e| e[1..].eq_ignore_ascii_case(ext)));
874 if exts.is_empty() || has {
875 return vec![path.to_path_buf()];
876 }
877 exts.iter()
878 .map(|ext| {
879 let mut name = path.as_os_str().to_owned();
880 name.push(ext);
881 PathBuf::from(name)
882 })
883 .collect()
884}
885
886fn is_lld(name: &str) -> bool {
888 matches!(name, "ld.lld" | "ld64.lld" | "lld" | "lld-link")
889}
890
891fn lld_dirs(root: &Path, program_files: Option<PathBuf>) -> Vec<PathBuf> {
901 let mut dirs: Vec<PathBuf> = [
902 "opt/homebrew/opt/lld/bin",
903 "opt/homebrew/opt/llvm/bin",
904 "usr/local/opt/lld/bin",
905 "usr/local/opt/llvm/bin",
906 "home/linuxbrew/.linuxbrew/opt/lld/bin",
907 "home/linuxbrew/.linuxbrew/opt/llvm/bin",
908 ]
909 .iter()
910 .map(|dir| root.join(dir))
911 .collect();
912 for (parent, prefix) in [("usr/lib", "llvm-"), ("usr/lib64", "llvm")] {
913 let mut versions: Vec<(u32, PathBuf)> = fs::read_dir(root.join(parent))
914 .into_iter()
915 .flatten()
916 .flatten()
917 .filter_map(|entry| {
918 let name = entry.file_name();
919 let version = name.to_str()?.strip_prefix(prefix)?.parse().ok()?;
920 Some((version, entry.path().join("bin")))
921 })
922 .collect();
923 versions.sort_by_key(|(version, _)| std::cmp::Reverse(*version));
924 dirs.extend(versions.into_iter().map(|(_, dir)| dir));
925 }
926 if let Some(dir) = program_files {
927 dirs.push(dir.join("LLVM").join("bin"));
928 }
929 dirs
930}
931
932fn lld_advice() -> &'static str {
938 if cfg!(target_os = "macos") {
939 "`brew install lld` installs one, and rucc finds it in Homebrew's directory without a \
940 change to PATH"
941 } else if cfg!(windows) {
942 "the LLVM installer from https://github.com/llvm/llvm-project/releases, or `winget install \
943 LLVM.LLVM`, installs one in %ProgramFiles%\\LLVM\\bin, where rucc finds it without a \
944 change to PATH"
945 } else {
946 "on Debian and Ubuntu `apt install lld-19` installs one in /usr/lib/llvm-19/bin and on \
947 Fedora `dnf install lld` installs one on PATH, and rucc finds either without a change to \
948 PATH"
949 }
950}
951
952pub const LLD_EXPORTAS: u32 = 19;
957
958pub fn suitable(target: Triple, linker: &Linker) -> Result<(), Error> {
980 if (target.os, target.env) != (Os::Windows, Env::Gnu) {
981 return Ok(());
982 }
983 let Some(found) = lld_major(&reported_version(&linker.path)) else { return Ok(()) };
984 if found >= LLD_EXPORTAS {
985 return Ok(());
986 }
987 Err(Error::TooOld {
988 name: linker.name.clone(),
989 found,
990 target: target.tuple().to_canonical_string(),
991 })
992}
993
994fn reported_version(path: &Path) -> String {
1000 let Ok(out) = Command::new(path).arg("--version").output() else { return String::new() };
1001 String::from_utf8_lossy(&out.stdout).into_owned()
1002}
1003
1004fn lld_major(text: &str) -> Option<u32> {
1012 let mut words = text.split_whitespace();
1013 words.find(|word| *word == "LLD")?;
1014 words.next()?.split('.').next()?.parse().ok()
1015}
1016
1017#[cfg(unix)]
1019fn executable(path: &Path) -> bool {
1020 use std::os::unix::fs::PermissionsExt as _;
1021 path.metadata().is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
1022}
1023
1024#[cfg(not(unix))]
1029fn executable(path: &Path) -> bool {
1030 path.is_file()
1031}
1032
1033pub fn line(
1045 target: Triple,
1046 opts: &LinkOptions,
1047 items: &[Item],
1048 output: &str,
1049) -> Result<Vec<String>, Error> {
1050 if opts.relocatable {
1051 return relocatable_line(target, opts, items, output);
1052 }
1053 if target.os == Os::Darwin {
1054 return darwin_line(target, opts, items, output);
1055 }
1056 if let Some(sysroot) = cross_sysroot(target, opts) {
1057 return cross_line(target, opts, items, output, &sysroot);
1058 }
1059 if target.os != Os::Linux {
1060 return Err(Error::Target { triple: target.to_string() });
1061 }
1062 let machine = emulation(target);
1063 let root = opts.sysroot.as_deref();
1064 let distro = distro_cross(target, opts);
1067 let dirs = match &distro {
1068 Some(distro) => vec![distro.lib()],
1069 None => library_dirs(target, root),
1070 };
1071 let runtime = match distro {
1074 Some(distro) => distro.gcc,
1075 None => runtime_dirs(target, root),
1076 };
1077 let ours = if opts.no_builtins_lib { None } else { builtins_archive(target, &opts.prefixes) };
1078 let mut args = vec![
1079 "-o".to_owned(),
1080 output.to_owned(),
1081 "-m".to_owned(),
1085 machine.to_owned(),
1086 "--eh-frame-hdr".to_owned(),
1089 "--hash-style=gnu".to_owned(),
1093 ];
1094
1095 let pie = opts.pie.unwrap_or(!opts.is_static && !opts.shared);
1096 if opts.shared {
1097 args.push("-shared".to_owned());
1098 } else if opts.is_static {
1099 args.push("-static".to_owned());
1100 } else if pie {
1101 args.push("-pie".to_owned());
1102 } else {
1103 args.push("-no-pie".to_owned());
1104 }
1105 if !opts.is_static && !opts.shared {
1106 args.push("-dynamic-linker".to_owned());
1107 args.push(target_path(root, loader(target)));
1108 }
1109 if opts.export_dynamic {
1110 args.push("--export-dynamic".to_owned());
1111 }
1112 if opts.strip {
1113 args.push("-s".to_owned());
1114 }
1115
1116 if opts.wants_startfiles() {
1117 for name in startfile(opts, pie).into_iter().chain(["crti.o"]) {
1118 if let Some(path) = find_file(&dirs, name) {
1119 args.push(path.display().to_string());
1120 }
1121 }
1122 let begin = if opts.shared || pie {
1128 "crtbeginS.o"
1129 } else if opts.is_static {
1130 "crtbeginT.o"
1131 } else {
1132 "crtbegin.o"
1133 };
1134 if let Some(path) = find_file(&runtime, begin).or_else(|| find_file(&runtime, "crtbegin.o"))
1135 {
1136 args.push(path.display().to_string());
1137 }
1138 }
1139
1140 for dir in &opts.search {
1141 args.push(format!("-L{}", dir.display()));
1142 }
1143 for dir in &dirs {
1144 args.push(format!("-L{}", dir.display()));
1145 }
1146 for dir in &runtime {
1149 args.push(format!("-L{}", dir.display()));
1150 }
1151
1152 for item in items {
1153 match item {
1154 Item::File(path) => args.push(path.clone()),
1155 Item::Library(name) => args.push(format!("-l{name}")),
1156 Item::Linker(arg) => args.push(arg.clone()),
1157 }
1158 }
1159 args.extend(runtime_items(opts, &runtime, ours.as_deref()));
1162
1163 if opts.wants_startfiles() {
1164 if opts.wants_fastmath() {
1169 if let Some(path) = find_file(&runtime, "crtfastmath.o") {
1170 args.push(path.display().to_string());
1171 }
1172 }
1173 let end = if opts.shared || pie { "crtendS.o" } else { "crtend.o" };
1174 if let Some(path) = find_file(&runtime, end).or_else(|| find_file(&runtime, "crtend.o")) {
1175 args.push(path.display().to_string());
1176 }
1177 if let Some(path) = find_file(&dirs, "crtn.o") {
1178 args.push(path.display().to_string());
1179 }
1180 }
1181
1182 Ok(args)
1183}
1184
1185fn relocatable_line(
1191 target: Triple,
1192 opts: &LinkOptions,
1193 items: &[Item],
1194 output: &str,
1195) -> Result<Vec<String>, Error> {
1196 if target.os == Os::Darwin {
1197 let mut args = vec![
1200 "-r".to_owned(),
1201 "-arch".to_owned(),
1202 darwin_arch(target)?.to_owned(),
1203 "-o".to_owned(),
1204 output.to_owned(),
1205 ];
1206 for dir in &opts.search {
1207 args.push(format!("-L{}", dir.display()));
1208 }
1209 push_items(&mut args, items);
1210 return Ok(args);
1211 }
1212 if target.os != Os::Linux {
1213 return Err(Error::Target { triple: target.to_string() });
1214 }
1215 let mut args = vec![
1216 "-o".to_owned(),
1217 output.to_owned(),
1218 "-m".to_owned(),
1219 emulation(target).to_owned(),
1220 "-r".to_owned(),
1221 ];
1222 if opts.strip {
1223 args.push("-s".to_owned());
1224 }
1225 for dir in &opts.search {
1226 args.push(format!("-L{}", dir.display()));
1227 }
1228 for item in items {
1229 match item {
1230 Item::File(path) => args.push(path.clone()),
1231 Item::Library(name) => args.push(format!("-l{name}")),
1232 Item::Linker(arg) => args.push(arg.clone()),
1233 }
1234 }
1235 Ok(args)
1236}
1237
1238pub fn darwin_line(
1255 target: Triple,
1256 opts: &LinkOptions,
1257 items: &[Item],
1258 output: &str,
1259) -> Result<Vec<String>, Error> {
1260 let arch = darwin_arch(target)?;
1261 if opts.is_static && !opts.shared {
1262 return Err(Error::Cross {
1263 why: "there is no static C library for Apple platforms, so -static cannot make a \
1264 program for one"
1265 .to_owned(),
1266 });
1267 }
1268 let Some(sdk) = crate::library::sdk(opts.sysroot.as_deref()) else {
1269 return Err(Error::Cross {
1270 why: format!(
1271 "no SDK was found to link {} against. Install the command line tools with \
1272 `xcode-select --install`, or name one with -isysroot <dir> or SDKROOT",
1273 target_tuple(target, opts).to_canonical_string()
1274 ),
1275 });
1276 };
1277 let tuple = target_tuple(target, opts);
1278 let (platform, default) = match (tuple.os(), tuple.env()) {
1279 (rucc_tuple::Os::IOs, rucc_tuple::Env::Simulator) => ("ios-simulator", "14.0"),
1280 (rucc_tuple::Os::IOs, rucc_tuple::Env::MacAbi) => ("mac-catalyst", "14.0"),
1281 (rucc_tuple::Os::IOs, _) => ("ios", "14.0"),
1282 _ => ("macos", "11.0"),
1283 };
1284 let minimum = opts
1285 .os_version
1286 .or_else(|| tuple.os_version())
1287 .map_or_else(|| default.to_owned(), |version| version.to_string());
1288 let version = sdk_version(&sdk).unwrap_or_else(|| minimum.clone());
1292
1293 let mut args = vec![
1294 "-arch".to_owned(),
1295 arch.to_owned(),
1296 "-platform_version".to_owned(),
1297 platform.to_owned(),
1298 minimum,
1299 version,
1300 "-syslibroot".to_owned(),
1301 sdk.display().to_string(),
1302 "-o".to_owned(),
1303 output.to_owned(),
1304 ];
1305 if opts.shared {
1306 args.push("-dylib".to_owned());
1307 }
1308 if opts.export_dynamic {
1309 args.push("-export_dynamic".to_owned());
1310 }
1311 if opts.strip {
1312 args.push("-S".to_owned());
1315 args.push("-x".to_owned());
1316 }
1317 for dir in &opts.search {
1318 args.push(format!("-L{}", dir.display()));
1319 }
1320 push_items(&mut args, items);
1321 if opts.wants_runtime() && !opts.no_builtins_lib {
1322 if let Some(ours) = builtins_archive(target, &opts.prefixes) {
1323 args.push(ours.display().to_string());
1324 }
1325 }
1326 if opts.wants_defaultlibs() {
1327 args.push("-lSystem".to_owned());
1328 }
1329 Ok(args)
1330}
1331
1332fn darwin_arch(target: Triple) -> Result<&'static str, Error> {
1334 match target.arch {
1335 Arch::Aarch64 => Ok("arm64"),
1336 Arch::X86_64 => Ok("x86_64"),
1337 _ => Err(Error::Target { triple: target.to_string() }),
1338 }
1339}
1340
1341fn sdk_version(sdk: &Path) -> Option<String> {
1347 let valid = |text: &str| {
1348 !text.is_empty()
1349 && text
1350 .split('.')
1351 .all(|part| !part.is_empty() && part.bytes().all(|b| b.is_ascii_digit()))
1352 };
1353 if let Ok(text) = fs::read_to_string(sdk.join("SDKSettings.json")) {
1354 if let Some(at) = text.find("\"Version\"") {
1355 let rest = &text[at + "\"Version\"".len()..];
1356 let rest = rest.trim_start().strip_prefix(':')?.trim_start().strip_prefix('"')?;
1357 let version = &rest[..rest.find('"')?];
1358 if valid(version) {
1359 return Some(version.to_owned());
1360 }
1361 }
1362 }
1363 let name = sdk.file_name()?.to_str()?.strip_suffix(".sdk")?;
1364 let version = name.trim_start_matches(|c: char| c.is_ascii_alphabetic());
1365 valid(version).then(|| version.to_owned())
1366}
1367
1368fn push_items(args: &mut Vec<String>, items: &[Item]) {
1370 for item in items {
1371 match item {
1372 Item::File(path) => args.push(path.clone()),
1373 Item::Library(name) => args.push(format!("-l{name}")),
1374 Item::Linker(arg) => args.push(arg.clone()),
1375 }
1376 }
1377}
1378
1379fn startfile(opts: &LinkOptions, pie: bool) -> Option<&'static str> {
1391 if opts.shared {
1392 None
1393 } else if opts.profile {
1394 Some(if pie && opts.is_static { "grcrt1.o" } else { "gcrt1.o" })
1395 } else if pie {
1396 Some("Scrt1.o")
1397 } else {
1398 Some("crt1.o")
1399 }
1400}
1401
1402fn runtime_items(opts: &LinkOptions, runtime: &[PathBuf], ours: Option<&Path>) -> Vec<String> {
1426 let mut args = Vec::new();
1427 if !opts.wants_defaultlibs() && !opts.wants_runtime() {
1428 return args;
1429 }
1430 let has_gcc = find_file(runtime, "libgcc.a").is_some();
1434
1435 if opts.is_static {
1436 args.push("--start-group".to_owned());
1437 }
1438 if opts.wants_defaultlibs() {
1439 args.push("-lc".to_owned());
1440 }
1441 if opts.wants_runtime() {
1442 if let Some(path) = ours {
1443 args.push(path.display().to_string());
1444 }
1445 if has_gcc {
1446 args.push("-lgcc".to_owned());
1447 if opts.is_static {
1448 args.push("-lgcc_eh".to_owned());
1449 }
1450 }
1451 }
1452 if opts.is_static {
1453 args.push("--end-group".to_owned());
1454 } else if opts.wants_runtime() && has_gcc {
1455 args.push("--as-needed".to_owned());
1457 args.push("-lgcc_s".to_owned());
1458 args.push("--no-as-needed".to_owned());
1459 }
1460 args
1461}
1462
1463#[must_use]
1470pub fn runtime_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
1471 let libc = match target.env {
1472 Env::Musl => "musl",
1473 Env::None | Env::Gnu | Env::Msvc => "gnu",
1474 };
1475 let arch = target.arch.as_str();
1476 let names = [
1480 format!("{arch}-linux-{libc}"),
1481 format!("{arch}-pc-linux-{libc}"),
1482 format!("{arch}-redhat-linux"),
1483 format!("{arch}-suse-linux"),
1484 format!("{arch}-alpine-linux-{libc}"),
1485 ];
1486 let mut found = Vec::new();
1487 for base in ["/usr/lib/gcc", "/usr/lib64/gcc", "/usr/local/lib/gcc"] {
1488 for name in &names {
1489 found.extend(newest_first(&under(sysroot, &format!("{base}/{name}"))));
1490 }
1491 }
1492 found
1493}
1494
1495fn newest_first(dir: &Path) -> Vec<PathBuf> {
1497 let Ok(entries) = fs::read_dir(dir) else { return Vec::new() };
1498 let mut versions: Vec<(Vec<u64>, PathBuf)> = entries
1499 .flatten()
1500 .map(|e| e.path())
1501 .filter(|p| p.is_dir())
1502 .map(|p| (version_key(&p), p))
1503 .collect();
1504 versions.sort_by(|a, b| b.0.cmp(&a.0));
1508 versions.into_iter().map(|(_, path)| path).collect()
1509}
1510
1511fn version_key(dir: &Path) -> Vec<u64> {
1516 let name = dir.file_name().unwrap_or_default().to_string_lossy();
1517 name.split('.').map(|part| part.parse::<u64>().unwrap_or(0)).collect()
1518}
1519
1520#[must_use]
1526pub fn builtins_archive(target: Triple, prefixes: &[PathBuf]) -> Option<PathBuf> {
1527 const NAME: &str = rucc_sysroot::link::BUILTINS;
1530 let spellings = [target.to_string(), target.tuple().to_string()];
1534 let mut places: Vec<PathBuf> = Vec::new();
1535 for prefix in prefixes {
1536 for triple in &spellings {
1537 places.push(prefix.join(triple).join(NAME));
1538 }
1539 places.push(prefix.join(NAME));
1540 }
1541 if let Some(dir) =
1542 std::env::current_exe().ok().and_then(|exe| exe.parent().map(Path::to_path_buf))
1543 {
1544 for triple in &spellings {
1547 places.push(dir.join("lib").join("rucc").join(triple).join(NAME));
1548 }
1549 if let Some(up) = dir.parent() {
1550 for triple in &spellings {
1551 places.push(up.join("lib").join("rucc").join(triple).join(NAME));
1553 for profile in ["release", "debug"] {
1556 places.push(up.join(triple).join(profile).join(NAME));
1557 }
1558 }
1559 }
1560 places.push(dir.join(NAME));
1561 }
1562 places.into_iter().find(|path| path.is_file())
1563}
1564
1565fn emulation(target: Triple) -> &'static str {
1567 match target.arch {
1568 Arch::X86_64 => "elf_x86_64",
1569 Arch::Aarch64 => "aarch64linux",
1570 Arch::Riscv64 => "elf64lriscv",
1571 }
1572}
1573
1574fn loader(target: Triple) -> &'static str {
1579 match (target.arch, target.env) {
1580 (Arch::X86_64, Env::Musl) => "/lib/ld-musl-x86_64.so.1",
1581 (Arch::X86_64, _) => "/lib64/ld-linux-x86-64.so.2",
1582 (Arch::Aarch64, Env::Musl) => "/lib/ld-musl-aarch64.so.1",
1583 (Arch::Aarch64, _) => "/lib/ld-linux-aarch64.so.1",
1584 (Arch::Riscv64, Env::Musl) => "/lib/ld-musl-riscv64.so.1",
1585 (Arch::Riscv64, _) => "/lib/ld-linux-riscv64-lp64d.so.1",
1586 }
1587}
1588
1589#[must_use]
1596pub fn candidates(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
1597 let multiarch = multiarch(target);
1598 [
1599 format!("/usr/lib/{multiarch}"),
1600 format!("/lib/{multiarch}"),
1601 "/usr/lib64".to_owned(),
1602 "/lib64".to_owned(),
1603 "/usr/lib".to_owned(),
1604 "/lib".to_owned(),
1605 ]
1606 .into_iter()
1607 .map(|dir| under(sysroot, &dir))
1608 .collect()
1609}
1610
1611#[must_use]
1616pub fn multiarch(target: Triple) -> String {
1617 let libc = match target.env {
1618 Env::Musl => "musl",
1619 Env::None | Env::Gnu | Env::Msvc => "gnu",
1620 };
1621 format!("{}-linux-{libc}", target.arch.as_str())
1622}
1623
1624fn library_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
1626 candidates(target, sysroot).into_iter().filter(|dir| dir.is_dir()).collect()
1627}
1628
1629#[must_use]
1634pub fn search_dirs(link: &LinkOptions, target: Triple) -> Vec<PathBuf> {
1635 let mut dirs = link.search.clone();
1636 if let Some(sysroot) = cross_sysroot(target, link) {
1641 dirs.push(sysroot.lib());
1642 if rucc_sysroot::link::libc(sysroot.target()) == rucc_sysroot::link::Libc::Stub {
1643 dirs.push(sysroot.stubs().to_path_buf());
1644 }
1645 return dirs;
1646 }
1647 if let Some(distro) = distro_cross(target, link) {
1648 dirs.push(distro.lib());
1649 return dirs;
1650 }
1651 dirs.extend(candidates(target, link.sysroot.as_deref()));
1652 dirs
1653}
1654
1655#[must_use]
1660pub fn find_in_search(link: &LinkOptions, target: Triple, name: &str) -> Option<PathBuf> {
1661 find_file(&search_dirs(link, target), name)
1662}
1663
1664fn find_file(dirs: &[PathBuf], name: &str) -> Option<PathBuf> {
1666 dirs.iter().map(|dir| dir.join(name)).find(|path| path.is_file())
1667}
1668
1669fn under(sysroot: Option<&Path>, path: &str) -> PathBuf {
1671 match sysroot {
1672 Some(root) => root.join(path.strip_prefix('/').unwrap_or(path)),
1675 None => PathBuf::from(path),
1676 }
1677}
1678
1679fn target_path(sysroot: Option<&Path>, path: &str) -> String {
1686 match sysroot {
1687 Some(root) => {
1688 let root = root.display().to_string();
1689 format!("{}/{}", root.trim_end_matches(['/', '\\']), path.trim_start_matches('/'))
1690 }
1691 None => path.to_owned(),
1692 }
1693}
1694
1695#[must_use]
1697pub fn render(linker: &Linker, args: &[String]) -> String {
1698 let mut out = linker.path.display().to_string();
1699 for arg in args {
1700 out.push(' ');
1701 if arg.is_empty() || arg.contains(char::is_whitespace) {
1702 out.push('"');
1703 out.push_str(arg);
1704 out.push('"');
1705 } else {
1706 out.push_str(arg);
1707 }
1708 }
1709 out
1710}
1711
1712const WINDOWS_LINE: usize = 30_000;
1717
1718const UNIX_LINE: usize = 256 * 1024;
1721
1722fn too_long(args: &[String], limit: usize) -> bool {
1724 args.iter().map(|arg| arg.len() + 3).sum::<usize>() > limit
1725}
1726
1727fn split_for_file(args: &[String]) -> (Vec<String>, Vec<String>) {
1732 let mut front = Vec::new();
1733 let mut rest = Vec::with_capacity(args.len());
1734 let mut words = args.iter();
1735 while let Some(arg) = words.next() {
1736 if arg == "-m" {
1737 front.push(arg.clone());
1738 front.extend(words.next().cloned());
1739 } else {
1740 rest.push(arg.clone());
1741 }
1742 }
1743 (front, rest)
1744}
1745
1746fn windows_quoting(linker: &Linker) -> bool {
1752 let file = linker.path.file_stem().and_then(|stem| stem.to_str()).unwrap_or_default();
1753 cfg!(windows) && (is_lld(&linker.name) || file.starts_with("ld.lld") || file.starts_with("lld"))
1754}
1755
1756fn response_text(args: &[String], windows: bool) -> String {
1763 let mut text = String::new();
1764 for arg in args {
1765 text.push('"');
1766 let mut slashes = 0;
1767 for c in arg.chars() {
1768 match c {
1769 '\\' if windows => slashes += 1,
1770 '"' if windows => {
1771 text.extend(std::iter::repeat_n('\\', slashes * 2 + 1));
1772 text.push('"');
1773 slashes = 0;
1774 }
1775 _ if windows => {
1776 text.extend(std::iter::repeat_n('\\', slashes));
1777 text.push(c);
1778 slashes = 0;
1779 }
1780 '"' | '\\' => {
1781 text.push('\\');
1782 text.push(c);
1783 }
1784 _ => text.push(c),
1785 }
1786 }
1787 text.extend(std::iter::repeat_n('\\', slashes * 2));
1788 text.push_str("\"\n");
1789 }
1790 text
1791}
1792
1793pub fn run(linker: &Linker, args: &[String]) -> Result<(), Error> {
1805 let spawn = |why: std::io::Error| Error::Spawn {
1806 path: linker.path.display().to_string(),
1807 why: why.to_string(),
1808 };
1809 let mut command = Command::new(&linker.path);
1810 let mut written = None;
1811 if too_long(args, if cfg!(windows) { WINDOWS_LINE } else { UNIX_LINE }) {
1812 let (front, rest) = split_for_file(args);
1813 let path = std::env::temp_dir().join(format!("rucc-link-{}.rsp", std::process::id()));
1814 fs::write(&path, response_text(&rest, windows_quoting(linker))).map_err(spawn)?;
1815 let mut at = OsString::from("@");
1816 at.push(&path);
1817 command.args(front).arg(at);
1818 written = Some(path);
1819 } else {
1820 command.args(args.iter().map(OsString::from));
1821 }
1822 let status = command.status();
1823 if let Some(path) = written {
1824 let _ = fs::remove_file(path);
1825 }
1826 let status = status.map_err(spawn)?;
1827 if status.success() {
1828 return Ok(());
1829 }
1830 Err(Error::Refused {
1834 status: match status.code() {
1835 Some(code) => format!("exited with status {code}"),
1836 None => "was killed before it finished".to_owned(),
1837 },
1838 })
1839}
1840
1841#[cfg(test)]
1842mod tests {
1843 use super::*;
1844
1845 fn linux() -> Triple {
1846 Triple::new(Arch::X86_64, Os::Linux, Env::Gnu)
1847 }
1848
1849 fn one(name: &str) -> Vec<Item> {
1850 vec![Item::File(name.to_owned())]
1851 }
1852
1853 #[test]
1854 fn the_fast_one_is_looked_for_first_and_the_platforms_own_last() {
1855 let names = order(linux(), &LinkOptions::default());
1856 assert_eq!(names.first().map(String::as_str), Some("ld.mold"));
1857 assert_eq!(names.last().map(String::as_str), Some("ld"));
1858 }
1859
1860 #[test]
1861 fn naming_one_is_the_whole_of_the_order() {
1862 let opts = LinkOptions { use_ld: Some("gold".to_owned()), ..LinkOptions::default() };
1863 assert_eq!(order(linux(), &opts), ["ld.gold", "gold"]);
1864 }
1865
1866 #[test]
1867 fn a_dynamic_program_names_the_loader_that_will_start_it() {
1868 let args = line(linux(), &LinkOptions::default(), &one("a.o"), "a.out").expect("a line");
1869 let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
1870 assert!(args[at + 1].ends_with("/lib64/ld-linux-x86-64.so.2"), "{args:?}");
1871 }
1872
1873 #[test]
1875 fn a_relocatable_link_is_the_objects_and_nothing_a_program_needs() {
1876 let opts = LinkOptions { relocatable: true, ..LinkOptions::default() };
1877 let args = line(linux(), &opts, &one("a.o"), "built-in.o").expect("a line");
1878 assert_eq!(args, ["-o", "built-in.o", "-m", "elf_x86_64", "-r", "a.o"]);
1879 }
1880
1881 #[test]
1882 fn a_static_program_names_no_loader_because_nothing_will_start_it() {
1883 let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
1884 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
1885 assert!(args.contains(&"-static".to_owned()), "{args:?}");
1886 assert!(!args.contains(&"-dynamic-linker".to_owned()), "{args:?}");
1887 }
1888
1889 #[test]
1890 fn the_startup_file_of_a_program_that_moves_is_not_the_one_of_a_program_that_does_not() {
1891 let moving = LinkOptions { pie: Some(true), ..LinkOptions::default() };
1892 let fixed = LinkOptions { pie: Some(false), ..LinkOptions::default() };
1893 let named = |opts: &LinkOptions| {
1894 line(linux(), opts, &one("a.o"), "a.out")
1895 .expect("a line")
1896 .iter()
1897 .filter_map(|a| Path::new(a).file_name().map(|n| n.to_string_lossy().into_owned()))
1898 .find(|n| n.ends_with("crt1.o"))
1899 };
1900 if let Some(name) = named(&moving) {
1903 assert_eq!(name, "Scrt1.o");
1904 assert_eq!(named(&fixed).as_deref(), Some("crt1.o"));
1905 }
1906 }
1907
1908 #[test]
1917 fn a_profiled_program_is_started_by_the_startup_file_that_counts() {
1918 let profile = LinkOptions { profile: true, ..LinkOptions::default() };
1919 assert_eq!(startfile(&profile, false), Some("gcrt1.o"));
1920 assert_eq!(startfile(&profile, true), Some("gcrt1.o"));
1921 let still = LinkOptions { is_static: true, ..profile.clone() };
1922 assert_eq!(startfile(&still, true), Some("grcrt1.o"));
1923 assert_eq!(startfile(&still, false), Some("gcrt1.o"));
1924 let shared = LinkOptions { shared: true, ..profile };
1925 assert_eq!(startfile(&shared, false), None);
1926 }
1927
1928 #[test]
1930 fn a_program_that_is_not_profiled_is_started_by_the_usual_one() {
1931 let plain = LinkOptions::default();
1932 assert_eq!(startfile(&plain, false), Some("crt1.o"));
1933 assert_eq!(startfile(&plain, true), Some("Scrt1.o"));
1934 }
1935
1936 #[test]
1937 fn asking_for_no_startup_files_leaves_out_both_ends_of_them() {
1938 let opts = LinkOptions { no_startfiles: true, ..LinkOptions::default() };
1939 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
1940 assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
1941 assert!(!args.iter().any(|a| a.ends_with("crtn.o")), "{args:?}");
1942 assert!(args.contains(&"-lc".to_owned()), "{args:?}");
1944 }
1945
1946 #[test]
1947 fn asking_for_no_library_at_all_leaves_out_the_startup_files_too() {
1948 let opts = LinkOptions { no_stdlib: true, ..LinkOptions::default() };
1949 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
1950 assert!(!args.contains(&"-lc".to_owned()), "{args:?}");
1951 assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
1952 }
1953
1954 #[test]
1955 fn the_library_comes_after_the_objects_that_need_it() {
1956 let items = vec![Item::File("a.o".to_owned()), Item::Library("m".to_owned())];
1957 let args = line(linux(), &LinkOptions::default(), &items, "a.out").expect("a line");
1958 let obj = args.iter().position(|a| a == "a.o").expect("the object");
1959 let m = args.iter().position(|a| a == "-lm").expect("the library");
1960 let c = args.iter().position(|a| a == "-lc").expect("the library");
1961 assert!(obj < m && m < c, "{args:?}");
1962 }
1963
1964 #[test]
1965 fn what_the_user_told_the_linker_stays_where_the_user_wrote_it() {
1966 let items = vec![
1970 Item::File("a.o".to_owned()),
1971 Item::Linker("--whole-archive".to_owned()),
1972 Item::File("libaesni.a".to_owned()),
1973 Item::Linker("--no-whole-archive".to_owned()),
1974 Item::Library("m".to_owned()),
1975 ];
1976 let args = line(linux(), &LinkOptions::default(), &items, "a.out").expect("a line");
1977 let at = |what: &str| args.iter().position(|a| a == what).expect(what);
1978 assert!(at("a.o") < at("--whole-archive"), "{args:?}");
1979 assert!(at("--whole-archive") < at("libaesni.a"), "{args:?}");
1980 assert!(at("libaesni.a") < at("--no-whole-archive"), "{args:?}");
1981 assert!(at("--no-whole-archive") < at("-lm"), "{args:?}");
1982 assert!(at("-lm") < at("-lc"), "{args:?}");
1983 }
1984
1985 #[test]
1986 fn a_sysroot_moves_every_path_this_decided_and_none_the_user_wrote() {
1987 let opts = LinkOptions {
1988 sysroot: Some(PathBuf::from("/nowhere-at-all")),
1989 search: vec![PathBuf::from("/opt/mine")],
1990 ..LinkOptions::default()
1991 };
1992 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
1993 let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
1994 assert_eq!(args[at + 1], "/nowhere-at-all/lib64/ld-linux-x86-64.so.2");
1995 assert!(args.contains(&"-L/opt/mine".to_owned()), "{args:?}");
1996 }
1997
1998 #[test]
1999 fn a_platform_with_no_link_line_is_said_so_rather_than_linked_wrongly() {
2000 let triple = Triple::new(Arch::X86_64, Os::Windows, Env::Msvc);
2001 let error = line(triple, &LinkOptions::default(), &one("a.o"), "a.out")
2002 .expect_err("no line for it");
2003 assert!(matches!(error, Error::Target { .. }), "{error:?}");
2004 }
2005
2006 fn an_sdk(name: &str, settings: Option<&str>) -> PathBuf {
2009 let dir = std::env::temp_dir()
2010 .join(format!("rucc-sdk-{}-{}", std::process::id(), name.replace('.', "-")))
2011 .join(name);
2012 fs::create_dir_all(&dir).expect("a temporary directory");
2013 if let Some(settings) = settings {
2014 fs::write(dir.join("SDKSettings.json"), settings).expect("a settings file");
2015 }
2016 dir
2017 }
2018
2019 fn mac() -> Triple {
2020 Triple::new(Arch::Aarch64, Os::Darwin, Env::None)
2021 }
2022
2023 #[test]
2024 fn a_mac_link_names_the_platform_the_sdk_and_libsystem() {
2025 let sdk = an_sdk(
2026 "MacOSX.sdk",
2027 Some(
2028 "{\"CanonicalName\": \"macosx15.2\", \"Version\" : \"15.2\", \"MaximumDeploymentTarget\": \"15.2.99\"}",
2029 ),
2030 );
2031 let opts = LinkOptions {
2032 sysroot: Some(sdk.clone()),
2033 search: vec![PathBuf::from("/opt/mine")],
2034 no_builtins_lib: true,
2035 ..LinkOptions::default()
2036 };
2037 let items = vec![Item::File("a.o".to_owned()), Item::Library("m".to_owned())];
2038 let args = line(mac(), &opts, &items, "a.out").expect("a line");
2039 let sdk = sdk.display().to_string();
2040 let want: Vec<&str> = vec![
2041 "-arch",
2042 "arm64",
2043 "-platform_version",
2044 "macos",
2045 "11.0",
2046 "15.2",
2047 "-syslibroot",
2048 &sdk,
2049 "-o",
2050 "a.out",
2051 "-L/opt/mine",
2052 "a.o",
2053 "-lm",
2054 "-lSystem",
2055 ];
2056 assert_eq!(args, want);
2057
2058 let opts =
2060 LinkOptions { os_version: Some(rucc_tuple::Version::new(13, 4)), shared: true, ..opts };
2061 let args = line(mac(), &opts, &one("a.o"), "liba.dylib").expect("a line");
2062 assert_eq!(args[3..6], ["macos", "13.4", "15.2"]);
2063 assert!(args.contains(&"-dylib".to_owned()), "{args:?}");
2064 assert!(!args.iter().any(|arg| arg.contains("pie")), "{args:?}");
2065 }
2066
2067 #[test]
2068 fn the_sdk_version_comes_from_its_name_when_it_has_no_settings() {
2069 let sdk = an_sdk("MacOSX14.5.sdk", None);
2070 let opts =
2071 LinkOptions { sysroot: Some(sdk), no_builtins_lib: true, ..LinkOptions::default() };
2072 let args = line(mac(), &opts, &one("a.o"), "a.out").expect("a line");
2073 assert_eq!(args[3..6], ["macos", "11.0", "14.5"]);
2074 let sdk = an_sdk("Somewhere", None);
2076 let opts = LinkOptions {
2077 sysroot: Some(sdk),
2078 os_version: Some(rucc_tuple::Version::major(12)),
2079 ..opts
2080 };
2081 let args = line(mac(), &opts, &one("a.o"), "a.out").expect("a line");
2082 assert_eq!(args[3..6], ["macos", "12", "12"]);
2083 }
2084
2085 #[test]
2086 fn a_mac_link_looks_for_a_mach_o_linker_and_nothing_else() {
2087 assert_eq!(order(mac(), &LinkOptions::default()), ["ld", "ld64.lld"]);
2088 }
2089
2090 #[test]
2091 fn a_static_mac_program_is_refused_and_a_relocatable_one_is_not() {
2092 let sdk = an_sdk("MacOSX.sdk", None);
2093 let opts = LinkOptions { sysroot: Some(sdk), is_static: true, ..LinkOptions::default() };
2094 let error = line(mac(), &opts, &one("a.o"), "a.out").expect_err("no static line");
2095 let Error::Cross { why } = &error else { panic!("{error:?}") };
2096 assert!(why.contains("-static"), "{why}");
2097 let args = relocatable_line(mac(), &LinkOptions::default(), &one("a.o"), "b.o")
2098 .expect("ld64 takes -r");
2099 assert_eq!(args, ["-r", "-arch", "arm64", "-o", "b.o", "a.o"]);
2100 }
2101
2102 #[test]
2103 fn the_line_is_printed_the_way_it_would_be_typed() {
2104 let linker = Linker { name: "ld".to_owned(), path: PathBuf::from("/usr/bin/ld") };
2105 let args = ["-o".to_owned(), "a b".to_owned()];
2106 assert_eq!(render(&linker, &args), "/usr/bin/ld -o \"a b\"");
2107 }
2108
2109 #[test]
2110 fn a_linker_that_is_not_there_is_said_by_name() {
2111 let opts = LinkOptions {
2112 use_ld: Some("a-linker-nobody-has".to_owned()),
2113 ..LinkOptions::default()
2114 };
2115 let error = find(linux(), &opts).expect_err("not on this machine");
2116 assert_eq!(error, Error::Named { name: "a-linker-nobody-has".to_owned() });
2117 }
2118 fn a_gcc_dir(name: &str) -> PathBuf {
2121 let dir = std::env::temp_dir().join(format!("rucc-link-{name}-{}", std::process::id()));
2122 fs::create_dir_all(&dir).expect("a temporary directory");
2123 fs::write(dir.join("libgcc.a"), b"not really an archive").expect("a file in it");
2124 dir
2125 }
2126
2127 #[test]
2128 fn the_c_library_supplies_the_block_routines_and_our_runtime_does_not_displace_them() {
2129 let gcc = a_gcc_dir("order");
2130 let ours = PathBuf::from("/somewhere/librucc_builtins.a");
2131 let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
2132 let at_libc = args.iter().position(|a| a == "-lc").expect("libc");
2133 let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
2134 assert!(at_libc < at_ours, "{args:?}");
2137 }
2138
2139 #[test]
2140 fn a_static_link_puts_them_in_a_group_because_two_of_them_refer_to_each_other() {
2141 let gcc = a_gcc_dir("group");
2142 let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
2143 let args = runtime_items(&opts, &[gcc], None);
2144 assert_eq!(args.first().map(String::as_str), Some("--start-group"), "{args:?}");
2145 assert_eq!(args.last().map(String::as_str), Some("--end-group"), "{args:?}");
2146 assert!(args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
2149 }
2150
2151 #[test]
2152 fn a_dynamic_link_needs_no_group_and_asks_for_the_shared_half_only_if_something_wants_it() {
2153 let gcc = a_gcc_dir("dynamic");
2154 let args = runtime_items(&LinkOptions::default(), &[gcc], None);
2155 assert!(!args.contains(&"--start-group".to_owned()), "{args:?}");
2156 assert!(!args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
2157 let at = args.iter().position(|a| a == "-lgcc_s").expect("the shared half");
2158 assert_eq!(args[at - 1], "--as-needed", "{args:?}");
2159 assert_eq!(args[at + 1], "--no-as-needed", "{args:?}");
2160 }
2161
2162 #[test]
2163 fn our_own_runtime_comes_before_the_machines_because_the_two_are_interchangeable() {
2164 let gcc = a_gcc_dir("ours");
2165 let ours = PathBuf::from("/somewhere/librucc_builtins.a");
2166 let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
2167 let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
2168 let at_gcc = args.iter().position(|a| a == "-lgcc").expect("libgcc");
2169 assert!(at_ours < at_gcc, "{args:?}");
2170 }
2171
2172 #[test]
2173 fn no_builtins_lib_leaves_ours_off_and_keeps_the_machines() {
2174 let gcc = a_gcc_dir("theirs");
2175 let opts = LinkOptions { no_builtins_lib: true, ..LinkOptions::default() };
2176 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
2177 assert!(!args.iter().any(|a| a.ends_with("librucc_builtins.a")), "{args:?}");
2178 assert!(runtime_items(&opts, &[gcc], None).contains(&"-lgcc".to_owned()));
2181 }
2182
2183 #[test]
2184 fn nodefaultlibs_leaves_the_whole_runtime_off_and_not_only_the_c_library() {
2185 let gcc = a_gcc_dir("none");
2186 let opts = LinkOptions { no_defaultlibs: true, ..LinkOptions::default() };
2187 assert!(runtime_items(&opts, &[gcc], None).is_empty());
2188 }
2189
2190 #[test]
2191 fn a_machine_with_no_gcc_on_it_gets_no_names_for_libraries_that_are_not_there() {
2192 let empty = std::env::temp_dir().join("rucc-link-empty-not-a-gcc");
2193 let args = runtime_items(&LinkOptions::default(), &[empty], None);
2194 assert_eq!(args, ["-lc"], "{args:?}");
2195 }
2196
2197 #[test]
2198 fn a_gcc_version_directory_is_read_as_a_version_and_not_as_a_word() {
2199 assert!(version_key(Path::new("/usr/lib/gcc/x/13")) > version_key(Path::new("/x/9")));
2200 assert!(version_key(Path::new("/x/10.2")) > version_key(Path::new("/x/10")));
2201 assert!(version_key(Path::new("/x/snapshot")) < version_key(Path::new("/x/1")));
2203 }
2204
2205 #[test]
2213 fn the_fast_math_startup_file_is_wanted_where_gccs_spec_wants_it() {
2214 let fast = LinkOptions { fast_math: true, ..LinkOptions::default() };
2215 assert!(!LinkOptions::default().wants_fastmath());
2216 assert!(fast.wants_fastmath());
2217 assert!(!LinkOptions { shared: true, ..fast.clone() }.wants_fastmath());
2218 assert!(!LinkOptions { daz_ftz: Some(false), ..fast.clone() }.wants_fastmath());
2219 let forced = LinkOptions { shared: true, daz_ftz: Some(true), ..LinkOptions::default() };
2220 assert!(forced.wants_fastmath());
2221 }
2222
2223 fn cached() -> LinkOptions {
2224 LinkOptions {
2225 cache: Some(PathBuf::from("/cache")),
2226 prefixes: vec![a_builtins_dir()],
2227 ..LinkOptions::default()
2228 }
2229 }
2230
2231 fn a_builtins_dir() -> PathBuf {
2237 let dir = std::env::temp_dir().join(format!("rucc-link-ours-{}", std::process::id()));
2238 fs::create_dir_all(&dir).expect("a temporary directory");
2239 fs::write(dir.join("librucc_builtins.a"), b"not really an archive").expect("a file in it");
2240 dir
2241 }
2242
2243 #[test]
2246 fn the_runtime_is_found_under_the_tuple_as_well_as_the_triple() {
2247 let dir = std::env::temp_dir().join(format!("rucc-link-tuple-{}", std::process::id()));
2248 let target: Triple = "aarch64-linux-musl".parse().expect("a triple");
2249 let under = dir.join(target.tuple().to_string());
2250 fs::create_dir_all(&under).expect("a temporary directory");
2251 fs::write(under.join("librucc_builtins.a"), b"not really an archive")
2252 .expect("a file in it");
2253 let found = builtins_archive(target, std::slice::from_ref(&dir));
2254 assert_eq!(found, Some(under.join("librucc_builtins.a")));
2255 let _ = fs::remove_dir_all(&dir);
2256 }
2257
2258 fn a_sysroot(target: Triple) -> Sysroot {
2260 Sysroot::in_cache(Path::new("/cache"), target.tuple())
2261 }
2262
2263 fn foreign() -> Triple {
2269 Triple::new(Arch::X86_64, Os::None, Env::None)
2270 }
2271
2272 #[test]
2273 fn a_cross_link_reads_the_targets_own_sysroot_and_nothing_of_this_machine() {
2274 let target = Triple::new(Arch::Aarch64, Os::Linux, Env::Musl);
2275 let sysroot = a_sysroot(target);
2276 let root = sysroot.root().display().to_string();
2279 let lib = sysroot.lib();
2280 let args = cross_line(target, &cached(), &one("a.o"), "a.out", &sysroot).expect("a line");
2281 assert!(args.contains(&format!("--sysroot={root}")), "{args:?}");
2282 assert!(args.contains(&format!("-L{}", lib.display())), "{args:?}");
2283 assert!(args.contains(&lib.join("libc.a").display().to_string()), "{args:?}");
2284 let at = args.iter().position(|a| a == "-dynamic-linker").expect("the loader");
2285 assert_eq!(args[at + 1], "/lib/ld-musl-aarch64.so.1", "{args:?}");
2286 for arg in &args {
2289 assert!(!arg.contains("/usr/lib"), "{arg} in {args:?}");
2290 assert!(!arg.contains("/lib64"), "{arg} in {args:?}");
2291 }
2292 }
2293
2294 #[test]
2295 fn a_freestanding_target_links_against_our_runtime_instead_of_being_refused() {
2296 let args = line(foreign(), &cached(), &one("a.o"), "a.out").expect("a line");
2297 assert!(args.iter().any(|a| a.ends_with("librucc_builtins.a")), "{args:?}");
2298 assert!(!args.iter().any(|a| a.ends_with("libc.a")), "{args:?}");
2301 assert!(!args.contains(&"-lc".to_owned()), "{args:?}");
2302 assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
2303 }
2304
2305 #[test]
2307 fn a_driver_with_no_cache_to_look_in_says_so_rather_than_guessing() {
2308 let error = line(foreign(), &LinkOptions::default(), &one("a.o"), "a.out")
2309 .expect_err("no line for it");
2310 assert!(matches!(error, Error::Target { .. }), "{error:?}");
2311 }
2312
2313 #[test]
2314 fn a_static_link_against_a_libc_that_is_a_stub_is_refused_rather_than_attempted() {
2315 let target = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
2316 let opts = LinkOptions { is_static: true, ..cached() };
2317 let error = cross_line(target, &opts, &one("a.o"), "a.out", &a_sysroot(target))
2318 .expect_err("there is no libc.a in a stub sysroot");
2319 let Error::Cross { why } = &error else { panic!("{error:?}") };
2320 assert!(why.contains("stub"), "{why}");
2323 }
2324
2325 #[test]
2326 fn a_target_whose_linker_wants_a_different_line_is_refused_by_name() {
2327 for target in [
2328 Triple::new(Arch::Aarch64, Os::Darwin, Env::None),
2329 Triple::new(Arch::X86_64, Os::Windows, Env::Msvc),
2330 ] {
2331 let error = cross_line(target, &cached(), &one("a.o"), "a.out", &a_sysroot(target))
2332 .expect_err("no line for that format");
2333 let Error::Cross { why } = &error else { panic!("{error:?}") };
2334 assert!(why.contains(&target.tuple().to_canonical_string()), "{why}");
2335 }
2336 }
2337
2338 #[test]
2339 fn a_mingw_target_links_and_looks_for_a_linker_that_can_write_a_pe_image() {
2340 let target = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2341 let args = cross_line(target, &cached(), &one("a.o"), "a.exe", &a_sysroot(target))
2342 .expect("a line for mingw-w64");
2343 let at = |flag: &str| args.iter().position(|arg| arg == flag).expect(flag);
2344 assert_eq!(args[at("-m") + 1], "i386pep");
2345 assert_eq!(args[at("--subsystem") + 1], "console");
2346 assert!(args.iter().any(|arg| arg.ends_with("libmsvcrt.a")), "{args:?}");
2347 let names = cross_order(target);
2350 assert_eq!(names.first().map(String::as_str), Some("ld.lld"));
2351 assert!(names.contains(&"x86_64-w64-mingw32-ld".to_owned()), "{names:?}");
2352 }
2353
2354 #[test]
2355 fn our_runtime_comes_from_beside_the_compiler_rather_than_from_inside_the_sysroot() {
2356 let target = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2361 let sysroot = a_sysroot(target);
2362 let opts = cached();
2363 let args = cross_line(target, &opts, &one("a.o"), "a.exe", &sysroot).expect("a line");
2364 let ours: Vec<&String> =
2365 args.iter().filter(|arg| arg.ends_with("librucc_builtins.a")).collect();
2366 assert_eq!(ours.len(), 1, "{args:?}");
2367 assert_eq!(ours[0], &opts.prefixes[0].join("librucc_builtins.a").display().to_string());
2368 assert!(!ours[0].starts_with(&sysroot.lib().display().to_string()), "{args:?}");
2369 assert_eq!(args.last(), Some(ours[0]), "{args:?}");
2371 }
2372
2373 #[test]
2374 fn a_cross_link_with_no_runtime_to_find_says_which_command_writes_one() {
2375 let target = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2379 let opts = LinkOptions { prefixes: Vec::new(), ..cached() };
2380 let error = cross_line(target, &opts, &one("a.o"), "a.exe", &a_sysroot(target))
2381 .expect_err("there is no runtime for it to find");
2382 let Error::Cross { why } = &error else { panic!("{error:?}") };
2383 assert!(why.contains("cargo xtask builtins"), "{why}");
2384 assert!(why.contains("-fno-builtins-lib"), "{why}");
2385
2386 let without = LinkOptions { no_builtins_lib: true, ..opts };
2388 let args = cross_line(target, &without, &one("a.o"), "a.exe", &a_sysroot(target))
2389 .expect("a line without ours on it");
2390 assert!(!args.iter().any(|arg| arg.ends_with("librucc_builtins.a")), "{args:?}");
2391 }
2392
2393 #[test]
2394 fn the_version_an_lld_prints_is_read_and_nothing_elses_is() {
2395 assert_eq!(lld_major("LLD 18.1.8 (compatible with GNU linkers)\n"), Some(18));
2398 assert_eq!(lld_major("Ubuntu LLD 18.1.3 (compatible with GNU linkers)\n"), Some(18));
2399 assert_eq!(lld_major("LLD 20.1.2 (compatible with GNU linkers)\n"), Some(20));
2400
2401 assert_eq!(lld_major("GNU ld (GNU Binutils for Ubuntu) 2.42\n"), None);
2404 assert_eq!(lld_major("mold 2.4.1 (compatible with GNU ld)\n"), None);
2405 assert_eq!(lld_major(""), None);
2406 }
2407
2408 #[cfg(unix)]
2412 fn a_linker_that_says(tag: &str, text: &str) -> Linker {
2413 use std::os::unix::fs::PermissionsExt as _;
2414 let dir = std::env::temp_dir().join(format!("rucc-link-ld-{}", std::process::id()));
2415 fs::create_dir_all(&dir).expect("a temporary directory");
2416 let path = dir.join(format!("ld.lld-{tag}"));
2417 fs::write(&path, format!("#!/bin/sh\necho '{text}'\n")).expect("a script");
2418 fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).expect("an executable one");
2419 Linker { name: "ld.lld".to_owned(), path }
2420 }
2421
2422 #[test]
2423 #[cfg(unix)]
2424 fn an_lld_too_old_to_read_exportas_is_refused_for_windows_gnu_and_nowhere_else() {
2425 let windows = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2428 let old = a_linker_that_says("18", "LLD 18.1.8 (compatible with GNU linkers)");
2429 let error = suitable(windows, &old).expect_err("18 cannot link this");
2430 let Error::TooOld { name, found, target } = &error else { panic!("{error:?}") };
2431 assert_eq!((name.as_str(), *found, target.as_str()), ("ld.lld", 18, "x86_64-windows-gnu"));
2432 assert!(error.to_string().contains("IMPORT_NAME_EXPORTAS"), "{error}");
2433
2434 let linux = Triple::new(Arch::X86_64, Os::Linux, Env::Musl);
2437 assert_eq!(suitable(linux, &old), Ok(()));
2438
2439 let new = a_linker_that_says("19", "LLD 19.1.0 (compatible with GNU linkers)");
2441 assert_eq!(suitable(windows, &new), Ok(()));
2442 }
2443
2444 #[test]
2445 #[cfg(unix)]
2446 fn an_old_lld_first_in_line_is_passed_over_for_a_newer_one_behind_it() {
2447 use std::os::unix::fs::PermissionsExt as _;
2451 let root = std::env::temp_dir().join(format!("rucc-link-two-lld-{}", std::process::id()));
2452 let mut prefixes = Vec::new();
2453 for (dir, version) in [("old", "18.1.3"), ("new", "19.1.7")] {
2454 let dir = root.join(dir);
2455 fs::create_dir_all(&dir).expect("a temporary directory");
2456 let path = dir.join("ld.rucc-test-lld");
2457 fs::write(&path, format!("#!/bin/sh\necho 'Ubuntu LLD {version}'\n"))
2458 .expect("a script");
2459 fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).expect("executable");
2460 prefixes.push(dir);
2461 }
2462 let windows = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2463 let opts = LinkOptions {
2464 use_ld: Some("rucc-test-lld".to_owned()),
2465 prefixes,
2466 ..LinkOptions::default()
2467 };
2468 let found = find(windows, &opts).expect("the newer one");
2469 assert_eq!(found.path, root.join("new").join("ld.rucc-test-lld"));
2470
2471 let opts = LinkOptions { prefixes: vec![root.join("old")], ..opts };
2473 let error = find(windows, &opts).expect_err("only 18 is here");
2474 assert!(matches!(error, Error::TooOld { found: 18, .. }), "{error:?}");
2475 let _ = fs::remove_dir_all(&root);
2476 }
2477
2478 #[test]
2479 fn a_long_line_goes_in_a_response_file_that_reads_back_as_it_was() {
2480 let args: Vec<String> =
2481 ["-o", r"C:\Users\a b\out.exe", "-m", "i386pep", r#"say "hi""#, "", "plain.o"]
2482 .map(str::to_owned)
2483 .to_vec();
2484 let (front, rest) = split_for_file(&args);
2485 assert_eq!(front, ["-m", "i386pep"]);
2486 assert_eq!(
2487 crate::response_words(&response_text(&rest, false)),
2488 [&args[..2], &args[4..]].concat()
2489 );
2490 assert_eq!(
2492 response_text(&rest, true),
2493 "\"-o\"\n\"C:\\Users\\a b\\out.exe\"\n\"say \\\"hi\\\"\"\n\"\"\n\"plain.o\"\n"
2494 );
2495 assert_eq!(response_text(&[r"C:\dir\".to_owned()], true), "\"C:\\dir\\\\\"\n");
2496 assert!(!too_long(&args, 1000));
2497 assert!(too_long(&vec!["x".repeat(100); 400], WINDOWS_LINE));
2498 }
2499
2500 #[test]
2501 fn a_program_name_is_tried_with_each_pathext_extension_it_does_not_already_have() {
2502 let exts = vec![".com".to_owned(), ".exe".to_owned()];
2503 let dir = Path::new("bin");
2504 assert_eq!(
2505 spellings(&dir.join("ld.lld"), &exts),
2506 [dir.join("ld.lld.com"), dir.join("ld.lld.exe")]
2507 );
2508 assert_eq!(spellings(&dir.join("lld-link.EXE"), &exts), [dir.join("lld-link.EXE")]);
2509 assert_eq!(spellings(&dir.join("ld.lld"), &[]), [dir.join("ld.lld")]);
2510 }
2511
2512 #[test]
2513 #[cfg(unix)]
2514 fn an_lld_off_path_is_found_under_its_own_name() {
2515 use std::os::unix::fs::PermissionsExt as _;
2516 let dir = std::env::temp_dir().join(format!("rucc-link-off-path-{}", std::process::id()));
2517 fs::create_dir_all(&dir).expect("a temporary directory");
2518 let lld = dir.join("ld.lld");
2519 fs::write(&lld, "#!/bin/sh\n").expect("a file");
2520 fs::set_permissions(&lld, fs::Permissions::from_mode(0o755)).expect("permissions");
2521 let found =
2522 linker_candidates("ld.lld", &LinkOptions::default(), std::slice::from_ref(&dir));
2523 let _ = fs::remove_dir_all(&dir);
2524 assert!(found.contains(&lld), "{found:?}");
2525 }
2526
2527 #[test]
2528 fn lld_is_looked_for_where_package_managers_put_it_newest_version_first() {
2529 let root = std::env::temp_dir().join(format!("rucc-link-lld-dirs-{}", std::process::id()));
2530 for dir in ["usr/lib/llvm-18/bin", "usr/lib/llvm-19/bin", "usr/lib/llvm-9/bin", "usr/lib/x"]
2531 {
2532 fs::create_dir_all(root.join(dir)).expect("a temporary directory");
2533 }
2534 let dirs = lld_dirs(&root, Some(PathBuf::from("C:/Program Files")));
2535 let under_lib: Vec<_> =
2536 dirs.iter().filter(|dir| dir.starts_with(root.join("usr/lib"))).collect();
2537 assert_eq!(
2538 under_lib,
2539 [
2540 &root.join("usr/lib/llvm-19/bin"),
2541 &root.join("usr/lib/llvm-18/bin"),
2542 &root.join("usr/lib/llvm-9/bin")
2543 ]
2544 );
2545 assert!(dirs.contains(&root.join("opt/homebrew/opt/lld/bin")), "{dirs:?}");
2546 assert_eq!(dirs.last(), Some(&PathBuf::from("C:/Program Files").join("LLVM").join("bin")));
2547 let _ = fs::remove_dir_all(&root);
2548 }
2549
2550 #[test]
2551 fn no_linker_says_where_to_get_lld_only_when_lld_was_looked_for() {
2552 let cross = Error::NoLinker { tried: vec!["ld.lld".to_owned(), "lld".to_owned()] };
2553 assert!(cross.to_string().contains("lld 19 or newer"), "{cross}");
2554 let native = Error::NoLinker { tried: vec!["ld".to_owned()] };
2555 assert_eq!(native.to_string(), "no linker was found; tried ld");
2556 }
2557
2558 #[test]
2559 #[cfg(unix)]
2560 fn a_linker_that_will_not_say_what_it_is_is_left_alone() {
2561 let windows = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2565 let quiet = a_linker_that_says("gnu", "GNU ld (GNU Binutils for Ubuntu) 2.42");
2566 assert_eq!(suitable(windows, &quiet), Ok(()));
2567
2568 let missing = Linker { name: "ld.lld".to_owned(), path: PathBuf::from("/no/such/linker") };
2569 assert_eq!(suitable(windows, &missing), Ok(()));
2570 }
2571
2572 #[test]
2573 fn profiling_a_cross_link_is_refused_because_the_startup_file_is_compiled_code() {
2574 let target = Triple::new(Arch::X86_64, Os::Linux, Env::Musl);
2575 let opts = LinkOptions { profile: true, ..cached() };
2576 let error = cross_line(target, &opts, &one("a.o"), "a.out", &a_sysroot(target))
2577 .expect_err("there is no gcrt1.o in a generated sysroot");
2578 let Error::Cross { why } = &error else { panic!("{error:?}") };
2579 assert!(why.contains("gcrt1.o"), "{why}");
2580 }
2581
2582 #[test]
2583 fn a_distributions_cross_tree_is_used_when_there_is_no_sysroot_of_ours() {
2584 let usr = std::env::temp_dir().join(format!("rucc-link-usr-{}", std::process::id()));
2585 let root = usr.join("aarch64-linux-gnu");
2586 for dir in ["include", "lib"] {
2587 fs::create_dir_all(root.join(dir)).expect("a scratch tree");
2588 }
2589 for version in ["9", "13"] {
2590 fs::create_dir_all(usr.join("lib/gcc-cross/aarch64-linux-gnu").join(version))
2591 .expect("a scratch gcc");
2592 }
2593 let host = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
2594 let arm = Triple::new(Arch::Aarch64, Os::Linux, Env::Gnu);
2595 let opts = LinkOptions { usr: Some(usr.clone()), ..cached() };
2596 let distro = distro_for(arm, &opts, Some(host)).expect("the packages are there");
2597 assert_eq!(distro.include(), root.join("include"));
2598 assert_eq!(distro.lib(), root.join("lib"));
2599 assert_eq!(
2600 distro.gcc,
2601 [13, 9].map(|v| usr.join("lib/gcc-cross/aarch64-linux-gnu").join(v.to_string()))
2602 );
2603 assert!(cross_for(arm, &opts, Some(host)).is_none());
2605 assert!(distro_for(host, &opts, Some(host)).is_none());
2607 let riscv = Triple::new(Arch::Riscv64, Os::Linux, Env::Gnu);
2608 assert!(distro_for(riscv, &opts, Some(host)).is_none());
2609 let named = LinkOptions { sysroot: Some(PathBuf::from("/opt/root")), ..opts.clone() };
2610 assert!(distro_for(arm, &named, Some(host)).is_none());
2611 let pinned = LinkOptions {
2612 pinned: Some("aarch64-linux-gnu.2.28".parse::<TargetTuple>().expect("a release")),
2613 ..opts.clone()
2614 };
2615 assert!(distro_for(arm, &pinned, Some(host)).is_none());
2616 let cache = usr.join("cache");
2618 fs::create_dir_all(Sysroot::in_cache(&cache, arm.tuple()).lib()).expect("a sysroot");
2619 let fetched = LinkOptions { cache: Some(cache), ..opts };
2620 assert!(distro_for(arm, &fetched, Some(host)).is_none());
2621 let _ = fs::remove_dir_all(&usr);
2622 }
2623
2624 #[test]
2625 fn the_host_takes_the_host_line_and_a_tree_the_user_named_takes_it_too() {
2626 let host = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
2627 let other = Triple::new(Arch::Riscv64, Os::Linux, Env::Musl);
2628 assert!(cross_for(host, &cached(), Some(host)).is_none());
2629 assert!(cross_for(other, &cached(), Some(host)).is_some());
2630 let named = LinkOptions { sysroot: Some(PathBuf::from("/opt/root")), ..cached() };
2633 assert!(cross_for(other, &named, Some(host)).is_none());
2634 assert!(cross_for(other, &cached(), None).is_some());
2636 }
2637
2638 #[test]
2639 fn a_windows_host_compiles_for_itself_against_the_fetched_tree() {
2640 let host = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2642 let at =
2643 cross_for(host, &cached(), Some(host)).expect("the cache is the only tree there is");
2644 assert!(at.root().ends_with("x86_64-windows-gnu"), "{:?}", at.root());
2645 }
2646
2647 #[test]
2648 fn a_pinned_release_on_this_machines_own_target_is_a_cross_compile() {
2649 let host = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
2654 let pinned = LinkOptions {
2655 pinned: Some(
2656 "x86_64-linux-gnu.2.28".parse::<TargetTuple>().expect("a spelling with a release"),
2657 ),
2658 ..cached()
2659 };
2660 let at = cross_for(host, &pinned, Some(host)).expect("a pin is a cross compile");
2661 assert!(at.root().ends_with("x86_64-linux-gnu.2.28"), "{:?}", at.root());
2664 let bare = LinkOptions { pinned: None, ..cached() };
2667 assert!(cross_for(host, &bare, Some(host)).is_none());
2668 }
2669
2670 #[test]
2671 fn a_glibc_cross_link_writes_its_stubs_beside_the_sysroot_once() {
2672 let cache = std::env::temp_dir().join(format!("rucc-link-stubs-{}", std::process::id()));
2673 let _ = fs::remove_dir_all(&cache);
2674 let target = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
2675 let pinned = LinkOptions {
2676 cache: Some(cache.clone()),
2677 pinned: Some("x86_64-linux-gnu.2.28".parse().expect("a spelling with a release")),
2678 ..LinkOptions::default()
2679 };
2680 write_stubs(target, &pinned).expect("x86_64 glibc has a description");
2681 let dir = cache.join("stubs").join("x86_64-linux-gnu.2.28");
2682 let libc = dir.join("libc.so");
2683 let bytes = fs::read(&libc).expect("libc.so was written");
2684 assert!(bytes.starts_with(b"\x7fELF"));
2685 assert!(dir.join("libm.so").is_file());
2686 assert!(!dir.join("libpthread.so").exists());
2688 let before = fs::metadata(&libc).and_then(|m| m.modified()).expect("a time");
2691 write_stubs(target, &pinned).expect("again");
2692 let after = fs::metadata(&libc).and_then(|m| m.modified()).expect("a time");
2693 assert_eq!(before, after);
2694 let musl = LinkOptions {
2696 cache: Some(cache.clone()),
2697 pinned: Some("x86_64-linux-musl".parse().expect("musl")),
2698 ..LinkOptions::default()
2699 };
2700 write_stubs(Triple::new(Arch::X86_64, Os::Linux, Env::Musl), &musl).expect("nothing");
2701 assert!(!cache.join("stubs").join("x86_64-linux-musl").exists());
2702 let _ = fs::remove_dir_all(&cache);
2703 }
2704
2705 #[test]
2706 fn what_a_cross_link_searches_is_the_sysroot_and_not_this_machine() {
2707 let dirs = search_dirs(&cached(), foreign());
2708 assert_eq!(dirs.len(), 1, "{dirs:?}");
2711 assert!(dirs[0].starts_with("/cache"), "{dirs:?}");
2712 assert!(dirs[0].ends_with("lib"), "{dirs:?}");
2713 let mine = LinkOptions { search: vec![PathBuf::from("/opt/mine")], ..cached() };
2715 assert_eq!(search_dirs(&mine, foreign())[0], PathBuf::from("/opt/mine"));
2716 }
2717
2718 #[test]
2719 fn gnu_ld_is_not_looked_for_against_the_fetched_mingw_sysroot() {
2720 let windows = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2721 assert_eq!(order(windows, &cached()), ["ld.lld", "lld"]);
2722 }
2723
2724 #[test]
2725 fn the_linker_looked_for_on_a_cross_link_is_one_that_can_cross() {
2726 let names = cross_order(Triple::new(Arch::Aarch64, Os::Linux, Env::Gnu));
2727 assert_eq!(names.first().map(String::as_str), Some("ld.lld"));
2728 assert!(names.contains(&"aarch64-linux-gnu-ld".to_owned()), "{names:?}");
2729 assert!(!names.iter().any(|name| name.contains("mold")), "{names:?}");
2732 assert!(!names.contains(&"ld".to_owned()), "{names:?}");
2733 assert_eq!(order(foreign(), &cached()), ["ld.lld", "lld"]);
2735 }
2736
2737 #[test]
2738 fn the_four_flags_become_the_five_modes_they_describe() {
2739 let plain = LinkOptions::default();
2740 assert_eq!(mode(&plain), LinkMode::Dynamic);
2741 assert_eq!(
2742 mode(&LinkOptions { pie: Some(false), ..plain.clone() }),
2743 LinkMode::DynamicNoPie
2744 );
2745 assert_eq!(mode(&LinkOptions { is_static: true, ..plain.clone() }), LinkMode::Static);
2746 let both = LinkOptions { is_static: true, pie: Some(true), ..plain.clone() };
2747 assert_eq!(mode(&both), LinkMode::StaticPie);
2748 assert_eq!(mode(&LinkOptions { shared: true, ..plain }), LinkMode::Shared);
2749 }
2750
2751 #[test]
2752 fn a_sysroot_that_has_not_been_built_is_named_before_anything_is_compiled() {
2753 let opts = LinkOptions {
2754 cache: Some(std::env::temp_dir().join("rucc-a-cache-nobody-filled")),
2755 ..LinkOptions::default()
2756 };
2757 let error = preflight(foreign(), &opts).expect_err("nothing has built one");
2758 let Error::Sysroot { dir, pinned, .. } = &error else { panic!("{error:?}") };
2759 assert!(dir.ends_with("x86_64-none"), "{dir}");
2760 assert!(!pinned, "nothing should be pinned for a bare metal target");
2763 let said = error.to_string();
2764 assert!(said.contains("pins none for it to fetch"), "{said}");
2765 }
2766
2767 #[test]
2771 fn a_sysroot_that_could_be_fetched_is_told_what_to_run() {
2772 let said = Error::Sysroot {
2773 target: "x86_64-linux-musl".to_owned(),
2774 dir: "/somewhere/sysroots/x86_64-linux-musl".to_owned(),
2775 pinned: true,
2776 }
2777 .to_string();
2778 assert!(said.contains("`rucc --fetch x86_64-linux-musl`"), "{said}");
2779 assert!(said.contains("--sysroot=<dir>"), "{said}");
2782 }
2783
2784 #[test]
2785 fn a_link_against_this_machine_has_nothing_to_check_before_it_starts() {
2786 assert!(preflight(linux(), &LinkOptions::default()).is_ok());
2789 }
2790
2791 #[test]
2792 fn a_runtime_directory_that_is_not_on_this_machine_is_not_offered() {
2793 let dirs = runtime_dirs(linux(), Some(Path::new("/definitely/not/a/sysroot")));
2794 assert!(dirs.is_empty(), "{dirs:?}");
2795 }
2796}