use std::path::{Path, PathBuf};
use std::process::Command;
use rucc_target::{Env, Os, Triple};
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Machine {
pub host: Option<Triple>,
pub sysroot: Option<PathBuf>,
pub sdk: Option<PathBuf>,
pub include: Option<String>,
}
#[must_use]
pub fn candidates(target: Triple, machine: &Machine) -> Vec<PathBuf> {
if machine.sysroot.is_none() && machine.host.is_some_and(|host| host.os != target.os) {
return Vec::new();
}
let root = machine.sysroot.as_deref();
match target.os {
Os::Linux => linux(target, root),
Os::Darwin => darwin(machine.sdk.as_deref().or(root)),
Os::Windows => windows(machine.include.as_deref()),
Os::None => Vec::new(),
}
}
fn linux(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());
["/usr/local/include".into(), format!("/usr/include/{multiarch}"), "/usr/include".into()]
.into_iter()
.map(|dir| under(sysroot, &dir))
.collect()
}
fn darwin(sdk: Option<&Path>) -> Vec<PathBuf> {
sdk.map(|sdk| vec![sdk.join("usr/include")]).unwrap_or_default()
}
fn windows(include: Option<&str>) -> Vec<PathBuf> {
include
.unwrap_or_default()
.split(';')
.map(str::trim)
.filter(|dir| !dir.is_empty())
.map(PathBuf::from)
.collect()
}
fn under(sysroot: Option<&Path>, dir: &str) -> PathBuf {
match sysroot {
Some(root) => root.join(dir.strip_prefix('/').unwrap_or(dir)),
None => PathBuf::from(dir),
}
}
#[must_use]
pub fn system_dirs(target: Triple, sysroot: Option<&Path>) -> Vec<PathBuf> {
let machine = Machine {
host: Triple::host(),
sysroot: sysroot.map(Path::to_path_buf),
sdk: if target.os == Os::Darwin { sdk(sysroot) } else { None },
include: if target.os == Os::Windows { std::env::var("INCLUDE").ok() } else { None },
};
candidates(target, &machine).into_iter().filter(|dir| dir.is_dir()).collect()
}
fn sdk(sysroot: Option<&Path>) -> Option<PathBuf> {
if let Some(root) = sysroot {
return Some(root.to_path_buf());
}
if let Some(root) = std::env::var_os("SDKROOT") {
let root = PathBuf::from(root);
if root.is_dir() {
return Some(root);
}
}
if let Some(root) = xcrun() {
return Some(root);
}
let tools = PathBuf::from("/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk");
tools.is_dir().then_some(tools)
}
fn xcrun() -> Option<PathBuf> {
let out = Command::new("/usr/bin/xcrun").args(["--show-sdk-path"]).output().ok()?;
if !out.status.success() {
return None;
}
let path = PathBuf::from(String::from_utf8(out.stdout).ok()?.trim());
(path.is_absolute() && path.is_dir()).then_some(path)
}
#[cfg(test)]
mod tests {
use super::*;
use rucc_target::Arch;
fn triple(os: Os, env: Env) -> Triple {
Triple::new(Arch::X86_64, os, env)
}
fn on(host: Os) -> Machine {
Machine { host: Some(triple(host, Env::Gnu)), ..Machine::default() }
}
#[test]
fn the_local_directory_comes_before_the_distributions_and_the_specific_before_the_general() {
let dirs = candidates(triple(Os::Linux, Env::Gnu), &on(Os::Linux));
let dirs: Vec<String> = dirs.iter().map(|d| d.display().to_string()).collect();
assert_eq!(dirs, ["/usr/local/include", "/usr/include/x86_64-linux-gnu", "/usr/include"]);
}
#[test]
fn the_directory_headers_are_kept_apart_in_is_named_after_the_targets_own_library() {
let of = |env| candidates(triple(Os::Linux, env), &on(Os::Linux))[1].display().to_string();
assert_eq!(of(Env::Musl), "/usr/include/x86_64-linux-musl");
assert_eq!(of(Env::Gnu), "/usr/include/x86_64-linux-gnu");
assert_eq!(of(Env::None), "/usr/include/x86_64-linux-gnu");
}
#[test]
fn a_sysroot_is_in_front_of_every_one_of_them_rather_than_replacing_the_root() {
let machine = Machine { sysroot: Some("/opt/cross".into()), ..on(Os::Linux) };
let dirs = candidates(triple(Os::Linux, Env::Gnu), &machine);
let under = |dir| PathBuf::from("/opt/cross").join(dir);
assert_eq!(
dirs,
[
under("usr/local/include"),
under("usr/include/x86_64-linux-gnu"),
under("usr/include")
]
);
}
#[test]
fn this_machines_headers_are_not_offered_to_a_program_being_built_for_another_system() {
assert!(candidates(triple(Os::Windows, Env::Msvc), &on(Os::Linux)).is_empty());
assert!(candidates(triple(Os::Linux, Env::Gnu), &on(Os::Darwin)).is_empty());
let machine = Machine { sysroot: Some("/opt/cross".into()), ..on(Os::Darwin) };
assert!(!candidates(triple(Os::Linux, Env::Gnu), &machine).is_empty());
}
#[test]
fn an_unknown_host_offers_the_targets_own_directories_rather_than_none() {
let machine = Machine { host: None, ..Machine::default() };
assert_eq!(candidates(triple(Os::Linux, Env::Gnu), &machine).len(), 3);
}
#[test]
fn an_apple_target_is_the_sdk_and_nothing_else_and_nothing_without_one() {
let machine = Machine { sdk: Some("/S.sdk".into()), ..on(Os::Darwin) };
let dirs = candidates(triple(Os::Darwin, Env::None), &machine);
assert_eq!(dirs, [PathBuf::from("/S.sdk/usr/include")]);
assert!(candidates(triple(Os::Darwin, Env::None), &on(Os::Darwin)).is_empty());
}
#[test]
fn windows_is_told_where_its_headers_are_and_is_not_guessed_at() {
let machine =
Machine { include: Some(r"C:\vc\include;C:\sdk\ucrt ;".to_owned()), ..on(Os::Windows) };
let dirs = candidates(triple(Os::Windows, Env::Msvc), &machine);
assert_eq!(dirs, [PathBuf::from(r"C:\vc\include"), PathBuf::from(r"C:\sdk\ucrt")]);
assert!(candidates(triple(Os::Windows, Env::Msvc), &on(Os::Windows)).is_empty());
}
#[test]
fn a_freestanding_target_has_no_library_to_find_the_headers_of() {
let machine = Machine { sysroot: Some("/opt/cross".into()), ..Machine::default() };
assert!(candidates(triple(Os::None, Env::None), &machine).is_empty());
}
#[test]
fn what_is_offered_on_this_machine_is_there_because_it_was_checked_for() {
for dir in system_dirs(Triple::host().unwrap_or(triple(Os::Linux, Env::Gnu)), None) {
assert!(dir.is_dir(), "{}", dir.display());
}
}
}