use std::fmt;
use std::path::{Path, PathBuf};
use rucc_tuple::{Arch, DataModel, Endian, Env, ObjectFormat, TargetTuple};
use crate::layout::Sysroot;
use crate::link::{BUILTINS, Libc, LinkLine, LinkMode, libc, loader};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Item {
File(PathBuf),
Library(String),
}
#[derive(Debug, Clone, Default)]
pub struct Invocation<'a> {
pub inputs: &'a [Item],
pub output: Option<&'a Path>,
pub mode: LinkMode,
pub search: &'a [PathBuf],
pub passthrough: &'a [String],
pub no_startfiles: bool,
pub no_defaultlibs: bool,
pub no_builtins_lib: bool,
pub export_dynamic: bool,
pub strip: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Unsupported {
Format {
target: String,
format: &'static str,
},
MsvcAbi {
target: String,
},
Machine {
target: String,
},
StaticStub {
target: String,
},
}
impl fmt::Display for Unsupported {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Unsupported::Format { target, format } => write!(
f,
"there is no cross link line for {target} yet, because its object format is \
{format} and that linker takes a different line rather than a different spelling \
of this one"
),
Unsupported::MsvcAbi { target } => write!(
f,
"there is no cross link line for {target}, because it is Microsoft's ABI: the \
linker for it takes a different command line and the import libraries a program \
there links against come from the Windows SDK, which cannot be redistributed. \
Build for the mingw-w64 environment instead, which needs nothing installed, or \
pass --sysroot=<dir> naming an SDK you have"
),
Unsupported::Machine { target } => write!(
f,
"there is no PE machine type for {target}, so there is nothing to write after -m \
and a linker would guess the machine from the first object it read"
),
Unsupported::StaticStub { target } => write!(
f,
"{target} cannot be linked statically against a generated sysroot, because its \
libc there is a stub: it carries the names the platform's libc exports and none of \
the code behind them, which is what a dynamic link reads and not what a static one \
needs. Link it dynamically, or use a musl target, which ships a real libc.a"
),
}
}
}
impl std::error::Error for Unsupported {}
pub fn argv(
target: TargetTuple,
sysroot: &Sysroot,
options: &Invocation<'_>,
) -> Result<Vec<String>, Unsupported> {
let format = target.object_format();
match format {
ObjectFormat::Elf => elf(target, sysroot, options),
ObjectFormat::Coff if target.env() == Env::Gnu => coff(target, sysroot, options),
ObjectFormat::Coff => Err(Unsupported::MsvcAbi { target: target.to_canonical_string() }),
_ => Err(Unsupported::Format {
target: target.to_canonical_string(),
format: format.as_str(),
}),
}
}
fn elf(
target: TargetTuple,
sysroot: &Sysroot,
options: &Invocation<'_>,
) -> Result<Vec<String>, Unsupported> {
let statically = matches!(options.mode, LinkMode::Static | LinkMode::StaticPie);
if statically && libc(target) == Libc::Stub {
return Err(Unsupported::StaticStub { target: target.to_canonical_string() });
}
let mut args = output(options);
if let Some(name) = emulation(target) {
args.push("-m".to_owned());
args.push(name.to_owned());
}
args.push(sysroot_flag(sysroot));
args.extend(mode_flags(target, options.mode));
args.extend(hardening());
if options.export_dynamic {
args.push("--export-dynamic".to_owned());
}
if options.strip {
args.push("-s".to_owned());
}
args.extend(body(sysroot, options));
Ok(args)
}
fn coff(
target: TargetTuple,
sysroot: &Sysroot,
options: &Invocation<'_>,
) -> Result<Vec<String>, Unsupported> {
let Some(machine) = pe_machine(target) else {
return Err(Unsupported::Machine { target: target.to_canonical_string() });
};
let mut args = output(options);
args.push("-m".to_owned());
args.push(machine.to_owned());
args.push(sysroot_flag(sysroot));
if options.mode == LinkMode::Shared {
args.push("-shared".to_owned());
} else {
args.push("--subsystem".to_owned());
args.push("console".to_owned());
}
if matches!(options.mode, LinkMode::Static | LinkMode::StaticPie) {
args.push("-static".to_owned());
}
args.extend(pe_hardening(target));
if options.export_dynamic {
args.push("--export-all-symbols".to_owned());
}
if options.strip {
args.push("-s".to_owned());
}
args.extend(body(sysroot, options));
Ok(args)
}
fn output(options: &Invocation<'_>) -> Vec<String> {
match options.output {
Some(path) => vec!["-o".to_owned(), path.display().to_string()],
None => Vec::new(),
}
}
fn sysroot_flag(sysroot: &Sysroot) -> String {
format!("--sysroot={}", sysroot.root().display())
}
fn body(sysroot: &Sysroot, options: &Invocation<'_>) -> Vec<String> {
let mut args = Vec::new();
let line = LinkLine::for_target(sysroot, options.mode);
if !options.no_startfiles {
args.extend(shown(&line.start));
}
for dir in options.search {
args.push(format!("-L{}", dir.display()));
}
args.push(format!("-L{}", sysroot.lib().display()));
for input in options.inputs {
match input {
Item::File(path) => args.push(path.display().to_string()),
Item::Library(name) => args.push(format!("-l{name}")),
}
}
if !options.no_defaultlibs {
args.extend(shown(&libraries(&line, options)));
}
if !options.no_startfiles {
args.extend(shown(&line.end));
}
args.extend(options.passthrough.iter().cloned());
args
}
fn libraries(line: &LinkLine, options: &Invocation<'_>) -> Vec<PathBuf> {
let mut libraries = line.libraries.clone();
if options.no_builtins_lib {
libraries.retain(|path| path.file_name().is_none_or(|name| name != BUILTINS));
}
libraries
}
fn mode_flags(target: TargetTuple, mode: LinkMode) -> Vec<String> {
let mut args = Vec::new();
match mode {
LinkMode::Static => args.push("-static".to_owned()),
LinkMode::StaticPie => {
args.push("-static".to_owned());
args.push("-pie".to_owned());
args.push("--no-dynamic-linker".to_owned());
}
LinkMode::Dynamic => args.push("-pie".to_owned()),
LinkMode::DynamicNoPie => args.push("-no-pie".to_owned()),
LinkMode::Shared => args.push("-shared".to_owned()),
}
if matches!(mode, LinkMode::Dynamic | LinkMode::DynamicNoPie) {
if let Some(path) = loader(target) {
args.push("-dynamic-linker".to_owned());
args.push(path.to_owned());
}
}
args
}
fn hardening() -> Vec<String> {
[
"--eh-frame-hdr",
"--hash-style=gnu",
"-z",
"relro",
"-z",
"now",
"-z",
"noexecstack",
"--build-id=none",
]
.iter()
.map(|flag| (*flag).to_owned())
.collect()
}
fn pe_hardening(target: TargetTuple) -> Vec<String> {
let mut args = vec!["--dynamicbase".to_owned(), "--nxcompat".to_owned()];
if target.pointer_width() == 64 {
args.push("--high-entropy-va".to_owned());
}
args.push("--no-insert-timestamp".to_owned());
args
}
#[must_use]
pub fn pe_machine(target: TargetTuple) -> Option<&'static str> {
if target.object_format() != ObjectFormat::Coff || target.env() != Env::Gnu {
return None;
}
Some(match target.arch() {
Arch::X86_64 => "i386pep",
Arch::X86 => "i386pe",
Arch::Aarch64 => "arm64pe",
Arch::Arm => "thumb2pe",
_ => return None,
})
}
fn shown(paths: &[PathBuf]) -> Vec<String> {
paths.iter().map(|path| path.display().to_string()).collect()
}
#[must_use]
pub fn emulation(target: TargetTuple) -> Option<&'static str> {
if target.object_format() != ObjectFormat::Elf {
return None;
}
let narrow = target.data_model() == DataModel::Ilp32On64;
let little = target.endian() == Endian::Little;
Some(match target.arch() {
Arch::X86_64 if narrow => "elf32_x86_64",
Arch::X86_64 => "elf_x86_64",
Arch::X86 => "elf_i386",
Arch::Aarch64 | Arch::Arm64Ec => match (little, narrow) {
(true, false) => "aarch64linux",
(true, true) => "aarch64linux32",
(false, false) => "aarch64linuxb",
(false, true) => "aarch64linux32b",
},
Arch::Arm if little => "armelf_linux_eabi",
Arch::Arm => "armelfb_linux_eabi",
Arch::Riscv64 if little => "elf64lriscv",
Arch::Riscv64 => "elf64briscv",
Arch::Riscv32 if little => "elf32lriscv",
Arch::Riscv32 => "elf32briscv",
Arch::S390x => "elf64_s390",
Arch::PowerPc64 if little => "elf64lppc",
Arch::PowerPc64 => "elf64ppc",
Arch::LoongArch64 => "elf64loongarch",
Arch::Wasm32 => return None,
})
}
#[cfg(test)]
mod tests {
use std::path::Path;
use rucc_tuple::TargetTuple;
use super::{Invocation, Item, Unsupported, argv, emulation, pe_machine};
use crate::layout::Sysroot;
use crate::link::LinkMode;
fn target(spelling: &str) -> TargetTuple {
spelling.parse().expect("a tuple the table knows")
}
fn sysroot(spelling: &str) -> Sysroot {
Sysroot::in_cache(Path::new("/cache"), target(spelling))
}
fn line(spelling: &str, mode: LinkMode) -> Vec<String> {
let one = [Item::File(Path::new("main.o").to_path_buf())];
let options = Invocation {
inputs: &one,
output: Some(Path::new("main")),
mode,
..Invocation::default()
};
argv(target(spelling), &sysroot(spelling), &options).expect("a line")
}
#[test]
fn nothing_on_the_line_comes_from_the_host() {
for spelling in ["aarch64-linux-musl", "x86_64-linux-gnu", "riscv64-linux-musl"] {
for mode in [LinkMode::Dynamic, LinkMode::DynamicNoPie, LinkMode::Shared] {
for arg in line(spelling, mode) {
let host = ["/usr/lib", "/usr/local", "/lib64/", "/lib/x86_64"]
.iter()
.any(|bad| arg.starts_with(bad));
let is_loader = arg.contains("ld-musl") || arg.contains("ld-linux");
assert!(!host || is_loader, "{spelling} {mode:?} {arg}");
}
}
}
}
#[test]
fn every_file_of_ours_is_under_the_sysroot() {
let spelling = "aarch64-linux-musl";
let root = sysroot(spelling).root().display().to_string();
for arg in line(spelling, LinkMode::Static) {
let ours = arg.starts_with('/') && (arg.ends_with(".o") || arg.ends_with(".a"));
assert!(!ours || arg.starts_with(&root), "{arg}");
}
}
#[test]
fn the_static_line_names_no_loader_because_nothing_will_start_it() {
let args = line("aarch64-linux-musl", LinkMode::Static);
assert!(args.contains(&"-static".to_owned()), "{args:?}");
assert!(!args.contains(&"-dynamic-linker".to_owned()), "{args:?}");
}
#[test]
fn a_static_position_independent_link_is_three_flags_and_not_the_driver_one() {
let args = line("x86_64-linux-musl", LinkMode::StaticPie);
assert!(!args.iter().any(|arg| arg == "-static-pie"), "{args:?}");
for flag in ["-static", "-pie", "--no-dynamic-linker"] {
assert!(args.contains(&flag.to_owned()), "{flag} missing from {args:?}");
}
}
#[test]
fn a_dynamic_program_names_the_loader_that_will_start_it_and_a_shared_object_does_not() {
let program = line("x86_64-linux-gnu", LinkMode::Dynamic);
let at = program.iter().position(|arg| arg == "-dynamic-linker").expect("the flag");
assert_eq!(program[at + 1], "/lib64/ld-linux-x86-64.so.2");
let library = line("x86_64-linux-gnu", LinkMode::Shared);
assert!(!library.contains(&"-dynamic-linker".to_owned()), "{library:?}");
assert!(library.contains(&"-shared".to_owned()), "{library:?}");
}
#[test]
fn the_start_file_of_a_program_that_moves_is_not_the_one_of_a_program_that_does_not() {
let named = |mode| {
line("x86_64-linux-gnu", mode)
.iter()
.filter_map(|arg| {
Path::new(arg).file_name().map(|n| n.to_string_lossy().into_owned())
})
.find(|name| name.ends_with("crt1.o"))
};
assert_eq!(named(LinkMode::Dynamic).as_deref(), Some("Scrt1.o"));
assert_eq!(named(LinkMode::DynamicNoPie).as_deref(), Some("crt1.o"));
assert_eq!(named(LinkMode::Shared), None);
}
#[test]
fn the_library_comes_after_the_objects_that_need_it() {
let inputs = [Item::File(Path::new("main.o").to_path_buf()), Item::Library("m".to_owned())];
let options =
Invocation { inputs: &inputs, mode: LinkMode::Static, ..Invocation::default() };
let args = argv(target("x86_64-linux-musl"), &sysroot("x86_64-linux-musl"), &options)
.expect("a line");
let object = args.iter().position(|arg| arg == "main.o").expect("the object");
let asked = args.iter().position(|arg| arg == "-lm").expect("the library");
let libc = args.iter().position(|arg| arg.ends_with("libc.a")).expect("the libc");
let end = args.iter().position(|arg| arg.ends_with("crtn.o")).expect("the end file");
assert!(object < asked && asked < libc && libc < end, "{args:?}");
}
#[test]
fn a_static_glibc_link_is_refused_by_name_rather_than_attempted() {
let options = Invocation { mode: LinkMode::Static, ..Invocation::default() };
let error = argv(target("x86_64-linux-gnu"), &sysroot("x86_64-linux-gnu"), &options)
.expect_err("refused");
assert!(matches!(error, Unsupported::StaticStub { .. }), "{error:?}");
assert!(error.to_string().contains("musl"), "the way out is not in the message");
assert!(argv(target("x86_64-linux-musl"), &sysroot("x86_64-linux-musl"), &options).is_ok());
}
#[test]
fn a_format_with_no_line_of_its_own_is_refused_by_name_rather_than_approximated() {
for spelling in ["aarch64-macos", "wasm32-wasi"] {
let options = Invocation { mode: LinkMode::Dynamic, ..Invocation::default() };
let error =
argv(target(spelling), &sysroot(spelling), &options).expect_err("no line for it");
assert!(matches!(error, Unsupported::Format { .. }), "{spelling} {error:?}");
}
}
#[test]
fn the_msvc_abi_is_refused_on_its_own_grounds_and_the_way_out_is_in_the_message() {
for spelling in ["x86_64-windows-msvc", "aarch64-windows-msvc", "arm64ec-windows-msvc"] {
let options = Invocation { mode: LinkMode::Dynamic, ..Invocation::default() };
let error = argv(target(spelling), &sysroot(spelling), &options).expect_err("refused");
assert!(matches!(error, Unsupported::MsvcAbi { .. }), "{spelling} {error:?}");
assert!(error.to_string().contains("mingw-w64"), "{spelling} {error}");
}
}
#[test]
fn what_the_user_told_the_linker_comes_after_what_this_told_it() {
let passthrough = ["--no-eh-frame-hdr".to_owned()];
let options = Invocation {
mode: LinkMode::Dynamic,
passthrough: &passthrough,
..Invocation::default()
};
let args = argv(target("x86_64-linux-gnu"), &sysroot("x86_64-linux-gnu"), &options)
.expect("a line");
assert_eq!(args.last().map(String::as_str), Some("--no-eh-frame-hdr"));
}
#[test]
fn asking_for_no_start_files_leaves_out_both_ends_of_them() {
let options =
Invocation { mode: LinkMode::Dynamic, no_startfiles: true, ..Invocation::default() };
let args = argv(target("x86_64-linux-gnu"), &sysroot("x86_64-linux-gnu"), &options)
.expect("a line");
assert!(!args.iter().any(|arg| arg.ends_with("crt1.o")), "{args:?}");
assert!(!args.iter().any(|arg| arg.ends_with("crtn.o")), "{args:?}");
assert!(args.iter().any(|arg| arg.ends_with("libc.so")), "{args:?}");
}
#[test]
fn a_narrow_mode_of_a_wide_architecture_is_a_different_output_format() {
assert_eq!(emulation(target("x86_64-linux-gnux32")), Some("elf32_x86_64"));
assert_eq!(emulation(target("x86_64-linux-gnu")), Some("elf_x86_64"));
}
#[test]
fn byte_order_is_in_the_output_format_name() {
assert_eq!(emulation(target("s390x-linux-gnu")), Some("elf64_s390"));
assert_eq!(emulation(target("powerpc64le-linux-gnu")), Some("elf64lppc"));
assert_eq!(emulation(target("riscv64-linux-musl")), Some("elf64lriscv"));
}
#[test]
fn rdynamic_reaches_the_linker_and_no_builtins_lib_takes_our_runtime_off() {
let one = [Item::File(Path::new("main.o").to_path_buf())];
let both = Invocation {
inputs: &one,
output: Some(Path::new("main")),
mode: LinkMode::Dynamic,
export_dynamic: true,
no_builtins_lib: true,
..Invocation::default()
};
let spelling = "x86_64-linux-musl";
let args = argv(target(spelling), &sysroot(spelling), &both).expect("a line");
assert!(args.contains(&"--export-dynamic".to_owned()), "{args:?}");
assert!(!args.iter().any(|arg| arg.ends_with("librucc_builtins.a")), "{args:?}");
assert!(args.iter().any(|arg| arg.ends_with("libc.a")), "{args:?}");
}
#[test]
fn a_mingw_line_names_the_pe_machine_and_the_subsystem_and_no_loader() {
let args = line("x86_64-windows-gnu", LinkMode::Dynamic);
let at = args.iter().position(|arg| arg == "-m").expect("the machine flag");
assert_eq!(args[at + 1], "i386pep");
let at = args.iter().position(|arg| arg == "--subsystem").expect("the subsystem flag");
assert_eq!(args[at + 1], "console");
for absent in ["-dynamic-linker", "-pie", "-no-pie", "--eh-frame-hdr"] {
assert!(!args.contains(&absent.to_owned()), "{absent} in {args:?}");
}
}
#[test]
fn a_mingw_line_carries_the_crt_and_the_win32_libraries_in_single_pass_order() {
let args = line("x86_64-windows-gnu", LinkMode::Dynamic);
let at = |name: &str| {
args.iter().position(|arg| arg.ends_with(name)).unwrap_or_else(|| panic!("{name}"))
};
assert!(at("crt2.o") < at("main.o"), "{args:?}");
assert!(!args.iter().any(|arg| arg.ends_with("crtn.o")), "{args:?}");
assert!(at("main.o") < at("libmingw32.a"), "{args:?}");
assert!(at("libmingwex.a") < at("libmsvcrt.a"), "{args:?}");
assert!(at("libmsvcrt.a") < at("libkernel32.a"), "{args:?}");
assert!(at("libkernel32.a") < at("librucc_builtins.a"), "{args:?}");
}
#[test]
fn a_dll_takes_the_other_start_file_and_no_subsystem() {
let args = line("x86_64-windows-gnu", LinkMode::Shared);
assert!(args.contains(&"-shared".to_owned()), "{args:?}");
let named = |name: &str| {
args.iter().any(|arg| Path::new(arg).file_name().is_some_and(|file| file == name))
};
assert!(named("dllcrt2.o"), "{args:?}");
assert!(!named("crt2.o"), "{args:?}");
assert!(!args.contains(&"--subsystem".to_owned()), "{args:?}");
}
#[test]
fn a_static_windows_link_is_not_refused_because_the_crt_there_is_a_dll_on_every_machine() {
let args = line("x86_64-windows-gnu", LinkMode::Static);
assert!(args.contains(&"-static".to_owned()), "{args:?}");
assert!(args.iter().any(|arg| arg.ends_with("libmsvcrt.a")), "{args:?}");
}
#[test]
fn the_pe_header_carries_no_timestamp_so_that_two_links_produce_one_file() {
for spelling in ["x86_64-windows-gnu", "i686-windows-gnu", "aarch64-windows-gnu"] {
let args = line(spelling, LinkMode::Dynamic);
assert!(args.contains(&"--no-insert-timestamp".to_owned()), "{spelling} {args:?}");
assert!(args.contains(&"--dynamicbase".to_owned()), "{spelling} {args:?}");
let wide = args.contains(&"--high-entropy-va".to_owned());
assert_eq!(wide, spelling != "i686-windows-gnu", "{spelling} {args:?}");
}
}
#[test]
fn the_pe_machine_is_the_one_the_linker_knows_and_not_the_one_the_architecture_is_called() {
assert_eq!(pe_machine(target("x86_64-windows-gnu")), Some("i386pep"));
assert_eq!(pe_machine(target("i686-windows-gnu")), Some("i386pe"));
assert_eq!(pe_machine(target("aarch64-windows-gnu")), Some("arm64pe"));
assert_eq!(pe_machine(target("x86_64-linux-gnu")), None);
assert_eq!(pe_machine(target("x86_64-windows-msvc")), None);
assert_eq!(emulation(target("x86_64-windows-gnu")), None);
}
#[test]
fn a_format_with_no_emulation_names_none_rather_than_its_architecture_s() {
for spelling in
["aarch64-macos", "x86_64-windows-gnu", "x86_64-windows-msvc", "wasm32-wasi"]
{
assert_eq!(emulation(target(spelling)), None, "{spelling}");
}
}
#[test]
fn a_freestanding_link_has_no_libc_and_no_start_files_and_still_has_our_runtime() {
let args = line("armv7m-none-eabi", LinkMode::Static);
assert!(!args.iter().any(|arg| arg.ends_with("crt1.o")), "{args:?}");
assert!(!args.iter().any(|arg| arg.ends_with("crti.o")), "{args:?}");
assert!(!args.iter().any(|arg| arg.ends_with("crtn.o")), "{args:?}");
assert!(!args.iter().any(|arg| arg.ends_with("libc.a")), "{args:?}");
assert!(args.iter().any(|arg| arg.ends_with("librucc_builtins.a")), "{args:?}");
assert!(args.contains(&"-static".to_owned()), "{args:?}");
assert!(!args.contains(&"-dynamic-linker".to_owned()), "{args:?}");
}
#[test]
fn the_platforms_whose_libc_we_stub_refuse_a_static_link_too_and_not_only_glibc() {
for spelling in ["aarch64-linux-android", "x86_64-freebsd", "x86_64-illumos"] {
let options = Invocation { mode: LinkMode::Static, ..Invocation::default() };
let error = argv(target(spelling), &sysroot(spelling), &options).expect_err("refused");
assert!(matches!(error, Unsupported::StaticStub { .. }), "{spelling} {error:?}");
}
}
#[test]
fn the_same_line_comes_out_every_time_it_is_asked_for() {
for mode in [LinkMode::Dynamic, LinkMode::Shared] {
assert_eq!(line("aarch64-linux-gnu", mode), line("aarch64-linux-gnu", mode));
}
}
}