use std::ffi::OsString;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use rucc_target::{Arch, Env, Os, Triple};
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct LinkOptions {
pub use_ld: Option<String>,
pub search: Vec<PathBuf>,
pub passthrough: Vec<String>,
pub prefixes: Vec<PathBuf>,
pub sysroot: 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 no_builtins_lib: bool,
}
impl LinkOptions {
fn wants_startfiles(&self) -> bool {
!self.no_stdlib && !self.no_startfiles
}
fn wants_defaultlibs(&self) -> bool {
!self.no_stdlib && !self.no_defaultlibs
}
fn wants_runtime(&self) -> bool {
!self.no_stdlib && !self.no_defaultlibs
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Item {
File(String),
Library(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}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
NoLinker {
tried: Vec<String>,
},
Named {
name: String,
},
Target {
triple: 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(", "))
}
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::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()];
}
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(),
],
}
}
pub fn find(target: Triple, opts: &LinkOptions) -> Result<Linker, Error> {
let tried = order(target, opts);
for name in &tried {
if name.contains(std::path::MAIN_SEPARATOR) || name.contains('/') {
let path = PathBuf::from(name);
if path.is_file() {
return Ok(Linker { name: name.clone(), path });
}
continue;
}
for dir in &opts.prefixes {
let path = dir.join(name);
if path.is_file() {
return Ok(Linker { name: name.clone(), path });
}
}
if let Some(path) = on_path(name) {
return Ok(Linker { name: name.clone(), path });
}
}
match &opts.use_ld {
Some(name) => Err(Error::Named { name: name.clone() }),
None => Err(Error::NoLinker { tried }),
}
}
fn on_path(name: &str) -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
std::env::split_paths(&path).map(|dir| dir.join(name)).find(|p| executable(p))
}
#[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 target.os != Os::Linux {
return Err(Error::Target { triple: target.to_string() });
}
let machine = emulation(target);
let root = opts.sysroot.as_deref();
let dirs = library_dirs(target, root);
let runtime = 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() {
let first = if opts.shared {
None
} else if pie {
Some("Scrt1.o")
} else {
Some("crt1.o")
};
for name in first.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}")),
}
}
args.extend(runtime_items(opts, &runtime, ours.as_deref()));
if opts.wants_startfiles() {
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());
}
}
args.extend(opts.passthrough.iter().cloned());
Ok(args)
}
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 {
let dir = under(sysroot, &format!("{base}/{name}"));
let Ok(entries) = fs::read_dir(&dir) else { continue };
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));
found.extend(versions.into_iter().map(|(_, path)| path));
}
}
found
}
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 = "librucc_builtins.a";
let triple = target.to_string();
let mut places: Vec<PathBuf> = Vec::new();
for prefix in prefixes {
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))
{
if let Some(up) = dir.parent() {
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 libc = match target.env {
Env::Musl => "musl",
Env::None | Env::Gnu | Env::Msvc => "gnu",
};
let multiarch = format!("{}-linux-{libc}", target.arch.as_str());
[
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()
}
fn library_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
candidates(target, sysroot).into_iter().filter(|dir| dir.is_dir()).collect()
}
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
}
pub fn run(linker: &Linker, args: &[String]) -> Result<(), Error> {
let args: Vec<OsString> = args.iter().map(OsString::from).collect();
let status = Command::new(&linker.path).args(&args).status().map_err(|why| Error::Spawn {
path: linker.path.display().to_string(),
why: why.to_string(),
})?;
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_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 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_comes_after_what_this_told_it() {
let opts = LinkOptions {
passthrough: vec!["--no-eh-frame-hdr".to_owned()],
..LinkOptions::default()
};
let args = line(linux(), &opts, &one("a.o"), "a.out").expect("a line");
assert_eq!(args.last().map(String::as_str), Some("--no-eh-frame-hdr"));
}
#[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() {
for triple in [
Triple::new(Arch::X86_64, Os::Darwin, Env::Gnu),
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:?}");
}
}
#[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 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:?}");
}
}