1use std::ffi::OsString;
76use std::fs;
77use std::path::{Path, PathBuf};
78use std::process::Command;
79
80use rucc_sysroot::layout::{Kernel, Sysroot};
81use rucc_sysroot::{Chip, Crt, LinkMode, argv};
82use rucc_target::{Arch, Env, Os, Triple};
83use rucc_tuple::TargetTuple;
84
85#[derive(Debug, Default, Clone, PartialEq, Eq)]
91pub struct LinkOptions {
92 pub use_ld: Option<String>,
94 pub search: Vec<PathBuf>,
96 pub prefixes: Vec<PathBuf>,
98 pub sysroot: Option<PathBuf>,
100 pub cache: Option<PathBuf>,
107 pub usr: Option<PathBuf>,
115 pub is_static: bool,
117 pub shared: bool,
119 pub pie: Option<bool>,
121 pub no_stdlib: bool,
123 pub no_startfiles: bool,
125 pub no_defaultlibs: bool,
127 pub export_dynamic: bool,
129 pub strip: bool,
131 pub gui: bool,
133 pub unicode: bool,
135 pub crt: Crt,
137 pub no_builtins_lib: bool,
140 pub pinned: Option<TargetTuple>,
149 pub profile: bool,
156 pub fast_math: bool,
163 pub daz_ftz: Option<bool>,
166 pub relocatable: bool,
172 pub os_version: Option<rucc_tuple::Version>,
178}
179
180impl LinkOptions {
181 fn wants_fastmath(&self) -> bool {
183 self.daz_ftz.unwrap_or(self.fast_math && !self.shared)
184 }
185
186 fn wants_startfiles(&self) -> bool {
188 !self.no_stdlib && !self.no_startfiles && !self.relocatable
189 }
190
191 fn wants_defaultlibs(&self) -> bool {
193 !self.no_stdlib && !self.no_defaultlibs && !self.relocatable
194 }
195
196 fn wants_runtime(&self) -> bool {
202 !self.no_stdlib && !self.no_defaultlibs && !self.relocatable
203 }
204}
205
206#[derive(Debug, Clone, PartialEq, Eq)]
212pub enum Item {
213 File(String),
215 Library(String),
217 Linker(String),
227}
228
229impl std::fmt::Display for Item {
230 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231 match self {
232 Item::File(path) => f.write_str(path),
233 Item::Library(name) => write!(f, "-l{name}"),
234 Item::Linker(arg) => write!(f, "-Wl,{arg}"),
235 }
236 }
237}
238
239#[derive(Debug, Clone, PartialEq, Eq)]
241pub enum Error {
242 NoLinker {
244 tried: Vec<String>,
246 },
247 Named {
249 name: String,
251 },
252 Target {
254 triple: String,
256 },
257 Cross {
264 why: String,
266 },
267 Sysroot {
269 target: String,
271 dir: String,
273 pinned: bool,
276 },
277 TooOld {
283 name: String,
285 found: u32,
287 target: String,
289 },
290 Spawn {
292 path: String,
294 why: String,
296 },
297 Refused {
299 status: String,
301 },
302}
303
304impl std::fmt::Display for Error {
305 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306 match self {
307 Error::NoLinker { tried } => {
308 write!(f, "no linker was found; tried {}", tried.join(", "))?;
309 if tried.iter().any(|name| is_lld(name)) {
312 write!(
313 f,
314 ". lld {LLD_EXPORTAS} or newer links for every target, and {}",
315 lld_advice()
316 )?;
317 }
318 Ok(())
319 }
320 Error::Named { name } => {
321 write!(f, "-fuse-ld={name} asks for a linker that is not on this machine")
322 }
323 Error::Target { triple } => {
324 write!(f, "there is no link line for {triple} in this compiler yet")
325 }
326 Error::Cross { why } => f.write_str(why),
327 Error::Sysroot { target, dir, pinned: true } => write!(
333 f,
334 "there is no sysroot for {target} at {dir}, so there is nothing to link it \
335 against. `rucc --fetch {target}` gets the one this release pins, or pass \
336 --sysroot=<dir> to name a tree you have already"
337 ),
338 Error::Sysroot { target, dir, pinned: false } => write!(
339 f,
340 "there is no sysroot for {target} at {dir}, so there is nothing to link it \
341 against, and this release pins none for it to fetch. Pass --sysroot=<dir> to name \
342 a tree you have already, or see spec/cross-compile/13-distribution.md section \
343 13.2 for the cache that will hold one"
344 ),
345 Error::TooOld { name, found, target } => write!(
348 f,
349 "{name} is lld {found} and cannot link for {target}. mingw-w64 writes a few hundred \
350 of its aliases, `_crt_atexit == atexit` among them, as IMPORT_NAME_EXPORTAS \
351 records in its import libraries, which lld learned to read in {LLD_EXPORTAS}. An \
352 older one neither reads them nor says so: it writes an import by ordinal zero, the \
353 link succeeds, and the program dies at startup. No newer lld was found either. \
354 For lld {LLD_EXPORTAS} or newer, {}, or name one with -fuse-ld=",
355 lld_advice()
356 ),
357 Error::Spawn { path, why } => write!(f, "could not run the linker at {path}: {why}"),
358 Error::Refused { status } => write!(f, "the linker {status}"),
359 }
360 }
361}
362
363impl std::error::Error for Error {}
364
365#[derive(Debug, Clone, PartialEq, Eq)]
367pub struct Linker {
368 pub name: String,
370 pub path: PathBuf,
372}
373
374#[must_use]
381pub fn order(target: Triple, opts: &LinkOptions) -> Vec<String> {
382 if let Some(named) = &opts.use_ld {
383 return vec![format!("ld.{named}"), named.clone()];
385 }
386 if target.os == Os::Darwin {
390 return vec!["ld".to_owned(), "ld64.lld".to_owned()];
391 }
392 if is_msvc(target) {
396 return vec!["lld-link".to_owned(), "link.exe".to_owned()];
397 }
398 if cross_sysroot(target, opts).is_some() {
399 let mut names = cross_order(target);
400 if (target.os, target.env) == (Os::Windows, Env::Gnu) {
406 names.retain(|name| is_lld(name));
407 }
408 return names;
409 }
410 if distro_cross(target, opts).is_some() {
411 return cross_order(target);
412 }
413 match target.os {
414 Os::Windows => vec!["lld-link".to_owned(), "link.exe".to_owned()],
415 _ => vec![
416 "ld.mold".to_owned(),
417 "mold".to_owned(),
418 "ld.lld".to_owned(),
419 "lld".to_owned(),
420 "ld".to_owned(),
421 ],
422 }
423}
424
425fn is_msvc(target: Triple) -> bool {
427 (target.os, target.env) == (Os::Windows, Env::Msvc)
428}
429
430fn msvc_sysroot(target: Triple, opts: &LinkOptions) -> Result<Sysroot, Error> {
442 let tuple = target_tuple(target, opts);
443 match &opts.sysroot {
444 Some(root) => Ok(Sysroot::at(root.clone(), tuple)),
445 None => Err(Error::Cross {
446 why: format!(
447 "a link for {tuple} is against Microsoft's C runtime and the Windows SDK, which \
448 this compiler may not ship. `rucc --fetch {tuple}` gets them from Microsoft once \
449 you accept Microsoft's licence and prints the --sysroot=<dir> to pass",
450 tuple = tuple.to_canonical_string()
451 ),
452 }),
453 }
454}
455
456fn cross_order(target: Triple) -> Vec<String> {
472 let mut names = vec!["ld.lld".to_owned(), "lld".to_owned()];
473 match (target.os, target.env) {
474 (Os::Linux, _) => names.push(format!("{}-ld", multiarch(target))),
475 (Os::Windows, Env::Gnu) => names.push(format!("{}-w64-mingw32-ld", target.arch.as_str())),
476 _ => {}
477 }
478 names
479}
480
481#[must_use]
508pub fn cross_sysroot(target: Triple, opts: &LinkOptions) -> Option<Sysroot> {
509 cross_for(target, opts, Triple::host())
510}
511
512fn cross_for(target: Triple, opts: &LinkOptions, host: Option<Triple>) -> Option<Sysroot> {
514 if opts.sysroot.is_some() {
515 return None;
516 }
517 let tuple = target_tuple(target, opts);
518 if host == Some(target) && target.os != Os::Windows && tuple.env_version().is_none() {
522 return None;
523 }
524 if distro_for(target, opts, host).is_some() {
525 return None;
526 }
527 let cache = opts.cache.as_deref()?;
528 Some(Sysroot::in_cache(cache, tuple))
529}
530
531#[derive(Debug, Clone, PartialEq, Eq)]
538pub struct Distro {
539 pub root: PathBuf,
541 pub gcc: Vec<PathBuf>,
544}
545
546impl Distro {
547 #[must_use]
549 pub fn include(&self) -> PathBuf {
550 self.root.join("include")
551 }
552
553 #[must_use]
555 pub fn lib(&self) -> PathBuf {
556 self.root.join("lib")
557 }
558}
559
560#[must_use]
568pub fn distro_cross(target: Triple, opts: &LinkOptions) -> Option<Distro> {
569 distro_for(target, opts, Triple::host())
570}
571
572fn distro_for(target: Triple, opts: &LinkOptions, host: Option<Triple>) -> Option<Distro> {
574 if opts.sysroot.is_some() || target.os != Os::Linux || host == Some(target) {
575 return None;
576 }
577 let tuple = target_tuple(target, opts);
578 if tuple.env_version().is_some() {
579 return None;
580 }
581 if opts.cache.as_deref().is_some_and(|cache| Sysroot::in_cache(cache, tuple).lib().is_dir()) {
582 return None;
583 }
584 let usr = opts.usr.as_deref()?;
585 let name = multiarch(target);
586 let root = usr.join(&name);
587 if !root.join("include").is_dir() || !root.join("lib").is_dir() {
588 return None;
589 }
590 let gcc = newest_first(&usr.join("lib/gcc-cross").join(&name));
591 Some(Distro { root, gcc })
592}
593
594fn target_tuple(target: Triple, opts: &LinkOptions) -> TargetTuple {
602 opts.pinned.unwrap_or_else(|| target.tuple())
603}
604
605#[must_use]
616pub fn cross_kernel(target: Triple, opts: &LinkOptions) -> Option<Kernel> {
617 kernel_for(target, opts, Triple::host())
618}
619
620fn kernel_for(target: Triple, opts: &LinkOptions, host: Option<Triple>) -> Option<Kernel> {
622 cross_for(target, opts, host)?;
623 Kernel::for_target(opts.cache.as_deref()?, target.tuple())
624}
625
626fn mode(opts: &LinkOptions) -> LinkMode {
633 let pie = opts.pie.unwrap_or(!opts.is_static && !opts.shared);
634 if opts.shared {
635 LinkMode::Shared
636 } else if opts.is_static {
637 if pie { LinkMode::StaticPie } else { LinkMode::Static }
638 } else if pie {
639 LinkMode::Dynamic
640 } else {
641 LinkMode::DynamicNoPie
642 }
643}
644
645fn cross_line(
652 target: Triple,
653 opts: &LinkOptions,
654 items: &[Item],
655 output: &str,
656 sysroot: &Sysroot,
657) -> Result<Vec<String>, Error> {
658 if opts.profile {
659 return Err(Error::Cross {
663 why: format!(
664 "-pg needs gcrt1.o, or gcrt2.o on Windows, the startup file that starts and stops \
665 the counting, and a generated sysroot for {target} does not have one. Profile on \
666 the host, or pass --sysroot=<dir> naming a tree that has it"
667 ),
668 });
669 }
670 let inputs: Vec<argv::Item> = items
671 .iter()
672 .map(|item| match item {
673 Item::File(path) => argv::Item::File(PathBuf::from(path)),
674 Item::Library(name) => argv::Item::Library(name.clone()),
675 Item::Linker(arg) => argv::Item::Linker(arg.clone()),
676 })
677 .collect();
678 let output = PathBuf::from(output);
679 let ours = builtins_archive(target, &opts.prefixes);
684 if ours.is_none() && opts.wants_runtime() && !opts.no_builtins_lib {
685 let tuple = target.tuple().to_canonical_string();
689 return Err(Error::Cross {
690 why: format!(
691 "a cross link ends with librucc_builtins.a, this compiler's own runtime for \
692 {tuple}, and there is none beside the compiler or under a -B prefix. A sysroot \
693 does not carry it, because it is our output rather than the platform's. Build it \
694 with `cargo xtask builtins --target={tuple}`, or pass -fno-builtins-lib to link \
695 without it"
696 ),
697 });
698 }
699 let invocation = argv::Invocation {
700 inputs: &inputs,
701 output: Some(&output),
702 mode: mode(opts),
703 search: &opts.search,
704 no_startfiles: !opts.wants_startfiles(),
705 no_defaultlibs: !opts.wants_defaultlibs(),
706 no_builtins_lib: opts.no_builtins_lib,
707 builtins: ours.as_deref(),
708 export_dynamic: opts.export_dynamic,
709 strip: opts.strip,
710 gui: opts.gui,
711 unicode: opts.unicode,
712 crt: opts.crt,
713 };
714 argv::argv(target.tuple(), sysroot, &invocation)
715 .map_err(|why| Error::Cross { why: why.to_string() })
716}
717
718pub fn preflight(target: Triple, opts: &LinkOptions) -> Result<(), Error> {
735 if opts.relocatable {
736 return relocatable_line(target, opts, &[], "a.out").map(drop);
737 }
738 if target.os == Os::Darwin {
741 return darwin_line(target, opts, &[], "a.out").map(drop);
742 }
743 if is_msvc(target) {
744 return msvc_preflight(target, opts);
745 }
746 let Some(sysroot) = cross_sysroot(target, opts) else { return Ok(()) };
747 let shape = LinkOptions { no_builtins_lib: true, ..opts.clone() };
751 cross_line(target, &shape, &[], "a.out", &sysroot)?;
752 if !sysroot.lib().is_dir() {
756 let tuple = target_tuple(target, opts).to_canonical_string();
757 return Err(Error::Sysroot {
758 dir: sysroot.root().display().to_string(),
759 pinned: rucc_sysroot::pinned_for_target(target_tuple(target, opts)).is_some(),
760 target: tuple,
761 });
762 }
763 cross_line(target, opts, &[], "a.out", &sysroot)?;
766 Ok(())
767}
768
769fn msvc_preflight(target: Triple, opts: &LinkOptions) -> Result<(), Error> {
775 let sysroot = msvc_sysroot(target, opts)?;
776 let shape = LinkOptions { no_builtins_lib: true, ..opts.clone() };
777 cross_line(target, &shape, &[], "a.exe", &sysroot)?;
778 let tuple = target_tuple(target, opts);
779 if let Some(chip) = Chip::of(tuple) {
780 let crt = sysroot.root().join("crt").join("lib").join(chip.in_tree());
781 if !crt.is_dir() {
782 return Err(Error::Cross {
783 why: format!(
784 "{} has no {}, so it is not a tree for {tuple} to link against. `rucc \
785 --fetch {tuple}` lays one out and prints where",
786 sysroot.root().display(),
787 crt.display(),
788 tuple = tuple.to_canonical_string()
789 ),
790 });
791 }
792 }
793 cross_line(target, opts, &[], "a.exe", &sysroot)?;
794 Ok(())
795}
796
797pub fn write_stubs(target: Triple, opts: &LinkOptions) -> Result<(), Error> {
817 let Some(sysroot) = cross_sysroot(target, opts) else { return Ok(()) };
818 let tuple = sysroot.target();
819 if rucc_stub::glibc::architecture(tuple).is_none() {
820 return Ok(());
821 }
822 let Ok(Some(minor)) = rucc_sysroot::bundled_glibc_minor(tuple) else { return Ok(()) };
823 let files = rucc_stub::glibc::stubs(tuple, minor).map_err(|why| Error::Cross {
824 why: format!("the glibc stubs for {}: {why}", tuple.to_canonical_string()),
825 })?;
826 let dir = sysroot.stubs();
827 let failed = |path: &Path, why: std::io::Error| Error::Cross {
828 why: format!("{} cannot be written: {why}", path.display()),
829 };
830 fs::create_dir_all(dir).map_err(|why| failed(dir, why))?;
831 for file in files {
832 let path = dir.join(&file.name);
833 if fs::read(&path).is_ok_and(|there| there == file.bytes) {
834 continue;
835 }
836 let temporary = dir.join(format!(".{}.{}", file.name, std::process::id()));
837 fs::write(&temporary, &file.bytes).map_err(|why| failed(&temporary, why))?;
838 fs::rename(&temporary, &path).map_err(|why| {
839 let _ = fs::remove_file(&temporary);
840 failed(&path, why)
841 })?;
842 }
843 Ok(())
844}
845
846pub fn find(target: Triple, opts: &LinkOptions) -> Result<Linker, Error> {
857 let tried = order(target, opts);
858 let places = lld_dirs(Path::new("/"), std::env::var_os("ProgramFiles").map(PathBuf::from));
859 let mut refused = None;
863 for name in &tried {
864 for path in linker_candidates(name, opts, &places) {
865 let linker = Linker { name: name.clone(), path };
866 match suitable(target, &linker) {
867 Ok(()) => return Ok(linker),
868 Err(why) => {
869 refused.get_or_insert(why);
870 }
871 }
872 }
873 }
874 if let Some(why) = refused {
875 return Err(why);
876 }
877 match &opts.use_ld {
878 Some(name) => Err(Error::Named { name: name.clone() }),
879 None => Err(Error::NoLinker { tried }),
880 }
881}
882
883fn linker_candidates(name: &str, opts: &LinkOptions, places: &[PathBuf]) -> Vec<PathBuf> {
890 if name.contains(std::path::MAIN_SEPARATOR) || name.contains('/') {
891 let path = PathBuf::from(name);
892 return if path.is_file() { vec![path] } else { Vec::new() };
893 }
894 let pathext = pathext();
895 let mut found: Vec<PathBuf> = opts
896 .prefixes
897 .iter()
898 .flat_map(|dir| spellings(&dir.join(name), &pathext))
899 .filter(|path| path.is_file())
900 .collect();
901 if let Some(path) = std::env::var_os("PATH") {
902 found.extend(
903 std::env::split_paths(&path)
904 .flat_map(|dir| spellings(&dir.join(name), &pathext))
905 .filter(|p| executable(p)),
906 );
907 }
908 if is_lld(name) {
912 for dir in places {
913 for file in spellings(&dir.join(name), &pathext) {
914 if executable(&file) && !found.contains(&file) {
915 found.push(file);
916 }
917 }
918 }
919 }
920 found
921}
922
923fn pathext() -> Vec<String> {
930 if !cfg!(windows) {
931 return Vec::new();
932 }
933 let list = std::env::var("PATHEXT").unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".to_owned());
934 list.split(';').filter(|ext| ext.starts_with('.')).map(str::to_ascii_lowercase).collect()
935}
936
937fn spellings(path: &Path, exts: &[String]) -> Vec<PathBuf> {
944 let has = path
945 .extension()
946 .and_then(|ext| ext.to_str())
947 .is_some_and(|ext| exts.iter().any(|e| e[1..].eq_ignore_ascii_case(ext)));
948 if exts.is_empty() || has {
949 return vec![path.to_path_buf()];
950 }
951 exts.iter()
952 .map(|ext| {
953 let mut name = path.as_os_str().to_owned();
954 name.push(ext);
955 PathBuf::from(name)
956 })
957 .collect()
958}
959
960fn is_lld(name: &str) -> bool {
962 matches!(name, "ld.lld" | "ld64.lld" | "lld" | "lld-link")
963}
964
965fn lld_dirs(root: &Path, program_files: Option<PathBuf>) -> Vec<PathBuf> {
975 let mut dirs: Vec<PathBuf> = [
976 "opt/homebrew/opt/lld/bin",
977 "opt/homebrew/opt/llvm/bin",
978 "usr/local/opt/lld/bin",
979 "usr/local/opt/llvm/bin",
980 "home/linuxbrew/.linuxbrew/opt/lld/bin",
981 "home/linuxbrew/.linuxbrew/opt/llvm/bin",
982 ]
983 .iter()
984 .map(|dir| root.join(dir))
985 .collect();
986 for (parent, prefix) in [("usr/lib", "llvm-"), ("usr/lib64", "llvm")] {
987 let mut versions: Vec<(u32, PathBuf)> = fs::read_dir(root.join(parent))
988 .into_iter()
989 .flatten()
990 .flatten()
991 .filter_map(|entry| {
992 let name = entry.file_name();
993 let version = name.to_str()?.strip_prefix(prefix)?.parse().ok()?;
994 Some((version, entry.path().join("bin")))
995 })
996 .collect();
997 versions.sort_by_key(|(version, _)| std::cmp::Reverse(*version));
998 dirs.extend(versions.into_iter().map(|(_, dir)| dir));
999 }
1000 if let Some(dir) = program_files {
1001 dirs.push(dir.join("LLVM").join("bin"));
1002 }
1003 dirs
1004}
1005
1006fn lld_advice() -> &'static str {
1012 if cfg!(target_os = "macos") {
1013 "`brew install lld` installs one, and rucc finds it in Homebrew's directory without a \
1014 change to PATH"
1015 } else if cfg!(windows) {
1016 "the LLVM installer from https://github.com/llvm/llvm-project/releases, or `winget install \
1017 LLVM.LLVM`, installs one in %ProgramFiles%\\LLVM\\bin, where rucc finds it without a \
1018 change to PATH"
1019 } else {
1020 "on Debian and Ubuntu `apt install lld-19` installs one in /usr/lib/llvm-19/bin and on \
1021 Fedora `dnf install lld` installs one on PATH, and rucc finds either without a change to \
1022 PATH"
1023 }
1024}
1025
1026pub const LLD_EXPORTAS: u32 = 19;
1031
1032pub fn suitable(target: Triple, linker: &Linker) -> Result<(), Error> {
1054 if (target.os, target.env) != (Os::Windows, Env::Gnu) {
1055 return Ok(());
1056 }
1057 let Some(found) = lld_major(&reported_version(&linker.path)) else { return Ok(()) };
1058 if found >= LLD_EXPORTAS {
1059 return Ok(());
1060 }
1061 Err(Error::TooOld {
1062 name: linker.name.clone(),
1063 found,
1064 target: target.tuple().to_canonical_string(),
1065 })
1066}
1067
1068fn reported_version(path: &Path) -> String {
1074 let Ok(out) = Command::new(path).arg("--version").output() else { return String::new() };
1075 String::from_utf8_lossy(&out.stdout).into_owned()
1076}
1077
1078fn lld_major(text: &str) -> Option<u32> {
1086 let mut words = text.split_whitespace();
1087 words.find(|word| *word == "LLD")?;
1088 words.next()?.split('.').next()?.parse().ok()
1089}
1090
1091#[cfg(unix)]
1093fn executable(path: &Path) -> bool {
1094 use std::os::unix::fs::PermissionsExt as _;
1095 path.metadata().is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
1096}
1097
1098#[cfg(not(unix))]
1103fn executable(path: &Path) -> bool {
1104 path.is_file()
1105}
1106
1107pub fn line(
1119 target: Triple,
1120 opts: &LinkOptions,
1121 items: &[Item],
1122 output: &str,
1123) -> Result<Vec<String>, Error> {
1124 if opts.relocatable {
1125 return relocatable_line(target, opts, items, output);
1126 }
1127 if target.os == Os::Darwin {
1128 return darwin_line(target, opts, items, output);
1129 }
1130 if is_msvc(target) {
1131 return cross_line(target, opts, items, output, &msvc_sysroot(target, opts)?);
1132 }
1133 if let Some(sysroot) = cross_sysroot(target, opts) {
1134 return cross_line(target, opts, items, output, &sysroot);
1135 }
1136 if target.os != Os::Linux {
1137 return Err(Error::Target { triple: target.to_string() });
1138 }
1139 let machine = emulation(target);
1140 let root = opts.sysroot.as_deref();
1141 let distro = distro_cross(target, opts);
1144 let dirs = match &distro {
1145 Some(distro) => vec![distro.lib()],
1146 None => library_dirs(target, root),
1147 };
1148 let runtime = match distro {
1151 Some(distro) => distro.gcc,
1152 None => runtime_dirs(target, root),
1153 };
1154 let ours = if opts.no_builtins_lib { None } else { builtins_archive(target, &opts.prefixes) };
1155 let mut args = vec![
1156 "-o".to_owned(),
1157 output.to_owned(),
1158 "-m".to_owned(),
1162 machine.to_owned(),
1163 "--eh-frame-hdr".to_owned(),
1166 "--hash-style=gnu".to_owned(),
1170 ];
1171
1172 let pie = opts.pie.unwrap_or(!opts.is_static && !opts.shared);
1173 if opts.shared {
1174 args.push("-shared".to_owned());
1175 } else if opts.is_static {
1176 args.push("-static".to_owned());
1177 } else if pie {
1178 args.push("-pie".to_owned());
1179 } else {
1180 args.push("-no-pie".to_owned());
1181 }
1182 if !opts.is_static && !opts.shared {
1183 args.push("-dynamic-linker".to_owned());
1184 args.push(target_path(root, loader(target)));
1185 }
1186 if opts.export_dynamic {
1187 args.push("--export-dynamic".to_owned());
1188 }
1189 if opts.strip {
1190 args.push("-s".to_owned());
1191 }
1192
1193 if opts.wants_startfiles() {
1194 for name in startfile(opts, pie).into_iter().chain(["crti.o"]) {
1195 if let Some(path) = find_file(&dirs, name) {
1196 args.push(path.display().to_string());
1197 }
1198 }
1199 let begin = if opts.shared || pie {
1205 "crtbeginS.o"
1206 } else if opts.is_static {
1207 "crtbeginT.o"
1208 } else {
1209 "crtbegin.o"
1210 };
1211 if let Some(path) = find_file(&runtime, begin).or_else(|| find_file(&runtime, "crtbegin.o"))
1212 {
1213 args.push(path.display().to_string());
1214 }
1215 }
1216
1217 for dir in &opts.search {
1218 args.push(format!("-L{}", dir.display()));
1219 }
1220 for dir in &dirs {
1221 args.push(format!("-L{}", dir.display()));
1222 }
1223 for dir in &runtime {
1226 args.push(format!("-L{}", dir.display()));
1227 }
1228
1229 for item in items {
1230 match item {
1231 Item::File(path) => args.push(path.clone()),
1232 Item::Library(name) => args.push(format!("-l{name}")),
1233 Item::Linker(arg) => args.push(arg.clone()),
1234 }
1235 }
1236 args.extend(runtime_items(opts, &runtime, ours.as_deref()));
1239
1240 if opts.wants_startfiles() {
1241 if opts.wants_fastmath() {
1246 if let Some(path) = find_file(&runtime, "crtfastmath.o") {
1247 args.push(path.display().to_string());
1248 }
1249 }
1250 let end = if opts.shared || pie { "crtendS.o" } else { "crtend.o" };
1251 if let Some(path) = find_file(&runtime, end).or_else(|| find_file(&runtime, "crtend.o")) {
1252 args.push(path.display().to_string());
1253 }
1254 if let Some(path) = find_file(&dirs, "crtn.o") {
1255 args.push(path.display().to_string());
1256 }
1257 }
1258
1259 Ok(args)
1260}
1261
1262fn relocatable_line(
1268 target: Triple,
1269 opts: &LinkOptions,
1270 items: &[Item],
1271 output: &str,
1272) -> Result<Vec<String>, Error> {
1273 if target.os == Os::Darwin {
1274 let mut args = vec![
1277 "-r".to_owned(),
1278 "-arch".to_owned(),
1279 darwin_arch(target)?.to_owned(),
1280 "-o".to_owned(),
1281 output.to_owned(),
1282 ];
1283 for dir in &opts.search {
1284 args.push(format!("-L{}", dir.display()));
1285 }
1286 push_items(&mut args, items);
1287 return Ok(args);
1288 }
1289 if target.os != Os::Linux {
1290 return Err(Error::Target { triple: target.to_string() });
1291 }
1292 let mut args = vec![
1293 "-o".to_owned(),
1294 output.to_owned(),
1295 "-m".to_owned(),
1296 emulation(target).to_owned(),
1297 "-r".to_owned(),
1298 ];
1299 if opts.strip {
1300 args.push("-s".to_owned());
1301 }
1302 for dir in &opts.search {
1303 args.push(format!("-L{}", dir.display()));
1304 }
1305 for item in items {
1306 match item {
1307 Item::File(path) => args.push(path.clone()),
1308 Item::Library(name) => args.push(format!("-l{name}")),
1309 Item::Linker(arg) => args.push(arg.clone()),
1310 }
1311 }
1312 Ok(args)
1313}
1314
1315pub fn darwin_line(
1332 target: Triple,
1333 opts: &LinkOptions,
1334 items: &[Item],
1335 output: &str,
1336) -> Result<Vec<String>, Error> {
1337 let arch = darwin_arch(target)?;
1338 if opts.is_static && !opts.shared {
1339 return Err(Error::Cross {
1340 why: "there is no static C library for Apple platforms, so -static cannot make a \
1341 program for one"
1342 .to_owned(),
1343 });
1344 }
1345 let Some(sdk) = crate::library::sdk(opts.sysroot.as_deref()) else {
1346 return Err(Error::Cross {
1347 why: format!(
1348 "no SDK was found to link {} against. Install the command line tools with \
1349 `xcode-select --install`, or name one with -isysroot <dir> or SDKROOT",
1350 target_tuple(target, opts).to_canonical_string()
1351 ),
1352 });
1353 };
1354 let tuple = target_tuple(target, opts);
1355 let (platform, default) = match (tuple.os(), tuple.env()) {
1356 (rucc_tuple::Os::IOs, rucc_tuple::Env::Simulator) => ("ios-simulator", "14.0"),
1357 (rucc_tuple::Os::IOs, rucc_tuple::Env::MacAbi) => ("mac-catalyst", "14.0"),
1358 (rucc_tuple::Os::IOs, _) => ("ios", "14.0"),
1359 _ => ("macos", "11.0"),
1360 };
1361 let minimum = opts
1362 .os_version
1363 .or_else(|| tuple.os_version())
1364 .map_or_else(|| default.to_owned(), |version| version.to_string());
1365 let version = sdk_version(&sdk).unwrap_or_else(|| minimum.clone());
1369
1370 let mut args = vec![
1371 "-arch".to_owned(),
1372 arch.to_owned(),
1373 "-platform_version".to_owned(),
1374 platform.to_owned(),
1375 minimum,
1376 version,
1377 "-syslibroot".to_owned(),
1378 sdk.display().to_string(),
1379 "-o".to_owned(),
1380 output.to_owned(),
1381 ];
1382 if opts.shared {
1383 args.push("-dylib".to_owned());
1384 }
1385 if opts.export_dynamic {
1386 args.push("-export_dynamic".to_owned());
1387 }
1388 if opts.strip {
1389 args.push("-S".to_owned());
1392 args.push("-x".to_owned());
1393 }
1394 for dir in &opts.search {
1395 args.push(format!("-L{}", dir.display()));
1396 }
1397 push_items(&mut args, items);
1398 if opts.wants_runtime() && !opts.no_builtins_lib {
1399 if let Some(ours) = builtins_archive(target, &opts.prefixes) {
1400 args.push(ours.display().to_string());
1401 }
1402 }
1403 if opts.wants_defaultlibs() {
1404 args.push("-lSystem".to_owned());
1405 }
1406 Ok(args)
1407}
1408
1409fn darwin_arch(target: Triple) -> Result<&'static str, Error> {
1411 match target.arch {
1412 Arch::Aarch64 => Ok("arm64"),
1413 Arch::X86_64 => Ok("x86_64"),
1414 _ => Err(Error::Target { triple: target.to_string() }),
1415 }
1416}
1417
1418fn sdk_version(sdk: &Path) -> Option<String> {
1424 let valid = |text: &str| {
1425 !text.is_empty()
1426 && text
1427 .split('.')
1428 .all(|part| !part.is_empty() && part.bytes().all(|b| b.is_ascii_digit()))
1429 };
1430 if let Ok(text) = fs::read_to_string(sdk.join("SDKSettings.json")) {
1431 if let Some(at) = text.find("\"Version\"") {
1432 let rest = &text[at + "\"Version\"".len()..];
1433 let rest = rest.trim_start().strip_prefix(':')?.trim_start().strip_prefix('"')?;
1434 let version = &rest[..rest.find('"')?];
1435 if valid(version) {
1436 return Some(version.to_owned());
1437 }
1438 }
1439 }
1440 let name = sdk.file_name()?.to_str()?.strip_suffix(".sdk")?;
1441 let version = name.trim_start_matches(|c: char| c.is_ascii_alphabetic());
1442 valid(version).then(|| version.to_owned())
1443}
1444
1445fn push_items(args: &mut Vec<String>, items: &[Item]) {
1447 for item in items {
1448 match item {
1449 Item::File(path) => args.push(path.clone()),
1450 Item::Library(name) => args.push(format!("-l{name}")),
1451 Item::Linker(arg) => args.push(arg.clone()),
1452 }
1453 }
1454}
1455
1456fn startfile(opts: &LinkOptions, pie: bool) -> Option<&'static str> {
1468 if opts.shared {
1469 None
1470 } else if opts.profile {
1471 Some(if pie && opts.is_static { "grcrt1.o" } else { "gcrt1.o" })
1472 } else if pie {
1473 Some("Scrt1.o")
1474 } else {
1475 Some("crt1.o")
1476 }
1477}
1478
1479fn runtime_items(opts: &LinkOptions, runtime: &[PathBuf], ours: Option<&Path>) -> Vec<String> {
1503 let mut args = Vec::new();
1504 if !opts.wants_defaultlibs() && !opts.wants_runtime() {
1505 return args;
1506 }
1507 let has_gcc = find_file(runtime, "libgcc.a").is_some();
1511
1512 if opts.is_static {
1513 args.push("--start-group".to_owned());
1514 }
1515 if opts.wants_defaultlibs() {
1516 args.push("-lc".to_owned());
1517 }
1518 if opts.wants_runtime() {
1519 if let Some(path) = ours {
1520 args.push(path.display().to_string());
1521 }
1522 if has_gcc {
1523 args.push("-lgcc".to_owned());
1524 if opts.is_static {
1525 args.push("-lgcc_eh".to_owned());
1526 }
1527 }
1528 }
1529 if opts.is_static {
1530 args.push("--end-group".to_owned());
1531 } else if opts.wants_runtime() && has_gcc {
1532 args.push("--as-needed".to_owned());
1534 args.push("-lgcc_s".to_owned());
1535 args.push("--no-as-needed".to_owned());
1536 }
1537 args
1538}
1539
1540#[must_use]
1547pub fn runtime_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
1548 let libc = match target.env {
1549 Env::Musl => "musl",
1550 Env::None | Env::Gnu | Env::Msvc => "gnu",
1551 };
1552 let arch = target.arch.as_str();
1553 let names = [
1557 format!("{arch}-linux-{libc}"),
1558 format!("{arch}-pc-linux-{libc}"),
1559 format!("{arch}-redhat-linux"),
1560 format!("{arch}-suse-linux"),
1561 format!("{arch}-alpine-linux-{libc}"),
1562 ];
1563 let mut found = Vec::new();
1564 for base in ["/usr/lib/gcc", "/usr/lib64/gcc", "/usr/local/lib/gcc"] {
1565 for name in &names {
1566 found.extend(newest_first(&under(sysroot, &format!("{base}/{name}"))));
1567 }
1568 }
1569 found
1570}
1571
1572fn newest_first(dir: &Path) -> Vec<PathBuf> {
1574 let Ok(entries) = fs::read_dir(dir) else { return Vec::new() };
1575 let mut versions: Vec<(Vec<u64>, PathBuf)> = entries
1576 .flatten()
1577 .map(|e| e.path())
1578 .filter(|p| p.is_dir())
1579 .map(|p| (version_key(&p), p))
1580 .collect();
1581 versions.sort_by(|a, b| b.0.cmp(&a.0));
1585 versions.into_iter().map(|(_, path)| path).collect()
1586}
1587
1588fn version_key(dir: &Path) -> Vec<u64> {
1593 let name = dir.file_name().unwrap_or_default().to_string_lossy();
1594 name.split('.').map(|part| part.parse::<u64>().unwrap_or(0)).collect()
1595}
1596
1597#[must_use]
1603pub fn builtins_archive(target: Triple, prefixes: &[PathBuf]) -> Option<PathBuf> {
1604 const NAME: &str = rucc_sysroot::link::BUILTINS;
1607 let spellings = [target.to_string(), target.tuple().to_string()];
1611 let mut places: Vec<PathBuf> = Vec::new();
1612 for prefix in prefixes {
1613 for triple in &spellings {
1614 places.push(prefix.join(triple).join(NAME));
1615 }
1616 places.push(prefix.join(NAME));
1617 }
1618 if let Some(dir) =
1619 std::env::current_exe().ok().and_then(|exe| exe.parent().map(Path::to_path_buf))
1620 {
1621 for triple in &spellings {
1624 places.push(dir.join("lib").join("rucc").join(triple).join(NAME));
1625 }
1626 if let Some(up) = dir.parent() {
1627 for triple in &spellings {
1628 places.push(up.join("lib").join("rucc").join(triple).join(NAME));
1630 for profile in ["release", "debug"] {
1633 places.push(up.join(triple).join(profile).join(NAME));
1634 }
1635 }
1636 }
1637 places.push(dir.join(NAME));
1638 }
1639 places.into_iter().find(|path| path.is_file())
1640}
1641
1642fn emulation(target: Triple) -> &'static str {
1644 match target.arch {
1645 Arch::X86_64 => "elf_x86_64",
1646 Arch::Aarch64 => "aarch64linux",
1647 Arch::Riscv64 => "elf64lriscv",
1648 }
1649}
1650
1651fn loader(target: Triple) -> &'static str {
1656 match (target.arch, target.env) {
1657 (Arch::X86_64, Env::Musl) => "/lib/ld-musl-x86_64.so.1",
1658 (Arch::X86_64, _) => "/lib64/ld-linux-x86-64.so.2",
1659 (Arch::Aarch64, Env::Musl) => "/lib/ld-musl-aarch64.so.1",
1660 (Arch::Aarch64, _) => "/lib/ld-linux-aarch64.so.1",
1661 (Arch::Riscv64, Env::Musl) => "/lib/ld-musl-riscv64.so.1",
1662 (Arch::Riscv64, _) => "/lib/ld-linux-riscv64-lp64d.so.1",
1663 }
1664}
1665
1666#[must_use]
1673pub fn candidates(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
1674 let multiarch = multiarch(target);
1675 [
1676 format!("/usr/lib/{multiarch}"),
1677 format!("/lib/{multiarch}"),
1678 "/usr/lib64".to_owned(),
1679 "/lib64".to_owned(),
1680 "/usr/lib".to_owned(),
1681 "/lib".to_owned(),
1682 ]
1683 .into_iter()
1684 .map(|dir| under(sysroot, &dir))
1685 .collect()
1686}
1687
1688#[must_use]
1693pub fn multiarch(target: Triple) -> String {
1694 let libc = match target.env {
1695 Env::Musl => "musl",
1696 Env::None | Env::Gnu | Env::Msvc => "gnu",
1697 };
1698 format!("{}-linux-{libc}", target.arch.as_str())
1699}
1700
1701fn library_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
1703 candidates(target, sysroot).into_iter().filter(|dir| dir.is_dir()).collect()
1704}
1705
1706#[must_use]
1711pub fn search_dirs(link: &LinkOptions, target: Triple) -> Vec<PathBuf> {
1712 let mut dirs = link.search.clone();
1713 if let Some(sysroot) = cross_sysroot(target, link) {
1718 dirs.push(sysroot.lib());
1719 if rucc_sysroot::link::libc(sysroot.target()) == rucc_sysroot::link::Libc::Stub {
1720 dirs.push(sysroot.stubs().to_path_buf());
1721 }
1722 return dirs;
1723 }
1724 if let Some(distro) = distro_cross(target, link) {
1725 dirs.push(distro.lib());
1726 return dirs;
1727 }
1728 dirs.extend(candidates(target, link.sysroot.as_deref()));
1729 dirs
1730}
1731
1732#[must_use]
1737pub fn find_in_search(link: &LinkOptions, target: Triple, name: &str) -> Option<PathBuf> {
1738 find_file(&search_dirs(link, target), name)
1739}
1740
1741fn find_file(dirs: &[PathBuf], name: &str) -> Option<PathBuf> {
1743 dirs.iter().map(|dir| dir.join(name)).find(|path| path.is_file())
1744}
1745
1746fn under(sysroot: Option<&Path>, path: &str) -> PathBuf {
1748 match sysroot {
1749 Some(root) => root.join(path.strip_prefix('/').unwrap_or(path)),
1752 None => PathBuf::from(path),
1753 }
1754}
1755
1756fn target_path(sysroot: Option<&Path>, path: &str) -> String {
1763 match sysroot {
1764 Some(root) => {
1765 let root = root.display().to_string();
1766 format!("{}/{}", root.trim_end_matches(['/', '\\']), path.trim_start_matches('/'))
1767 }
1768 None => path.to_owned(),
1769 }
1770}
1771
1772#[must_use]
1774pub fn render(linker: &Linker, args: &[String]) -> String {
1775 let mut out = linker.path.display().to_string();
1776 for arg in args {
1777 out.push(' ');
1778 if arg.is_empty() || arg.contains(char::is_whitespace) {
1779 out.push('"');
1780 out.push_str(arg);
1781 out.push('"');
1782 } else {
1783 out.push_str(arg);
1784 }
1785 }
1786 out
1787}
1788
1789const WINDOWS_LINE: usize = 30_000;
1794
1795const UNIX_LINE: usize = 256 * 1024;
1798
1799fn too_long(args: &[String], limit: usize) -> bool {
1801 args.iter().map(|arg| arg.len() + 3).sum::<usize>() > limit
1802}
1803
1804fn split_for_file(args: &[String]) -> (Vec<String>, Vec<String>) {
1809 let mut front = Vec::new();
1810 let mut rest = Vec::with_capacity(args.len());
1811 let mut words = args.iter();
1812 while let Some(arg) = words.next() {
1813 if arg == "-m" {
1814 front.push(arg.clone());
1815 front.extend(words.next().cloned());
1816 } else {
1817 rest.push(arg.clone());
1818 }
1819 }
1820 (front, rest)
1821}
1822
1823fn windows_quoting(linker: &Linker) -> bool {
1831 let file = linker.path.file_stem().and_then(|stem| stem.to_str()).unwrap_or_default();
1832 let microsoft = linker.name == "link.exe" || file.eq_ignore_ascii_case("link");
1833 cfg!(windows)
1834 && (is_lld(&linker.name)
1835 || file.starts_with("ld.lld")
1836 || file.starts_with("lld")
1837 || microsoft)
1838}
1839
1840fn response_text(args: &[String], windows: bool) -> String {
1847 let mut text = String::new();
1848 for arg in args {
1849 text.push('"');
1850 let mut slashes = 0;
1851 for c in arg.chars() {
1852 match c {
1853 '\\' if windows => slashes += 1,
1854 '"' if windows => {
1855 text.extend(std::iter::repeat_n('\\', slashes * 2 + 1));
1856 text.push('"');
1857 slashes = 0;
1858 }
1859 _ if windows => {
1860 text.extend(std::iter::repeat_n('\\', slashes));
1861 text.push(c);
1862 slashes = 0;
1863 }
1864 '"' | '\\' => {
1865 text.push('\\');
1866 text.push(c);
1867 }
1868 _ => text.push(c),
1869 }
1870 }
1871 text.extend(std::iter::repeat_n('\\', slashes * 2));
1872 text.push_str("\"\n");
1873 }
1874 text
1875}
1876
1877pub fn run(linker: &Linker, args: &[String]) -> Result<(), Error> {
1889 let spawn = |why: std::io::Error| Error::Spawn {
1890 path: linker.path.display().to_string(),
1891 why: why.to_string(),
1892 };
1893 let mut command = Command::new(&linker.path);
1894 let mut written = None;
1895 if too_long(args, if cfg!(windows) { WINDOWS_LINE } else { UNIX_LINE }) {
1896 let (front, rest) = split_for_file(args);
1897 let path = std::env::temp_dir().join(format!("rucc-link-{}.rsp", std::process::id()));
1898 fs::write(&path, response_text(&rest, windows_quoting(linker))).map_err(spawn)?;
1899 let mut at = OsString::from("@");
1900 at.push(&path);
1901 command.args(front).arg(at);
1902 written = Some(path);
1903 } else {
1904 command.args(args.iter().map(OsString::from));
1905 }
1906 let status = command.status();
1907 if let Some(path) = written {
1908 let _ = fs::remove_file(path);
1909 }
1910 let status = status.map_err(spawn)?;
1911 if status.success() {
1912 return Ok(());
1913 }
1914 Err(Error::Refused {
1918 status: match status.code() {
1919 Some(code) => format!("exited with status {code}"),
1920 None => "was killed before it finished".to_owned(),
1921 },
1922 })
1923}
1924
1925#[cfg(test)]
1926mod tests {
1927 use super::*;
1928
1929 fn linux() -> Triple {
1930 Triple::new(Arch::X86_64, Os::Linux, Env::Gnu)
1931 }
1932
1933 fn one(name: &str) -> Vec<Item> {
1934 vec![Item::File(name.to_owned())]
1935 }
1936
1937 #[test]
1938 fn the_fast_one_is_looked_for_first_and_the_platforms_own_last() {
1939 let names = order(linux(), &LinkOptions::default());
1940 assert_eq!(names.first().map(String::as_str), Some("ld.mold"));
1941 assert_eq!(names.last().map(String::as_str), Some("ld"));
1942 }
1943
1944 #[test]
1945 fn naming_one_is_the_whole_of_the_order() {
1946 let opts = LinkOptions { use_ld: Some("gold".to_owned()), ..LinkOptions::default() };
1947 assert_eq!(order(linux(), &opts), ["ld.gold", "gold"]);
1948 }
1949
1950 #[test]
1951 fn a_dynamic_program_names_the_loader_that_will_start_it() {
1952 let args = line(linux(), &LinkOptions::default(), &one("a.o"), "a.out").expect("a line");
1953 let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
1954 assert!(args[at + 1].ends_with("/lib64/ld-linux-x86-64.so.2"), "{args:?}");
1955 }
1956
1957 #[test]
1959 fn a_relocatable_link_is_the_objects_and_nothing_a_program_needs() {
1960 let opts = LinkOptions { relocatable: true, ..LinkOptions::default() };
1961 let args = line(linux(), &opts, &one("a.o"), "built-in.o").expect("a line");
1962 assert_eq!(args, ["-o", "built-in.o", "-m", "elf_x86_64", "-r", "a.o"]);
1963 }
1964
1965 #[test]
1966 fn a_static_program_names_no_loader_because_nothing_will_start_it() {
1967 let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
1968 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
1969 assert!(args.contains(&"-static".to_owned()), "{args:?}");
1970 assert!(!args.contains(&"-dynamic-linker".to_owned()), "{args:?}");
1971 }
1972
1973 #[test]
1974 fn the_startup_file_of_a_program_that_moves_is_not_the_one_of_a_program_that_does_not() {
1975 let moving = LinkOptions { pie: Some(true), ..LinkOptions::default() };
1976 let fixed = LinkOptions { pie: Some(false), ..LinkOptions::default() };
1977 let named = |opts: &LinkOptions| {
1978 line(linux(), opts, &one("a.o"), "a.out")
1979 .expect("a line")
1980 .iter()
1981 .filter_map(|a| Path::new(a).file_name().map(|n| n.to_string_lossy().into_owned()))
1982 .find(|n| n.ends_with("crt1.o"))
1983 };
1984 if let Some(name) = named(&moving) {
1987 assert_eq!(name, "Scrt1.o");
1988 assert_eq!(named(&fixed).as_deref(), Some("crt1.o"));
1989 }
1990 }
1991
1992 #[test]
2001 fn a_profiled_program_is_started_by_the_startup_file_that_counts() {
2002 let profile = LinkOptions { profile: true, ..LinkOptions::default() };
2003 assert_eq!(startfile(&profile, false), Some("gcrt1.o"));
2004 assert_eq!(startfile(&profile, true), Some("gcrt1.o"));
2005 let still = LinkOptions { is_static: true, ..profile.clone() };
2006 assert_eq!(startfile(&still, true), Some("grcrt1.o"));
2007 assert_eq!(startfile(&still, false), Some("gcrt1.o"));
2008 let shared = LinkOptions { shared: true, ..profile };
2009 assert_eq!(startfile(&shared, false), None);
2010 }
2011
2012 #[test]
2014 fn a_program_that_is_not_profiled_is_started_by_the_usual_one() {
2015 let plain = LinkOptions::default();
2016 assert_eq!(startfile(&plain, false), Some("crt1.o"));
2017 assert_eq!(startfile(&plain, true), Some("Scrt1.o"));
2018 }
2019
2020 #[test]
2021 fn asking_for_no_startup_files_leaves_out_both_ends_of_them() {
2022 let opts = LinkOptions { no_startfiles: true, ..LinkOptions::default() };
2023 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
2024 assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
2025 assert!(!args.iter().any(|a| a.ends_with("crtn.o")), "{args:?}");
2026 assert!(args.contains(&"-lc".to_owned()), "{args:?}");
2028 }
2029
2030 #[test]
2031 fn asking_for_no_library_at_all_leaves_out_the_startup_files_too() {
2032 let opts = LinkOptions { no_stdlib: true, ..LinkOptions::default() };
2033 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
2034 assert!(!args.contains(&"-lc".to_owned()), "{args:?}");
2035 assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
2036 }
2037
2038 #[test]
2039 fn the_library_comes_after_the_objects_that_need_it() {
2040 let items = vec![Item::File("a.o".to_owned()), Item::Library("m".to_owned())];
2041 let args = line(linux(), &LinkOptions::default(), &items, "a.out").expect("a line");
2042 let obj = args.iter().position(|a| a == "a.o").expect("the object");
2043 let m = args.iter().position(|a| a == "-lm").expect("the library");
2044 let c = args.iter().position(|a| a == "-lc").expect("the library");
2045 assert!(obj < m && m < c, "{args:?}");
2046 }
2047
2048 #[test]
2049 fn what_the_user_told_the_linker_stays_where_the_user_wrote_it() {
2050 let items = vec![
2054 Item::File("a.o".to_owned()),
2055 Item::Linker("--whole-archive".to_owned()),
2056 Item::File("libaesni.a".to_owned()),
2057 Item::Linker("--no-whole-archive".to_owned()),
2058 Item::Library("m".to_owned()),
2059 ];
2060 let args = line(linux(), &LinkOptions::default(), &items, "a.out").expect("a line");
2061 let at = |what: &str| args.iter().position(|a| a == what).expect(what);
2062 assert!(at("a.o") < at("--whole-archive"), "{args:?}");
2063 assert!(at("--whole-archive") < at("libaesni.a"), "{args:?}");
2064 assert!(at("libaesni.a") < at("--no-whole-archive"), "{args:?}");
2065 assert!(at("--no-whole-archive") < at("-lm"), "{args:?}");
2066 assert!(at("-lm") < at("-lc"), "{args:?}");
2067 }
2068
2069 #[test]
2070 fn a_sysroot_moves_every_path_this_decided_and_none_the_user_wrote() {
2071 let opts = LinkOptions {
2072 sysroot: Some(PathBuf::from("/nowhere-at-all")),
2073 search: vec![PathBuf::from("/opt/mine")],
2074 ..LinkOptions::default()
2075 };
2076 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
2077 let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
2078 assert_eq!(args[at + 1], "/nowhere-at-all/lib64/ld-linux-x86-64.so.2");
2079 assert!(args.contains(&"-L/opt/mine".to_owned()), "{args:?}");
2080 }
2081
2082 #[test]
2083 fn a_platform_with_no_link_line_is_said_so_rather_than_linked_wrongly() {
2084 let triple = Triple::new(Arch::X86_64, Os::None, Env::None);
2085 let error = line(triple, &LinkOptions::default(), &one("a.o"), "a.out")
2086 .expect_err("no line for it");
2087 assert!(matches!(error, Error::Target { .. }), "{error:?}");
2088 }
2089
2090 fn an_sdk(name: &str, settings: Option<&str>) -> PathBuf {
2093 let dir = std::env::temp_dir()
2094 .join(format!("rucc-sdk-{}-{}", std::process::id(), name.replace('.', "-")))
2095 .join(name);
2096 fs::create_dir_all(&dir).expect("a temporary directory");
2097 if let Some(settings) = settings {
2098 fs::write(dir.join("SDKSettings.json"), settings).expect("a settings file");
2099 }
2100 dir
2101 }
2102
2103 fn mac() -> Triple {
2104 Triple::new(Arch::Aarch64, Os::Darwin, Env::None)
2105 }
2106
2107 #[test]
2108 fn a_mac_link_names_the_platform_the_sdk_and_libsystem() {
2109 let sdk = an_sdk(
2110 "MacOSX.sdk",
2111 Some(
2112 "{\"CanonicalName\": \"macosx15.2\", \"Version\" : \"15.2\", \"MaximumDeploymentTarget\": \"15.2.99\"}",
2113 ),
2114 );
2115 let opts = LinkOptions {
2116 sysroot: Some(sdk.clone()),
2117 search: vec![PathBuf::from("/opt/mine")],
2118 no_builtins_lib: true,
2119 ..LinkOptions::default()
2120 };
2121 let items = vec![Item::File("a.o".to_owned()), Item::Library("m".to_owned())];
2122 let args = line(mac(), &opts, &items, "a.out").expect("a line");
2123 let sdk = sdk.display().to_string();
2124 let want: Vec<&str> = vec![
2125 "-arch",
2126 "arm64",
2127 "-platform_version",
2128 "macos",
2129 "11.0",
2130 "15.2",
2131 "-syslibroot",
2132 &sdk,
2133 "-o",
2134 "a.out",
2135 "-L/opt/mine",
2136 "a.o",
2137 "-lm",
2138 "-lSystem",
2139 ];
2140 assert_eq!(args, want);
2141
2142 let opts =
2144 LinkOptions { os_version: Some(rucc_tuple::Version::new(13, 4)), shared: true, ..opts };
2145 let args = line(mac(), &opts, &one("a.o"), "liba.dylib").expect("a line");
2146 assert_eq!(args[3..6], ["macos", "13.4", "15.2"]);
2147 assert!(args.contains(&"-dylib".to_owned()), "{args:?}");
2148 assert!(!args.iter().any(|arg| arg.contains("pie")), "{args:?}");
2149 }
2150
2151 #[test]
2152 fn the_sdk_version_comes_from_its_name_when_it_has_no_settings() {
2153 let sdk = an_sdk("MacOSX14.5.sdk", None);
2154 let opts =
2155 LinkOptions { sysroot: Some(sdk), no_builtins_lib: true, ..LinkOptions::default() };
2156 let args = line(mac(), &opts, &one("a.o"), "a.out").expect("a line");
2157 assert_eq!(args[3..6], ["macos", "11.0", "14.5"]);
2158 let sdk = an_sdk("Somewhere", None);
2160 let opts = LinkOptions {
2161 sysroot: Some(sdk),
2162 os_version: Some(rucc_tuple::Version::major(12)),
2163 ..opts
2164 };
2165 let args = line(mac(), &opts, &one("a.o"), "a.out").expect("a line");
2166 assert_eq!(args[3..6], ["macos", "12", "12"]);
2167 }
2168
2169 #[test]
2170 fn a_mac_link_looks_for_a_mach_o_linker_and_nothing_else() {
2171 assert_eq!(order(mac(), &LinkOptions::default()), ["ld", "ld64.lld"]);
2172 }
2173
2174 #[test]
2175 fn a_static_mac_program_is_refused_and_a_relocatable_one_is_not() {
2176 let sdk = an_sdk("MacOSX.sdk", None);
2177 let opts = LinkOptions { sysroot: Some(sdk), is_static: true, ..LinkOptions::default() };
2178 let error = line(mac(), &opts, &one("a.o"), "a.out").expect_err("no static line");
2179 let Error::Cross { why } = &error else { panic!("{error:?}") };
2180 assert!(why.contains("-static"), "{why}");
2181 let args = relocatable_line(mac(), &LinkOptions::default(), &one("a.o"), "b.o")
2182 .expect("ld64 takes -r");
2183 assert_eq!(args, ["-r", "-arch", "arm64", "-o", "b.o", "a.o"]);
2184 }
2185
2186 #[test]
2187 fn the_line_is_printed_the_way_it_would_be_typed() {
2188 let linker = Linker { name: "ld".to_owned(), path: PathBuf::from("/usr/bin/ld") };
2189 let args = ["-o".to_owned(), "a b".to_owned()];
2190 assert_eq!(render(&linker, &args), "/usr/bin/ld -o \"a b\"");
2191 }
2192
2193 #[test]
2194 fn a_linker_that_is_not_there_is_said_by_name() {
2195 let opts = LinkOptions {
2196 use_ld: Some("a-linker-nobody-has".to_owned()),
2197 ..LinkOptions::default()
2198 };
2199 let error = find(linux(), &opts).expect_err("not on this machine");
2200 assert_eq!(error, Error::Named { name: "a-linker-nobody-has".to_owned() });
2201 }
2202 fn a_gcc_dir(name: &str) -> PathBuf {
2205 let dir = std::env::temp_dir().join(format!("rucc-link-{name}-{}", std::process::id()));
2206 fs::create_dir_all(&dir).expect("a temporary directory");
2207 fs::write(dir.join("libgcc.a"), b"not really an archive").expect("a file in it");
2208 dir
2209 }
2210
2211 #[test]
2212 fn the_c_library_supplies_the_block_routines_and_our_runtime_does_not_displace_them() {
2213 let gcc = a_gcc_dir("order");
2214 let ours = PathBuf::from("/somewhere/librucc_builtins.a");
2215 let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
2216 let at_libc = args.iter().position(|a| a == "-lc").expect("libc");
2217 let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
2218 assert!(at_libc < at_ours, "{args:?}");
2221 }
2222
2223 #[test]
2224 fn a_static_link_puts_them_in_a_group_because_two_of_them_refer_to_each_other() {
2225 let gcc = a_gcc_dir("group");
2226 let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
2227 let args = runtime_items(&opts, &[gcc], None);
2228 assert_eq!(args.first().map(String::as_str), Some("--start-group"), "{args:?}");
2229 assert_eq!(args.last().map(String::as_str), Some("--end-group"), "{args:?}");
2230 assert!(args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
2233 }
2234
2235 #[test]
2236 fn a_dynamic_link_needs_no_group_and_asks_for_the_shared_half_only_if_something_wants_it() {
2237 let gcc = a_gcc_dir("dynamic");
2238 let args = runtime_items(&LinkOptions::default(), &[gcc], None);
2239 assert!(!args.contains(&"--start-group".to_owned()), "{args:?}");
2240 assert!(!args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
2241 let at = args.iter().position(|a| a == "-lgcc_s").expect("the shared half");
2242 assert_eq!(args[at - 1], "--as-needed", "{args:?}");
2243 assert_eq!(args[at + 1], "--no-as-needed", "{args:?}");
2244 }
2245
2246 #[test]
2247 fn our_own_runtime_comes_before_the_machines_because_the_two_are_interchangeable() {
2248 let gcc = a_gcc_dir("ours");
2249 let ours = PathBuf::from("/somewhere/librucc_builtins.a");
2250 let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
2251 let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
2252 let at_gcc = args.iter().position(|a| a == "-lgcc").expect("libgcc");
2253 assert!(at_ours < at_gcc, "{args:?}");
2254 }
2255
2256 #[test]
2257 fn no_builtins_lib_leaves_ours_off_and_keeps_the_machines() {
2258 let gcc = a_gcc_dir("theirs");
2259 let opts = LinkOptions { no_builtins_lib: true, ..LinkOptions::default() };
2260 let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
2261 assert!(!args.iter().any(|a| a.ends_with("librucc_builtins.a")), "{args:?}");
2262 assert!(runtime_items(&opts, &[gcc], None).contains(&"-lgcc".to_owned()));
2265 }
2266
2267 #[test]
2268 fn nodefaultlibs_leaves_the_whole_runtime_off_and_not_only_the_c_library() {
2269 let gcc = a_gcc_dir("none");
2270 let opts = LinkOptions { no_defaultlibs: true, ..LinkOptions::default() };
2271 assert!(runtime_items(&opts, &[gcc], None).is_empty());
2272 }
2273
2274 #[test]
2275 fn a_machine_with_no_gcc_on_it_gets_no_names_for_libraries_that_are_not_there() {
2276 let empty = std::env::temp_dir().join("rucc-link-empty-not-a-gcc");
2277 let args = runtime_items(&LinkOptions::default(), &[empty], None);
2278 assert_eq!(args, ["-lc"], "{args:?}");
2279 }
2280
2281 #[test]
2282 fn a_gcc_version_directory_is_read_as_a_version_and_not_as_a_word() {
2283 assert!(version_key(Path::new("/usr/lib/gcc/x/13")) > version_key(Path::new("/x/9")));
2284 assert!(version_key(Path::new("/x/10.2")) > version_key(Path::new("/x/10")));
2285 assert!(version_key(Path::new("/x/snapshot")) < version_key(Path::new("/x/1")));
2287 }
2288
2289 #[test]
2297 fn the_fast_math_startup_file_is_wanted_where_gccs_spec_wants_it() {
2298 let fast = LinkOptions { fast_math: true, ..LinkOptions::default() };
2299 assert!(!LinkOptions::default().wants_fastmath());
2300 assert!(fast.wants_fastmath());
2301 assert!(!LinkOptions { shared: true, ..fast.clone() }.wants_fastmath());
2302 assert!(!LinkOptions { daz_ftz: Some(false), ..fast.clone() }.wants_fastmath());
2303 let forced = LinkOptions { shared: true, daz_ftz: Some(true), ..LinkOptions::default() };
2304 assert!(forced.wants_fastmath());
2305 }
2306
2307 fn cached() -> LinkOptions {
2308 LinkOptions {
2309 cache: Some(PathBuf::from("/cache")),
2310 prefixes: vec![a_builtins_dir()],
2311 ..LinkOptions::default()
2312 }
2313 }
2314
2315 fn a_builtins_dir() -> PathBuf {
2321 let dir = std::env::temp_dir().join(format!("rucc-link-ours-{}", std::process::id()));
2322 fs::create_dir_all(&dir).expect("a temporary directory");
2323 fs::write(dir.join("librucc_builtins.a"), b"not really an archive").expect("a file in it");
2324 dir
2325 }
2326
2327 #[test]
2330 fn the_runtime_is_found_under_the_tuple_as_well_as_the_triple() {
2331 let dir = std::env::temp_dir().join(format!("rucc-link-tuple-{}", std::process::id()));
2332 let target: Triple = "aarch64-linux-musl".parse().expect("a triple");
2333 let under = dir.join(target.tuple().to_string());
2334 fs::create_dir_all(&under).expect("a temporary directory");
2335 fs::write(under.join("librucc_builtins.a"), b"not really an archive")
2336 .expect("a file in it");
2337 let found = builtins_archive(target, std::slice::from_ref(&dir));
2338 assert_eq!(found, Some(under.join("librucc_builtins.a")));
2339 let _ = fs::remove_dir_all(&dir);
2340 }
2341
2342 fn a_sysroot(target: Triple) -> Sysroot {
2344 Sysroot::in_cache(Path::new("/cache"), target.tuple())
2345 }
2346
2347 fn foreign() -> Triple {
2353 Triple::new(Arch::X86_64, Os::None, Env::None)
2354 }
2355
2356 #[test]
2357 fn a_cross_link_reads_the_targets_own_sysroot_and_nothing_of_this_machine() {
2358 let target = Triple::new(Arch::Aarch64, Os::Linux, Env::Musl);
2359 let sysroot = a_sysroot(target);
2360 let root = sysroot.root().display().to_string();
2363 let lib = sysroot.lib();
2364 let args = cross_line(target, &cached(), &one("a.o"), "a.out", &sysroot).expect("a line");
2365 assert!(args.contains(&format!("--sysroot={root}")), "{args:?}");
2366 assert!(args.contains(&format!("-L{}", lib.display())), "{args:?}");
2367 assert!(args.contains(&lib.join("libc.a").display().to_string()), "{args:?}");
2368 let at = args.iter().position(|a| a == "-dynamic-linker").expect("the loader");
2369 assert_eq!(args[at + 1], "/lib/ld-musl-aarch64.so.1", "{args:?}");
2370 for arg in &args {
2373 assert!(!arg.contains("/usr/lib"), "{arg} in {args:?}");
2374 assert!(!arg.contains("/lib64"), "{arg} in {args:?}");
2375 }
2376 }
2377
2378 #[test]
2379 fn a_freestanding_target_links_against_our_runtime_instead_of_being_refused() {
2380 let args = line(foreign(), &cached(), &one("a.o"), "a.out").expect("a line");
2381 assert!(args.iter().any(|a| a.ends_with("librucc_builtins.a")), "{args:?}");
2382 assert!(!args.iter().any(|a| a.ends_with("libc.a")), "{args:?}");
2385 assert!(!args.contains(&"-lc".to_owned()), "{args:?}");
2386 assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
2387 }
2388
2389 #[test]
2391 fn a_driver_with_no_cache_to_look_in_says_so_rather_than_guessing() {
2392 let error = line(foreign(), &LinkOptions::default(), &one("a.o"), "a.out")
2393 .expect_err("no line for it");
2394 assert!(matches!(error, Error::Target { .. }), "{error:?}");
2395 }
2396
2397 #[test]
2398 fn a_static_link_against_a_libc_that_is_a_stub_is_refused_rather_than_attempted() {
2399 let target = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
2400 let opts = LinkOptions { is_static: true, ..cached() };
2401 let error = cross_line(target, &opts, &one("a.o"), "a.out", &a_sysroot(target))
2402 .expect_err("there is no libc.a in a stub sysroot");
2403 let Error::Cross { why } = &error else { panic!("{error:?}") };
2404 assert!(why.contains("stub"), "{why}");
2407 }
2408
2409 #[test]
2410 fn a_target_whose_linker_wants_a_different_line_is_refused_by_name() {
2411 let target = Triple::new(Arch::Aarch64, Os::Darwin, Env::None);
2412 let error = cross_line(target, &cached(), &one("a.o"), "a.out", &a_sysroot(target))
2413 .expect_err("no line for that format");
2414 let Error::Cross { why } = &error else { panic!("{error:?}") };
2415 assert!(why.contains(&target.tuple().to_canonical_string()), "{why}");
2416 }
2417
2418 #[test]
2419 fn an_msvc_link_is_against_the_tree_sysroot_names_and_says_what_to_run_without_one() {
2420 let target = Triple::new(Arch::X86_64, Os::Windows, Env::Msvc);
2421 let error = line(target, &cached(), &one("a.obj"), "a.exe").expect_err("no tree named");
2423 let Error::Cross { why } = &error else { panic!("{error:?}") };
2424 assert!(why.contains("rucc --fetch x86_64-windows-msvc"), "{why}");
2425
2426 let tree = PathBuf::from("/trees/msvc");
2427 let named = LinkOptions { sysroot: Some(tree.clone()), ..cached() };
2428 let args = line(target, &named, &one("a.obj"), "a.exe").expect("a line");
2429 let crt = format!("-libpath:{}", tree.join("crt/lib").join("x86_64").display());
2430 assert!(args.contains(&crt), "{args:?}");
2431 assert!(args.contains(&"-out:a.exe".to_owned()), "{args:?}");
2432 assert!(args.contains(&"libcmt.lib".to_owned()), "{args:?}");
2433 assert!(args.iter().any(|arg| arg.ends_with("librucc_builtins.a")), "{args:?}");
2434 let dll = LinkOptions { crt: Crt::Dll, ..named };
2435 let args = line(target, &dll, &one("a.obj"), "a.exe").expect("a line");
2436 assert!(args.contains(&"msvcrt.lib".to_owned()), "{args:?}");
2437 assert!(!args.contains(&"libcmt.lib".to_owned()), "{args:?}");
2438
2439 assert_eq!(order(target, &cached()).first().map(String::as_str), Some("lld-link"));
2441 assert_eq!(order(target, &LinkOptions::default())[0], "lld-link");
2442 }
2443
2444 #[test]
2445 fn an_msvc_tree_without_the_crt_for_this_architecture_is_said_before_anything_is_compiled() {
2446 let target = Triple::new(Arch::X86_64, Os::Windows, Env::Msvc);
2447 let tree = std::env::temp_dir().join(format!("rucc-msvc-tree-{}", std::process::id()));
2448 fs::create_dir_all(tree.join("crt/lib/aarch64")).expect("a temporary directory");
2449 let named = LinkOptions { sysroot: Some(tree.clone()), ..cached() };
2450 let error = preflight(target, &named).expect_err("a tree for the other architecture");
2451 let Error::Cross { why } = &error else { panic!("{error:?}") };
2452 assert!(why.contains("rucc --fetch x86_64-windows-msvc"), "{why}");
2453 fs::create_dir_all(tree.join("crt/lib/x86_64")).expect("the right one");
2454 preflight(target, &named).expect("a tree for this one");
2455 let _ = fs::remove_dir_all(&tree);
2456 }
2457
2458 #[test]
2459 fn a_mingw_target_links_and_looks_for_a_linker_that_can_write_a_pe_image() {
2460 let target = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2461 let args = cross_line(target, &cached(), &one("a.o"), "a.exe", &a_sysroot(target))
2462 .expect("a line for mingw-w64");
2463 let at = |flag: &str| args.iter().position(|arg| arg == flag).expect(flag);
2464 assert_eq!(args[at("-m") + 1], "i386pep");
2465 assert_eq!(args[at("--subsystem") + 1], "console");
2466 assert!(args.iter().any(|arg| arg.ends_with("libmsvcrt.a")), "{args:?}");
2467 let names = cross_order(target);
2470 assert_eq!(names.first().map(String::as_str), Some("ld.lld"));
2471 assert!(names.contains(&"x86_64-w64-mingw32-ld".to_owned()), "{names:?}");
2472 }
2473
2474 #[test]
2475 fn our_runtime_comes_from_beside_the_compiler_rather_than_from_inside_the_sysroot() {
2476 let target = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2481 let sysroot = a_sysroot(target);
2482 let opts = cached();
2483 let args = cross_line(target, &opts, &one("a.o"), "a.exe", &sysroot).expect("a line");
2484 let ours: Vec<&String> =
2485 args.iter().filter(|arg| arg.ends_with("librucc_builtins.a")).collect();
2486 assert_eq!(ours.len(), 1, "{args:?}");
2487 assert_eq!(ours[0], &opts.prefixes[0].join("librucc_builtins.a").display().to_string());
2488 assert!(!ours[0].starts_with(&sysroot.lib().display().to_string()), "{args:?}");
2489 assert_eq!(args.last(), Some(ours[0]), "{args:?}");
2491 }
2492
2493 #[test]
2494 fn a_cross_link_with_no_runtime_to_find_says_which_command_writes_one() {
2495 let target = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2499 let opts = LinkOptions { prefixes: Vec::new(), ..cached() };
2500 let error = cross_line(target, &opts, &one("a.o"), "a.exe", &a_sysroot(target))
2501 .expect_err("there is no runtime for it to find");
2502 let Error::Cross { why } = &error else { panic!("{error:?}") };
2503 assert!(why.contains("cargo xtask builtins"), "{why}");
2504 assert!(why.contains("-fno-builtins-lib"), "{why}");
2505
2506 let without = LinkOptions { no_builtins_lib: true, ..opts };
2508 let args = cross_line(target, &without, &one("a.o"), "a.exe", &a_sysroot(target))
2509 .expect("a line without ours on it");
2510 assert!(!args.iter().any(|arg| arg.ends_with("librucc_builtins.a")), "{args:?}");
2511 }
2512
2513 #[test]
2514 fn the_version_an_lld_prints_is_read_and_nothing_elses_is() {
2515 assert_eq!(lld_major("LLD 18.1.8 (compatible with GNU linkers)\n"), Some(18));
2518 assert_eq!(lld_major("Ubuntu LLD 18.1.3 (compatible with GNU linkers)\n"), Some(18));
2519 assert_eq!(lld_major("LLD 20.1.2 (compatible with GNU linkers)\n"), Some(20));
2520
2521 assert_eq!(lld_major("GNU ld (GNU Binutils for Ubuntu) 2.42\n"), None);
2524 assert_eq!(lld_major("mold 2.4.1 (compatible with GNU ld)\n"), None);
2525 assert_eq!(lld_major(""), None);
2526 }
2527
2528 #[cfg(unix)]
2532 fn a_linker_that_says(tag: &str, text: &str) -> Linker {
2533 use std::os::unix::fs::PermissionsExt as _;
2534 let dir = std::env::temp_dir().join(format!("rucc-link-ld-{}", std::process::id()));
2535 fs::create_dir_all(&dir).expect("a temporary directory");
2536 let path = dir.join(format!("ld.lld-{tag}"));
2537 fs::write(&path, format!("#!/bin/sh\necho '{text}'\n")).expect("a script");
2538 fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).expect("an executable one");
2539 Linker { name: "ld.lld".to_owned(), path }
2540 }
2541
2542 #[test]
2543 #[cfg(unix)]
2544 fn an_lld_too_old_to_read_exportas_is_refused_for_windows_gnu_and_nowhere_else() {
2545 let windows = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2548 let old = a_linker_that_says("18", "LLD 18.1.8 (compatible with GNU linkers)");
2549 let error = suitable(windows, &old).expect_err("18 cannot link this");
2550 let Error::TooOld { name, found, target } = &error else { panic!("{error:?}") };
2551 assert_eq!((name.as_str(), *found, target.as_str()), ("ld.lld", 18, "x86_64-windows-gnu"));
2552 assert!(error.to_string().contains("IMPORT_NAME_EXPORTAS"), "{error}");
2553
2554 let linux = Triple::new(Arch::X86_64, Os::Linux, Env::Musl);
2557 assert_eq!(suitable(linux, &old), Ok(()));
2558
2559 let new = a_linker_that_says("19", "LLD 19.1.0 (compatible with GNU linkers)");
2561 assert_eq!(suitable(windows, &new), Ok(()));
2562 }
2563
2564 #[test]
2565 #[cfg(unix)]
2566 fn an_old_lld_first_in_line_is_passed_over_for_a_newer_one_behind_it() {
2567 use std::os::unix::fs::PermissionsExt as _;
2571 let root = std::env::temp_dir().join(format!("rucc-link-two-lld-{}", std::process::id()));
2572 let mut prefixes = Vec::new();
2573 for (dir, version) in [("old", "18.1.3"), ("new", "19.1.7")] {
2574 let dir = root.join(dir);
2575 fs::create_dir_all(&dir).expect("a temporary directory");
2576 let path = dir.join("ld.rucc-test-lld");
2577 fs::write(&path, format!("#!/bin/sh\necho 'Ubuntu LLD {version}'\n"))
2578 .expect("a script");
2579 fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).expect("executable");
2580 prefixes.push(dir);
2581 }
2582 let windows = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2583 let opts = LinkOptions {
2584 use_ld: Some("rucc-test-lld".to_owned()),
2585 prefixes,
2586 ..LinkOptions::default()
2587 };
2588 let found = find(windows, &opts).expect("the newer one");
2589 assert_eq!(found.path, root.join("new").join("ld.rucc-test-lld"));
2590
2591 let opts = LinkOptions { prefixes: vec![root.join("old")], ..opts };
2593 let error = find(windows, &opts).expect_err("only 18 is here");
2594 assert!(matches!(error, Error::TooOld { found: 18, .. }), "{error:?}");
2595 let _ = fs::remove_dir_all(&root);
2596 }
2597
2598 #[test]
2599 fn a_long_line_goes_in_a_response_file_that_reads_back_as_it_was() {
2600 let args: Vec<String> =
2601 ["-o", r"C:\Users\a b\out.exe", "-m", "i386pep", r#"say "hi""#, "", "plain.o"]
2602 .map(str::to_owned)
2603 .to_vec();
2604 let (front, rest) = split_for_file(&args);
2605 assert_eq!(front, ["-m", "i386pep"]);
2606 assert_eq!(
2607 crate::response_words(&response_text(&rest, false)),
2608 [&args[..2], &args[4..]].concat()
2609 );
2610 assert_eq!(
2612 response_text(&rest, true),
2613 "\"-o\"\n\"C:\\Users\\a b\\out.exe\"\n\"say \\\"hi\\\"\"\n\"\"\n\"plain.o\"\n"
2614 );
2615 assert_eq!(response_text(&[r"C:\dir\".to_owned()], true), "\"C:\\dir\\\\\"\n");
2616 assert!(!too_long(&args, 1000));
2617 assert!(too_long(&vec!["x".repeat(100); 400], WINDOWS_LINE));
2618 }
2619
2620 #[test]
2621 fn a_program_name_is_tried_with_each_pathext_extension_it_does_not_already_have() {
2622 let exts = vec![".com".to_owned(), ".exe".to_owned()];
2623 let dir = Path::new("bin");
2624 assert_eq!(
2625 spellings(&dir.join("ld.lld"), &exts),
2626 [dir.join("ld.lld.com"), dir.join("ld.lld.exe")]
2627 );
2628 assert_eq!(spellings(&dir.join("lld-link.EXE"), &exts), [dir.join("lld-link.EXE")]);
2629 assert_eq!(spellings(&dir.join("ld.lld"), &[]), [dir.join("ld.lld")]);
2630 }
2631
2632 #[test]
2633 #[cfg(unix)]
2634 fn an_lld_off_path_is_found_under_its_own_name() {
2635 use std::os::unix::fs::PermissionsExt as _;
2636 let dir = std::env::temp_dir().join(format!("rucc-link-off-path-{}", std::process::id()));
2637 fs::create_dir_all(&dir).expect("a temporary directory");
2638 let lld = dir.join("ld.lld");
2639 fs::write(&lld, "#!/bin/sh\n").expect("a file");
2640 fs::set_permissions(&lld, fs::Permissions::from_mode(0o755)).expect("permissions");
2641 let found =
2642 linker_candidates("ld.lld", &LinkOptions::default(), std::slice::from_ref(&dir));
2643 let _ = fs::remove_dir_all(&dir);
2644 assert!(found.contains(&lld), "{found:?}");
2645 }
2646
2647 #[test]
2648 fn lld_is_looked_for_where_package_managers_put_it_newest_version_first() {
2649 let root = std::env::temp_dir().join(format!("rucc-link-lld-dirs-{}", std::process::id()));
2650 for dir in ["usr/lib/llvm-18/bin", "usr/lib/llvm-19/bin", "usr/lib/llvm-9/bin", "usr/lib/x"]
2651 {
2652 fs::create_dir_all(root.join(dir)).expect("a temporary directory");
2653 }
2654 let dirs = lld_dirs(&root, Some(PathBuf::from("C:/Program Files")));
2655 let under_lib: Vec<_> =
2656 dirs.iter().filter(|dir| dir.starts_with(root.join("usr/lib"))).collect();
2657 assert_eq!(
2658 under_lib,
2659 [
2660 &root.join("usr/lib/llvm-19/bin"),
2661 &root.join("usr/lib/llvm-18/bin"),
2662 &root.join("usr/lib/llvm-9/bin")
2663 ]
2664 );
2665 assert!(dirs.contains(&root.join("opt/homebrew/opt/lld/bin")), "{dirs:?}");
2666 assert_eq!(dirs.last(), Some(&PathBuf::from("C:/Program Files").join("LLVM").join("bin")));
2667 let _ = fs::remove_dir_all(&root);
2668 }
2669
2670 #[test]
2671 fn no_linker_says_where_to_get_lld_only_when_lld_was_looked_for() {
2672 let cross = Error::NoLinker { tried: vec!["ld.lld".to_owned(), "lld".to_owned()] };
2673 assert!(cross.to_string().contains("lld 19 or newer"), "{cross}");
2674 let native = Error::NoLinker { tried: vec!["ld".to_owned()] };
2675 assert_eq!(native.to_string(), "no linker was found; tried ld");
2676 }
2677
2678 #[test]
2679 #[cfg(unix)]
2680 fn a_linker_that_will_not_say_what_it_is_is_left_alone() {
2681 let windows = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2685 let quiet = a_linker_that_says("gnu", "GNU ld (GNU Binutils for Ubuntu) 2.42");
2686 assert_eq!(suitable(windows, &quiet), Ok(()));
2687
2688 let missing = Linker { name: "ld.lld".to_owned(), path: PathBuf::from("/no/such/linker") };
2689 assert_eq!(suitable(windows, &missing), Ok(()));
2690 }
2691
2692 #[test]
2693 fn profiling_a_cross_link_is_refused_because_the_startup_file_is_compiled_code() {
2694 let target = Triple::new(Arch::X86_64, Os::Linux, Env::Musl);
2695 let opts = LinkOptions { profile: true, ..cached() };
2696 let error = cross_line(target, &opts, &one("a.o"), "a.out", &a_sysroot(target))
2697 .expect_err("there is no gcrt1.o in a generated sysroot");
2698 let Error::Cross { why } = &error else { panic!("{error:?}") };
2699 assert!(why.contains("gcrt1.o"), "{why}");
2700 }
2701
2702 #[test]
2703 fn a_distributions_cross_tree_is_used_when_there_is_no_sysroot_of_ours() {
2704 let usr = std::env::temp_dir().join(format!("rucc-link-usr-{}", std::process::id()));
2705 let root = usr.join("aarch64-linux-gnu");
2706 for dir in ["include", "lib"] {
2707 fs::create_dir_all(root.join(dir)).expect("a scratch tree");
2708 }
2709 for version in ["9", "13"] {
2710 fs::create_dir_all(usr.join("lib/gcc-cross/aarch64-linux-gnu").join(version))
2711 .expect("a scratch gcc");
2712 }
2713 let host = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
2714 let arm = Triple::new(Arch::Aarch64, Os::Linux, Env::Gnu);
2715 let opts = LinkOptions { usr: Some(usr.clone()), ..cached() };
2716 let distro = distro_for(arm, &opts, Some(host)).expect("the packages are there");
2717 assert_eq!(distro.include(), root.join("include"));
2718 assert_eq!(distro.lib(), root.join("lib"));
2719 assert_eq!(
2720 distro.gcc,
2721 [13, 9].map(|v| usr.join("lib/gcc-cross/aarch64-linux-gnu").join(v.to_string()))
2722 );
2723 assert!(cross_for(arm, &opts, Some(host)).is_none());
2725 assert!(distro_for(host, &opts, Some(host)).is_none());
2727 let riscv = Triple::new(Arch::Riscv64, Os::Linux, Env::Gnu);
2728 assert!(distro_for(riscv, &opts, Some(host)).is_none());
2729 let named = LinkOptions { sysroot: Some(PathBuf::from("/opt/root")), ..opts.clone() };
2730 assert!(distro_for(arm, &named, Some(host)).is_none());
2731 let pinned = LinkOptions {
2732 pinned: Some("aarch64-linux-gnu.2.28".parse::<TargetTuple>().expect("a release")),
2733 ..opts.clone()
2734 };
2735 assert!(distro_for(arm, &pinned, Some(host)).is_none());
2736 let cache = usr.join("cache");
2738 fs::create_dir_all(Sysroot::in_cache(&cache, arm.tuple()).lib()).expect("a sysroot");
2739 let fetched = LinkOptions { cache: Some(cache), ..opts };
2740 assert!(distro_for(arm, &fetched, Some(host)).is_none());
2741 let _ = fs::remove_dir_all(&usr);
2742 }
2743
2744 #[test]
2745 fn the_host_takes_the_host_line_and_a_tree_the_user_named_takes_it_too() {
2746 let host = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
2747 let other = Triple::new(Arch::Riscv64, Os::Linux, Env::Musl);
2748 assert!(cross_for(host, &cached(), Some(host)).is_none());
2749 assert!(cross_for(other, &cached(), Some(host)).is_some());
2750 let named = LinkOptions { sysroot: Some(PathBuf::from("/opt/root")), ..cached() };
2753 assert!(cross_for(other, &named, Some(host)).is_none());
2754 assert!(cross_for(other, &cached(), None).is_some());
2756 }
2757
2758 #[test]
2759 fn a_windows_host_compiles_for_itself_against_the_fetched_tree() {
2760 let host = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2762 let at =
2763 cross_for(host, &cached(), Some(host)).expect("the cache is the only tree there is");
2764 assert!(at.root().ends_with("x86_64-windows-gnu"), "{:?}", at.root());
2765 }
2766
2767 #[test]
2768 fn a_pinned_release_on_this_machines_own_target_is_a_cross_compile() {
2769 let host = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
2774 let pinned = LinkOptions {
2775 pinned: Some(
2776 "x86_64-linux-gnu.2.28".parse::<TargetTuple>().expect("a spelling with a release"),
2777 ),
2778 ..cached()
2779 };
2780 let at = cross_for(host, &pinned, Some(host)).expect("a pin is a cross compile");
2781 assert!(at.root().ends_with("x86_64-linux-gnu.2.28"), "{:?}", at.root());
2784 let bare = LinkOptions { pinned: None, ..cached() };
2787 assert!(cross_for(host, &bare, Some(host)).is_none());
2788 }
2789
2790 #[test]
2791 fn a_glibc_cross_link_writes_its_stubs_beside_the_sysroot_once() {
2792 let cache = std::env::temp_dir().join(format!("rucc-link-stubs-{}", std::process::id()));
2793 let _ = fs::remove_dir_all(&cache);
2794 let target = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
2795 let pinned = LinkOptions {
2796 cache: Some(cache.clone()),
2797 pinned: Some("x86_64-linux-gnu.2.28".parse().expect("a spelling with a release")),
2798 ..LinkOptions::default()
2799 };
2800 write_stubs(target, &pinned).expect("x86_64 glibc has a description");
2801 let dir = cache.join("stubs").join("x86_64-linux-gnu.2.28");
2802 let libc = dir.join("libc.so");
2803 let bytes = fs::read(&libc).expect("libc.so was written");
2804 assert!(bytes.starts_with(b"\x7fELF"));
2805 assert!(dir.join("libm.so").is_file());
2806 assert!(!dir.join("libpthread.so").exists());
2808 let before = fs::metadata(&libc).and_then(|m| m.modified()).expect("a time");
2811 write_stubs(target, &pinned).expect("again");
2812 let after = fs::metadata(&libc).and_then(|m| m.modified()).expect("a time");
2813 assert_eq!(before, after);
2814 let musl = LinkOptions {
2816 cache: Some(cache.clone()),
2817 pinned: Some("x86_64-linux-musl".parse().expect("musl")),
2818 ..LinkOptions::default()
2819 };
2820 write_stubs(Triple::new(Arch::X86_64, Os::Linux, Env::Musl), &musl).expect("nothing");
2821 assert!(!cache.join("stubs").join("x86_64-linux-musl").exists());
2822 let _ = fs::remove_dir_all(&cache);
2823 }
2824
2825 #[test]
2826 fn what_a_cross_link_searches_is_the_sysroot_and_not_this_machine() {
2827 let dirs = search_dirs(&cached(), foreign());
2828 assert_eq!(dirs.len(), 1, "{dirs:?}");
2831 assert!(dirs[0].starts_with("/cache"), "{dirs:?}");
2832 assert!(dirs[0].ends_with("lib"), "{dirs:?}");
2833 let mine = LinkOptions { search: vec![PathBuf::from("/opt/mine")], ..cached() };
2835 assert_eq!(search_dirs(&mine, foreign())[0], PathBuf::from("/opt/mine"));
2836 }
2837
2838 #[test]
2839 fn gnu_ld_is_not_looked_for_against_the_fetched_mingw_sysroot() {
2840 let windows = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
2841 assert_eq!(order(windows, &cached()), ["ld.lld", "lld"]);
2842 }
2843
2844 #[test]
2845 fn the_linker_looked_for_on_a_cross_link_is_one_that_can_cross() {
2846 let names = cross_order(Triple::new(Arch::Aarch64, Os::Linux, Env::Gnu));
2847 assert_eq!(names.first().map(String::as_str), Some("ld.lld"));
2848 assert!(names.contains(&"aarch64-linux-gnu-ld".to_owned()), "{names:?}");
2849 assert!(!names.iter().any(|name| name.contains("mold")), "{names:?}");
2852 assert!(!names.contains(&"ld".to_owned()), "{names:?}");
2853 assert_eq!(order(foreign(), &cached()), ["ld.lld", "lld"]);
2855 }
2856
2857 #[test]
2858 fn the_four_flags_become_the_five_modes_they_describe() {
2859 let plain = LinkOptions::default();
2860 assert_eq!(mode(&plain), LinkMode::Dynamic);
2861 assert_eq!(
2862 mode(&LinkOptions { pie: Some(false), ..plain.clone() }),
2863 LinkMode::DynamicNoPie
2864 );
2865 assert_eq!(mode(&LinkOptions { is_static: true, ..plain.clone() }), LinkMode::Static);
2866 let both = LinkOptions { is_static: true, pie: Some(true), ..plain.clone() };
2867 assert_eq!(mode(&both), LinkMode::StaticPie);
2868 assert_eq!(mode(&LinkOptions { shared: true, ..plain }), LinkMode::Shared);
2869 }
2870
2871 #[test]
2872 fn a_sysroot_that_has_not_been_built_is_named_before_anything_is_compiled() {
2873 let opts = LinkOptions {
2874 cache: Some(std::env::temp_dir().join("rucc-a-cache-nobody-filled")),
2875 ..LinkOptions::default()
2876 };
2877 let error = preflight(foreign(), &opts).expect_err("nothing has built one");
2878 let Error::Sysroot { dir, pinned, .. } = &error else { panic!("{error:?}") };
2879 assert!(dir.ends_with("x86_64-none"), "{dir}");
2880 assert!(!pinned, "nothing should be pinned for a bare metal target");
2883 let said = error.to_string();
2884 assert!(said.contains("pins none for it to fetch"), "{said}");
2885 }
2886
2887 #[test]
2891 fn a_sysroot_that_could_be_fetched_is_told_what_to_run() {
2892 let said = Error::Sysroot {
2893 target: "x86_64-linux-musl".to_owned(),
2894 dir: "/somewhere/sysroots/x86_64-linux-musl".to_owned(),
2895 pinned: true,
2896 }
2897 .to_string();
2898 assert!(said.contains("`rucc --fetch x86_64-linux-musl`"), "{said}");
2899 assert!(said.contains("--sysroot=<dir>"), "{said}");
2902 }
2903
2904 #[test]
2905 fn a_link_against_this_machine_has_nothing_to_check_before_it_starts() {
2906 assert!(preflight(linux(), &LinkOptions::default()).is_ok());
2909 }
2910
2911 #[test]
2912 fn a_runtime_directory_that_is_not_on_this_machine_is_not_offered() {
2913 let dirs = runtime_dirs(linux(), Some(Path::new("/definitely/not/a/sysroot")));
2914 assert!(dirs.is_empty(), "{dirs:?}");
2915 }
2916}