use std::ffi::OsStr;
use std::fmt;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::OnceLock;
pub fn skip(reason: impl fmt::Display) {
println!("SKIPPED: {reason}");
}
pub fn tools_required() -> bool {
std::env::var_os("QLD_REQUIRE_TOOLS").is_some_and(|v| !v.is_empty() && v != "0")
}
pub fn find_program(name: impl AsRef<OsStr>) -> Option<PathBuf> {
let name = Path::new(name.as_ref());
if name.as_os_str().is_empty() {
return None;
}
if name.components().count() > 1 {
return is_executable(name).then(|| name.to_path_buf());
}
let path = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&path) {
let candidate = dir.join(name);
if is_executable(&candidate) {
return Some(candidate);
}
if cfg!(windows) {
let candidate = candidate.with_extension("exe");
if is_executable(&candidate) {
return Some(candidate);
}
}
}
None
}
pub fn find_first(names: &[&str]) -> Option<PathBuf> {
names.iter().find_map(find_program)
}
pub fn find_with_override(var: &str, names: &[&str]) -> Option<PathBuf> {
match std::env::var_os(var) {
Some(value) if value.is_empty() => None,
Some(value) => find_program(value),
None => find_first(names),
}
}
#[cfg(unix)]
fn is_executable(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path).is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
}
#[cfg(not(unix))]
fn is_executable(path: &Path) -> bool {
path.is_file()
}
pub fn capture(program: &Path, args: &[&str]) -> Option<String> {
let output = Command::new(program)
.args(args)
.env("LC_ALL", "C")
.stdin(Stdio::null())
.stderr(Stdio::null())
.output()
.ok()?;
output
.status
.success()
.then(|| String::from_utf8_lossy(&output.stdout).into_owned())
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Triple {
pub arch: String,
pub os: String,
pub env: String,
}
impl Triple {
pub fn parse(text: &str) -> Option<Self> {
let parts: Vec<&str> = text.trim().split('-').filter(|p| !p.is_empty()).collect();
let arch = match *parts.first()? {
"amd64" => "x86_64",
"arm64" => "aarch64",
"i386" | "i486" | "i586" => "i686",
other => other,
}
.to_string();
let known_os = [
"linux", "darwin", "windows", "freebsd", "netbsd", "openbsd", "none",
];
let os_index = parts
.iter()
.enumerate()
.skip(1)
.find(|(_, p)| known_os.iter().any(|os| p.starts_with(os)))
.map(|(i, _)| i);
let (os, env) = match os_index {
Some(i) => {
let os = known_os
.iter()
.find(|os| parts[i].starts_with(*os))
.copied()
.unwrap_or("unknown");
(os.to_string(), parts[i + 1..].join("-"))
}
None => match parts.len() {
1 => ("unknown".to_string(), String::new()),
2 => (parts[1].to_string(), String::new()),
_ => (parts[2].to_string(), parts[3..].join("-")),
},
};
Some(Self { arch, os, env })
}
pub fn is_linux(&self) -> bool {
self.os == "linux"
}
pub fn tool_prefixes(&self) -> Vec<String> {
let tail = if self.env.is_empty() {
self.os.clone()
} else {
format!("{}-{}", self.os, self.env)
};
["", "unknown-", "pc-", "none-"]
.iter()
.map(|vendor| format!("{}-{vendor}{tail}-", self.arch))
.collect()
}
pub fn cross_tool(&self, tool: &str) -> Option<PathBuf> {
self.tool_prefixes()
.iter()
.find_map(|prefix| find_program(format!("{prefix}{tool}")))
}
pub fn qemu_arch(&self) -> &str {
match self.arch.as_str() {
"i686" => "i386",
"powerpc64le" => "ppc64le",
"powerpc64" => "ppc64",
a if a.starts_with("armv") => "arm",
a => a,
}
}
pub fn qemu(&self) -> Option<PathBuf> {
let arch = self.qemu_arch();
find_first(&[&format!("qemu-{arch}"), &format!("qemu-{arch}-static")])
}
pub fn qemu_sysroot(&self) -> Option<PathBuf> {
self.tool_prefixes()
.iter()
.map(|prefix| PathBuf::from("/usr").join(prefix.trim_end_matches('-')))
.find(|dir| dir.is_dir())
}
}
impl fmt::Display for Triple {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}-{}", self.arch, self.os)?;
if !self.env.is_empty() {
write!(f, "-{}", self.env)?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct Tools {
pub sh: Option<PathBuf>,
pub cc: Option<PathBuf>,
pub cxx: Option<PathBuf>,
pub ar: Option<PathBuf>,
pub readelf: Option<PathBuf>,
pub gnu_ld: Option<PathBuf>,
pub lld: Option<PathBuf>,
pub mold: Option<PathBuf>,
pub gold: Option<PathBuf>,
pub host: Option<Triple>,
}
pub fn tools() -> &'static Tools {
static TOOLS: OnceLock<Tools> = OnceLock::new();
TOOLS.get_or_init(Tools::discover)
}
impl Tools {
fn discover() -> Self {
let cc = find_with_override("QLD_TEST_CC", &["cc", "gcc", "clang"]);
let host = cc
.as_deref()
.and_then(|cc| capture(cc, &["-dumpmachine"]))
.and_then(|text| Triple::parse(&text));
let gnu_ld = match std::env::var_os("QLD_TEST_GNU_LD") {
Some(value) if value.is_empty() => None,
Some(value) => find_program(value),
None => find_program("ld.bfd").or_else(|| {
find_program("ld").filter(|ld| {
capture(ld, &["--version"]).is_some_and(|v| v.starts_with("GNU ld"))
})
}),
};
Self {
sh: find_first(&["sh"]),
cc,
cxx: find_with_override("QLD_TEST_CXX", &["c++", "g++", "clang++"]),
ar: find_with_override("QLD_TEST_AR", &["ar", "llvm-ar"]),
readelf: find_with_override("QLD_TEST_READELF", &["readelf", "llvm-readelf"]),
gnu_ld,
lld: find_with_override("QLD_TEST_LLD", &["ld.lld", "lld"]),
mold: find_with_override("QLD_TEST_MOLD", &["mold"]),
gold: find_with_override("QLD_TEST_GOLD", &["ld.gold", "gold"]),
host,
}
}
}
pub fn driver_is_clang(driver: &Path) -> bool {
static CACHE: OnceLock<std::sync::Mutex<Vec<(PathBuf, bool)>>> = OnceLock::new();
let cache = CACHE.get_or_init(Default::default);
if let Some(&(_, known)) = cache
.lock()
.unwrap_or_else(|e| e.into_inner())
.iter()
.find(|(path, _)| path == driver)
{
return known;
}
let is_clang = capture(driver, &["--version"]).is_some_and(|v| v.contains("clang"));
cache
.lock()
.unwrap_or_else(|e| e.into_inner())
.push((driver.to_path_buf(), is_clang));
is_clang
}