use std::ffi::OsString;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use rucc_sysroot::layout::{Kernel, Sysroot};
use rucc_sysroot::{LinkMode, argv};
use rucc_target::{Arch, Env, Os, Triple};
use rucc_tuple::TargetTuple;
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct LinkOptions {
pub use_ld: Option<String>,
pub search: Vec<PathBuf>,
pub prefixes: Vec<PathBuf>,
pub sysroot: Option<PathBuf>,
pub cache: Option<PathBuf>,
pub usr: Option<PathBuf>,
pub is_static: bool,
pub shared: bool,
pub pie: Option<bool>,
pub no_stdlib: bool,
pub no_startfiles: bool,
pub no_defaultlibs: bool,
pub export_dynamic: bool,
pub strip: bool,
pub gui: bool,
pub unicode: bool,
pub no_builtins_lib: bool,
pub pinned: Option<TargetTuple>,
pub profile: bool,
pub fast_math: bool,
pub daz_ftz: Option<bool>,
pub relocatable: bool,
pub os_version: Option<rucc_tuple::Version>,
}
impl LinkOptions {
fn wants_fastmath(&self) -> bool {
self.daz_ftz.unwrap_or(self.fast_math && !self.shared)
}
fn wants_startfiles(&self) -> bool {
!self.no_stdlib && !self.no_startfiles && !self.relocatable
}
fn wants_defaultlibs(&self) -> bool {
!self.no_stdlib && !self.no_defaultlibs && !self.relocatable
}
fn wants_runtime(&self) -> bool {
!self.no_stdlib && !self.no_defaultlibs && !self.relocatable
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Item {
File(String),
Library(String),
Linker(String),
}
impl std::fmt::Display for Item {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Item::File(path) => f.write_str(path),
Item::Library(name) => write!(f, "-l{name}"),
Item::Linker(arg) => write!(f, "-Wl,{arg}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
NoLinker {
tried: Vec<String>,
},
Named {
name: String,
},
Target {
triple: String,
},
Cross {
why: String,
},
Sysroot {
target: String,
dir: String,
pinned: bool,
},
TooOld {
name: String,
found: u32,
target: String,
},
Spawn {
path: String,
why: String,
},
Refused {
status: String,
},
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::NoLinker { tried } => {
write!(f, "no linker was found; tried {}", tried.join(", "))?;
if tried.iter().any(|name| is_lld(name)) {
write!(
f,
". lld {LLD_EXPORTAS} or newer links for every target, and {}",
lld_advice()
)?;
}
Ok(())
}
Error::Named { name } => {
write!(f, "-fuse-ld={name} asks for a linker that is not on this machine")
}
Error::Target { triple } => {
write!(f, "there is no link line for {triple} in this compiler yet")
}
Error::Cross { why } => f.write_str(why),
Error::Sysroot { target, dir, pinned: true } => write!(
f,
"there is no sysroot for {target} at {dir}, so there is nothing to link it \
against. `rucc --fetch {target}` gets the one this release pins, or pass \
--sysroot=<dir> to name a tree you have already"
),
Error::Sysroot { target, dir, pinned: false } => write!(
f,
"there is no sysroot for {target} at {dir}, so there is nothing to link it \
against, and this release pins none for it to fetch. Pass --sysroot=<dir> to name \
a tree you have already, or see spec/cross-compile/13-distribution.md section \
13.2 for the cache that will hold one"
),
Error::TooOld { name, found, target } => write!(
f,
"{name} is lld {found} and cannot link for {target}. mingw-w64 writes a few hundred \
of its aliases, `_crt_atexit == atexit` among them, as IMPORT_NAME_EXPORTAS \
records in its import libraries, which lld learned to read in {LLD_EXPORTAS}. An \
older one neither reads them nor says so: it writes an import by ordinal zero, the \
link succeeds, and the program dies at startup. No newer lld was found either. \
For lld {LLD_EXPORTAS} or newer, {}, or name one with -fuse-ld=",
lld_advice()
),
Error::Spawn { path, why } => write!(f, "could not run the linker at {path}: {why}"),
Error::Refused { status } => write!(f, "the linker {status}"),
}
}
}
impl std::error::Error for Error {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Linker {
pub name: String,
pub path: PathBuf,
}
#[must_use]
pub fn order(target: Triple, opts: &LinkOptions) -> Vec<String> {
if let Some(named) = &opts.use_ld {
return vec![format!("ld.{named}"), named.clone()];
}
if target.os == Os::Darwin {
return vec!["ld".to_owned(), "ld64.lld".to_owned()];
}
if cross_sysroot(target, opts).is_some() {
let mut names = cross_order(target);
if (target.os, target.env) == (Os::Windows, Env::Gnu) {
names.retain(|name| is_lld(name));
}
return names;
}
if distro_cross(target, opts).is_some() {
return cross_order(target);
}
match target.os {
Os::Windows => vec!["lld-link".to_owned(), "link.exe".to_owned()],
_ => vec![
"ld.mold".to_owned(),
"mold".to_owned(),
"ld.lld".to_owned(),
"lld".to_owned(),
"ld".to_owned(),
],
}
}
fn cross_order(target: Triple) -> Vec<String> {
let mut names = vec!["ld.lld".to_owned(), "lld".to_owned()];
match (target.os, target.env) {
(Os::Linux, _) => names.push(format!("{}-ld", multiarch(target))),
(Os::Windows, Env::Gnu) => names.push(format!("{}-w64-mingw32-ld", target.arch.as_str())),
_ => {}
}
names
}
#[must_use]
pub fn cross_sysroot(target: Triple, opts: &LinkOptions) -> Option<Sysroot> {
cross_for(target, opts, Triple::host())
}
fn cross_for(target: Triple, opts: &LinkOptions, host: Option<Triple>) -> Option<Sysroot> {
if opts.sysroot.is_some() {
return None;
}
let tuple = target_tuple(target, opts);
if host == Some(target) && tuple.env_version().is_none() {
return None;
}
if distro_for(target, opts, host).is_some() {
return None;
}
let cache = opts.cache.as_deref()?;
Some(Sysroot::in_cache(cache, tuple))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Distro {
pub root: PathBuf,
pub gcc: Vec<PathBuf>,
}
impl Distro {
#[must_use]
pub fn include(&self) -> PathBuf {
self.root.join("include")
}
#[must_use]
pub fn lib(&self) -> PathBuf {
self.root.join("lib")
}
}
#[must_use]
pub fn distro_cross(target: Triple, opts: &LinkOptions) -> Option<Distro> {
distro_for(target, opts, Triple::host())
}
fn distro_for(target: Triple, opts: &LinkOptions, host: Option<Triple>) -> Option<Distro> {
if opts.sysroot.is_some() || target.os != Os::Linux || host == Some(target) {
return None;
}
let tuple = target_tuple(target, opts);
if tuple.env_version().is_some() {
return None;
}
if opts.cache.as_deref().is_some_and(|cache| Sysroot::in_cache(cache, tuple).lib().is_dir()) {
return None;
}
let usr = opts.usr.as_deref()?;
let name = multiarch(target);
let root = usr.join(&name);
if !root.join("include").is_dir() || !root.join("lib").is_dir() {
return None;
}
let gcc = newest_first(&usr.join("lib/gcc-cross").join(&name));
Some(Distro { root, gcc })
}
fn target_tuple(target: Triple, opts: &LinkOptions) -> TargetTuple {
opts.pinned.unwrap_or_else(|| target.tuple())
}
#[must_use]
pub fn cross_kernel(target: Triple, opts: &LinkOptions) -> Option<Kernel> {
kernel_for(target, opts, Triple::host())
}
fn kernel_for(target: Triple, opts: &LinkOptions, host: Option<Triple>) -> Option<Kernel> {
cross_for(target, opts, host)?;
Kernel::for_target(opts.cache.as_deref()?, target.tuple())
}
fn mode(opts: &LinkOptions) -> LinkMode {
let pie = opts.pie.unwrap_or(!opts.is_static && !opts.shared);
if opts.shared {
LinkMode::Shared
} else if opts.is_static {
if pie { LinkMode::StaticPie } else { LinkMode::Static }
} else if pie {
LinkMode::Dynamic
} else {
LinkMode::DynamicNoPie
}
}
fn cross_line(
target: Triple,
opts: &LinkOptions,
items: &[Item],
output: &str,
sysroot: &Sysroot,
) -> Result<Vec<String>, Error> {
if opts.profile {
return Err(Error::Cross {
why: format!(
"-pg needs gcrt1.o, or gcrt2.o on Windows, the startup file that starts and stops \
the counting, and a generated sysroot for {target} does not have one. Profile on \
the host, or pass --sysroot=<dir> naming a tree that has it"
),
});
}
let inputs: Vec<argv::Item> = items
.iter()
.map(|item| match item {
Item::File(path) => argv::Item::File(PathBuf::from(path)),
Item::Library(name) => argv::Item::Library(name.clone()),
Item::Linker(arg) => argv::Item::Linker(arg.clone()),
})
.collect();
let output = PathBuf::from(output);
let ours = builtins_archive(target, &opts.prefixes);
if ours.is_none() && opts.wants_runtime() && !opts.no_builtins_lib {
let tuple = target.tuple().to_canonical_string();
return Err(Error::Cross {
why: format!(
"a cross link ends with librucc_builtins.a, this compiler's own runtime for \
{tuple}, and there is none beside the compiler or under a -B prefix. A sysroot \
does not carry it, because it is our output rather than the platform's. Build it \
with `cargo xtask builtins --target={tuple}`, or pass -fno-builtins-lib to link \
without it"
),
});
}
let invocation = argv::Invocation {
inputs: &inputs,
output: Some(&output),
mode: mode(opts),
search: &opts.search,
no_startfiles: !opts.wants_startfiles(),
no_defaultlibs: !opts.wants_defaultlibs(),
no_builtins_lib: opts.no_builtins_lib,
builtins: ours.as_deref(),
export_dynamic: opts.export_dynamic,
strip: opts.strip,
gui: opts.gui,
unicode: opts.unicode,
};
argv::argv(target.tuple(), sysroot, &invocation)
.map_err(|why| Error::Cross { why: why.to_string() })
}
pub fn preflight(target: Triple, opts: &LinkOptions) -> Result<(), Error> {
if opts.relocatable {
return relocatable_line(target, opts, &[], "a.out").map(drop);
}
if target.os == Os::Darwin {
return darwin_line(target, opts, &[], "a.out").map(drop);
}
let Some(sysroot) = cross_sysroot(target, opts) else { return Ok(()) };
let shape = LinkOptions { no_builtins_lib: true, ..opts.clone() };
cross_line(target, &shape, &[], "a.out", &sysroot)?;
if !sysroot.lib().is_dir() {
let tuple = target_tuple(target, opts).to_canonical_string();
return Err(Error::Sysroot {
dir: sysroot.root().display().to_string(),
pinned: rucc_sysroot::pinned_for_target(target_tuple(target, opts)).is_some(),
target: tuple,
});
}
cross_line(target, opts, &[], "a.out", &sysroot)?;
Ok(())
}
pub fn write_stubs(target: Triple, opts: &LinkOptions) -> Result<(), Error> {
let Some(sysroot) = cross_sysroot(target, opts) else { return Ok(()) };
let tuple = sysroot.target();
if rucc_stub::glibc::architecture(tuple).is_none() {
return Ok(());
}
let Ok(Some(minor)) = rucc_sysroot::bundled_glibc_minor(tuple) else { return Ok(()) };
let files = rucc_stub::glibc::stubs(tuple, minor).map_err(|why| Error::Cross {
why: format!("the glibc stubs for {}: {why}", tuple.to_canonical_string()),
})?;
let dir = sysroot.stubs();
let failed = |path: &Path, why: std::io::Error| Error::Cross {
why: format!("{} cannot be written: {why}", path.display()),
};
fs::create_dir_all(dir).map_err(|why| failed(dir, why))?;
for file in files {
let path = dir.join(&file.name);
if fs::read(&path).is_ok_and(|there| there == file.bytes) {
continue;
}
let temporary = dir.join(format!(".{}.{}", file.name, std::process::id()));
fs::write(&temporary, &file.bytes).map_err(|why| failed(&temporary, why))?;
fs::rename(&temporary, &path).map_err(|why| {
let _ = fs::remove_file(&temporary);
failed(&path, why)
})?;
}
Ok(())
}
pub fn find(target: Triple, opts: &LinkOptions) -> Result<Linker, Error> {
let tried = order(target, opts);
let places = lld_dirs(Path::new("/"), std::env::var_os("ProgramFiles").map(PathBuf::from));
let mut refused = None;
for name in &tried {
for path in linker_candidates(name, opts, &places) {
let linker = Linker { name: name.clone(), path };
match suitable(target, &linker) {
Ok(()) => return Ok(linker),
Err(why) => {
refused.get_or_insert(why);
}
}
}
}
if let Some(why) = refused {
return Err(why);
}
match &opts.use_ld {
Some(name) => Err(Error::Named { name: name.clone() }),
None => Err(Error::NoLinker { tried }),
}
}
fn linker_candidates(name: &str, opts: &LinkOptions, places: &[PathBuf]) -> Vec<PathBuf> {
if name.contains(std::path::MAIN_SEPARATOR) || name.contains('/') {
let path = PathBuf::from(name);
return if path.is_file() { vec![path] } else { Vec::new() };
}
let pathext = pathext();
let mut found: Vec<PathBuf> = opts
.prefixes
.iter()
.flat_map(|dir| spellings(&dir.join(name), &pathext))
.filter(|path| path.is_file())
.collect();
if let Some(path) = std::env::var_os("PATH") {
found.extend(
std::env::split_paths(&path)
.flat_map(|dir| spellings(&dir.join(name), &pathext))
.filter(|p| executable(p)),
);
}
if is_lld(name) {
for dir in places {
for file in spellings(&dir.join(name), &pathext) {
if executable(&file) && !found.contains(&file) {
found.push(file);
}
}
}
}
found
}
fn pathext() -> Vec<String> {
if !cfg!(windows) {
return Vec::new();
}
let list = std::env::var("PATHEXT").unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".to_owned());
list.split(';').filter(|ext| ext.starts_with('.')).map(str::to_ascii_lowercase).collect()
}
fn spellings(path: &Path, exts: &[String]) -> Vec<PathBuf> {
let has = path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| exts.iter().any(|e| e[1..].eq_ignore_ascii_case(ext)));
if exts.is_empty() || has {
return vec![path.to_path_buf()];
}
exts.iter()
.map(|ext| {
let mut name = path.as_os_str().to_owned();
name.push(ext);
PathBuf::from(name)
})
.collect()
}
fn is_lld(name: &str) -> bool {
matches!(name, "ld.lld" | "ld64.lld" | "lld" | "lld-link")
}
fn lld_dirs(root: &Path, program_files: Option<PathBuf>) -> Vec<PathBuf> {
let mut dirs: Vec<PathBuf> = [
"opt/homebrew/opt/lld/bin",
"opt/homebrew/opt/llvm/bin",
"usr/local/opt/lld/bin",
"usr/local/opt/llvm/bin",
"home/linuxbrew/.linuxbrew/opt/lld/bin",
"home/linuxbrew/.linuxbrew/opt/llvm/bin",
]
.iter()
.map(|dir| root.join(dir))
.collect();
for (parent, prefix) in [("usr/lib", "llvm-"), ("usr/lib64", "llvm")] {
let mut versions: Vec<(u32, PathBuf)> = fs::read_dir(root.join(parent))
.into_iter()
.flatten()
.flatten()
.filter_map(|entry| {
let name = entry.file_name();
let version = name.to_str()?.strip_prefix(prefix)?.parse().ok()?;
Some((version, entry.path().join("bin")))
})
.collect();
versions.sort_by_key(|(version, _)| std::cmp::Reverse(*version));
dirs.extend(versions.into_iter().map(|(_, dir)| dir));
}
if let Some(dir) = program_files {
dirs.push(dir.join("LLVM").join("bin"));
}
dirs
}
fn lld_advice() -> &'static str {
if cfg!(target_os = "macos") {
"`brew install lld` installs one, and rucc finds it in Homebrew's directory without a \
change to PATH"
} else if cfg!(windows) {
"the LLVM installer from https://github.com/llvm/llvm-project/releases, or `winget install \
LLVM.LLVM`, installs one in %ProgramFiles%\\LLVM\\bin, where rucc finds it without a \
change to PATH"
} else {
"on Debian and Ubuntu `apt install lld-19` installs one in /usr/lib/llvm-19/bin and on \
Fedora `dnf install lld` installs one on PATH, and rucc finds either without a change to \
PATH"
}
}
pub const LLD_EXPORTAS: u32 = 19;
pub fn suitable(target: Triple, linker: &Linker) -> Result<(), Error> {
if (target.os, target.env) != (Os::Windows, Env::Gnu) {
return Ok(());
}
let Some(found) = lld_major(&reported_version(&linker.path)) else { return Ok(()) };
if found >= LLD_EXPORTAS {
return Ok(());
}
Err(Error::TooOld {
name: linker.name.clone(),
found,
target: target.tuple().to_canonical_string(),
})
}
fn reported_version(path: &Path) -> String {
let Ok(out) = Command::new(path).arg("--version").output() else { return String::new() };
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn lld_major(text: &str) -> Option<u32> {
let mut words = text.split_whitespace();
words.find(|word| *word == "LLD")?;
words.next()?.split('.').next()?.parse().ok()
}
#[cfg(unix)]
fn executable(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt as _;
path.metadata().is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
}
#[cfg(not(unix))]
fn executable(path: &Path) -> bool {
path.is_file()
}
pub fn line(
target: Triple,
opts: &LinkOptions,
items: &[Item],
output: &str,
) -> Result<Vec<String>, Error> {
if opts.relocatable {
return relocatable_line(target, opts, items, output);
}
if target.os == Os::Darwin {
return darwin_line(target, opts, items, output);
}
if let Some(sysroot) = cross_sysroot(target, opts) {
return cross_line(target, opts, items, output, &sysroot);
}
if target.os != Os::Linux {
return Err(Error::Target { triple: target.to_string() });
}
let machine = emulation(target);
let root = opts.sysroot.as_deref();
let distro = distro_cross(target, opts);
let dirs = match &distro {
Some(distro) => vec![distro.lib()],
None => library_dirs(target, root),
};
let runtime = match distro {
Some(distro) => distro.gcc,
None => runtime_dirs(target, root),
};
let ours = if opts.no_builtins_lib { None } else { builtins_archive(target, &opts.prefixes) };
let mut args = vec![
"-o".to_owned(),
output.to_owned(),
"-m".to_owned(),
machine.to_owned(),
"--eh-frame-hdr".to_owned(),
"--hash-style=gnu".to_owned(),
];
let pie = opts.pie.unwrap_or(!opts.is_static && !opts.shared);
if opts.shared {
args.push("-shared".to_owned());
} else if opts.is_static {
args.push("-static".to_owned());
} else if pie {
args.push("-pie".to_owned());
} else {
args.push("-no-pie".to_owned());
}
if !opts.is_static && !opts.shared {
args.push("-dynamic-linker".to_owned());
args.push(target_path(root, loader(target)));
}
if opts.export_dynamic {
args.push("--export-dynamic".to_owned());
}
if opts.strip {
args.push("-s".to_owned());
}
if opts.wants_startfiles() {
for name in startfile(opts, pie).into_iter().chain(["crti.o"]) {
if let Some(path) = find_file(&dirs, name) {
args.push(path.display().to_string());
}
}
let begin = if opts.shared || pie {
"crtbeginS.o"
} else if opts.is_static {
"crtbeginT.o"
} else {
"crtbegin.o"
};
if let Some(path) = find_file(&runtime, begin).or_else(|| find_file(&runtime, "crtbegin.o"))
{
args.push(path.display().to_string());
}
}
for dir in &opts.search {
args.push(format!("-L{}", dir.display()));
}
for dir in &dirs {
args.push(format!("-L{}", dir.display()));
}
for dir in &runtime {
args.push(format!("-L{}", dir.display()));
}
for item in items {
match item {
Item::File(path) => args.push(path.clone()),
Item::Library(name) => args.push(format!("-l{name}")),
Item::Linker(arg) => args.push(arg.clone()),
}
}
args.extend(runtime_items(opts, &runtime, ours.as_deref()));
if opts.wants_startfiles() {
if opts.wants_fastmath() {
if let Some(path) = find_file(&runtime, "crtfastmath.o") {
args.push(path.display().to_string());
}
}
let end = if opts.shared || pie { "crtendS.o" } else { "crtend.o" };
if let Some(path) = find_file(&runtime, end).or_else(|| find_file(&runtime, "crtend.o")) {
args.push(path.display().to_string());
}
if let Some(path) = find_file(&dirs, "crtn.o") {
args.push(path.display().to_string());
}
}
Ok(args)
}
fn relocatable_line(
target: Triple,
opts: &LinkOptions,
items: &[Item],
output: &str,
) -> Result<Vec<String>, Error> {
if target.os == Os::Darwin {
let mut args = vec![
"-r".to_owned(),
"-arch".to_owned(),
darwin_arch(target)?.to_owned(),
"-o".to_owned(),
output.to_owned(),
];
for dir in &opts.search {
args.push(format!("-L{}", dir.display()));
}
push_items(&mut args, items);
return Ok(args);
}
if target.os != Os::Linux {
return Err(Error::Target { triple: target.to_string() });
}
let mut args = vec![
"-o".to_owned(),
output.to_owned(),
"-m".to_owned(),
emulation(target).to_owned(),
"-r".to_owned(),
];
if opts.strip {
args.push("-s".to_owned());
}
for dir in &opts.search {
args.push(format!("-L{}", dir.display()));
}
for item in items {
match item {
Item::File(path) => args.push(path.clone()),
Item::Library(name) => args.push(format!("-l{name}")),
Item::Linker(arg) => args.push(arg.clone()),
}
}
Ok(args)
}
pub fn darwin_line(
target: Triple,
opts: &LinkOptions,
items: &[Item],
output: &str,
) -> Result<Vec<String>, Error> {
let arch = darwin_arch(target)?;
if opts.is_static && !opts.shared {
return Err(Error::Cross {
why: "there is no static C library for Apple platforms, so -static cannot make a \
program for one"
.to_owned(),
});
}
let Some(sdk) = crate::library::sdk(opts.sysroot.as_deref()) else {
return Err(Error::Cross {
why: format!(
"no SDK was found to link {} against. Install the command line tools with \
`xcode-select --install`, or name one with -isysroot <dir> or SDKROOT",
target_tuple(target, opts).to_canonical_string()
),
});
};
let tuple = target_tuple(target, opts);
let (platform, default) = match (tuple.os(), tuple.env()) {
(rucc_tuple::Os::IOs, rucc_tuple::Env::Simulator) => ("ios-simulator", "14.0"),
(rucc_tuple::Os::IOs, rucc_tuple::Env::MacAbi) => ("mac-catalyst", "14.0"),
(rucc_tuple::Os::IOs, _) => ("ios", "14.0"),
_ => ("macos", "11.0"),
};
let minimum = opts
.os_version
.or_else(|| tuple.os_version())
.map_or_else(|| default.to_owned(), |version| version.to_string());
let version = sdk_version(&sdk).unwrap_or_else(|| minimum.clone());
let mut args = vec![
"-arch".to_owned(),
arch.to_owned(),
"-platform_version".to_owned(),
platform.to_owned(),
minimum,
version,
"-syslibroot".to_owned(),
sdk.display().to_string(),
"-o".to_owned(),
output.to_owned(),
];
if opts.shared {
args.push("-dylib".to_owned());
}
if opts.export_dynamic {
args.push("-export_dynamic".to_owned());
}
if opts.strip {
args.push("-S".to_owned());
args.push("-x".to_owned());
}
for dir in &opts.search {
args.push(format!("-L{}", dir.display()));
}
push_items(&mut args, items);
if opts.wants_runtime() && !opts.no_builtins_lib {
if let Some(ours) = builtins_archive(target, &opts.prefixes) {
args.push(ours.display().to_string());
}
}
if opts.wants_defaultlibs() {
args.push("-lSystem".to_owned());
}
Ok(args)
}
fn darwin_arch(target: Triple) -> Result<&'static str, Error> {
match target.arch {
Arch::Aarch64 => Ok("arm64"),
Arch::X86_64 => Ok("x86_64"),
_ => Err(Error::Target { triple: target.to_string() }),
}
}
fn sdk_version(sdk: &Path) -> Option<String> {
let valid = |text: &str| {
!text.is_empty()
&& text
.split('.')
.all(|part| !part.is_empty() && part.bytes().all(|b| b.is_ascii_digit()))
};
if let Ok(text) = fs::read_to_string(sdk.join("SDKSettings.json")) {
if let Some(at) = text.find("\"Version\"") {
let rest = &text[at + "\"Version\"".len()..];
let rest = rest.trim_start().strip_prefix(':')?.trim_start().strip_prefix('"')?;
let version = &rest[..rest.find('"')?];
if valid(version) {
return Some(version.to_owned());
}
}
}
let name = sdk.file_name()?.to_str()?.strip_suffix(".sdk")?;
let version = name.trim_start_matches(|c: char| c.is_ascii_alphabetic());
valid(version).then(|| version.to_owned())
}
fn push_items(args: &mut Vec<String>, items: &[Item]) {
for item in items {
match item {
Item::File(path) => args.push(path.clone()),
Item::Library(name) => args.push(format!("-l{name}")),
Item::Linker(arg) => args.push(arg.clone()),
}
}
}
fn startfile(opts: &LinkOptions, pie: bool) -> Option<&'static str> {
if opts.shared {
None
} else if opts.profile {
Some(if pie && opts.is_static { "grcrt1.o" } else { "gcrt1.o" })
} else if pie {
Some("Scrt1.o")
} else {
Some("crt1.o")
}
}
fn runtime_items(opts: &LinkOptions, runtime: &[PathBuf], ours: Option<&Path>) -> Vec<String> {
let mut args = Vec::new();
if !opts.wants_defaultlibs() && !opts.wants_runtime() {
return args;
}
let has_gcc = find_file(runtime, "libgcc.a").is_some();
if opts.is_static {
args.push("--start-group".to_owned());
}
if opts.wants_defaultlibs() {
args.push("-lc".to_owned());
}
if opts.wants_runtime() {
if let Some(path) = ours {
args.push(path.display().to_string());
}
if has_gcc {
args.push("-lgcc".to_owned());
if opts.is_static {
args.push("-lgcc_eh".to_owned());
}
}
}
if opts.is_static {
args.push("--end-group".to_owned());
} else if opts.wants_runtime() && has_gcc {
args.push("--as-needed".to_owned());
args.push("-lgcc_s".to_owned());
args.push("--no-as-needed".to_owned());
}
args
}
#[must_use]
pub fn runtime_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
let libc = match target.env {
Env::Musl => "musl",
Env::None | Env::Gnu | Env::Msvc => "gnu",
};
let arch = target.arch.as_str();
let names = [
format!("{arch}-linux-{libc}"),
format!("{arch}-pc-linux-{libc}"),
format!("{arch}-redhat-linux"),
format!("{arch}-suse-linux"),
format!("{arch}-alpine-linux-{libc}"),
];
let mut found = Vec::new();
for base in ["/usr/lib/gcc", "/usr/lib64/gcc", "/usr/local/lib/gcc"] {
for name in &names {
found.extend(newest_first(&under(sysroot, &format!("{base}/{name}"))));
}
}
found
}
fn newest_first(dir: &Path) -> Vec<PathBuf> {
let Ok(entries) = fs::read_dir(dir) else { return Vec::new() };
let mut versions: Vec<(Vec<u64>, PathBuf)> = entries
.flatten()
.map(|e| e.path())
.filter(|p| p.is_dir())
.map(|p| (version_key(&p), p))
.collect();
versions.sort_by(|a, b| b.0.cmp(&a.0));
versions.into_iter().map(|(_, path)| path).collect()
}
fn version_key(dir: &Path) -> Vec<u64> {
let name = dir.file_name().unwrap_or_default().to_string_lossy();
name.split('.').map(|part| part.parse::<u64>().unwrap_or(0)).collect()
}
#[must_use]
pub fn builtins_archive(target: Triple, prefixes: &[PathBuf]) -> Option<PathBuf> {
const NAME: &str = rucc_sysroot::link::BUILTINS;
let spellings = [target.to_string(), target.tuple().to_string()];
let mut places: Vec<PathBuf> = Vec::new();
for prefix in prefixes {
for triple in &spellings {
places.push(prefix.join(triple).join(NAME));
}
places.push(prefix.join(NAME));
}
if let Some(dir) =
std::env::current_exe().ok().and_then(|exe| exe.parent().map(Path::to_path_buf))
{
for triple in &spellings {
places.push(dir.join("lib").join("rucc").join(triple).join(NAME));
}
if let Some(up) = dir.parent() {
for triple in &spellings {
places.push(up.join("lib").join("rucc").join(triple).join(NAME));
for profile in ["release", "debug"] {
places.push(up.join(triple).join(profile).join(NAME));
}
}
}
places.push(dir.join(NAME));
}
places.into_iter().find(|path| path.is_file())
}
fn emulation(target: Triple) -> &'static str {
match target.arch {
Arch::X86_64 => "elf_x86_64",
Arch::Aarch64 => "aarch64linux",
Arch::Riscv64 => "elf64lriscv",
}
}
fn loader(target: Triple) -> &'static str {
match (target.arch, target.env) {
(Arch::X86_64, Env::Musl) => "/lib/ld-musl-x86_64.so.1",
(Arch::X86_64, _) => "/lib64/ld-linux-x86-64.so.2",
(Arch::Aarch64, Env::Musl) => "/lib/ld-musl-aarch64.so.1",
(Arch::Aarch64, _) => "/lib/ld-linux-aarch64.so.1",
(Arch::Riscv64, Env::Musl) => "/lib/ld-musl-riscv64.so.1",
(Arch::Riscv64, _) => "/lib/ld-linux-riscv64-lp64d.so.1",
}
}
#[must_use]
pub fn candidates(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
let multiarch = multiarch(target);
[
format!("/usr/lib/{multiarch}"),
format!("/lib/{multiarch}"),
"/usr/lib64".to_owned(),
"/lib64".to_owned(),
"/usr/lib".to_owned(),
"/lib".to_owned(),
]
.into_iter()
.map(|dir| under(sysroot, &dir))
.collect()
}
#[must_use]
pub fn multiarch(target: Triple) -> String {
let libc = match target.env {
Env::Musl => "musl",
Env::None | Env::Gnu | Env::Msvc => "gnu",
};
format!("{}-linux-{libc}", target.arch.as_str())
}
fn library_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
candidates(target, sysroot).into_iter().filter(|dir| dir.is_dir()).collect()
}
#[must_use]
pub fn search_dirs(link: &LinkOptions, target: Triple) -> Vec<PathBuf> {
let mut dirs = link.search.clone();
if let Some(sysroot) = cross_sysroot(target, link) {
dirs.push(sysroot.lib());
if rucc_sysroot::link::libc(sysroot.target()) == rucc_sysroot::link::Libc::Stub {
dirs.push(sysroot.stubs().to_path_buf());
}
return dirs;
}
if let Some(distro) = distro_cross(target, link) {
dirs.push(distro.lib());
return dirs;
}
dirs.extend(candidates(target, link.sysroot.as_deref()));
dirs
}
#[must_use]
pub fn find_in_search(link: &LinkOptions, target: Triple, name: &str) -> Option<PathBuf> {
find_file(&search_dirs(link, target), name)
}
fn find_file(dirs: &[PathBuf], name: &str) -> Option<PathBuf> {
dirs.iter().map(|dir| dir.join(name)).find(|path| path.is_file())
}
fn under(sysroot: Option<&Path>, path: &str) -> PathBuf {
match sysroot {
Some(root) => root.join(path.strip_prefix('/').unwrap_or(path)),
None => PathBuf::from(path),
}
}
fn target_path(sysroot: Option<&Path>, path: &str) -> String {
match sysroot {
Some(root) => {
let root = root.display().to_string();
format!("{}/{}", root.trim_end_matches(['/', '\\']), path.trim_start_matches('/'))
}
None => path.to_owned(),
}
}
#[must_use]
pub fn render(linker: &Linker, args: &[String]) -> String {
let mut out = linker.path.display().to_string();
for arg in args {
out.push(' ');
if arg.is_empty() || arg.contains(char::is_whitespace) {
out.push('"');
out.push_str(arg);
out.push('"');
} else {
out.push_str(arg);
}
}
out
}
const WINDOWS_LINE: usize = 30_000;
const UNIX_LINE: usize = 256 * 1024;
fn too_long(args: &[String], limit: usize) -> bool {
args.iter().map(|arg| arg.len() + 3).sum::<usize>() > limit
}
fn split_for_file(args: &[String]) -> (Vec<String>, Vec<String>) {
let mut front = Vec::new();
let mut rest = Vec::with_capacity(args.len());
let mut words = args.iter();
while let Some(arg) = words.next() {
if arg == "-m" {
front.push(arg.clone());
front.extend(words.next().cloned());
} else {
rest.push(arg.clone());
}
}
(front, rest)
}
fn windows_quoting(linker: &Linker) -> bool {
let file = linker.path.file_stem().and_then(|stem| stem.to_str()).unwrap_or_default();
cfg!(windows) && (is_lld(&linker.name) || file.starts_with("ld.lld") || file.starts_with("lld"))
}
fn response_text(args: &[String], windows: bool) -> String {
let mut text = String::new();
for arg in args {
text.push('"');
let mut slashes = 0;
for c in arg.chars() {
match c {
'\\' if windows => slashes += 1,
'"' if windows => {
text.extend(std::iter::repeat_n('\\', slashes * 2 + 1));
text.push('"');
slashes = 0;
}
_ if windows => {
text.extend(std::iter::repeat_n('\\', slashes));
text.push(c);
slashes = 0;
}
'"' | '\\' => {
text.push('\\');
text.push(c);
}
_ => text.push(c),
}
}
text.extend(std::iter::repeat_n('\\', slashes * 2));
text.push_str("\"\n");
}
text
}
pub fn run(linker: &Linker, args: &[String]) -> Result<(), Error> {
let spawn = |why: std::io::Error| Error::Spawn {
path: linker.path.display().to_string(),
why: why.to_string(),
};
let mut command = Command::new(&linker.path);
let mut written = None;
if too_long(args, if cfg!(windows) { WINDOWS_LINE } else { UNIX_LINE }) {
let (front, rest) = split_for_file(args);
let path = std::env::temp_dir().join(format!("rucc-link-{}.rsp", std::process::id()));
fs::write(&path, response_text(&rest, windows_quoting(linker))).map_err(spawn)?;
let mut at = OsString::from("@");
at.push(&path);
command.args(front).arg(at);
written = Some(path);
} else {
command.args(args.iter().map(OsString::from));
}
let status = command.status();
if let Some(path) = written {
let _ = fs::remove_file(path);
}
let status = status.map_err(spawn)?;
if status.success() {
return Ok(());
}
Err(Error::Refused {
status: match status.code() {
Some(code) => format!("exited with status {code}"),
None => "was killed before it finished".to_owned(),
},
})
}
#[cfg(test)]
mod tests {
use super::*;
fn linux() -> Triple {
Triple::new(Arch::X86_64, Os::Linux, Env::Gnu)
}
fn one(name: &str) -> Vec<Item> {
vec![Item::File(name.to_owned())]
}
#[test]
fn the_fast_one_is_looked_for_first_and_the_platforms_own_last() {
let names = order(linux(), &LinkOptions::default());
assert_eq!(names.first().map(String::as_str), Some("ld.mold"));
assert_eq!(names.last().map(String::as_str), Some("ld"));
}
#[test]
fn naming_one_is_the_whole_of_the_order() {
let opts = LinkOptions { use_ld: Some("gold".to_owned()), ..LinkOptions::default() };
assert_eq!(order(linux(), &opts), ["ld.gold", "gold"]);
}
#[test]
fn a_dynamic_program_names_the_loader_that_will_start_it() {
let args = line(linux(), &LinkOptions::default(), &one("a.o"), "a.out").expect("a line");
let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
assert!(args[at + 1].ends_with("/lib64/ld-linux-x86-64.so.2"), "{args:?}");
}
#[test]
fn a_relocatable_link_is_the_objects_and_nothing_a_program_needs() {
let opts = LinkOptions { relocatable: true, ..LinkOptions::default() };
let args = line(linux(), &opts, &one("a.o"), "built-in.o").expect("a line");
assert_eq!(args, ["-o", "built-in.o", "-m", "elf_x86_64", "-r", "a.o"]);
}
#[test]
fn a_static_program_names_no_loader_because_nothing_will_start_it() {
let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
assert!(args.contains(&"-static".to_owned()), "{args:?}");
assert!(!args.contains(&"-dynamic-linker".to_owned()), "{args:?}");
}
#[test]
fn the_startup_file_of_a_program_that_moves_is_not_the_one_of_a_program_that_does_not() {
let moving = LinkOptions { pie: Some(true), ..LinkOptions::default() };
let fixed = LinkOptions { pie: Some(false), ..LinkOptions::default() };
let named = |opts: &LinkOptions| {
line(linux(), opts, &one("a.o"), "a.out")
.expect("a line")
.iter()
.filter_map(|a| Path::new(a).file_name().map(|n| n.to_string_lossy().into_owned()))
.find(|n| n.ends_with("crt1.o"))
};
if let Some(name) = named(&moving) {
assert_eq!(name, "Scrt1.o");
assert_eq!(named(&fixed).as_deref(), Some("crt1.o"));
}
}
#[test]
fn a_profiled_program_is_started_by_the_startup_file_that_counts() {
let profile = LinkOptions { profile: true, ..LinkOptions::default() };
assert_eq!(startfile(&profile, false), Some("gcrt1.o"));
assert_eq!(startfile(&profile, true), Some("gcrt1.o"));
let still = LinkOptions { is_static: true, ..profile.clone() };
assert_eq!(startfile(&still, true), Some("grcrt1.o"));
assert_eq!(startfile(&still, false), Some("gcrt1.o"));
let shared = LinkOptions { shared: true, ..profile };
assert_eq!(startfile(&shared, false), None);
}
#[test]
fn a_program_that_is_not_profiled_is_started_by_the_usual_one() {
let plain = LinkOptions::default();
assert_eq!(startfile(&plain, false), Some("crt1.o"));
assert_eq!(startfile(&plain, true), Some("Scrt1.o"));
}
#[test]
fn asking_for_no_startup_files_leaves_out_both_ends_of_them() {
let opts = LinkOptions { no_startfiles: true, ..LinkOptions::default() };
let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
assert!(!args.iter().any(|a| a.ends_with("crtn.o")), "{args:?}");
assert!(args.contains(&"-lc".to_owned()), "{args:?}");
}
#[test]
fn asking_for_no_library_at_all_leaves_out_the_startup_files_too() {
let opts = LinkOptions { no_stdlib: true, ..LinkOptions::default() };
let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
assert!(!args.contains(&"-lc".to_owned()), "{args:?}");
assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
}
#[test]
fn the_library_comes_after_the_objects_that_need_it() {
let items = vec![Item::File("a.o".to_owned()), Item::Library("m".to_owned())];
let args = line(linux(), &LinkOptions::default(), &items, "a.out").expect("a line");
let obj = args.iter().position(|a| a == "a.o").expect("the object");
let m = args.iter().position(|a| a == "-lm").expect("the library");
let c = args.iter().position(|a| a == "-lc").expect("the library");
assert!(obj < m && m < c, "{args:?}");
}
#[test]
fn what_the_user_told_the_linker_stays_where_the_user_wrote_it() {
let items = vec![
Item::File("a.o".to_owned()),
Item::Linker("--whole-archive".to_owned()),
Item::File("libaesni.a".to_owned()),
Item::Linker("--no-whole-archive".to_owned()),
Item::Library("m".to_owned()),
];
let args = line(linux(), &LinkOptions::default(), &items, "a.out").expect("a line");
let at = |what: &str| args.iter().position(|a| a == what).expect(what);
assert!(at("a.o") < at("--whole-archive"), "{args:?}");
assert!(at("--whole-archive") < at("libaesni.a"), "{args:?}");
assert!(at("libaesni.a") < at("--no-whole-archive"), "{args:?}");
assert!(at("--no-whole-archive") < at("-lm"), "{args:?}");
assert!(at("-lm") < at("-lc"), "{args:?}");
}
#[test]
fn a_sysroot_moves_every_path_this_decided_and_none_the_user_wrote() {
let opts = LinkOptions {
sysroot: Some(PathBuf::from("/nowhere-at-all")),
search: vec![PathBuf::from("/opt/mine")],
..LinkOptions::default()
};
let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
let at = args.iter().position(|a| a == "-dynamic-linker").expect("the flag");
assert_eq!(args[at + 1], "/nowhere-at-all/lib64/ld-linux-x86-64.so.2");
assert!(args.contains(&"-L/opt/mine".to_owned()), "{args:?}");
}
#[test]
fn a_platform_with_no_link_line_is_said_so_rather_than_linked_wrongly() {
let triple = Triple::new(Arch::X86_64, Os::Windows, Env::Msvc);
let error = line(triple, &LinkOptions::default(), &one("a.o"), "a.out")
.expect_err("no line for it");
assert!(matches!(error, Error::Target { .. }), "{error:?}");
}
fn an_sdk(name: &str, settings: Option<&str>) -> PathBuf {
let dir = std::env::temp_dir()
.join(format!("rucc-sdk-{}-{}", std::process::id(), name.replace('.', "-")))
.join(name);
fs::create_dir_all(&dir).expect("a temporary directory");
if let Some(settings) = settings {
fs::write(dir.join("SDKSettings.json"), settings).expect("a settings file");
}
dir
}
fn mac() -> Triple {
Triple::new(Arch::Aarch64, Os::Darwin, Env::None)
}
#[test]
fn a_mac_link_names_the_platform_the_sdk_and_libsystem() {
let sdk = an_sdk(
"MacOSX.sdk",
Some(
"{\"CanonicalName\": \"macosx15.2\", \"Version\" : \"15.2\", \"MaximumDeploymentTarget\": \"15.2.99\"}",
),
);
let opts = LinkOptions {
sysroot: Some(sdk.clone()),
search: vec![PathBuf::from("/opt/mine")],
no_builtins_lib: true,
..LinkOptions::default()
};
let items = vec![Item::File("a.o".to_owned()), Item::Library("m".to_owned())];
let args = line(mac(), &opts, &items, "a.out").expect("a line");
let sdk = sdk.display().to_string();
let want: Vec<&str> = vec![
"-arch",
"arm64",
"-platform_version",
"macos",
"11.0",
"15.2",
"-syslibroot",
&sdk,
"-o",
"a.out",
"-L/opt/mine",
"a.o",
"-lm",
"-lSystem",
];
assert_eq!(args, want);
let opts =
LinkOptions { os_version: Some(rucc_tuple::Version::new(13, 4)), shared: true, ..opts };
let args = line(mac(), &opts, &one("a.o"), "liba.dylib").expect("a line");
assert_eq!(args[3..6], ["macos", "13.4", "15.2"]);
assert!(args.contains(&"-dylib".to_owned()), "{args:?}");
assert!(!args.iter().any(|arg| arg.contains("pie")), "{args:?}");
}
#[test]
fn the_sdk_version_comes_from_its_name_when_it_has_no_settings() {
let sdk = an_sdk("MacOSX14.5.sdk", None);
let opts =
LinkOptions { sysroot: Some(sdk), no_builtins_lib: true, ..LinkOptions::default() };
let args = line(mac(), &opts, &one("a.o"), "a.out").expect("a line");
assert_eq!(args[3..6], ["macos", "11.0", "14.5"]);
let sdk = an_sdk("Somewhere", None);
let opts = LinkOptions {
sysroot: Some(sdk),
os_version: Some(rucc_tuple::Version::major(12)),
..opts
};
let args = line(mac(), &opts, &one("a.o"), "a.out").expect("a line");
assert_eq!(args[3..6], ["macos", "12", "12"]);
}
#[test]
fn a_mac_link_looks_for_a_mach_o_linker_and_nothing_else() {
assert_eq!(order(mac(), &LinkOptions::default()), ["ld", "ld64.lld"]);
}
#[test]
fn a_static_mac_program_is_refused_and_a_relocatable_one_is_not() {
let sdk = an_sdk("MacOSX.sdk", None);
let opts = LinkOptions { sysroot: Some(sdk), is_static: true, ..LinkOptions::default() };
let error = line(mac(), &opts, &one("a.o"), "a.out").expect_err("no static line");
let Error::Cross { why } = &error else { panic!("{error:?}") };
assert!(why.contains("-static"), "{why}");
let args = relocatable_line(mac(), &LinkOptions::default(), &one("a.o"), "b.o")
.expect("ld64 takes -r");
assert_eq!(args, ["-r", "-arch", "arm64", "-o", "b.o", "a.o"]);
}
#[test]
fn the_line_is_printed_the_way_it_would_be_typed() {
let linker = Linker { name: "ld".to_owned(), path: PathBuf::from("/usr/bin/ld") };
let args = ["-o".to_owned(), "a b".to_owned()];
assert_eq!(render(&linker, &args), "/usr/bin/ld -o \"a b\"");
}
#[test]
fn a_linker_that_is_not_there_is_said_by_name() {
let opts = LinkOptions {
use_ld: Some("a-linker-nobody-has".to_owned()),
..LinkOptions::default()
};
let error = find(linux(), &opts).expect_err("not on this machine");
assert_eq!(error, Error::Named { name: "a-linker-nobody-has".to_owned() });
}
fn a_gcc_dir(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("rucc-link-{name}-{}", std::process::id()));
fs::create_dir_all(&dir).expect("a temporary directory");
fs::write(dir.join("libgcc.a"), b"not really an archive").expect("a file in it");
dir
}
#[test]
fn the_c_library_supplies_the_block_routines_and_our_runtime_does_not_displace_them() {
let gcc = a_gcc_dir("order");
let ours = PathBuf::from("/somewhere/librucc_builtins.a");
let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
let at_libc = args.iter().position(|a| a == "-lc").expect("libc");
let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
assert!(at_libc < at_ours, "{args:?}");
}
#[test]
fn a_static_link_puts_them_in_a_group_because_two_of_them_refer_to_each_other() {
let gcc = a_gcc_dir("group");
let opts = LinkOptions { is_static: true, ..LinkOptions::default() };
let args = runtime_items(&opts, &[gcc], None);
assert_eq!(args.first().map(String::as_str), Some("--start-group"), "{args:?}");
assert_eq!(args.last().map(String::as_str), Some("--end-group"), "{args:?}");
assert!(args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
}
#[test]
fn a_dynamic_link_needs_no_group_and_asks_for_the_shared_half_only_if_something_wants_it() {
let gcc = a_gcc_dir("dynamic");
let args = runtime_items(&LinkOptions::default(), &[gcc], None);
assert!(!args.contains(&"--start-group".to_owned()), "{args:?}");
assert!(!args.contains(&"-lgcc_eh".to_owned()), "{args:?}");
let at = args.iter().position(|a| a == "-lgcc_s").expect("the shared half");
assert_eq!(args[at - 1], "--as-needed", "{args:?}");
assert_eq!(args[at + 1], "--no-as-needed", "{args:?}");
}
#[test]
fn our_own_runtime_comes_before_the_machines_because_the_two_are_interchangeable() {
let gcc = a_gcc_dir("ours");
let ours = PathBuf::from("/somewhere/librucc_builtins.a");
let args = runtime_items(&LinkOptions::default(), &[gcc], Some(&ours));
let at_ours = args.iter().position(|a| a.ends_with("librucc_builtins.a")).expect("ours");
let at_gcc = args.iter().position(|a| a == "-lgcc").expect("libgcc");
assert!(at_ours < at_gcc, "{args:?}");
}
#[test]
fn no_builtins_lib_leaves_ours_off_and_keeps_the_machines() {
let gcc = a_gcc_dir("theirs");
let opts = LinkOptions { no_builtins_lib: true, ..LinkOptions::default() };
let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
assert!(!args.iter().any(|a| a.ends_with("librucc_builtins.a")), "{args:?}");
assert!(runtime_items(&opts, &[gcc], None).contains(&"-lgcc".to_owned()));
}
#[test]
fn nodefaultlibs_leaves_the_whole_runtime_off_and_not_only_the_c_library() {
let gcc = a_gcc_dir("none");
let opts = LinkOptions { no_defaultlibs: true, ..LinkOptions::default() };
assert!(runtime_items(&opts, &[gcc], None).is_empty());
}
#[test]
fn a_machine_with_no_gcc_on_it_gets_no_names_for_libraries_that_are_not_there() {
let empty = std::env::temp_dir().join("rucc-link-empty-not-a-gcc");
let args = runtime_items(&LinkOptions::default(), &[empty], None);
assert_eq!(args, ["-lc"], "{args:?}");
}
#[test]
fn a_gcc_version_directory_is_read_as_a_version_and_not_as_a_word() {
assert!(version_key(Path::new("/usr/lib/gcc/x/13")) > version_key(Path::new("/x/9")));
assert!(version_key(Path::new("/x/10.2")) > version_key(Path::new("/x/10")));
assert!(version_key(Path::new("/x/snapshot")) < version_key(Path::new("/x/1")));
}
#[test]
fn the_fast_math_startup_file_is_wanted_where_gccs_spec_wants_it() {
let fast = LinkOptions { fast_math: true, ..LinkOptions::default() };
assert!(!LinkOptions::default().wants_fastmath());
assert!(fast.wants_fastmath());
assert!(!LinkOptions { shared: true, ..fast.clone() }.wants_fastmath());
assert!(!LinkOptions { daz_ftz: Some(false), ..fast.clone() }.wants_fastmath());
let forced = LinkOptions { shared: true, daz_ftz: Some(true), ..LinkOptions::default() };
assert!(forced.wants_fastmath());
}
fn cached() -> LinkOptions {
LinkOptions {
cache: Some(PathBuf::from("/cache")),
prefixes: vec![a_builtins_dir()],
..LinkOptions::default()
}
}
fn a_builtins_dir() -> PathBuf {
let dir = std::env::temp_dir().join(format!("rucc-link-ours-{}", std::process::id()));
fs::create_dir_all(&dir).expect("a temporary directory");
fs::write(dir.join("librucc_builtins.a"), b"not really an archive").expect("a file in it");
dir
}
#[test]
fn the_runtime_is_found_under_the_tuple_as_well_as_the_triple() {
let dir = std::env::temp_dir().join(format!("rucc-link-tuple-{}", std::process::id()));
let target: Triple = "aarch64-linux-musl".parse().expect("a triple");
let under = dir.join(target.tuple().to_string());
fs::create_dir_all(&under).expect("a temporary directory");
fs::write(under.join("librucc_builtins.a"), b"not really an archive")
.expect("a file in it");
let found = builtins_archive(target, std::slice::from_ref(&dir));
assert_eq!(found, Some(under.join("librucc_builtins.a")));
let _ = fs::remove_dir_all(&dir);
}
fn a_sysroot(target: Triple) -> Sysroot {
Sysroot::in_cache(Path::new("/cache"), target.tuple())
}
fn foreign() -> Triple {
Triple::new(Arch::X86_64, Os::None, Env::None)
}
#[test]
fn a_cross_link_reads_the_targets_own_sysroot_and_nothing_of_this_machine() {
let target = Triple::new(Arch::Aarch64, Os::Linux, Env::Musl);
let sysroot = a_sysroot(target);
let root = sysroot.root().display().to_string();
let lib = sysroot.lib();
let args = cross_line(target, &cached(), &one("a.o"), "a.out", &sysroot).expect("a line");
assert!(args.contains(&format!("--sysroot={root}")), "{args:?}");
assert!(args.contains(&format!("-L{}", lib.display())), "{args:?}");
assert!(args.contains(&lib.join("libc.a").display().to_string()), "{args:?}");
let at = args.iter().position(|a| a == "-dynamic-linker").expect("the loader");
assert_eq!(args[at + 1], "/lib/ld-musl-aarch64.so.1", "{args:?}");
for arg in &args {
assert!(!arg.contains("/usr/lib"), "{arg} in {args:?}");
assert!(!arg.contains("/lib64"), "{arg} in {args:?}");
}
}
#[test]
fn a_freestanding_target_links_against_our_runtime_instead_of_being_refused() {
let args = line(foreign(), &cached(), &one("a.o"), "a.out").expect("a line");
assert!(args.iter().any(|a| a.ends_with("librucc_builtins.a")), "{args:?}");
assert!(!args.iter().any(|a| a.ends_with("libc.a")), "{args:?}");
assert!(!args.contains(&"-lc".to_owned()), "{args:?}");
assert!(!args.iter().any(|a| a.ends_with("crt1.o")), "{args:?}");
}
#[test]
fn a_driver_with_no_cache_to_look_in_says_so_rather_than_guessing() {
let error = line(foreign(), &LinkOptions::default(), &one("a.o"), "a.out")
.expect_err("no line for it");
assert!(matches!(error, Error::Target { .. }), "{error:?}");
}
#[test]
fn a_static_link_against_a_libc_that_is_a_stub_is_refused_rather_than_attempted() {
let target = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
let opts = LinkOptions { is_static: true, ..cached() };
let error = cross_line(target, &opts, &one("a.o"), "a.out", &a_sysroot(target))
.expect_err("there is no libc.a in a stub sysroot");
let Error::Cross { why } = &error else { panic!("{error:?}") };
assert!(why.contains("stub"), "{why}");
}
#[test]
fn a_target_whose_linker_wants_a_different_line_is_refused_by_name() {
for target in [
Triple::new(Arch::Aarch64, Os::Darwin, Env::None),
Triple::new(Arch::X86_64, Os::Windows, Env::Msvc),
] {
let error = cross_line(target, &cached(), &one("a.o"), "a.out", &a_sysroot(target))
.expect_err("no line for that format");
let Error::Cross { why } = &error else { panic!("{error:?}") };
assert!(why.contains(&target.tuple().to_canonical_string()), "{why}");
}
}
#[test]
fn a_mingw_target_links_and_looks_for_a_linker_that_can_write_a_pe_image() {
let target = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
let args = cross_line(target, &cached(), &one("a.o"), "a.exe", &a_sysroot(target))
.expect("a line for mingw-w64");
let at = |flag: &str| args.iter().position(|arg| arg == flag).expect(flag);
assert_eq!(args[at("-m") + 1], "i386pep");
assert_eq!(args[at("--subsystem") + 1], "console");
assert!(args.iter().any(|arg| arg.ends_with("libmsvcrt.a")), "{args:?}");
let names = cross_order(target);
assert_eq!(names.first().map(String::as_str), Some("ld.lld"));
assert!(names.contains(&"x86_64-w64-mingw32-ld".to_owned()), "{names:?}");
}
#[test]
fn our_runtime_comes_from_beside_the_compiler_rather_than_from_inside_the_sysroot() {
let target = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
let sysroot = a_sysroot(target);
let opts = cached();
let args = cross_line(target, &opts, &one("a.o"), "a.exe", &sysroot).expect("a line");
let ours: Vec<&String> =
args.iter().filter(|arg| arg.ends_with("librucc_builtins.a")).collect();
assert_eq!(ours.len(), 1, "{args:?}");
assert_eq!(ours[0], &opts.prefixes[0].join("librucc_builtins.a").display().to_string());
assert!(!ours[0].starts_with(&sysroot.lib().display().to_string()), "{args:?}");
assert_eq!(args.last(), Some(ours[0]), "{args:?}");
}
#[test]
fn a_cross_link_with_no_runtime_to_find_says_which_command_writes_one() {
let target = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
let opts = LinkOptions { prefixes: Vec::new(), ..cached() };
let error = cross_line(target, &opts, &one("a.o"), "a.exe", &a_sysroot(target))
.expect_err("there is no runtime for it to find");
let Error::Cross { why } = &error else { panic!("{error:?}") };
assert!(why.contains("cargo xtask builtins"), "{why}");
assert!(why.contains("-fno-builtins-lib"), "{why}");
let without = LinkOptions { no_builtins_lib: true, ..opts };
let args = cross_line(target, &without, &one("a.o"), "a.exe", &a_sysroot(target))
.expect("a line without ours on it");
assert!(!args.iter().any(|arg| arg.ends_with("librucc_builtins.a")), "{args:?}");
}
#[test]
fn the_version_an_lld_prints_is_read_and_nothing_elses_is() {
assert_eq!(lld_major("LLD 18.1.8 (compatible with GNU linkers)\n"), Some(18));
assert_eq!(lld_major("Ubuntu LLD 18.1.3 (compatible with GNU linkers)\n"), Some(18));
assert_eq!(lld_major("LLD 20.1.2 (compatible with GNU linkers)\n"), Some(20));
assert_eq!(lld_major("GNU ld (GNU Binutils for Ubuntu) 2.42\n"), None);
assert_eq!(lld_major("mold 2.4.1 (compatible with GNU ld)\n"), None);
assert_eq!(lld_major(""), None);
}
#[cfg(unix)]
fn a_linker_that_says(tag: &str, text: &str) -> Linker {
use std::os::unix::fs::PermissionsExt as _;
let dir = std::env::temp_dir().join(format!("rucc-link-ld-{}", std::process::id()));
fs::create_dir_all(&dir).expect("a temporary directory");
let path = dir.join(format!("ld.lld-{tag}"));
fs::write(&path, format!("#!/bin/sh\necho '{text}'\n")).expect("a script");
fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).expect("an executable one");
Linker { name: "ld.lld".to_owned(), path }
}
#[test]
#[cfg(unix)]
fn an_lld_too_old_to_read_exportas_is_refused_for_windows_gnu_and_nowhere_else() {
let windows = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
let old = a_linker_that_says("18", "LLD 18.1.8 (compatible with GNU linkers)");
let error = suitable(windows, &old).expect_err("18 cannot link this");
let Error::TooOld { name, found, target } = &error else { panic!("{error:?}") };
assert_eq!((name.as_str(), *found, target.as_str()), ("ld.lld", 18, "x86_64-windows-gnu"));
assert!(error.to_string().contains("IMPORT_NAME_EXPORTAS"), "{error}");
let linux = Triple::new(Arch::X86_64, Os::Linux, Env::Musl);
assert_eq!(suitable(linux, &old), Ok(()));
let new = a_linker_that_says("19", "LLD 19.1.0 (compatible with GNU linkers)");
assert_eq!(suitable(windows, &new), Ok(()));
}
#[test]
#[cfg(unix)]
fn an_old_lld_first_in_line_is_passed_over_for_a_newer_one_behind_it() {
use std::os::unix::fs::PermissionsExt as _;
let root = std::env::temp_dir().join(format!("rucc-link-two-lld-{}", std::process::id()));
let mut prefixes = Vec::new();
for (dir, version) in [("old", "18.1.3"), ("new", "19.1.7")] {
let dir = root.join(dir);
fs::create_dir_all(&dir).expect("a temporary directory");
let path = dir.join("ld.rucc-test-lld");
fs::write(&path, format!("#!/bin/sh\necho 'Ubuntu LLD {version}'\n"))
.expect("a script");
fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).expect("executable");
prefixes.push(dir);
}
let windows = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
let opts = LinkOptions {
use_ld: Some("rucc-test-lld".to_owned()),
prefixes,
..LinkOptions::default()
};
let found = find(windows, &opts).expect("the newer one");
assert_eq!(found.path, root.join("new").join("ld.rucc-test-lld"));
let opts = LinkOptions { prefixes: vec![root.join("old")], ..opts };
let error = find(windows, &opts).expect_err("only 18 is here");
assert!(matches!(error, Error::TooOld { found: 18, .. }), "{error:?}");
let _ = fs::remove_dir_all(&root);
}
#[test]
fn a_long_line_goes_in_a_response_file_that_reads_back_as_it_was() {
let args: Vec<String> =
["-o", r"C:\Users\a b\out.exe", "-m", "i386pep", r#"say "hi""#, "", "plain.o"]
.map(str::to_owned)
.to_vec();
let (front, rest) = split_for_file(&args);
assert_eq!(front, ["-m", "i386pep"]);
assert_eq!(
crate::response_words(&response_text(&rest, false)),
[&args[..2], &args[4..]].concat()
);
assert_eq!(
response_text(&rest, true),
"\"-o\"\n\"C:\\Users\\a b\\out.exe\"\n\"say \\\"hi\\\"\"\n\"\"\n\"plain.o\"\n"
);
assert_eq!(response_text(&[r"C:\dir\".to_owned()], true), "\"C:\\dir\\\\\"\n");
assert!(!too_long(&args, 1000));
assert!(too_long(&vec!["x".repeat(100); 400], WINDOWS_LINE));
}
#[test]
fn a_program_name_is_tried_with_each_pathext_extension_it_does_not_already_have() {
let exts = vec![".com".to_owned(), ".exe".to_owned()];
let dir = Path::new("bin");
assert_eq!(
spellings(&dir.join("ld.lld"), &exts),
[dir.join("ld.lld.com"), dir.join("ld.lld.exe")]
);
assert_eq!(spellings(&dir.join("lld-link.EXE"), &exts), [dir.join("lld-link.EXE")]);
assert_eq!(spellings(&dir.join("ld.lld"), &[]), [dir.join("ld.lld")]);
}
#[test]
#[cfg(unix)]
fn an_lld_off_path_is_found_under_its_own_name() {
use std::os::unix::fs::PermissionsExt as _;
let dir = std::env::temp_dir().join(format!("rucc-link-off-path-{}", std::process::id()));
fs::create_dir_all(&dir).expect("a temporary directory");
let lld = dir.join("ld.lld");
fs::write(&lld, "#!/bin/sh\n").expect("a file");
fs::set_permissions(&lld, fs::Permissions::from_mode(0o755)).expect("permissions");
let found =
linker_candidates("ld.lld", &LinkOptions::default(), std::slice::from_ref(&dir));
let _ = fs::remove_dir_all(&dir);
assert!(found.contains(&lld), "{found:?}");
}
#[test]
fn lld_is_looked_for_where_package_managers_put_it_newest_version_first() {
let root = std::env::temp_dir().join(format!("rucc-link-lld-dirs-{}", std::process::id()));
for dir in ["usr/lib/llvm-18/bin", "usr/lib/llvm-19/bin", "usr/lib/llvm-9/bin", "usr/lib/x"]
{
fs::create_dir_all(root.join(dir)).expect("a temporary directory");
}
let dirs = lld_dirs(&root, Some(PathBuf::from("C:/Program Files")));
let under_lib: Vec<_> =
dirs.iter().filter(|dir| dir.starts_with(root.join("usr/lib"))).collect();
assert_eq!(
under_lib,
[
&root.join("usr/lib/llvm-19/bin"),
&root.join("usr/lib/llvm-18/bin"),
&root.join("usr/lib/llvm-9/bin")
]
);
assert!(dirs.contains(&root.join("opt/homebrew/opt/lld/bin")), "{dirs:?}");
assert_eq!(dirs.last(), Some(&PathBuf::from("C:/Program Files").join("LLVM").join("bin")));
let _ = fs::remove_dir_all(&root);
}
#[test]
fn no_linker_says_where_to_get_lld_only_when_lld_was_looked_for() {
let cross = Error::NoLinker { tried: vec!["ld.lld".to_owned(), "lld".to_owned()] };
assert!(cross.to_string().contains("lld 19 or newer"), "{cross}");
let native = Error::NoLinker { tried: vec!["ld".to_owned()] };
assert_eq!(native.to_string(), "no linker was found; tried ld");
}
#[test]
#[cfg(unix)]
fn a_linker_that_will_not_say_what_it_is_is_left_alone() {
let windows = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
let quiet = a_linker_that_says("gnu", "GNU ld (GNU Binutils for Ubuntu) 2.42");
assert_eq!(suitable(windows, &quiet), Ok(()));
let missing = Linker { name: "ld.lld".to_owned(), path: PathBuf::from("/no/such/linker") };
assert_eq!(suitable(windows, &missing), Ok(()));
}
#[test]
fn profiling_a_cross_link_is_refused_because_the_startup_file_is_compiled_code() {
let target = Triple::new(Arch::X86_64, Os::Linux, Env::Musl);
let opts = LinkOptions { profile: true, ..cached() };
let error = cross_line(target, &opts, &one("a.o"), "a.out", &a_sysroot(target))
.expect_err("there is no gcrt1.o in a generated sysroot");
let Error::Cross { why } = &error else { panic!("{error:?}") };
assert!(why.contains("gcrt1.o"), "{why}");
}
#[test]
fn a_distributions_cross_tree_is_used_when_there_is_no_sysroot_of_ours() {
let usr = std::env::temp_dir().join(format!("rucc-link-usr-{}", std::process::id()));
let root = usr.join("aarch64-linux-gnu");
for dir in ["include", "lib"] {
fs::create_dir_all(root.join(dir)).expect("a scratch tree");
}
for version in ["9", "13"] {
fs::create_dir_all(usr.join("lib/gcc-cross/aarch64-linux-gnu").join(version))
.expect("a scratch gcc");
}
let host = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
let arm = Triple::new(Arch::Aarch64, Os::Linux, Env::Gnu);
let opts = LinkOptions { usr: Some(usr.clone()), ..cached() };
let distro = distro_for(arm, &opts, Some(host)).expect("the packages are there");
assert_eq!(distro.include(), root.join("include"));
assert_eq!(distro.lib(), root.join("lib"));
assert_eq!(
distro.gcc,
[13, 9].map(|v| usr.join("lib/gcc-cross/aarch64-linux-gnu").join(v.to_string()))
);
assert!(cross_for(arm, &opts, Some(host)).is_none());
assert!(distro_for(host, &opts, Some(host)).is_none());
let riscv = Triple::new(Arch::Riscv64, Os::Linux, Env::Gnu);
assert!(distro_for(riscv, &opts, Some(host)).is_none());
let named = LinkOptions { sysroot: Some(PathBuf::from("/opt/root")), ..opts.clone() };
assert!(distro_for(arm, &named, Some(host)).is_none());
let pinned = LinkOptions {
pinned: Some("aarch64-linux-gnu.2.28".parse::<TargetTuple>().expect("a release")),
..opts.clone()
};
assert!(distro_for(arm, &pinned, Some(host)).is_none());
let cache = usr.join("cache");
fs::create_dir_all(Sysroot::in_cache(&cache, arm.tuple()).lib()).expect("a sysroot");
let fetched = LinkOptions { cache: Some(cache), ..opts };
assert!(distro_for(arm, &fetched, Some(host)).is_none());
let _ = fs::remove_dir_all(&usr);
}
#[test]
fn the_host_takes_the_host_line_and_a_tree_the_user_named_takes_it_too() {
let host = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
let other = Triple::new(Arch::Riscv64, Os::Linux, Env::Musl);
assert!(cross_for(host, &cached(), Some(host)).is_none());
assert!(cross_for(other, &cached(), Some(host)).is_some());
let named = LinkOptions { sysroot: Some(PathBuf::from("/opt/root")), ..cached() };
assert!(cross_for(other, &named, Some(host)).is_none());
assert!(cross_for(other, &cached(), None).is_some());
}
#[test]
fn a_pinned_release_on_this_machines_own_target_is_a_cross_compile() {
let host = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
let pinned = LinkOptions {
pinned: Some(
"x86_64-linux-gnu.2.28".parse::<TargetTuple>().expect("a spelling with a release"),
),
..cached()
};
let at = cross_for(host, &pinned, Some(host)).expect("a pin is a cross compile");
assert!(at.root().ends_with("x86_64-linux-gnu.2.28"), "{:?}", at.root());
let bare = LinkOptions { pinned: None, ..cached() };
assert!(cross_for(host, &bare, Some(host)).is_none());
}
#[test]
fn a_glibc_cross_link_writes_its_stubs_beside_the_sysroot_once() {
let cache = std::env::temp_dir().join(format!("rucc-link-stubs-{}", std::process::id()));
let _ = fs::remove_dir_all(&cache);
let target = Triple::new(Arch::X86_64, Os::Linux, Env::Gnu);
let pinned = LinkOptions {
cache: Some(cache.clone()),
pinned: Some("x86_64-linux-gnu.2.28".parse().expect("a spelling with a release")),
..LinkOptions::default()
};
write_stubs(target, &pinned).expect("x86_64 glibc has a description");
let dir = cache.join("stubs").join("x86_64-linux-gnu.2.28");
let libc = dir.join("libc.so");
let bytes = fs::read(&libc).expect("libc.so was written");
assert!(bytes.starts_with(b"\x7fELF"));
assert!(dir.join("libm.so").is_file());
assert!(!dir.join("libpthread.so").exists());
let before = fs::metadata(&libc).and_then(|m| m.modified()).expect("a time");
write_stubs(target, &pinned).expect("again");
let after = fs::metadata(&libc).and_then(|m| m.modified()).expect("a time");
assert_eq!(before, after);
let musl = LinkOptions {
cache: Some(cache.clone()),
pinned: Some("x86_64-linux-musl".parse().expect("musl")),
..LinkOptions::default()
};
write_stubs(Triple::new(Arch::X86_64, Os::Linux, Env::Musl), &musl).expect("nothing");
assert!(!cache.join("stubs").join("x86_64-linux-musl").exists());
let _ = fs::remove_dir_all(&cache);
}
#[test]
fn what_a_cross_link_searches_is_the_sysroot_and_not_this_machine() {
let dirs = search_dirs(&cached(), foreign());
assert_eq!(dirs.len(), 1, "{dirs:?}");
assert!(dirs[0].starts_with("/cache"), "{dirs:?}");
assert!(dirs[0].ends_with("lib"), "{dirs:?}");
let mine = LinkOptions { search: vec![PathBuf::from("/opt/mine")], ..cached() };
assert_eq!(search_dirs(&mine, foreign())[0], PathBuf::from("/opt/mine"));
}
#[test]
fn gnu_ld_is_not_looked_for_against_the_fetched_mingw_sysroot() {
let windows = Triple::new(Arch::X86_64, Os::Windows, Env::Gnu);
assert_eq!(order(windows, &cached()), ["ld.lld", "lld"]);
}
#[test]
fn the_linker_looked_for_on_a_cross_link_is_one_that_can_cross() {
let names = cross_order(Triple::new(Arch::Aarch64, Os::Linux, Env::Gnu));
assert_eq!(names.first().map(String::as_str), Some("ld.lld"));
assert!(names.contains(&"aarch64-linux-gnu-ld".to_owned()), "{names:?}");
assert!(!names.iter().any(|name| name.contains("mold")), "{names:?}");
assert!(!names.contains(&"ld".to_owned()), "{names:?}");
assert_eq!(order(foreign(), &cached()), ["ld.lld", "lld"]);
}
#[test]
fn the_four_flags_become_the_five_modes_they_describe() {
let plain = LinkOptions::default();
assert_eq!(mode(&plain), LinkMode::Dynamic);
assert_eq!(
mode(&LinkOptions { pie: Some(false), ..plain.clone() }),
LinkMode::DynamicNoPie
);
assert_eq!(mode(&LinkOptions { is_static: true, ..plain.clone() }), LinkMode::Static);
let both = LinkOptions { is_static: true, pie: Some(true), ..plain.clone() };
assert_eq!(mode(&both), LinkMode::StaticPie);
assert_eq!(mode(&LinkOptions { shared: true, ..plain }), LinkMode::Shared);
}
#[test]
fn a_sysroot_that_has_not_been_built_is_named_before_anything_is_compiled() {
let opts = LinkOptions {
cache: Some(std::env::temp_dir().join("rucc-a-cache-nobody-filled")),
..LinkOptions::default()
};
let error = preflight(foreign(), &opts).expect_err("nothing has built one");
let Error::Sysroot { dir, pinned, .. } = &error else { panic!("{error:?}") };
assert!(dir.ends_with("x86_64-none"), "{dir}");
assert!(!pinned, "nothing should be pinned for a bare metal target");
let said = error.to_string();
assert!(said.contains("pins none for it to fetch"), "{said}");
}
#[test]
fn a_sysroot_that_could_be_fetched_is_told_what_to_run() {
let said = Error::Sysroot {
target: "x86_64-linux-musl".to_owned(),
dir: "/somewhere/sysroots/x86_64-linux-musl".to_owned(),
pinned: true,
}
.to_string();
assert!(said.contains("`rucc --fetch x86_64-linux-musl`"), "{said}");
assert!(said.contains("--sysroot=<dir>"), "{said}");
}
#[test]
fn a_link_against_this_machine_has_nothing_to_check_before_it_starts() {
assert!(preflight(linux(), &LinkOptions::default()).is_ok());
}
#[test]
fn a_runtime_directory_that_is_not_on_this_machine_is_not_offered() {
let dirs = runtime_dirs(linux(), Some(Path::new("/definitely/not/a/sysroot")));
assert!(dirs.is_empty(), "{dirs:?}");
}
}