#[cfg(unix)]
use super::SHIM_STAGING_NONCE;
use super::{
PATH_SHIM_NAMES, REAL_CC_ENV, REAL_CXX_ENV, RUSTC_SHIM_STEM, RUSTDOC_SHIM_STEM, is_same_binary,
};
use eyre::{Context, Result};
use log::debug;
use mbx_cache_cc::CcLanguage;
use mbx_cache_core::CacheDigest;
use std::collections::BTreeMap;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
#[cfg(unix)]
use std::sync::atomic::Ordering;
pub(super) struct CcShims {
pub(super) cc: Option<(PathBuf, PathBuf)>,
pub(super) cxx: Option<(PathBuf, PathBuf)>,
pub(super) targeted: Vec<TargetedCompiler>,
}
pub(super) struct TargetedCompiler {
pub(super) variable: String,
pub(super) shim_name: String,
pub(super) shim: PathBuf,
pub(super) real: PathBuf,
}
impl CcShims {
pub(super) fn apply_host(&self, environment: &mut BTreeMap<String, String>) {
for (installed, host, real) in [
(&self.cc, "HOST_CC", REAL_CC_ENV),
(&self.cxx, "HOST_CXX", REAL_CXX_ENV),
] {
if let Some((shim, compiler)) = installed {
environment.insert(host.into(), shim.to_string_lossy().into_owned());
environment.insert(real.into(), compiler.to_string_lossy().into_owned());
}
}
}
pub(super) fn apply_targeted(&self, environment: &mut BTreeMap<String, String>) {
for targeted in &self.targeted {
environment.insert(
targeted.variable.clone(),
targeted.shim.to_string_lossy().into_owned(),
);
}
}
pub(super) fn pins(&self) -> BTreeMap<String, PathBuf> {
self.targeted
.iter()
.map(|targeted| (targeted.shim_name.clone(), targeted.real.clone()))
.collect()
}
}
pub(super) fn targeted_compiler_language(variable: &str) -> Option<CcLanguage> {
match variable {
"TARGET_CC" => return Some(CcLanguage::C),
"TARGET_CXX" => return Some(CcLanguage::Cxx),
_ => {}
}
for (prefix, language) in [("CXX_", CcLanguage::Cxx), ("CC_", CcLanguage::C)] {
if let Some(target) = variable.strip_prefix(prefix)
&& is_target_triple(target)
{
return Some(language);
}
}
None
}
pub(super) fn is_target_triple(suffix: &str) -> bool {
!suffix.is_empty()
&& suffix.contains(['-', '_'])
&& suffix.bytes().all(|byte| {
byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'-' | b'_' | b'.')
})
}
pub(super) const CC_CRATE_ENV: &[&str] = &["CC", "CXX", "HOST_CC", "HOST_CXX"];
pub(super) fn install_cc_shims(shims_dir: &Path) -> Result<Option<CcShims>> {
if !cfg!(any(unix, windows)) {
return Ok(None);
}
let executable = std::env::current_exe().wrap_err("failed to locate the running mbx binary")?;
let real_cc = resolve_on_path(CcLanguage::C.default_driver());
let real_cxx = resolve_on_path(CcLanguage::Cxx.default_driver());
std::fs::create_dir_all(shims_dir)?;
let targeted = wrap_targeted_compilers(&executable, shims_dir)?;
if real_cc.is_none() && real_cxx.is_none() && targeted.is_empty() {
debug!("no C or C++ compiler was found on PATH; build script compiles are not cached");
return Ok(None);
}
let shim = |language: CcLanguage| -> Result<PathBuf> {
let destination = shims_dir.join(shim_file_name(language.shim_stem()));
link_path_shim(&executable, &destination)?;
Ok(destination)
};
let mut installed = CcShims {
cc: None,
cxx: None,
targeted,
};
if let Some(real) = real_cc {
installed.cc = Some((shim(CcLanguage::C)?, real));
}
if let Some(real) = real_cxx {
installed.cxx = Some((shim(CcLanguage::Cxx)?, real));
}
Ok(Some(installed))
}
fn wrap_targeted_compilers(executable: &Path, shims_dir: &Path) -> Result<Vec<TargetedCompiler>> {
let mut wrapped = Vec::new();
for (variable, value) in std::env::vars() {
let Some(language) = targeted_compiler_language(&variable) else {
continue;
};
let Some(real) = resolve_named_compiler(&value, executable, shims_dir) else {
debug!("{variable} does not name a single executable; it is left as it is");
continue;
};
let shim_name = format!(
"{}-{}",
language.shim_stem(),
variable.to_ascii_lowercase().replace(['.', '/'], "_")
);
let shim = shims_dir.join(shim_file_name(&shim_name));
link_path_shim(executable, &shim)?;
wrapped.push(TargetedCompiler {
variable,
shim_name,
shim,
real,
});
}
wrapped.sort_by(|left, right| left.variable.cmp(&right.variable));
Ok(wrapped)
}
pub(super) fn resolve_named_compiler(
value: &str,
executable: &Path,
shims: &Path,
) -> Option<PathBuf> {
let value = value.trim();
if value.is_empty() || value.split_whitespace().count() != 1 {
return None;
}
let candidate = Path::new(value);
let resolved = if candidate.is_absolute() {
candidate.is_file().then(|| candidate.to_path_buf())
} else {
resolve_on_path_excluding(value, executable, shims)
}?;
let inside_shims = resolved
.parent()
.is_some_and(|parent| canonical(parent) == canonical(shims));
(!inside_shims && !is_same_binary(&resolved, Some(executable))).then_some(resolved)
}
pub(super) fn resolve_on_path(name: &str) -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
std::env::split_paths(&path)
.map(|directory| directory.join(name))
.find(|candidate| candidate.is_file())
}
pub struct PathShims {
pub directory: PathBuf,
pub compilers: BTreeMap<String, PathBuf>,
}
pub fn install_path_shims(directory: &Path) -> Result<Option<PathShims>> {
if !cfg!(any(unix, windows)) {
return Ok(None);
}
let executable = std::env::current_exe().wrap_err("failed to locate the running mbx binary")?;
std::fs::create_dir_all(directory)?;
let mut compilers = BTreeMap::new();
for (name, _) in PATH_SHIM_NAMES {
let destination = directory.join(name);
let Some(real) = resolve_on_path_excluding(name, &executable, directory) else {
continue;
};
if canonical(&real) == canonical(&destination) {
debug!("{name} resolved to its own shim; leaving it uncached");
continue;
}
link_path_shim(&executable, &destination)?;
compilers.insert((*name).to_string(), real);
}
if compilers.is_empty() {
debug!("no C or C++ compilers were found on PATH; nothing to shim");
return Ok(None);
}
Ok(Some(PathShims {
directory: directory.to_path_buf(),
compilers,
}))
}
#[cfg(unix)]
fn link_path_shim(executable: &Path, destination: &Path) -> Result<()> {
let target = std::path::absolute(executable)?;
if std::fs::read_link(destination).is_ok_and(|existing| existing == target) {
return Ok(());
}
let staging = destination.with_file_name(format!(
".{}.{}.{}",
destination
.file_name()
.unwrap_or_default()
.to_string_lossy(),
std::process::id(),
SHIM_STAGING_NONCE.fetch_add(1, Ordering::Relaxed)
));
let _ = std::fs::remove_file(&staging);
std::os::unix::fs::symlink(&target, &staging)?;
std::fs::rename(&staging, destination)
.wrap_err_with(|| format!("failed to install the shim {}", destination.display()))?;
Ok(())
}
#[cfg(windows)]
pub(super) fn link_path_shim(executable: &Path, destination: &Path) -> Result<()> {
if destination.is_file() {
return Ok(());
}
match std::fs::hard_link(executable, destination) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(link_error) => {
std::fs::copy(executable, destination).wrap_err_with(|| {
format!(
"failed to install the shim {} by hard link ({link_error}) or copy",
destination.display()
)
})?;
}
}
Ok(())
}
#[cfg(not(any(unix, windows)))]
fn link_path_shim(_executable: &Path, _destination: &Path) -> Result<()> {
eyre::bail!("PATH shims are not supported on this platform")
}
fn resolve_on_path_excluding(name: &str, this_binary: &Path, shims: &Path) -> Option<PathBuf> {
resolve_in_path(&std::env::var_os("PATH")?, name, this_binary, shims)
}
pub(super) fn resolve_in_path(
path: &OsStr,
name: &str,
this_binary: &Path,
shims: &Path,
) -> Option<PathBuf> {
let shims = canonical(shims);
std::env::split_paths(path)
.filter(|directory| canonical(directory) != shims)
.map(|directory| directory.join(name))
.find(|candidate| candidate.is_file() && !is_same_binary(candidate, Some(this_binary)))
}
fn canonical(path: &Path) -> PathBuf {
std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}
pub(super) fn install_session_shims(
session_dir: &Path,
persistent_shims: &Path,
) -> Result<(PathBuf, PathBuf)> {
let executable = std::env::current_exe().wrap_err("failed to locate the running mbx binary")?;
let binary_shims = binary_shims_dir(persistent_shims, &executable)?;
std::fs::create_dir_all(&binary_shims)?;
let rustc = binary_shims.join(shim_file_name(RUSTC_SHIM_STEM));
link_path_shim(&executable, &rustc)?;
let rustdoc = install_shim_named(
&executable,
session_dir,
RUSTDOC_SHIM_STEM,
ShimLink::Tracking,
)?;
Ok((rustc, rustdoc))
}
fn binary_shims_dir(shims: &Path, executable: &Path) -> Result<PathBuf> {
let executable = std::path::absolute(executable)?;
let metadata = std::fs::metadata(&executable)?;
let modified = metadata
.modified()
.ok()
.and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok());
let mut identity = executable.as_os_str().as_encoded_bytes().to_vec();
identity.push(0);
identity.extend_from_slice(&metadata.len().to_le_bytes());
identity.extend_from_slice(
&modified
.map_or(0, |duration| duration.as_secs())
.to_le_bytes(),
);
identity.extend_from_slice(
&modified
.map_or(0, |duration| duration.subsec_nanos())
.to_le_bytes(),
);
let digest = CacheDigest::blake3(&identity);
Ok(shims.join("rust").join(digest.hash))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ShimLink {
Tracking,
Pinned,
}
pub fn shim_file_name(stem: &str) -> String {
if cfg!(windows) {
format!("{stem}.exe")
} else {
stem.into()
}
}
pub fn install_shim(executable: &Path, directory: &Path, link: ShimLink) -> Result<PathBuf> {
install_shim_named(executable, directory, RUSTC_SHIM_STEM, link)
}
pub fn install_shim_named(
executable: &Path,
directory: &Path,
stem: &str,
link: ShimLink,
) -> Result<PathBuf> {
let shim = directory.join(shim_file_name(stem));
let _ = std::fs::remove_file(&shim);
if link == ShimLink::Tracking && symlink_shim(executable, &shim) {
return Ok(shim);
}
if let Err(link_error) = std::fs::hard_link(executable, &shim) {
std::fs::copy(executable, &shim).wrap_err_with(|| {
format!("failed to install the {stem} shim by hard link ({link_error}) or copy")
})?;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
let mut permissions = std::fs::metadata(&shim)?.permissions();
permissions.set_mode(permissions.mode() | 0o100);
std::fs::set_permissions(&shim, permissions)?;
}
Ok(shim)
}
#[cfg(unix)]
pub(super) fn symlink_shim(executable: &Path, shim: &Path) -> bool {
let Ok(target) = std::path::absolute(executable) else {
return false;
};
target.exists() && std::os::unix::fs::symlink(&target, shim).is_ok()
}
#[cfg(windows)]
pub(super) fn symlink_shim(_executable: &Path, _shim: &Path) -> bool {
false
}