use crate::session;
use eyre::{Context, Result, bail};
use mbx_cache_core::{AgentRequest, AgentResponse, CacheDigest, PinnedFile, canonical_json};
use mbx_cache_rustc::LinkerIdentity;
use std::collections::BTreeMap;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::Command;
const IDENTITY_ENVIRONMENT: &[&str] = &[
"COMPILER_PATH",
"GCC_EXEC_PREFIX",
"LIBRARY_PATH",
"PATH",
"SDKROOT",
"MACOSX_DEPLOYMENT_TARGET",
"LIB",
"UCRTVersion",
"UniversalCRTSdkDir",
"VCToolsInstallDir",
"VCToolsVersion",
"WindowsSdkDir",
"WindowsSDKVersion",
];
#[derive(Clone, Copy)]
struct FileProbes {
startup: &'static [&'static str],
libc: &'static [&'static str],
rest: &'static [&'static str],
}
const GNU_PROBES: FileProbes = FileProbes {
startup: &["Scrt1.o", "crt1.o"],
libc: &["libc.so.6", "libc.so", "libc.a", "libc.musl-x86_64.so.1"],
rest: &[
"crti.o",
"crtn.o",
"crtbegin.o",
"crtbeginS.o",
"crtbeginT.o",
"crtend.o",
"crtendS.o",
],
};
const NO_PROBES: FileProbes = FileProbes {
startup: &[],
libc: &[],
rest: &[],
};
fn file_probes() -> FileProbes {
if cfg!(target_os = "linux") {
GNU_PROBES
} else {
NO_PROBES
}
}
pub(crate) fn identity_for(
override_linker: Option<&Path>,
fuse_ld: Option<&str>,
) -> Result<LinkerIdentity> {
if fuse_ld.is_some() && cfg!(windows) {
bail!("a `-fuse-ld` selection with a Windows linker is not one mbx can identify");
}
let driver = if let Some(linker) = override_linker {
let name = linker
.file_name()
.and_then(|name| name.to_str())
.unwrap_or_default()
.to_ascii_lowercase();
let recognized = if cfg!(windows) {
matches!(
name.as_str(),
"link" | "link.exe" | "lld-link" | "lld-link.exe"
)
} else {
matches!(name.as_str(), "clang" | "clang++")
};
if !recognized {
bail!("the selected linker is not one mbx can identify");
}
which::which(linker)
.wrap_err_with(|| format!("failed to find linker `{}`", linker.display()))?
} else if cfg!(windows) {
msvc_tool("link.exe").wrap_err("failed to find the linker `link.exe`")?
} else {
which::which("cc").wrap_err("failed to find the linker driver `cc`")?
};
let mut environment = IDENTITY_ENVIRONMENT
.iter()
.map(|name| ((*name).into(), std::env::var(name).ok()))
.collect::<BTreeMap<_, _>>();
let search_dirs_once = std::cell::OnceCell::new();
let search = || {
search_dirs_once
.get_or_init(|| {
if cfg!(windows) {
None
} else {
search_dirs(&driver)
}
})
.as_ref()
};
let fuse_ld = fuse_ld
.map(|selection| resolve_fuse_ld(&driver, selection, search()))
.transpose()?;
if let Some(located) = &fuse_ld {
environment.insert(
"MBX_FUSE_LD".into(),
Some(located.path.display().to_string()),
);
}
if let Some(cached) = find_recorded(&driver, &environment)? {
return Ok(cached);
}
let (identity, pins) = probe(&driver, fuse_ld.as_ref(), search())?;
record(&driver, &environment, &identity, pins)?;
Ok(identity)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct SearchDirs {
programs: Vec<PathBuf>,
libraries: Vec<PathBuf>,
}
fn search_dirs(driver: &Path) -> Option<SearchDirs> {
let output = Command::new(driver)
.arg("-print-search-dirs")
.env("LC_ALL", "C")
.env("LANGUAGE", "C")
.output()
.ok()?;
if !output.status.success() {
return None;
}
parse_search_dirs(&String::from_utf8_lossy(&output.stdout))
}
fn parse_search_dirs(text: &str) -> Option<SearchDirs> {
let list = |field: &str| {
text.lines()
.find_map(|line| line.strip_prefix(field))
.map(|value| value.trim().trim_start_matches('='))
.map(|value| {
std::env::split_paths(value)
.filter(|directory| !directory.as_os_str().is_empty())
.collect::<Vec<_>>()
})
.filter(|directories| !directories.is_empty())
};
Some(SearchDirs {
programs: list("programs:")?,
libraries: list("libraries:")?,
})
}
fn same_file(candidate: &Path, found: &Path) -> bool {
candidate == found
|| matches!(
(std::fs::canonicalize(candidate), std::fs::canonicalize(found)),
(Ok(left), Ok(right)) if left == right
)
}
fn pin_search(pins: &mut Pins, name: &OsStr, found: &Path, directories: &[PathBuf]) {
for directory in directories {
let candidate = directory.join(name);
pins.add(&candidate);
if same_file(&candidate, found) {
return;
}
}
pins.add(found);
}
#[derive(Debug)]
struct Located {
path: PathBuf,
pins: Pins,
}
impl Located {
fn named(path: PathBuf) -> Self {
let mut pins = Pins::default();
pins.add(&path);
Self { path, pins }
}
fn program(name: &OsStr, printed: &Path, search: Option<&SearchDirs>) -> Result<Self> {
let programs = search.map(|search| search.programs.as_slice());
if printed.is_absolute() {
let mut pins = Pins::default();
match programs {
Some(programs) => pin_search(&mut pins, name, printed, programs),
None => pins.0 = None,
}
return Ok(Self {
path: printed.to_path_buf(),
pins,
});
}
let mut pins = Pins::default();
match programs {
Some(programs) => {
for directory in programs {
pins.add(&directory.join(name));
}
}
None => pins.0 = None,
}
let searched = Located::searched(name)?;
pins.extend(searched.pins);
Ok(Self {
path: searched.path,
pins,
})
}
fn searched(name: &OsStr) -> Result<Self> {
let path = which::which(name)
.wrap_err_with(|| format!("failed to find `{}`", name.to_string_lossy()))?;
let mut pins = Pins::default();
if let Some(search) = std::env::var_os("PATH") {
for directory in std::env::split_paths(&search) {
if Some(directory.as_path()) == path.parent() {
break;
}
pins.add(&directory.join(name));
}
}
pins.add(&path);
Ok(Self { path, pins })
}
}
#[derive(Debug, Clone)]
struct Pins(Option<Vec<PinnedFile>>);
impl Default for Pins {
fn default() -> Self {
Self(Some(Vec::new()))
}
}
impl Pins {
fn add(&mut self, path: &Path) {
match (&mut self.0, PinnedFile::describe(path)) {
(Some(pins), Some(pin)) => pins.push(pin),
_ => self.0 = None,
}
}
fn extend(&mut self, more: Pins) {
match (&mut self.0, more.0) {
(Some(pins), Some(more)) => pins.extend(more),
_ => self.0 = None,
}
}
fn into_vec(self) -> Vec<PinnedFile> {
self.0.unwrap_or_default()
}
}
fn find_recorded(
driver: &Path,
environment: &BTreeMap<String, Option<String>>,
) -> Result<Option<LinkerIdentity>> {
let responses = session::request_agent(&[AgentRequest::FindExecutableIdentity {
executable: driver.to_path_buf(),
environment: environment.clone(),
}])?;
let Some(AgentResponse::ExecutableIdentity { stdout }) = responses.into_iter().next() else {
bail!("cache agent did not return the linker identity");
};
stdout
.map(|stdout| serde_json::from_slice(&stdout).wrap_err("invalid recorded linker identity"))
.transpose()
}
fn record(
driver: &Path,
environment: &BTreeMap<String, Option<String>>,
identity: &LinkerIdentity,
pins: Vec<PinnedFile>,
) -> Result<()> {
let responses = session::request_agent(&[AgentRequest::StoreExecutableIdentity {
executable: driver.to_path_buf(),
environment: environment.clone(),
stdout: canonical_json(identity)?,
pins,
}])?;
match responses.into_iter().next() {
Some(AgentResponse::ExecutableIdentity { .. }) => Ok(()),
Some(AgentResponse::Error { message }) => bail!(message),
_ => bail!("cache agent returned an unexpected linker identity response"),
}
}
fn probe(
driver: &Path,
fuse_ld: Option<&Located>,
search: Option<&SearchDirs>,
) -> Result<(LinkerIdentity, Vec<PinnedFile>)> {
if cfg!(windows) {
return Ok((probe_windows(driver)?, Vec::new()));
}
let mut pins = Pins::default();
pins.add(driver);
let (linker_version, linker) = linker_version(driver, fuse_ld, search)?;
pins.extend(linker);
let (crt_objects, objects) = crt_objects(driver, search)?;
pins.extend(objects);
let sdk = sdk_identity()?;
let pins = if sdk.is_some() {
Vec::new()
} else {
pins.into_vec()
};
let identity = LinkerIdentity {
driver: driver
.to_str()
.ok_or_else(|| eyre::eyre!("the linker driver path is not valid UTF-8"))?
.to_owned(),
driver_version: run(driver, &["--version"])?,
linker_version,
crt_objects,
sdk,
deployment_target: std::env::var("MACOSX_DEPLOYMENT_TARGET").ok(),
};
Ok((identity, pins))
}
fn probe_windows(linker: &Path) -> Result<LinkerIdentity> {
let is_lld = linker
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.to_ascii_lowercase().starts_with("lld-link"));
let linker_report = run_allowing_status(linker, if is_lld { &["--version"] } else { &["/?"] })?;
if !is_lld && !linker_report.contains("Microsoft (R) Incremental Linker") {
bail!(
"{} is not the MSVC linker, so the link cannot be identified",
linker.display()
);
}
let linker_version = linker_report
.lines()
.find(|line| !line.trim().is_empty())
.unwrap_or_default()
.trim()
.to_owned();
let cl =
msvc_tool("cl.exe").wrap_err("failed to find `cl.exe` for the MSVC toolchain identity")?;
let compiler_version = run_allowing_status(&cl, &["/Bv"])?;
let crt_objects = windows_crt_objects()?;
let sdk = windows_sdk_identity()?;
Ok(LinkerIdentity {
driver: linker
.to_str()
.ok_or_else(|| eyre::eyre!("the linker path is not valid UTF-8"))?
.to_owned(),
driver_version: compiler_version,
linker_version,
crt_objects,
sdk: Some(sdk),
deployment_target: None,
})
}
fn msvc_tool(name: &str) -> Result<PathBuf> {
let tools = std::env::var_os("VCToolsInstallDir")
.map(PathBuf::from)
.or_else(visual_studio_tools_dir);
if let Some(root) = tools {
let host = std::env::var("VSCMD_ARG_HOST_ARCH")
.ok()
.filter(|value| !value.is_empty())
.unwrap_or_else(native_msvc_arch);
let target = selected_msvc_arch();
let candidate = root
.join("bin")
.join(format!("Host{host}"))
.join(target)
.join(name);
if candidate.is_file() {
return Ok(candidate);
}
}
Ok(which::which(name)?)
}
fn selected_msvc_arch() -> String {
std::env::var("VSCMD_ARG_TGT_ARCH")
.ok()
.filter(|value| !value.is_empty())
.unwrap_or_else(native_msvc_arch)
}
fn visual_studio_tools_dir() -> Option<PathBuf> {
if !cfg!(windows) {
return None;
}
let program_files =
std::env::var_os("ProgramFiles(x86)").or_else(|| std::env::var_os("ProgramFiles"))?;
let vswhere = PathBuf::from(program_files)
.join("Microsoft Visual Studio")
.join("Installer")
.join("vswhere.exe");
let output = Command::new(vswhere)
.args([
"-latest",
"-products",
"*",
"-requires",
"Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
"-property",
"installationPath",
])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let installation = String::from_utf8(output.stdout).ok()?;
let installation = PathBuf::from(installation.trim());
let version = std::fs::read_to_string(
installation.join("VC/Auxiliary/Build/Microsoft.VCToolsVersion.default.txt"),
)
.ok()?;
Some(installation.join("VC/Tools/MSVC").join(version.trim()))
}
fn native_msvc_arch() -> String {
match std::env::consts::ARCH {
"x86_64" => "x64",
"x86" => "x86",
"aarch64" => "arm64",
other => other,
}
.to_owned()
}
fn run_allowing_status(program: &Path, arguments: &[&str]) -> Result<String> {
let output = Command::new(program)
.args(arguments)
.output()
.wrap_err_with(|| format!("failed to run {}", program.display()))?;
let mut text = String::from_utf8_lossy(&output.stdout).into_owned();
text.push_str(&String::from_utf8_lossy(&output.stderr));
let reported = text.trim();
if reported.is_empty() {
bail!("{} {arguments:?} reported nothing", program.display());
}
Ok(reported.to_owned())
}
fn windows_sdk_identity() -> Result<String> {
let mut values = ["VCToolsVersion", "WindowsSDKVersion", "UCRTVersion"]
.into_iter()
.filter_map(|name| {
std::env::var(name)
.ok()
.filter(|value| !value.is_empty())
.map(|value| format!("{name}={value}"))
})
.collect::<Vec<_>>();
if !values
.iter()
.any(|value| value.starts_with("VCToolsVersion="))
&& let Some(version) = std::env::var_os("VCToolsInstallDir")
.map(PathBuf::from)
.or_else(visual_studio_tools_dir)
.and_then(|path| path.file_name().map(|name| name.to_owned()))
{
values.push(format!("VCToolsVersion={}", version.to_string_lossy()));
}
if !values
.iter()
.any(|value| value.starts_with("WindowsSDKVersion="))
&& let Some((_, version)) = windows_sdk_root_and_version()
{
values.push(format!("WindowsSDKVersion={version}"));
}
if values.len() < 2 {
bail!("the MSVC toolset and Windows SDK versions could not be identified");
}
Ok(values.join("; "))
}
#[cfg(test)]
fn windows_sdk_identity_for(lookup: impl Fn(&str) -> Option<String>) -> Result<String> {
let values = ["VCToolsVersion", "WindowsSDKVersion", "UCRTVersion"]
.into_iter()
.filter_map(|name| {
lookup(name)
.filter(|value| !value.is_empty())
.map(|value| format!("{name}={value}"))
})
.collect::<Vec<_>>();
if values.len() < 2 {
bail!("the MSVC toolset and Windows SDK versions could not be identified");
}
Ok(values.join("; "))
}
fn windows_crt_objects() -> Result<BTreeMap<String, CacheDigest>> {
let mut directories = std::env::var_os("LIB")
.map(|value| std::env::split_paths(&value).collect::<Vec<_>>())
.unwrap_or_default();
let arch = selected_msvc_arch();
if let Some(tools) = std::env::var_os("VCToolsInstallDir")
.map(PathBuf::from)
.or_else(visual_studio_tools_dir)
{
directories.push(tools.join("lib").join(&arch));
}
if let Some((sdk, version)) = windows_sdk_root_and_version() {
directories.push(sdk.join("Lib").join(version).join("ucrt").join(&arch));
}
windows_crt_objects_in(&directories)
}
fn windows_sdk_root_and_version() -> Option<(PathBuf, String)> {
let root = std::env::var_os("WindowsSdkDir")
.map(PathBuf::from)
.or_else(|| {
std::env::var_os("ProgramFiles(x86)")
.or_else(|| std::env::var_os("ProgramFiles"))
.map(PathBuf::from)
.map(|path| path.join("Windows Kits/10"))
})?;
let version = std::env::var("WindowsSDKVersion")
.ok()
.map(|version| version.trim_matches(['/', '\\']).to_owned())
.filter(|version| !version.is_empty())
.or_else(|| {
let mut versions = std::fs::read_dir(root.join("Lib"))
.ok()?
.filter_map(Result::ok)
.filter(|entry| entry.path().join("ucrt").is_dir())
.filter_map(|entry| entry.file_name().into_string().ok())
.collect::<Vec<_>>();
versions.sort();
versions.pop()
})?;
Some((root, version))
}
fn windows_crt_objects_in(directories: &[PathBuf]) -> Result<BTreeMap<String, CacheDigest>> {
let names = [
"libcmt.lib",
"libcmtd.lib",
"msvcrt.lib",
"msvcrtd.lib",
"vcruntime.lib",
"vcruntimed.lib",
"ucrt.lib",
"ucrtd.lib",
];
let mut resolved = BTreeMap::<String, CacheDigest>::new();
for name in names {
if let Some(path) = directories
.iter()
.map(|dir| dir.join(name))
.find(|path| path.is_file())
{
let digest = CacheDigest::blake3_file(&path)
.wrap_err_with(|| format!("failed to hash CRT library {}", path.display()))?;
resolved.insert(name.into(), digest);
}
}
if !resolved.keys().any(|name| name.starts_with("vcruntime"))
|| !resolved.keys().any(|name| name.starts_with("ucrt"))
{
bail!("the MSVC and Universal CRT libraries could not both be identified");
}
Ok(resolved)
}
fn resolve_fuse_ld(driver: &Path, selection: &str, search: Option<&SearchDirs>) -> Result<Located> {
if Path::new(selection).is_absolute() {
return Ok(Located::named(PathBuf::from(selection)));
}
let name = format!("ld.{selection}");
let argument = format!("-print-prog-name={name}");
let printed = run(driver, &[argument.as_str()])
.wrap_err_with(|| format!("the linker driver named nothing for `{name}`"))?;
let candidate = PathBuf::from(&printed);
let printed = if candidate.is_absolute() && candidate.exists() {
candidate
} else {
PathBuf::from(&name)
};
Located::program(OsStr::new(&name), &printed, search).wrap_err_with(|| {
format!(
"`-fuse-ld={selection}` selects `{name}`, which neither the driver nor PATH provides"
)
})
}
fn linker_version(
driver: &Path,
fuse_ld: Option<&Located>,
search: Option<&SearchDirs>,
) -> Result<(String, Pins)> {
if let Some(located) = fuse_ld {
let program = located.path.as_path();
let output = Command::new(program)
.arg("-v")
.output()
.wrap_err_with(|| format!("failed to query the linker {}", program.display()))?;
let combined = if output.stdout.is_empty() {
output.stderr
} else {
output.stdout
};
let text = String::from_utf8_lossy(&combined);
let version = text.lines().next().unwrap_or_default().trim();
if version.is_empty() {
bail!("{} reported no version", program.display());
}
return Ok((
format!("{}: {version}", program.display()),
located.pins.clone(),
));
}
let named = PathBuf::from(
run(driver, &["-print-prog-name=ld"]).wrap_err("the linker driver named no linker")?,
);
let located = Located::program(OsStr::new("ld"), &named, search)
.wrap_err_with(|| format!("failed to find the linker `{}`", named.display()))?;
let linker = located.path.as_path();
let output = Command::new(linker)
.arg("-v")
.output()
.wrap_err_with(|| format!("failed to query the linker {}", linker.display()))?;
let combined = if output.stdout.is_empty() {
output.stderr
} else {
output.stdout
};
let text = String::from_utf8_lossy(&combined);
let version = text.lines().next().unwrap_or_default().trim();
if version.is_empty() {
bail!("{} reported no version", linker.display());
}
Ok((version.to_owned(), located.pins))
}
fn crt_objects(
driver: &Path,
search: Option<&SearchDirs>,
) -> Result<(BTreeMap<String, CacheDigest>, Pins)> {
let placed = std::cell::RefCell::new(Pins::default());
let objects = probe_files(&file_probes(), |name| {
let resolved = run(driver, &[&format!("-print-file-name={name}")]).ok()?;
let path = PathBuf::from(resolved.trim());
if !path.is_absolute() {
return None;
}
let mut placed = placed.borrow_mut();
match search {
Some(search) => pin_search(&mut placed, OsStr::new(name), &path, &search.libraries),
None => placed.0 = None,
}
Some(path)
})?;
Ok((objects, placed.into_inner()))
}
fn probe_files(
probes: &FileProbes,
place: impl Fn(&str) -> Option<PathBuf>,
) -> Result<BTreeMap<String, CacheDigest>> {
let resolved = probes
.startup
.iter()
.chain(probes.libc)
.chain(probes.rest)
.filter_map(|name| {
let digest = CacheDigest::blake3_file(&place(name)?).ok()?;
Some(((*name).to_owned(), digest))
})
.collect::<BTreeMap<_, _>>();
for (required, what) in [(probes.startup, "startup object"), (probes.libc, "libc")] {
if !required.is_empty() && !required.iter().any(|name| resolved.contains_key(*name)) {
bail!("the linker driver resolved no {what}, so its links cannot be identified");
}
}
Ok(resolved)
}
fn sdk_identity() -> Result<Option<String>> {
sdk_identity_for(std::env::var("SDKROOT").ok())
}
fn sdk_identity_for(root: Option<String>) -> Result<Option<String>> {
if !cfg!(target_os = "macos") {
return Ok(None);
}
let describe = || {
let sdk = root.as_deref().unwrap_or("macosx");
let version = xcrun(&["--sdk", sdk, "--show-sdk-version"])?;
let build = xcrun(&["--sdk", sdk, "--show-sdk-build-version"])?;
let path = match &root {
Some(root) => root.clone(),
None => xcrun(&["--sdk", sdk, "--show-sdk-path"])?,
};
Some(format!("{path} {version} ({build})"))
};
describe().map(Some).ok_or_else(|| {
eyre::eyre!("the macOS SDK could not be identified, so its links cannot be either")
})
}
fn xcrun(arguments: &[&str]) -> Option<String> {
let output = Command::new("xcrun").args(arguments).output().ok()?;
output
.status
.success()
.then(|| String::from_utf8_lossy(&output.stdout).trim().to_owned())
.filter(|value| !value.is_empty())
}
fn run(program: &Path, arguments: &[&str]) -> Result<String> {
let output = Command::new(program)
.args(arguments)
.output()
.wrap_err_with(|| format!("failed to run {}", program.display()))?;
if !output.status.success() {
bail!(
"{} {arguments:?} failed: {}",
program.display(),
String::from_utf8_lossy(&output.stderr)
);
}
let reported = String::from_utf8_lossy(&output.stdout).trim().to_owned();
if reported.is_empty() {
bail!("{} {arguments:?} reported nothing", program.display());
}
Ok(reported)
}
#[cfg(test)]
#[path = "linker_tests.rs"]
mod tests;