#[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,
legacy_cc_shim_name,
};
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()
.flat_map(|targeted| {
legacy_cc_shim_name(&targeted.shim_name)
.into_iter()
.chain(std::iter::once(targeted.shim_name.clone()))
.map(|name| (name, 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(crate) const CC_CRATE_ENV: &[&str] = &["CC", "CXX", "HOST_CC", "HOST_CXX"];
pub(crate) const SHIM_DIR_MARKER: &str = ".mbx-shims";
pub(crate) fn mark_shim_directory(directory: &Path) -> std::io::Result<()> {
if is_shim_directory(directory) {
return Ok(());
}
std::fs::write(directory.join(SHIM_DIR_MARKER), b"")
}
pub(crate) fn is_shim_directory(directory: &Path) -> bool {
directory.join(SHIM_DIR_MARKER).is_file()
}
pub(super) fn native_shim_target() -> Result<PathBuf> {
let executable = std::env::current_exe().wrap_err("failed to locate the running mbx binary")?;
Ok(std::fs::canonicalize(&executable).unwrap_or(executable))
}
pub(super) fn install_cc_shims(shims_dir: &Path) -> Result<Option<CcShims>> {
if !cfg!(any(unix, windows)) {
return Ok(None);
}
let executable = native_shim_target()?;
std::fs::create_dir_all(shims_dir)?;
mark_shim_directory(shims_dir).wrap_err("failed to mark the shim directory")?;
let real_cc = resolve_on_path_excluding(CcLanguage::C.default_driver(), &executable, shims_dir);
let real_cxx =
resolve_on_path_excluding(CcLanguage::Cxx.default_driver(), &executable, 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) || is_shim_directory(parent));
(!inside_shims && !is_same_binary(&resolved, Some(executable))).then_some(resolved)
}
pub(super) fn resolve_on_path(name: &str) -> Option<PathBuf> {
first_in_path(&std::env::var_os("PATH")?, name)
}
pub(super) fn first_in_path(path: &OsStr, name: &str) -> Option<PathBuf> {
std::env::split_paths(path)
.filter(|directory| !is_shim_directory(directory))
.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)?;
mark_shim_directory(directory).wrap_err("failed to mark the shim 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)]
pub(super) 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)))]
pub(super) 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 && !is_shim_directory(directory))
.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) struct SessionShims {
pub(super) rustc: PathBuf,
pub(super) rustdoc: PathBuf,
pub(super) native: PathBuf,
pub(super) lease: ShimLease,
}
pub(super) fn install_session_shims(
session_dir: &Path,
persistent_shims: &Path,
) -> Result<SessionShims> {
let executable = std::env::current_exe().wrap_err("failed to locate the running mbx binary")?;
let identity = binary_identity(&executable)?;
let installation = if cfg!(unix) {
installation_identity(&executable)?
} else {
identity.clone()
};
let binary_shims = persistent_shims.join("rust").join(&identity);
let native = persistent_shims.join("native").join(&installation);
let lease = {
let _registrar = shims_registrar(persistent_shims)?;
std::fs::create_dir_all(&binary_shims)?;
ShimLease::take(&binary_shims)?
};
std::fs::create_dir_all(&native)?;
mark_shim_directory(persistent_shims).wrap_err("failed to mark the shim directory")?;
mark_shim_directory(&binary_shims).wrap_err("failed to mark the shim directory")?;
mark_shim_directory(&native).wrap_err("failed to mark the shim directory")?;
touch_shim_directory(&binary_shims);
touch_shim_directory(&native);
remove_stranded_binary_shims(
persistent_shims,
&identity,
&installation,
STRANDED_SHIMS_AGE,
);
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(SessionShims {
rustc,
rustdoc,
native,
lease,
})
}
const SHIM_LEASES_DIR: &str = ".mbx-leases";
const SHIM_REGISTRAR: &str = ".registrar";
static SHIM_LEASE_NONCE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
fn shims_registrar(persistent_shims: &Path) -> Result<fslock::LockFile> {
let rust = persistent_shims.join("rust");
std::fs::create_dir_all(&rust)?;
let mut lock = fslock::LockFile::open(&rust.join(SHIM_REGISTRAR))?;
lock.lock()?;
Ok(lock)
}
pub(super) struct ShimLease {
path: PathBuf,
lock: Option<fslock::LockFile>,
}
impl ShimLease {
pub(super) fn take(directory: &Path) -> Result<Self> {
let leases = directory.join(SHIM_LEASES_DIR);
std::fs::create_dir_all(&leases)?;
let started = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |elapsed| elapsed.as_nanos());
let path = leases.join(format!(
"{}-{started}-{}.lease",
std::process::id(),
SHIM_LEASE_NONCE.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
));
let mut lock = fslock::LockFile::open(&path)?;
if !lock.try_lock()? {
eyre::bail!("the shim lease {} is already held", path.display());
}
Ok(Self {
path,
lock: Some(lock),
})
}
}
impl Drop for ShimLease {
fn drop(&mut self) {
self.lock.take();
let _ = std::fs::remove_file(&self.path);
}
}
#[cfg(unix)]
fn leased(directory: &Path) -> bool {
let Ok(listing) = std::fs::read_dir(directory.join(SHIM_LEASES_DIR)) else {
return false;
};
for entry in listing.flatten() {
let Ok(mut lock) = fslock::LockFile::open(&entry.path()) else {
return true;
};
match lock.try_lock() {
Ok(true) => {
drop(lock);
let _ = std::fs::remove_file(entry.path());
}
_ => return true,
}
}
false
}
const STRANDED_SHIMS_AGE: std::time::Duration = std::time::Duration::from_secs(7 * 24 * 60 * 60);
fn touch_shim_directory(directory: &Path) {
let refreshed = std::fs::File::options()
.write(true)
.open(directory.join(SHIM_DIR_MARKER))
.and_then(|marker| marker.set_modified(std::time::SystemTime::now()));
if let Err(error) = refreshed {
debug!(
"the shim directory {} was not refreshed: {error}",
directory.display()
);
}
}
#[cfg(unix)]
pub(super) fn remove_stranded_binary_shims(
persistent_shims: &Path,
own_binary: &str,
own_installation: &str,
unused_for: std::time::Duration,
) {
for (kind, own) in [("rust", own_binary), ("native", own_installation)] {
let Ok(listing) = std::fs::read_dir(persistent_shims.join(kind)) else {
continue;
};
for entry in listing.flatten() {
if entry.file_name() == own {
continue;
}
let directory = entry.path();
if !entry.file_type().is_ok_and(|kind| kind.is_dir())
|| !unused_for_at_least(&directory, unused_for)
{
continue;
}
let _registrar = if kind == "rust" {
match shims_registrar(persistent_shims) {
Ok(registrar) => Some(registrar),
Err(_) => continue,
}
} else {
None
};
let finished = every_link_dangles(&directory)
|| (kind == "rust"
&& directory.join(SHIM_LEASES_DIR).is_dir()
&& entry
.file_name()
.to_str()
.is_some_and(|name| superseded_binary(&directory, name)));
if !finished || (kind == "rust" && leased(&directory)) {
continue;
}
if let Err(error) = std::fs::remove_dir_all(&directory) {
debug!(
"the stranded shim directory {} was not removed: {error}",
directory.display()
);
}
}
}
}
#[cfg(not(unix))]
pub(super) fn remove_stranded_binary_shims(
_persistent_shims: &Path,
_own_binary: &str,
_own_installation: &str,
_unused_for: std::time::Duration,
) {
}
#[cfg(unix)]
fn unused_for_at_least(directory: &Path, age: std::time::Duration) -> bool {
std::fs::metadata(directory.join(SHIM_DIR_MARKER))
.and_then(|marker| marker.modified())
.ok()
.and_then(|modified| modified.elapsed().ok())
.is_some_and(|elapsed| elapsed >= age)
}
#[cfg(unix)]
fn every_link_dangles(directory: &Path) -> bool {
let Ok(listing) = std::fs::read_dir(directory) else {
return false;
};
let mut links = 0;
for entry in listing.flatten() {
if !entry.file_type().is_ok_and(|kind| kind.is_symlink()) {
continue;
}
if entry.path().exists() {
return false;
}
links += 1;
}
links > 0
}
#[cfg(unix)]
fn superseded_binary(directory: &Path, identity: &str) -> bool {
std::fs::read_link(directory.join(shim_file_name(RUSTC_SHIM_STEM)))
.ok()
.and_then(|binary| binary_identity(&binary).ok())
.is_some_and(|current| current != identity)
}
pub(super) fn installation_identity(executable: &Path) -> Result<String> {
let executable = match std::fs::canonicalize(executable) {
Ok(resolved) => resolved,
Err(_) => std::path::absolute(executable)?,
};
Ok(CacheDigest::blake3(executable.as_os_str().as_encoded_bytes()).hash)
}
pub(super) fn binary_identity(executable: &Path) -> Result<String> {
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(),
);
Ok(CacheDigest::blake3(&identity).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
}