use crate::config::Config;
use crate::events::{ActionDetail, ActionOutcome, EventWriter};
use crate::util::duration_ns;
use eyre::Result;
use log::{debug, warn};
use mbx_cache_cc::CcLanguage;
#[cfg(test)]
use mbx_cache_core::AGENT_PROTOCOL_VERSION;
use mbx_cache_core::{
ActionDiagnostic, AgentEvent, AgentEventObserver, AgentRemoteCache, AgentRequest,
AgentResponse, AgentStats, CacheAgent, CacheDigest, FileDigestCache, FileDigestScope,
FileIdentity, NoFileDigestCache, RecordedFileDigest, canonical_json,
};
use serde::Serialize;
use std::collections::{BTreeMap, VecDeque};
use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
#[cfg(unix)]
use std::sync::atomic::AtomicU64;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use tokio::sync::oneshot;
use tokio::task::JoinHandle;
mod client;
mod diagnostics;
mod server;
mod shims;
mod stats;
#[cfg(test)]
use client::validate_handshake_response;
use client::{request_agent_at, request_standalone_agent};
pub(crate) use diagnostics::{note, report_shim_warning, reserve_stderr_for_compiler};
use server::spawn_server;
#[cfg(all(test, windows))]
use shims::link_path_shim;
use shims::{CC_CRATE_ENV, CcShims, install_cc_shims, install_session_shims};
pub use shims::{
PathShims, ShimLink, install_path_shims, install_shim, install_shim_named, shim_file_name,
};
#[cfg(test)]
use shims::{
TargetedCompiler, resolve_in_path, resolve_named_compiler, resolve_on_path, symlink_shim,
targeted_compiler_language,
};
use stats::StatsReport;
pub(crate) use stats::display_stats;
pub(crate) use stats::session_was_active;
#[cfg(test)]
use stats::{
cache_misses, short_summary, should_display_short_stats, should_display_stats,
stale_manifest_note,
};
pub const RUSTC_SHIM_STEM: &str = "mbx-rustc";
pub const RUSTDOC_SHIM_STEM: &str = "mbx-rustdoc";
pub const CC_SHIM_STEM: &str = "mbx-cc";
pub const CXX_SHIM_STEM: &str = "mbx-cxx";
pub(crate) const PATH_SHIMS_ENV: &str = "MBX_CC_SHIM_COMPILERS";
pub(crate) const SOCKET_ENV: &str = "MBX_SOCKET";
pub(crate) const REAL_CC_ENV: &str = "MBX_REAL_CC";
pub(crate) const REAL_CXX_ENV: &str = "MBX_REAL_CXX";
pub(crate) const STAGING_ENV: &str = "MBX_STAGING_DIR";
pub(crate) const BUILD_ENV: &str = "MBX_BUILD";
pub(crate) const VERIFY_ENV: &str = "MBX_VERIFY";
pub(crate) const SHARE_OUT_DIR_ENV: &str = "MBX_SHARE_OUT_DIR";
pub(crate) const BUILD_SCRIPT_EXECUTION_ENV: &str = "MBX_BUILD_SCRIPT_EXECUTION";
pub(crate) const LEARNED_INCREMENTAL_ENV: &str = "MBX_LEARNED_INCREMENTAL";
pub(crate) const INCREMENTAL_ROOT_ENV: &str = "MBX_INCREMENTAL_ROOT";
pub const CACHE_LINKS_ENV: &str = "MBX_CACHE_LINKS";
pub const CACHE_EXPORT_GROUP_ENV: &str = "MBX_CACHE_EXPORT_GROUP";
pub(crate) const WORKSPACE_ROOT_ENV: &str = "MBX_WORKSPACE_ROOT";
pub(crate) const TARGET_DIR_ENV: &str = "MBX_TARGET_DIR";
pub(crate) const BUILD_SCRIPT_REAL_SUFFIX: &str = ".mbx-real";
const PREVIOUS_RUSTC_WRAPPER_ENV: &str = "MBX_PREVIOUS_RUSTC_WRAPPER";
const PREVIOUS_RUSTC_WORKSPACE_WRAPPER_ENV: &str = "MBX_PREVIOUS_RUSTC_WORKSPACE_WRAPPER";
const REAL_RUSTDOC_ENV: &str = "MBX_REAL_RUSTDOC";
pub(crate) const BYPASS_LOG_ENV: &str = "MBX_BYPASS_LOG";
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(unix)]
static SHIM_STAGING_NONCE: AtomicU64 = AtomicU64::new(0);
pub struct CacheSession {
socket: String,
rustc_shim: PathBuf,
rustdoc_shim: PathBuf,
cc_shims: Option<CcShims>,
staging: PathBuf,
verify: bool,
incremental: bool,
share_out_dir: bool,
build_script_execution: bool,
agent: CacheAgent,
events: Option<EventStream>,
scheduler_env: Vec<(String, String)>,
store: PathBuf,
incremental_root: PathBuf,
started: Instant,
shutdown: Mutex<Option<oneshot::Sender<()>>>,
server: Mutex<Option<JoinHandle<Result<()>>>>,
task: Arc<SessionTask>,
}
pub(super) struct SessionTask {
identity: std::sync::OnceLock<String>,
initialized: tokio::sync::OnceCell<bool>,
}
impl CacheSession {
pub async fn start(session_dir: &Path, config: &Config) -> Result<Self> {
Self::start_with_jobs(session_dir, config, None).await
}
pub(crate) async fn start_with_jobs(
session_dir: &Path,
config: &Config,
cargo_jobs: Option<u64>,
) -> Result<Self> {
let (shim, rustdoc_shim) =
install_session_shims(session_dir, &config.cache_dir.join("shims"))?;
let cc_shims = if config.cc {
install_cc_shims(&config.cache_dir.join("shims"))?
} else {
None
};
let staging = session_dir.join("staging");
std::fs::create_dir(&staging)?;
let store = config.store_dir();
let agent = if let Some(remote) = action_remote_cache(config, &store)? {
CacheAgent::new_remote_with_download_limit(
store.clone(),
VERSION,
remote,
config.gc.max_bytes,
)
} else {
CacheAgent::new(store.clone(), VERSION)
};
let events = config
.events
.then(|| Arc::new(EventWriter::new(&store)))
.map(EventStream::new);
let agent = match &events {
Some(events) => agent.with_observer(Arc::new(events.clone())),
None => agent,
};
let (shutdown_tx, shutdown_rx) = oneshot::channel();
let task = Arc::new(SessionTask {
identity: std::sync::OnceLock::new(),
initialized: tokio::sync::OnceCell::new(),
});
let (socket, server) =
spawn_server(session_dir, agent.clone(), Arc::clone(&task), shutdown_rx).await?;
Ok(Self {
socket,
rustc_shim: shim,
rustdoc_shim,
cc_shims,
staging,
verify: config.verify,
incremental: config.incremental,
share_out_dir: config.share_out_dir,
build_script_execution: config.build_script_execution,
agent,
events,
scheduler_env: crate::scheduler::session_environment_with_jobs(config, cargo_jobs),
store,
incremental_root: config.cache_dir.join("incremental"),
started: Instant::now(),
shutdown: Mutex::new(Some(shutdown_tx)),
server: Mutex::new(Some(server)),
task,
})
}
pub async fn begin(
&self,
workspace_root: &Path,
target_dir: &Path,
command: &[String],
environment: &mut BTreeMap<String, String>,
) -> Option<ActionRun> {
let identity = build_identity(workspace_root, command);
if let Some(events) = &self.events {
events.writer.started(workspace_root, command);
}
if let Err(error) =
crate::store::record_checkout(&self.store, &identity, workspace_root, target_dir)
{
warn!("this checkout was not recorded as a cache root: {error}");
}
let _ = self.task.identity.set(identity.clone());
let action_run = Some(ActionRun {
run: identity.clone(),
receipt: build_receipt_run(&identity),
identity: identity.clone(),
workspace_root: workspace_root.to_path_buf(),
export_group: std::env::var(CACHE_EXPORT_GROUP_ENV).ok(),
store: self.store.clone(),
agent: self.agent.clone(),
initialized: Some(Arc::clone(&self.task)),
});
let shim = self.rustc_shim.to_string_lossy().into_owned();
environment.insert(
WORKSPACE_ROOT_ENV.into(),
workspace_root.to_string_lossy().into_owned(),
);
environment.insert(
TARGET_DIR_ENV.into(),
target_dir.to_string_lossy().into_owned(),
);
environment.insert(
INCREMENTAL_ROOT_ENV.into(),
target_dir
.join("mbx-incremental")
.to_string_lossy()
.into_owned(),
);
match crate::incremental::touch(&self.incremental_root, workspace_root) {
Ok(root) => {
environment.insert(
INCREMENTAL_ROOT_ENV.into(),
root.to_string_lossy().into_owned(),
);
}
Err(error) => warn!("incremental state was not recorded: {error:#}"),
}
environment.insert(SOCKET_ENV.into(), self.socket.clone());
environment.insert(
STAGING_ENV.into(),
self.staging.to_string_lossy().into_owned(),
);
environment.insert(BUILD_ENV.into(), identity);
environment.insert(
VERIFY_ENV.into(),
if self.verify { "1" } else { "0" }.into(),
);
environment.insert(
SHARE_OUT_DIR_ENV.into(),
if self.share_out_dir { "1" } else { "0" }.into(),
);
environment.insert(
BUILD_SCRIPT_EXECUTION_ENV.into(),
if self.build_script_execution {
"1"
} else {
"0"
}
.into(),
);
for (name, value) in &self.scheduler_env {
environment.insert(name.clone(), value.clone());
}
if let Some(previous) = environment.insert("RUSTC_WRAPPER".into(), shim.clone())
&& previous != shim
{
warn!(
"RUSTC_WRAPPER is already set to {previous}; deferring to it, so this build is not cached"
);
environment.insert(PREVIOUS_RUSTC_WRAPPER_ENV.into(), previous);
}
if let Some(previous) = environment.get("RUSTC_WORKSPACE_WRAPPER") {
if executable_stem(OsStr::new(previous)) != Some("clippy-driver") {
warn!(
"RUSTC_WORKSPACE_WRAPPER is already set to {previous}; deferring to it for workspace crates, so those compilations are not cached"
);
}
environment.insert(
PREVIOUS_RUSTC_WORKSPACE_WRAPPER_ENV.into(),
previous.clone(),
);
}
let rustdoc = configured_rustdoc(environment);
environment.insert(REAL_RUSTDOC_ENV.into(), rustdoc);
environment.insert(
"RUSTDOC".into(),
self.rustdoc_shim.to_string_lossy().into_owned(),
);
self.begin_cc(environment);
if self.incremental {
environment.remove("CARGO_INCREMENTAL");
} else {
environment.insert("CARGO_INCREMENTAL".into(), "0".into());
}
action_run
}
fn begin_cc(&self, environment: &mut BTreeMap<String, String>) {
let Some(shims) = &self.cc_shims else {
return;
};
let host_chosen = CC_CRATE_ENV
.iter()
.find(|name| environment.contains_key(**name) || std::env::var_os(name).is_some());
match host_chosen {
Some(name) => {
debug!("{name} is already set; host C and C++ compilations are not cached");
let value = environment
.get(*name)
.cloned()
.or_else(|| std::env::var(name).ok())
.unwrap_or_else(|| "<non-UTF-8 value>".into());
append_cacheability_observation(
environment,
"cc-compiler-override",
&format!(
"{name} is already set to `{value}`, so host C and C++ compilations do not pass through mbx; unset it for this build to make them visible to the cache"
),
);
}
None => shims.apply_host(environment),
}
shims.apply_targeted(environment);
let pins = shims.pins();
if !pins.is_empty()
&& let Ok(encoded) = serde_json::to_string(&pins)
{
environment.insert(PATH_SHIMS_ENV.into(), encoded);
}
}
pub async fn begin_exec(
&self,
project_root: &Path,
command: &[String],
shims: &PathShims,
environment: &mut BTreeMap<String, String>,
) -> Option<ActionRun> {
let identity = exec_identity(project_root, command);
if let Some(events) = &self.events {
events.writer.started(project_root, command);
}
if let Err(error) =
crate::store::record_checkout(&self.store, &identity, project_root, project_root)
{
warn!("this checkout was not recorded as a cache root: {error}");
}
let (protocol_build, action_run) = match self.agent.begin_task(&identity).await {
Ok(run) => (
run.clone(),
Some(ActionRun {
receipt: run.clone(),
run,
identity: identity.clone(),
workspace_root: project_root.to_path_buf(),
export_group: std::env::var(CACHE_EXPORT_GROUP_ENV).ok(),
store: self.store.clone(),
agent: self.agent.clone(),
initialized: None,
}),
),
Err(error) => {
warn!("build action manifest was not loaded: {error}");
(identity, None)
}
};
environment.insert(
WORKSPACE_ROOT_ENV.into(),
project_root.to_string_lossy().into_owned(),
);
environment.insert(SOCKET_ENV.into(), self.socket.clone());
environment.insert(
STAGING_ENV.into(),
self.staging.to_string_lossy().into_owned(),
);
environment.insert(BUILD_ENV.into(), protocol_build);
environment.insert(
VERIFY_ENV.into(),
if self.verify { "1" } else { "0" }.into(),
);
for (name, value) in &self.scheduler_env {
environment.insert(name.clone(), value.clone());
}
if let Ok(pins) = serde_json::to_string(&shims.compilers) {
environment.insert(PATH_SHIMS_ENV.into(), pins);
}
let path = std::env::var_os("PATH").unwrap_or_default();
let paths = std::iter::once(shims.directory.clone()).chain(std::env::split_paths(&path));
if let Ok(joined) = std::env::join_paths(paths) {
environment.insert("PATH".into(), joined.to_string_lossy().into_owned());
}
action_run
}
pub async fn prefetch(&self, workspace_root: &Path, command: &[String]) -> Result<()> {
let identity = build_identity(workspace_root, command);
self.agent.prefetch_task(&identity).await?;
Ok(())
}
pub async fn finish(&self) -> Result<AgentStats> {
if let Some(shutdown) = self.shutdown.lock().unwrap().take() {
let _ = shutdown.send(());
}
let server = self.server.lock().unwrap().take();
let served = match server {
Some(server) => server
.await
.map_err(eyre::Report::from)
.and_then(|served| served),
None => Ok(()),
};
self.agent.cancel_prefetches().await;
self.agent.wait_for_uploads().await;
served?;
let mut stats = self.agent.stats();
stats.session_duration_ns = duration_ns(self.started.elapsed());
if let Some(events) = &self.events
&& let Ok(totals) = serde_json::to_value(StatsReport::from(&stats))
{
events.writer.finished(totals);
}
Ok(stats)
}
}
fn configured_rustdoc(environment: &BTreeMap<String, String>) -> String {
let configured = environment
.get("RUSTDOC")
.cloned()
.or_else(|| std::env::var("RUSTDOC").ok())
.unwrap_or_else(|| "rustdoc".into());
if Path::new(&configured).file_stem() == Some(OsStr::new(RUSTDOC_SHIM_STEM)) {
environment
.get(REAL_RUSTDOC_ENV)
.cloned()
.or_else(|| std::env::var(REAL_RUSTDOC_ENV).ok())
.unwrap_or_else(|| "rustdoc".into())
} else {
configured
}
}
impl Drop for CacheSession {
fn drop(&mut self) {
if let Some(shutdown) = self.shutdown.get_mut().unwrap().take() {
let _ = shutdown.send(());
}
if let Some(server) = self.server.get_mut().unwrap().take() {
server.abort();
}
}
}
#[derive(Clone)]
struct EventStream {
writer: Arc<EventWriter>,
diagnostics: Arc<Mutex<VecDeque<PendingDiagnostic>>>,
}
struct PendingDiagnostic {
outcome: String,
crate_name: Option<String>,
diagnostic: ActionDiagnostic,
}
impl EventStream {
fn new(writer: Arc<EventWriter>) -> Self {
Self {
writer,
diagnostics: Arc::new(Mutex::new(VecDeque::new())),
}
}
fn take_diagnostic(&self, outcome: &str, crate_name: Option<&str>) -> Option<ActionDiagnostic> {
let mut diagnostics = self.diagnostics.lock().unwrap();
let position = diagnostics.iter().position(|pending| {
pending.outcome == outcome && pending.crate_name.as_deref() == crate_name
})?;
diagnostics
.remove(position)
.map(|pending| pending.diagnostic)
}
}
impl AgentEventObserver for EventStream {
fn event(&self, event: AgentEvent) {
match event {
AgentEvent::ActionHit {
crate_name,
restore,
} => {
let diagnostic = self.take_diagnostic("hit", crate_name.as_deref());
self.writer.action_with_diagnostic(
ActionOutcome::Hit,
crate_name,
restore.duration_ns,
ActionDetail {
avoided_compiler_ns: restore.avoided_compiler_duration_ns,
output_files: restore.output_files,
output_bytes: restore.output_bytes,
reflinked_output_bytes: restore.reflinked_output_bytes,
copied_output_bytes: restore.copied_output_bytes,
},
diagnostic,
);
}
AgentEvent::Bypass { kind } => self.writer.action(
ActionOutcome::Bypass { reason: kind },
None,
0,
ActionDetail::default(),
),
AgentEvent::Unconsulted => {}
AgentEvent::CompilerInvocation {
outcome,
crate_name,
duration_ns,
} => {
let recorded_outcome = match outcome.as_str() {
"miss" => ActionOutcome::Miss,
"unconsulted" => ActionOutcome::Unconsulted,
_ => return,
};
let diagnostic = self.take_diagnostic(&outcome, crate_name.as_deref());
self.writer.action_with_diagnostic(
recorded_outcome,
crate_name,
duration_ns,
ActionDetail::default(),
diagnostic,
);
}
AgentEvent::Verification { matched, restore } => self.writer.action(
ActionOutcome::Verification { matched },
None,
restore.duration_ns,
ActionDetail::default(),
),
AgentEvent::ActionDiagnostic {
outcome,
crate_name,
diagnostic,
} => self
.diagnostics
.lock()
.unwrap()
.push_back(PendingDiagnostic {
outcome,
crate_name,
diagnostic,
}),
_ => {}
}
}
}
pub struct ActionRun {
run: String,
receipt: String,
identity: String,
workspace_root: PathBuf,
export_group: Option<String>,
store: PathBuf,
agent: CacheAgent,
initialized: Option<Arc<SessionTask>>,
}
impl ActionRun {
pub async fn commit(self) -> Result<()> {
if self
.initialized
.as_ref()
.is_some_and(|task| task.initialized.get() != Some(&true))
{
return Ok(());
}
let predictions = self.agent.commit_task_actions(&self.run).await?;
crate::store::record_build_receipt(
&self.store,
&self.receipt,
&self.identity,
&self.workspace_root,
self.export_group.as_deref(),
predictions,
)
}
}
fn build_receipt_run(identity: &str) -> String {
CacheDigest::blake3(
format!(
"{identity}\0{}\0{}",
std::process::id(),
crate::util::random_string(12)
)
.as_bytes(),
)
.hash
}
pub fn build_identity(workspace_root: &Path, command: &[String]) -> String {
mbx_cache_cargo::build_identity(workspace_root, command)
}
#[derive(Serialize)]
struct ExecIdentity<'a> {
version: u8,
project: &'a str,
command: &'a [String],
os: &'static str,
arch: &'static str,
}
pub fn exec_identity(project_root: &Path, command: &[String]) -> String {
let project = exec_marker(project_root);
let material = ExecIdentity {
version: 2,
project: &project,
command,
os: std::env::consts::OS,
arch: std::env::consts::ARCH,
};
let bytes = canonical_json(&material).expect("exec identity must serialize");
CacheDigest::blake3(&bytes).hash
}
fn exec_marker(project_root: &Path) -> String {
if let Ok(lock) = std::fs::read(project_root.join("Cargo.lock")) {
return CacheDigest::blake3(&lock).hash;
}
if let Some(origin) = project_origin_marker(project_root) {
return origin;
}
project_root
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned()
}
fn project_origin_marker(project_root: &Path) -> Option<String> {
if !project_root.join(".git").exists() {
if project_root.join(".jj").exists() {
return jj_origin_marker(project_root);
}
if project_root.join(".sl").exists() {
return sapling_origin_marker(project_root);
}
if project_root.join(".hg").exists() {
return mercurial_origin_marker(project_root);
}
}
git_origin_marker(project_root)
.or_else(|| jj_origin_marker(project_root))
.or_else(|| sapling_origin_marker(project_root))
.or_else(|| mercurial_origin_marker(project_root))
}
fn git_origin_marker(project_root: &Path) -> Option<String> {
let output = Command::new("git")
.arg("-C")
.arg(project_root)
.args(["config", "--get", "remote.origin.url"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
origin_marker_from_output(&output.stdout)
}
fn jj_origin_marker(project_root: &Path) -> Option<String> {
if !project_root.join(".jj").exists() {
return None;
}
let output = Command::new("jj")
.arg("-R")
.arg(project_root)
.arg("--ignore-working-copy")
.args(["git", "remote", "list"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let remotes = String::from_utf8_lossy(&output.stdout);
let url = jj_origin_url(&remotes)?;
Some(format!("origin\0{url}"))
}
fn jj_origin_url(remotes: &str) -> Option<&str> {
remotes.lines().find_map(|line| {
let mut fields = line.split_whitespace();
if fields.next()? == "origin" {
fields.next()
} else {
None
}
})
}
fn sapling_origin_marker(project_root: &Path) -> Option<String> {
default_path_origin_marker("sl", project_root, &["config", "paths.default"], ".sl")
}
fn mercurial_origin_marker(project_root: &Path) -> Option<String> {
default_path_origin_marker("hg", project_root, &["paths", "default"], ".hg")
}
fn default_path_origin_marker(
program: &str,
project_root: &Path,
arguments: &[&str],
marker: &str,
) -> Option<String> {
if !project_root.join(marker).exists() {
return None;
}
let output = Command::new(program)
.arg("-R")
.arg(project_root)
.args(arguments)
.output()
.ok()?;
if !output.status.success() {
return None;
}
origin_marker_from_output(&output.stdout)
}
fn origin_marker_from_output(output: &[u8]) -> Option<String> {
let url = String::from_utf8_lossy(output).trim().to_string();
(!url.is_empty()).then(|| format!("origin\0{url}"))
}
fn action_remote_cache(config: &Config, store: &Path) -> Result<Option<AgentRemoteCache>> {
let Some(client) = crate::remote::remote_client(config)? else {
return Ok(None);
};
let Some(mode) = crate::policy::effective_remote_cache_mode(config.remote.mode) else {
return Ok(None);
};
Ok(Some(AgentRemoteCache {
client,
mode,
staging_dir: store.join("remote"),
}))
}
pub fn is_rustc_shim() -> bool {
std::env::args_os()
.next()
.as_deref()
.map(Path::new)
.and_then(Path::file_stem)
.is_some_and(|stem| stem == OsStr::new(RUSTC_SHIM_STEM))
}
pub fn is_rustdoc_shim() -> bool {
std::env::args_os()
.next()
.as_deref()
.map(Path::new)
.and_then(Path::file_stem)
.is_some_and(|stem| stem == OsStr::new(RUSTDOC_SHIM_STEM))
}
pub fn run_rustdoc_shim() -> ExitCode {
let rustdoc = std::env::var_os(REAL_RUSTDOC_ENV).unwrap_or_else(|| "rustdoc".into());
let arguments = std::env::args_os().skip(1).collect::<Vec<_>>();
match crate::rustdoc::document(&rustdoc, &arguments) {
Ok(code) => code,
Err(error) => {
let _ = request_agent(&[AgentRequest::RecordBypass {
kind: "rustdoc".into(),
}]);
#[cfg(debug_assertions)]
eprintln!("mbx[warning]: rustdoc cache bypassed: {error:#}");
run_transparent_rustdoc(rustdoc, arguments)
}
}
}
fn run_transparent_rustdoc(rustdoc: OsString, arguments: Vec<OsString>) -> ExitCode {
let status = Command::new(&rustdoc).args(arguments).status();
match status {
Ok(status) => ExitCode::from(u8::try_from(status.code().unwrap_or(1)).unwrap_or(1)),
Err(error) => {
eprintln!("mbx[error]: the rustdoc shim failed to execute rustdoc: {error}");
ExitCode::FAILURE
}
}
}
pub fn is_build_script_shim() -> bool {
let Some(invoked) = std::env::args_os().next().map(PathBuf::from) else {
return false;
};
invoked
.file_stem()
.and_then(OsStr::to_str)
.is_some_and(|stem| stem == "build-script-build")
&& find_build_script_real_path(&invoked).is_some()
}
pub(crate) fn build_script_real_path(executable: &Path) -> PathBuf {
let mut name = executable.as_os_str().to_os_string();
name.push(BUILD_SCRIPT_REAL_SUFFIX);
PathBuf::from(name)
}
pub(crate) fn build_script_execution_requested() -> bool {
std::env::var_os(BUILD_SCRIPT_EXECUTION_ENV)
.is_some_and(|value| !value.is_empty() && value != "0")
}
pub(crate) fn find_build_script_real_path(executable: &Path) -> Option<PathBuf> {
let direct = build_script_real_path(executable);
if direct.is_file() {
return Some(direct);
}
let parent = executable.parent()?;
let mut matches = std::fs::read_dir(parent)
.ok()?
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| {
path.is_file()
&& path
.file_name()
.and_then(OsStr::to_str)
.is_some_and(|name| {
name.starts_with("build_script_build-")
&& name.ends_with(BUILD_SCRIPT_REAL_SUFFIX)
})
});
let found = matches.next()?;
matches.next().is_none().then_some(found)
}
pub fn run_build_script_shim() -> ExitCode {
if session_socket().is_none() || !build_script_execution_requested() {
return crate::build_script::run_real();
}
match crate::build_script::run() {
Ok(code) => code,
Err(error) => {
report_shim_warning(&format!("build-script cache bypassed: {error:#}"));
crate::build_script::run_real()
}
}
}
const PATH_SHIM_NAMES: &[(&str, CcLanguage)] = &[
("cc", CcLanguage::C),
("gcc", CcLanguage::C),
("clang", CcLanguage::C),
("c++", CcLanguage::Cxx),
("g++", CcLanguage::Cxx),
("clang++", CcLanguage::Cxx),
("cl.exe", CcLanguage::Cxx),
];
fn path_shim_language(name: &str) -> Option<CcLanguage> {
PATH_SHIM_NAMES
.iter()
.find(|(shim, _)| *shim == name)
.map(|(_, language)| *language)
}
pub fn is_cc_shim() -> Option<CcLanguage> {
let stem = std::env::args_os()
.next()
.as_deref()
.map(Path::new)
.and_then(Path::file_stem)?
.to_str()?
.to_string();
match stem.as_str() {
CC_SHIM_STEM => Some(CcLanguage::C),
CXX_SHIM_STEM => Some(CcLanguage::Cxx),
"cl" if cfg!(windows) => Some(CcLanguage::Cxx),
other if other.starts_with(&format!("{CXX_SHIM_STEM}-")) => Some(CcLanguage::Cxx),
other if other.starts_with(&format!("{CC_SHIM_STEM}-")) => Some(CcLanguage::C),
other => path_shim_language(other),
}
}
fn shim_invocation_name() -> Option<String> {
std::env::args_os()
.next()
.as_deref()
.map(Path::new)
.and_then(Path::file_name)?
.to_str()
.map(ToOwned::to_owned)
}
pub fn run_cc_shim(language: CcLanguage) -> ExitCode {
reserve_stderr_for_compiler();
let arguments = std::env::args_os().skip(1).collect::<Vec<_>>();
let compiler = match real_compiler(language) {
Ok(compiler) => compiler,
Err(error) => {
eprintln!(
"mbx[error]: the {} shim found no compiler to run: {error:#}",
language.shim_stem()
);
return ExitCode::from(1);
}
};
if session_socket().is_none() {
return run_transparent_cc(compiler, arguments);
}
match crate::cc::compile(&compiler, &arguments, language) {
Ok(exit_code) => return exit_code,
Err(error) => {
record_cc_bypass(&error);
#[cfg(debug_assertions)]
report_shim_warning(&format!("cc cache bypassed: {error:#}"));
}
}
run_transparent_cc(compiler, arguments)
}
fn real_compiler(language: CcLanguage) -> Result<OsString> {
let invoked = shim_invocation_name();
if let Some(name) = invoked.as_deref()
&& let Some(compiler) = pinned_path_shim(name)
{
return Ok(compiler.into_os_string());
}
let pinned = match language {
CcLanguage::C => REAL_CC_ENV,
CcLanguage::Cxx => REAL_CXX_ENV,
};
if let Some(compiler) = std::env::var_os(pinned).filter(|value| !value.is_empty()) {
return Ok(compiler);
}
let name = invoked
.as_deref()
.filter(|name| path_shim_language(name).is_some())
.map(ToOwned::to_owned)
.unwrap_or_else(|| language.default_driver().to_string());
let current = std::env::current_exe().ok();
let path = std::env::var_os("PATH").unwrap_or_default();
for directory in std::env::split_paths(&path) {
let candidate = directory.join(&name);
if !candidate.is_file() || is_same_binary(&candidate, current.as_deref()) {
continue;
}
return Ok(candidate.into_os_string());
}
eyre::bail!("no {name} was found on PATH")
}
fn pinned_path_shim(name: &str) -> Option<PathBuf> {
let pins = std::env::var(PATH_SHIMS_ENV).ok()?;
let pins: BTreeMap<String, PathBuf> = serde_json::from_str(&pins).ok()?;
pins.get(name).cloned()
}
fn is_same_binary(candidate: &Path, current: Option<&Path>) -> bool {
let Some(current) = current else {
return false;
};
let resolved = std::fs::canonicalize(candidate).unwrap_or_else(|_| candidate.to_path_buf());
let current_resolved = std::fs::canonicalize(current)
.ok()
.unwrap_or_else(|| current.to_path_buf());
if resolved == current_resolved {
return true;
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt as _;
if let (Ok(a), Ok(b)) = (std::fs::metadata(&resolved), std::fs::metadata(current)) {
return a.dev() == b.dev() && a.ino() == b.ino();
}
}
false
}
fn run_transparent_cc(compiler: OsString, arguments: Vec<OsString>) -> ExitCode {
let mut command = Command::new(&compiler);
command.args(&arguments);
#[cfg(unix)]
{
use std::os::unix::process::CommandExt as _;
let error = command.exec();
eprintln!("mbx[error]: the cc shim failed to execute {compiler:?}: {error}");
ExitCode::from(1)
}
#[cfg(not(unix))]
{
match command.status() {
Ok(status) => ExitCode::from(u8::try_from(status.code().unwrap_or(1)).unwrap_or(1)),
Err(error) => {
eprintln!("mbx[error]: the cc shim failed to execute {compiler:?}: {error}");
ExitCode::from(1)
}
}
}
}
pub fn run_rustc_shim() -> ExitCode {
let mut arguments = std::env::args_os().skip(1);
let Some(rustc) = arguments.next() else {
eprintln!("mbx[error]: the rustc shim expected the rustc executable as its first argument");
return ExitCode::from(1);
};
let arguments = arguments.collect::<Vec<_>>();
let is_workspace_wrapper = std::env::var_os(PREVIOUS_RUSTC_WORKSPACE_WRAPPER_ENV)
.is_some_and(|wrapper| wrapper == rustc);
let (wrapper_argument, compiler_arguments) = workspace_wrapper_arguments(&rustc, &arguments);
let cacheable_workspace_wrapper = is_workspace_wrapper && wrapper_argument.is_some();
if std::env::var_os(PREVIOUS_RUSTC_WRAPPER_ENV).is_none()
&& (!is_workspace_wrapper || cacheable_workspace_wrapper)
{
match crate::rustc::compile(&rustc, compiler_arguments, wrapper_argument) {
Ok(exit_code) => return exit_code,
Err(error) => {
record_bypass(&error);
#[cfg(debug_assertions)]
eprintln!("mbx[warning]: rustc cache bypassed: {error:#}");
}
}
}
run_transparent_rustc(rustc, arguments)
}
fn workspace_wrapper_arguments<'a>(
compiler: &OsStr,
arguments: &'a [OsString],
) -> (Option<&'a OsStr>, &'a [OsString]) {
let wrapper_argument = arguments.first().filter(|argument| {
executable_stem(compiler) == Some("clippy-driver")
&& executable_stem(argument) == Some("rustc")
});
match wrapper_argument {
Some(argument) => (Some(argument.as_os_str()), &arguments[1..]),
None => (None, arguments),
}
}
fn executable_stem(executable: &OsStr) -> Option<&str> {
Path::new(executable).file_stem()?.to_str()
}
fn run_transparent_rustc(rustc: OsString, arguments: Vec<OsString>) -> ExitCode {
let crate_name = crate_name_argument(&arguments);
let is_query = arguments.iter().any(|argument| {
argument == "-"
|| argument
.to_str()
.is_some_and(|argument| argument.starts_with("--print"))
});
let demand = crate_name
.as_deref()
.filter(|_| !is_query)
.map(|name| crate::scheduler::Demand::new(name, links_natively(&arguments)));
let permit = demand
.as_ref()
.and_then(|demand| crate::scheduler::pool().and_then(|pool| pool.admit(demand)));
let started = Instant::now();
let mut command = if let Some(wrapper) = std::env::var_os(PREVIOUS_RUSTC_WRAPPER_ENV) {
let mut command = Command::new(wrapper);
command.arg(&rustc);
command
} else {
Command::new(&rustc)
};
command.args(&arguments);
command.env_remove(PREVIOUS_RUSTC_WRAPPER_ENV);
command.env_remove(PREVIOUS_RUSTC_WORKSPACE_WRAPPER_ENV);
#[cfg(unix)]
{
use std::os::unix::process::CommandExt as _;
if permit.is_none() {
let error = command.exec();
eprintln!("mbx[error]: the rustc shim failed to execute rustc: {error}");
return ExitCode::from(1);
}
match command.status() {
Ok(status) => {
drop(permit);
if let Some(demand) = &demand {
crate::scheduler::record_compiler_memory(demand, &status);
}
record_compiler_invocation(
"bypass",
crate_name.as_deref(),
duration_ns(started.elapsed()),
);
crate::materialize::exit_code(status)
}
Err(error) => {
eprintln!("mbx[error]: the rustc shim failed to execute rustc: {error}");
ExitCode::from(1)
}
}
}
#[cfg(windows)]
{
use std::os::windows::io::AsRawHandle as _;
use windows_sys::Win32::System::Threading::GetExitCodeProcess;
match command.spawn().and_then(|mut child| {
let status = child.wait()?;
let mut exit_code = 1;
if unsafe { GetExitCodeProcess(child.as_raw_handle().cast(), &raw mut exit_code) } == 0
{
Err(std::io::Error::last_os_error())
} else {
Ok((exit_code, status))
}
}) {
Ok((exit_code, status)) => {
drop(permit);
if let Some(demand) = &demand {
crate::scheduler::record_compiler_memory(demand, &status);
}
record_compiler_invocation(
"bypass",
crate_name.as_deref(),
duration_ns(started.elapsed()),
);
unsafe { windows_sys::Win32::System::Threading::ExitProcess(exit_code) }
}
Err(error) => {
eprintln!("mbx[error]: the rustc shim failed to execute rustc: {error}");
ExitCode::from(1)
}
}
}
}
fn links_natively(arguments: &[OsString]) -> bool {
let mut produces_program = false;
let mut emits_link = true;
let mut arguments = arguments.iter();
while let Some(argument) = arguments.next() {
let Some(argument) = argument.to_str() else {
continue;
};
if argument == "--test" {
produces_program = true;
}
let kinds = if argument == "--crate-type" {
arguments.next().and_then(|kinds| kinds.to_str())
} else {
argument.strip_prefix("--crate-type=")
};
if kinds.is_some_and(|kinds| {
kinds.split(',').any(|kind| {
matches!(
kind,
"bin" | "cdylib" | "dylib" | "proc-macro" | "staticlib"
)
})
}) {
produces_program = true;
}
let emits = if argument == "--emit" {
arguments.next().and_then(|emits| emits.to_str())
} else {
argument.strip_prefix("--emit=")
};
if let Some(emits) = emits {
emits_link = emits
.split(',')
.any(|emit| emit.split('=').next() == Some("link"));
}
}
produces_program && emits_link
}
pub(crate) fn crate_name_argument(arguments: &[OsString]) -> Option<String> {
let mut arguments = arguments.iter();
while let Some(argument) = arguments.next() {
if argument == "--crate-name" {
return arguments
.next()
.and_then(|name| name.to_str())
.map(str::to_string);
}
if let Some(argument) = argument.to_str()
&& let Some(name) = argument.strip_prefix("--crate-name=")
{
return Some(name.to_string());
}
}
None
}
pub(crate) fn verify_requested() -> bool {
std::env::var_os(VERIFY_ENV).is_some_and(|value| !value.is_empty() && value != "0")
}
struct AgentFileDigestCache;
impl FileDigestCache for AgentFileDigestCache {
fn find(&self, scope: FileDigestScope, files: &[FileIdentity]) -> Vec<Option<CacheDigest>> {
if files.is_empty() {
return Vec::new();
}
let response = request_agent(&[AgentRequest::FindFileDigests {
scope,
files: files.to_vec(),
}]);
match response.map(|responses| responses.into_iter().next()) {
Ok(Some(AgentResponse::FileDigests { digests })) if digests.len() == files.len() => {
digests
}
_ => vec![None; files.len()],
}
}
fn record(&self, scope: FileDigestScope, entries: Vec<RecordedFileDigest>) {
if entries.is_empty() {
return;
}
let _ = request_agent(&[AgentRequest::RecordFileDigests { scope, entries }]);
}
}
pub(crate) fn file_digest_cache() -> &'static dyn FileDigestCache {
if verify_requested() {
&NoFileDigestCache
} else {
&AgentFileDigestCache
}
}
pub(crate) fn record_file_digests(scope: FileDigestScope, entries: Vec<RecordedFileDigest>) {
if !verify_requested() {
AgentFileDigestCache.record(scope, entries);
}
}
pub fn cache_links_supported() -> bool {
cfg!(any(target_os = "linux", target_os = "macos", windows))
}
pub(crate) fn cache_links_requested() -> bool {
cache_links_supported()
&& std::env::var_os(CACHE_LINKS_ENV).is_none_or(|value| !value.is_empty() && value != "0")
}
pub(crate) fn share_out_dir_requested() -> bool {
std::env::var_os(SHARE_OUT_DIR_ENV).is_some_and(|value| !value.is_empty() && value != "0")
}
pub(crate) fn learned_incremental_requested() -> bool {
std::env::var_os(LEARNED_INCREMENTAL_ENV).is_some_and(|value| !value.is_empty() && value != "0")
}
fn record_bypass(error: &eyre::Report) {
let reason = error.downcast_ref::<mbx_cache_rustc::BypassReason>();
let kind = reason.map_or("other", mbx_cache_rustc::BypassReason::kind);
append_bypass_log(
kind,
error,
reason.and_then(mbx_cache_rustc::BypassReason::remediation),
);
let _ = request_agent(&[AgentRequest::RecordBypass { kind: kind.into() }]);
}
fn record_cc_bypass(error: &eyre::Report) {
let reason = error.downcast_ref::<mbx_cache_cc::CcBypassReason>();
let kind = reason.map_or_else(
|| "cc-other".to_string(),
|reason| format!("cc-{}", reason.kind()),
);
append_bypass_log(
&kind,
error,
reason.and_then(mbx_cache_cc::CcBypassReason::remediation),
);
let _ = request_agent(&[AgentRequest::RecordBypass { kind }]);
}
pub(crate) fn record_unconsulted() {
let _ = request_agent(&[AgentRequest::RecordUnconsulted]);
}
pub(crate) fn record_compiler_invocation(
outcome: &str,
crate_name: Option<&str>,
duration_ns: u64,
) {
record_compiler_invocation_with_diagnostic(outcome, crate_name, duration_ns, None);
}
pub(crate) fn record_compiler_invocation_with_diagnostic(
outcome: &str,
crate_name: Option<&str>,
duration_ns: u64,
diagnostic: Option<mbx_cache_core::ActionDiagnostic>,
) {
let mut requests = Vec::new();
if let Some(diagnostic) = diagnostic
&& let Some(request) = action_diagnostic_request(outcome, crate_name, diagnostic)
{
requests.push(request);
}
requests.push(AgentRequest::RecordCompilerInvocation {
outcome: outcome.into(),
crate_name: crate_name.map(str::to_string),
duration_ns,
});
let _ = request_agent(&requests);
}
const ACTION_DIAGNOSTIC_PREFIX: &str = "@mbx-action-diagnostic\t";
#[derive(Serialize)]
struct ActionDiagnosticEnvelope<'a> {
outcome: &'a str,
crate_name: Option<&'a str>,
diagnostic: ActionDiagnostic,
}
pub(crate) fn action_diagnostic_request(
outcome: &str,
crate_name: Option<&str>,
diagnostic: ActionDiagnostic,
) -> Option<AgentRequest> {
let payload = serde_json::to_string(&ActionDiagnosticEnvelope {
outcome,
crate_name,
diagnostic,
})
.ok()?;
Some(AgentRequest::RecordWarning {
message: format!("{ACTION_DIAGNOSTIC_PREFIX}{payload}"),
})
}
fn append_bypass_log(kind: &str, error: &eyre::Report, remediation: Option<&str>) {
let Some(path) = std::env::var_os(BYPASS_LOG_ENV).filter(|path| !path.is_empty()) else {
return;
};
let suffix = remediation.map_or_else(String::new, |text| format!("\t{text}"));
let line = format!("{kind}\t{error:#}{suffix}\n");
if let Err(problem) = append_line(&path, &line) {
static WARNED: std::sync::Once = std::sync::Once::new();
WARNED.call_once(|| {
eprintln!(
"mbx[warning]: {BYPASS_LOG_ENV} was not written ({}): {problem}",
Path::new(&path).display()
);
});
}
}
fn append_cacheability_observation(
environment: &BTreeMap<String, String>,
kind: &str,
detail: &str,
) {
let Some(path) = environment
.get(BYPASS_LOG_ENV)
.filter(|path| !path.is_empty())
else {
return;
};
let line = format!("@observation\t{kind}\t{detail}\n");
if let Err(problem) = append_line(OsStr::new(path), &line) {
debug!("cacheability observation was not recorded: {problem}");
}
}
fn append_line(path: &OsStr, line: &str) -> std::io::Result<()> {
use std::io::Write as _;
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)?;
file.write_all(line.as_bytes())
}
pub(crate) fn request_agent(requests: &[AgentRequest]) -> Result<Vec<AgentResponse>> {
match session_socket() {
Some(socket) => request_agent_at(&socket, requests),
None => request_standalone_agent(requests),
}
}
fn session_socket() -> Option<OsString> {
std::env::var_os(SOCKET_ENV).filter(|socket| !socket.is_empty())
}
#[cfg(test)]
#[path = "session_tests.rs"]
mod tests;