use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::OnceLock;
use rucc_sysroot::{Kernel, Options, Sysroot, Wall, include_paths};
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.sdk.is_none()
&& machine.include.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(root, 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(sysroot: Option<&Path>, include: Option<&str>) -> Vec<PathBuf> {
if let Some(root) = sysroot {
return ["crt/include", "sdk/include/ucrt", "sdk/include/shared", "sdk/include/um"]
.into_iter()
.chain(["sdk/include/winrt", "sdk/include/cppwinrt"])
.map(|dir| root.join(dir))
.collect();
}
include
.unwrap_or_default()
.split(';')
.map(str::trim)
.filter(|dir| !dir.is_empty())
.map(PathBuf::from)
.collect()
}
fn msvc_dirs(vc: &Path, kit: &Path) -> Vec<PathBuf> {
let mut dirs = vec![vc.join("include")];
for dir in ["ucrt", "shared", "um", "winrt", "cppwinrt"] {
dirs.push(kit.join(dir));
}
dirs
}
fn version_key(name: &str) -> Option<Vec<u64>> {
let parts: Vec<u64> = name.split('.').map(|part| part.parse().ok()).collect::<Option<_>>()?;
(!parts.is_empty()).then_some(parts)
}
fn newest(dir: &Path) -> Option<PathBuf> {
let mut best: Option<(Vec<u64>, PathBuf)> = None;
for entry in std::fs::read_dir(dir).ok()?.flatten() {
let name = entry.file_name();
let Some(key) = name.to_str().and_then(version_key) else { continue };
if !entry.path().is_dir() {
continue;
}
if best.as_ref().is_none_or(|(found, _)| key > *found) {
best = Some((key, entry.path()));
}
}
best.map(|(_, path)| path)
}
fn installed_msvc() -> Option<String> {
static ANSWER: OnceLock<Option<String>> = OnceLock::new();
ANSWER
.get_or_init(|| {
let vc = newest(&visual_studio()?.join("VC").join("Tools").join("MSVC"))?;
let kit = newest(&windows_kit()?.join("Include"))?;
let dirs: Vec<String> =
msvc_dirs(&vc, &kit).iter().map(|dir| dir.display().to_string()).collect();
Some(dirs.join(";"))
})
.clone()
}
fn visual_studio() -> Option<PathBuf> {
let program_files = std::env::var_os("ProgramFiles(x86)")?;
let vswhere = PathBuf::from(program_files)
.join("Microsoft Visual Studio")
.join("Installer")
.join("vswhere.exe");
if !vswhere.is_file() {
return None;
}
let out = Command::new(vswhere)
.args(["-latest", "-products", "*", "-property", "installationPath"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let path = PathBuf::from(String::from_utf8(out.stdout).ok()?.lines().next()?.trim());
path.is_dir().then_some(path)
}
fn windows_kit() -> Option<PathBuf> {
const KEY: &str = r"HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots";
if let Ok(out) = Command::new("reg.exe").args(["query", KEY, "/v", "KitsRoot10"]).output() {
if let Ok(text) = String::from_utf8(out.stdout) {
for line in text.lines() {
let Some((name, value)) = line.trim().split_once("REG_SZ") else { continue };
if name.trim() != "KitsRoot10" {
continue;
}
let root = PathBuf::from(value.trim());
if root.is_dir() {
return Some(root);
}
}
}
}
let default = PathBuf::from(std::env::var_os("ProgramFiles(x86)")?).join("Windows Kits/10");
default.is_dir().then_some(default)
}
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 && target.env == Env::Msvc {
msvc(sysroot)
} else {
None
},
};
candidates(target, &machine).into_iter().filter(|dir| dir.is_dir()).collect()
}
#[must_use]
pub fn header_dirs(
target: Triple,
sysroot: Option<&Path>,
bundled: Option<&Sysroot>,
kernel: Option<&Kernel>,
) -> Vec<PathBuf> {
let dirs = system_dirs(target, sysroot);
let walled = Wall::of(target.tuple()).is_some();
let (named, sdk, host) = match (sysroot.is_some(), walled) {
(true, _) => (dirs, Vec::new(), Vec::new()),
(false, true) => (Vec::new(), dirs, Vec::new()),
(false, false) => (Vec::new(), Vec::new(), dirs),
};
let options = Options {
sysroot: &named,
sdk: &sdk,
bundled,
kernel,
host_include: &host,
..Options::default()
};
include_paths(target.tuple(), Triple::host().map(Triple::tuple), &options)
.into_iter()
.map(|entry| entry.path)
.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 msvc(sysroot: Option<&Path>) -> Option<String> {
if sysroot.is_some() {
return None;
}
match std::env::var("INCLUDE") {
Ok(include) if !include.trim().is_empty() => Some(include),
_ => installed_msvc(),
}
}
fn xcrun() -> Option<PathBuf> {
static ANSWER: OnceLock<Option<PathBuf>> = OnceLock::new();
ANSWER.get_or_init(ask_xcrun).clone()
}
fn ask_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 an_sdk_reaches_an_apple_target_from_a_host_that_is_not_a_mac() {
let machine = Machine { sdk: Some("/S.sdk".into()), ..on(Os::Linux) };
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::Linux)).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 an_sdk_reaches_an_msvc_target_from_a_host_that_is_not_windows() {
let machine = Machine { include: Some(r"C:\sdk\um".to_owned()), ..on(Os::Linux) };
let dirs = candidates(triple(Os::Windows, Env::Msvc), &machine);
assert_eq!(dirs, [PathBuf::from(r"C:\sdk\um")]);
}
#[test]
fn a_named_tree_for_an_msvc_target_is_the_layout_a_relocatable_one_has() {
let machine = Machine { sysroot: Some("/opt/xwin".into()), ..on(Os::Linux) };
let dirs = candidates(triple(Os::Windows, Env::Msvc), &machine);
let under = |dir| PathBuf::from("/opt/xwin").join(dir);
assert_eq!(
dirs,
[
under("crt/include"),
under("sdk/include/ucrt"),
under("sdk/include/shared"),
under("sdk/include/um"),
under("sdk/include/winrt"),
under("sdk/include/cppwinrt"),
]
);
}
#[test]
fn the_mingw_target_is_not_offered_microsofts_headers() {
let machine = Machine { include: Some(r"C:\sdk\um".to_owned()), ..on(Os::Windows) };
assert!(system_dirs(triple(Os::Windows, Env::Gnu), None).is_empty());
assert!(!candidates(triple(Os::Windows, Env::Gnu), &machine).is_empty());
}
#[test]
fn the_headers_of_an_installation_are_in_the_order_the_developer_prompt_puts_them() {
let vc = PathBuf::from(r"C:\BuildTools\VC\Tools\MSVC\14.44.35207");
let dirs = msvc_dirs(&vc, Path::new("/k"));
assert_eq!(dirs[0], vc.join("include"));
let rest: Vec<String> =
dirs[1..].iter().map(|dir| dir.file_name().unwrap().to_string_lossy().into()).collect();
assert_eq!(rest, ["ucrt", "shared", "um", "winrt", "cppwinrt"]);
}
#[test]
fn a_version_directory_is_compared_as_numbers_and_not_as_text() {
assert!(version_key("10.0.9.0") < version_key("10.0.22621.0"));
assert!(version_key("10.0.22621.0") < version_key("10.0.26100.0"));
assert!(version_key("14.44.35207") > version_key("14.39.33519"));
assert_eq!(version_key("Catalogs"), None);
assert_eq!(version_key("wdf"), None);
assert_eq!(version_key(""), None);
}
#[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());
}
}
}