use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::OnceLock;
fn in_path(name: &str) -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
std::env::split_paths(&path)
.map(|dir| dir.join(name))
.find(|candidate| candidate.is_file())
}
fn required(var: &str) -> bool {
std::env::var_os(var).is_some_and(|v| !v.is_empty() && v != "0")
}
fn run(dir: &Path, program: &Path, args: &[&str]) -> Output {
Command::new(program)
.args(args)
.current_dir(dir)
.env("LC_ALL", "C")
.output()
.unwrap_or_else(|error| panic!("cannot run {}: {error}", program.display()))
}
struct Fixtures {
ld: PathBuf,
dir: PathBuf,
}
fn fixtures() -> Result<&'static Fixtures, String> {
static ONCE: OnceLock<Result<Fixtures, String>> = OnceLock::new();
ONCE.get_or_init(build).as_ref().map_err(String::clone)
}
fn build() -> Result<Fixtures, String> {
if !cfg!(all(target_os = "linux", target_arch = "x86_64")) {
return Err("host is not x86-64 Linux".into());
}
let cc = in_path("gcc").ok_or("no gcc")?;
let ar = in_path("ar").ok_or("no ar")?;
let ld = in_path("ld").ok_or("no ld")?;
let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join("multilib");
let _ = fs::remove_dir_all(&dir);
for sub in ["lib32", "lib64"] {
fs::create_dir_all(dir.join(sub)).map_err(|e| e.to_string())?;
}
fs::write(dir.join("helper.c"), b"int helper(void) { return 7; }\n")
.map_err(|e| e.to_string())?;
fs::write(
dir.join("main.c"),
b"extern int helper(void); int main(void) { return helper() - 7; }\n",
)
.map_err(|e| e.to_string())?;
let compile = |args: &[&str]| -> Result<(), String> {
let output = run(&dir, &cc, args);
if output.status.success() {
Ok(())
} else {
Err(format!(
"gcc {} failed: {}",
args.join(" "),
String::from_utf8_lossy(&output.stderr)
))
}
};
compile(&["-c", "helper.c", "-o", "helper64.o"])?;
compile(&["-c", "main.c", "-o", "u.o"])?;
compile(&["-m32", "-c", "helper.c", "-fPIC", "-o", "helper32.o"])?;
compile(&["-m32", "-c", "main.c", "-o", "u32.o"])?;
compile(&["-m32", "-shared", "-o", "lib32/libpick.so", "helper32.o"])?;
for (archive, object) in [
("lib32/libpick.a", "helper32.o"),
("lib64/libpick.a", "helper64.o"),
] {
let output = run(&dir, &ar, &["rcs", archive, object]);
if !output.status.success() {
return Err(format!(
"ar rcs {archive} failed: {}",
String::from_utf8_lossy(&output.stderr)
));
}
}
Ok(Fixtures { ld, dir })
}
macro_rules! fixtures {
() => {
match fixtures() {
Ok(fixtures) => fixtures,
Err(why) => {
assert!(
!required("QLD_REQUIRE_I386_TOOLS"),
"QLD_REQUIRE_I386_TOOLS is set but the fixtures could not be built: {why}"
);
println!("SKIPPED: {why}");
return;
}
}
};
}
impl Fixtures {
fn qld(&self, args: &[&str]) -> Output {
run(&self.dir, Path::new(env!("CARGO_BIN_EXE_qld")), args)
}
fn gnu(&self, args: &[&str]) -> Output {
run(&self.dir, &self.ld, args)
}
}
fn skipped(output: &Output) -> Vec<String> {
let text = String::from_utf8_lossy(&output.stderr).into_owned();
let mut lines = Vec::new();
for line in text.lines() {
let Some(at) = line.find("skipping incompatible ") else {
continue;
};
if line.contains("cannot find") {
break;
}
let message = line.get(at..).unwrap_or_default().to_string();
if lines.contains(&message) {
continue;
}
lines.push(message);
}
lines
}
fn stderr(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).into_owned()
}
#[test]
fn every_candidate_incompatible() {
let fixtures = fixtures!();
let args = [
"-m",
"elf_x86_64",
"-e",
"main",
"-o",
"out",
"u.o",
"-Llib32",
"-lpick",
];
let ours = fixtures.qld(&args);
let theirs = fixtures.gnu(&args);
assert_eq!(
skipped(&ours),
vec![
"skipping incompatible lib32/libpick.so when searching for -lpick".to_string(),
"skipping incompatible lib32/libpick.a when searching for -lpick".to_string(),
],
"qld said: {}",
stderr(&ours)
);
assert_eq!(skipped(&ours), skipped(&theirs));
assert!(!ours.status.success(), "qld should have failed");
assert!(!theirs.status.success(), "GNU ld should have failed");
assert!(
stderr(&ours).contains("cannot find -lpick"),
"qld said: {}",
stderr(&ours)
);
}
#[test]
fn search_continues_into_the_next_directory() {
let fixtures = fixtures!();
let args = [
"-m",
"elf_x86_64",
"-e",
"main",
"-o",
"out",
"u.o",
"-Llib32",
"-Llib64",
"-lpick",
];
let ours = fixtures.qld(&args);
let theirs = fixtures.gnu(&args);
assert!(ours.status.success(), "qld said: {}", stderr(&ours));
assert!(theirs.status.success(), "GNU ld said: {}", stderr(&theirs));
assert_eq!(skipped(&ours), skipped(&theirs));
assert_eq!(skipped(&ours).len(), 2);
}
#[test]
fn static_only_skips_only_the_archive() {
let fixtures = fixtures!();
let args = [
"-m",
"elf_x86_64",
"-e",
"main",
"-o",
"out",
"u.o",
"-Bstatic",
"-Llib32",
"-Llib64",
"-lpick",
];
let ours = fixtures.qld(&args);
let theirs = fixtures.gnu(&args);
assert!(ours.status.success(), "qld said: {}", stderr(&ours));
assert_eq!(
skipped(&ours),
vec!["skipping incompatible lib32/libpick.a when searching for -lpick".to_string()]
);
assert_eq!(skipped(&ours), skipped(&theirs));
}
#[test]
fn exact_library_names_are_skipped_too() {
let fixtures = fixtures!();
let args = [
"-m",
"elf_x86_64",
"-e",
"main",
"-o",
"out",
"u.o",
"-Llib32",
"-Llib64",
"-l:libpick.a",
];
let ours = fixtures.qld(&args);
let theirs = fixtures.gnu(&args);
assert!(ours.status.success(), "qld said: {}", stderr(&ours));
assert_eq!(
skipped(&ours),
vec!["skipping incompatible lib32/libpick.a when searching for -l:libpick.a".to_string()]
);
assert_eq!(skipped(&ours), skipped(&theirs));
}
#[test]
fn script_group_skips_incompatible_libraries() {
let fixtures = fixtures!();
fs::write(fixtures.dir.join("group.ld"), b"GROUP ( -lpick )\n").unwrap();
let args = [
"-m",
"elf_x86_64",
"-e",
"main",
"-o",
"out",
"u.o",
"-Llib32",
"-Llib64",
"group.ld",
];
let ours = fixtures.qld(&args);
let theirs = fixtures.gnu(&args);
assert!(ours.status.success(), "qld said: {}", stderr(&ours));
assert_eq!(skipped(&ours), skipped(&theirs));
assert_eq!(skipped(&ours).len(), 2);
}
#[test]
fn a_32_bit_link_skips_a_64_bit_library() {
let fixtures = fixtures!();
let args = [
"-m", "elf_i386", "-e", "main", "-o", "out32", "u32.o", "-Llib64", "-Llib32", "-lpick",
];
let ours = fixtures.qld(&args);
let theirs = fixtures.gnu(&args);
assert!(ours.status.success(), "qld said: {}", stderr(&ours));
assert_eq!(
skipped(&ours),
vec!["skipping incompatible lib64/libpick.a when searching for -lpick".to_string()]
);
assert_eq!(skipped(&ours), skipped(&theirs));
}
#[test]
fn a_named_file_is_an_error_about_the_architecture() {
let fixtures = fixtures!();
for name in ["lib32/libpick.so", "lib32/libpick.a"] {
let ours = fixtures.qld(&["-m", "elf_x86_64", "-e", "main", "-o", "out", "u.o", name]);
let text = stderr(&ours);
assert!(!ours.status.success(), "qld linked {name}: {text}");
assert!(
text.contains("architecture of input file is incompatible with"),
"qld said, for {name}: {text}"
);
assert!(
!text.contains("malformed"),
"qld called {name} malformed: {text}"
);
assert!(
skipped(&ours).is_empty(),
"qld skipped a named file: {text}"
);
}
}