use crate::session;
use eyre::{Context, Result, bail};
use mbx_cache_core::{AgentRequest, AgentResponse, CacheDigest, canonical_json};
use mbx_cache_rustc::LinkerIdentity;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::Command;
const IDENTITY_ENVIRONMENT: &[&str] = &["SDKROOT", "MACOSX_DEPLOYMENT_TARGET"];
#[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() -> Result<LinkerIdentity> {
let driver = which::which("cc").wrap_err("failed to find the linker driver `cc`")?;
let environment = IDENTITY_ENVIRONMENT
.iter()
.map(|name| ((*name).into(), std::env::var(name).ok()))
.collect::<BTreeMap<_, _>>();
if let Some(cached) = find_recorded(&driver, &environment)? {
return Ok(cached);
}
let identity = probe(&driver)?;
record(&driver, &environment, &identity)?;
Ok(identity)
}
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,
) -> Result<()> {
let responses = session::request_agent(&[AgentRequest::StoreExecutableIdentity {
executable: driver.to_path_buf(),
environment: environment.clone(),
stdout: canonical_json(identity)?,
}])?;
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) -> Result<LinkerIdentity> {
Ok(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: linker_version(driver)?,
crt_objects: crt_objects(driver)?,
sdk: sdk_identity()?,
deployment_target: std::env::var("MACOSX_DEPLOYMENT_TARGET").ok(),
})
}
fn linker_version(driver: &Path) -> Result<String> {
let linker = PathBuf::from(
run(driver, &["-print-prog-name=ld"]).wrap_err("the linker driver named no linker")?,
);
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())
}
fn crt_objects(driver: &Path) -> Result<BTreeMap<String, CacheDigest>> {
probe_files(&file_probes(), |name| {
let resolved = run(driver, &[&format!("-print-file-name={name}")]).ok()?;
let path = PathBuf::from(resolved.trim());
path.is_absolute().then_some(path)
})
}
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;