use crate::materialize::{
CachedCompilation, CachedOutput, Materialization, StagedOutputs, apply_file_mode,
denormalize_output_text, executable_mode_matches, exit_code, file_mode, find_blobs,
normalize_output_text, persist_outputs, read_canonical_blob, read_verified_blob,
record_action_hit_with_diagnostic, record_verification, replay_bytes, resolve_executable,
stage_verified_cached_output, staging_directory, validate_file_mode,
};
use crate::{session, util::workspace_root};
use eyre::{Context, Result, bail};
use mbx_cache_core::{
ActionDiagnostic, ActionPrediction, AgentRequest, AgentResponse, CacheDigest, CacheDirectory,
CacheFileNode, FileDigestResolution, FileDigestScope, FileIdentity, FileSnapshot, PinnedFile,
RecordedFileDigest, RemoteActionResult, RestoreStats, RustcMetadata, canonical_json,
};
use mbx_cache_rustc::{
ActionContext, ActionInput, BypassReason, CompilerIdentity, DiscoveredInputs, LinkerIdentity,
ParseOptions, PathMapping, RustcAction, RustcDepInfo, RustcInputPrediction, RustcInvocation,
RustcOutputs, normalize_mapped_path,
};
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet};
use std::ffi::{OsStr, OsString};
use std::io::{BufRead, BufReader, Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode, Output, Stdio};
use std::sync::OnceLock;
use std::time::{Instant, SystemTime};
#[derive(Clone, Debug, Default)]
struct CompileTiming {
crate_name: String,
duration_ns: u64,
}
const HOT_STREAK_THRESHOLD: u32 = 3;
const WORKSPACE_HOT_STREAK_THRESHOLD: u32 = 1;
const CHURN_STATE_VERSION: u8 = 1;
const PRIVATE_ARTIFACT_VERSION: u8 = 1;
#[derive(Clone, Debug, Default)]
struct LearnedPlan {
hot: bool,
streak: u32,
directory: Option<PathBuf>,
record: Option<(PathBuf, CacheDigest)>,
}
impl LearnedPlan {
fn resolved(mut self, invocation: &CacheDigest, crate_name: &str) -> Self {
if self.hot {
self.directory = incremental_directory(invocation, crate_name);
}
self
}
fn record_compiled(&self) {
let Some((path, sources)) = &self.record else {
return;
};
if let Err(error) = write_churn_state(path, sources, self.streak) {
session::report_shim_warning(&format!(
"churn was not recorded for this crate: {error:#}"
));
}
}
fn engaged(&self) -> bool {
self.directory.is_some()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ChurnState {
version: u8,
sources: String,
streak: u32,
}
#[derive(Debug, Serialize, Deserialize)]
struct PrivateArtifact {
version: u8,
path: PathBuf,
}
#[derive(Serialize)]
struct BuildScriptBinaryIdentity<'a> {
digest: &'a CacheDigest,
kind: &'static str,
output_name: &'a str,
version: u8,
}
struct Compilation<'a> {
rustc: &'a OsStr,
invocation: &'a RustcInvocation,
working_dir: &'a Path,
portable: &'a Portable,
linker: Option<LinkerIdentity>,
}
pub(crate) fn compile(
rustc: &OsStr,
arguments: &[OsString],
wrapper_argument: Option<&OsStr>,
) -> Result<ExitCode> {
let setup = crate::phase_timing::phase("key");
let working_dir = std::env::current_dir()?;
let cache_native_links = session::cache_links_requested();
let expanded = RustcInvocation::expand_arguments(arguments);
let scanned = expanded.as_deref().unwrap_or(arguments);
let execution_only_build_script = session::build_script_execution_requested()
&& !cache_native_links
&& session::compiles_only_a_binary(scanned)
&& session::crate_name_argument(scanned)
.is_some_and(|name| session::is_cargo_build_script(&name));
let options =
ParseOptions::caching_native_links(cache_native_links || execution_only_build_script)
.with_custom_target_search(custom_target_may_resolve(rustc, scanned));
let arguments = with_oso_prefix(arguments, cache_native_links || execution_only_build_script);
let arguments = arguments.as_ref();
let initial_invocation = RustcInvocation::parse_with(arguments, options)?;
crate::out_dir::stabilize_for(initial_invocation.source());
let initial_outputs = initial_invocation.outputs(&working_dir)?;
let mut portable = Portable::detect(
&working_dir,
Some(&initial_outputs.directory),
initial_invocation.target(),
);
portable.map_external_native_paths(&initial_invocation, &working_dir);
let mut arguments = portable.applied_to(arguments);
if let Some(argument) = initial_invocation.portable_install_name(&initial_outputs) {
arguments.push(argument.into());
}
let invocation = RustcInvocation::parse_with(&arguments, options)?;
let outputs = invocation.outputs(&working_dir)?;
drop(setup);
if execution_only_build_script {
return compile_execution_only_build_script(
rustc,
wrapper_argument,
&arguments,
&working_dir,
&invocation,
&outputs,
&portable,
);
}
if let Some(root) = incremental_root() {
forget_private_artifacts(&root, &outputs);
}
let _verification = session::verification::select(|| {
let context = base_action_context(rustc, &working_dir, &portable)?;
Ok(invocation.invocation_digest(&context)?)
})?;
let verify = session::verify_requested();
let learned_enabled = session::learned_incremental_requested() && !verify;
let mut verification = None;
let mut action_lookup_attempted = false;
let mut current_diagnostic = None;
let compilation = Compilation {
rustc,
invocation: &invocation,
working_dir: &working_dir,
portable: &portable,
linker: linker_for(&invocation)?,
};
let private_inputs = links_private_artifact(&compilation);
let mut learned = if !verify
&& (session::eager_incremental_requested() || (learned_enabled && private_inputs))
{
eager_incremental_plan(&compilation)
} else {
reuse_hot_workspace_plan(&compilation, &outputs, learned_enabled)
};
if !private_inputs
&& !learned.engaged()
&& outputs.dep_info.is_file()
&& let Ok((candidates, discovered)) =
action_from_current_dep_info(&compilation, &outputs.dep_info)
{
current_diagnostic = candidates
.ordered()
.next()
.and_then(|action| compilation_action_diagnostic(&compilation, action).ok());
action_lookup_attempted = true;
match restore_candidates(
&candidates,
&outputs,
&discovered,
!verify,
&portable.mappings,
) {
Ok(Some((action, mut cached))) => {
match refresh_prediction(&compilation, &action, &discovered) {
Ok(timing) => {
cached.restore.avoided_compiler_duration_ns = timing.duration_ns;
}
Err(error) => {
session::report_shim_warning(&format!(
"compiler timing was not refreshed: {error:#}"
));
}
}
if learned_enabled && source_is_in_workspace(&compilation) {
record_learned_baseline(&compilation, &discovered);
}
if verify {
verification = Some(cached);
} else {
let diagnostic = candidates
.ordered()
.find(|candidate| candidate.digest == action)
.and_then(|candidate| {
compilation_action_diagnostic(&compilation, candidate).ok()
});
record_action_hit_with_diagnostic(
&action,
cached.restore,
invocation.crate_name(),
diagnostic,
);
install_build_script_shim(&invocation, &outputs, &action);
let _ = replay_bytes(&cached.stdout, &cached.stderr);
return Ok(ExitCode::SUCCESS);
}
}
Ok(None) => {
learned = plan_learned_reuse(&compilation, &discovered, learned_enabled);
}
Err(error) => {
session::report_shim_warning(&format!("result was not restored: {error:#}"));
}
}
}
let mut prediction_missing = private_inputs;
if !private_inputs && !learned.engaged() && !action_lookup_attempted {
match restore_predicted_result(
&compilation,
&outputs,
!verify,
&mut action_lookup_attempted,
learned_enabled,
&mut learned,
Some(&mut current_diagnostic),
) {
Ok(Some(cached)) => {
if verify {
verification = Some(cached);
} else {
install_build_script_shim(&invocation, &outputs, &cached.action);
let _ = replay_bytes(&cached.stdout, &cached.stderr);
return Ok(ExitCode::SUCCESS);
}
}
Ok(None) => {
prediction_missing = !action_lookup_attempted;
}
Err(error) => {
session::report_shim_warning(&format!("prediction was not restored: {error:#}"));
}
}
}
let flight = if verify || private_inputs || learned.engaged() {
None
} else {
join_flight(&compilation)
};
if let Some(flight) = &flight
&& (flight.flight.waited() || !action_lookup_attempted)
&& let Some(payload) = flight.flight.inherited()
{
match restore_flight_prediction(
&compilation,
&outputs,
&flight.invocation,
payload,
&mut action_lookup_attempted,
learned_enabled,
&mut learned,
Some(&mut current_diagnostic),
) {
Ok(Some(cached)) => {
install_build_script_shim(&invocation, &outputs, &cached.action);
let _ = replay_bytes(&cached.stdout, &cached.stderr);
return Ok(ExitCode::SUCCESS);
}
Ok(None) => {}
Err(error) => {
session::report_shim_warning(&format!(
"a flight prediction was not restored: {error:#}"
));
}
}
}
let mut remote_claim = None;
if let Some(flight) = &flight
&& !learned.engaged()
{
match session::request_agent(&[AgentRequest::JoinActionPromise {
adapter: "rustc".into(),
invocation: flight.invocation.clone(),
}]) {
Ok(responses) => match responses.into_iter().next() {
Some(AgentResponse::ActionPromise {
claim: Some(claim),
prediction: None,
}) => remote_claim = Some(claim),
Some(AgentResponse::ActionPromise {
claim: None,
prediction: Some(prediction),
}) if prediction.adapter == "rustc"
&& prediction.invocation == flight.invocation =>
{
let context = base_action_context(
compilation.rustc,
compilation.working_dir,
compilation.portable,
);
let restored = context.and_then(|context| {
restore_prediction_payload(
&compilation,
&outputs,
context,
&flight.invocation,
&prediction.payload,
Some(&prediction.action),
true,
&mut action_lookup_attempted,
learned_enabled,
&mut learned,
Some(&mut current_diagnostic),
)
});
match restored {
Ok(Some(cached)) => {
flight.flight.leave(&prediction.payload);
let _ = replay_bytes(&cached.stdout, &cached.stderr);
return Ok(ExitCode::SUCCESS);
}
Ok(None) => {}
Err(error) => session::report_shim_warning(&format!(
"a remote flight prediction was not restored: {error:#}"
)),
}
}
Some(AgentResponse::ActionPromise { .. }) => {}
Some(AgentResponse::Error { message }) => {
session::report_shim_warning(&format!(
"remote flight coordination failed: {message}"
));
}
_ => {}
},
Err(error) => {
session::report_shim_warning(&format!(
"remote flight coordination failed: {error:#}"
));
}
}
}
if prediction_missing && !action_lookup_attempted {
session::record_unconsulted();
}
let demand =
crate::scheduler::Demand::new(invocation.crate_name(), invocation.links_natively());
let permit = crate::scheduler::pool().and_then(|pool| pool.admit(&demand));
let required_inputs = invocation.required_inputs_in(&working_dir);
let source = working_dir.join(invocation.source());
let input_snapshots = crate::util::snapshot_compiler_inputs(
required_inputs.iter().map(PathBuf::as_path),
Some(&source),
);
let compilation_started = SystemTime::now();
let compiler_timer = Instant::now();
let mut command = compiler_command(rustc, wrapper_argument);
command.args(&arguments).current_dir(&working_dir);
let private_outputs = private_inputs || learned.engaged();
if let Some(directory) = learned.directory.as_deref() {
let mut flag = OsString::from("-Cincremental=");
flag.push(directory);
command.arg(flag);
}
if private_outputs {
let root = incremental_root();
let marked = root
.as_deref()
.ok_or_else(|| eyre::eyre!("no private artifact directory is available"))
.and_then(|root| record_private_artifacts(root, &outputs));
if let Err(error) = marked {
if let Some(root) = root.as_deref() {
forget_private_artifacts(root, &outputs);
}
session::report_shim_error(&format!(
"private artifacts could not be marked: {error:#}"
));
return Ok(ExitCode::FAILURE);
}
}
let forwarded = session::forward_compiler_notifications_requested();
crate::materialize::clear_linked_outputs(
outputs
.files
.iter()
.chain(std::iter::once(&outputs.dep_info))
.map(PathBuf::as_path),
);
let output = crate::phase_timing::measure("compiler", || {
run_compiler(
&mut command,
forwarded,
wrapper_argument.is_none() && !invocation.crate_name().starts_with("build_script_"),
)
})
.wrap_err("failed to execute rustc")?;
drop(permit);
crate::scheduler::record_compiler_memory(&demand, &output.status);
let timing = CompileTiming {
crate_name: invocation.crate_name().to_string(),
duration_ns: compiler_timer
.elapsed()
.as_nanos()
.try_into()
.unwrap_or(u64::MAX),
};
let recorded_outcome = match (
verification.is_some(),
learned.engaged(),
action_lookup_attempted,
) {
(true, _, _) => "verification",
(_, true, true) => "incremental-miss",
(_, true, false) => "incremental-unconsulted",
(_, false, true) => "miss",
(_, false, false) => "unconsulted",
};
if let Some(cached) = verification {
session::record_compiler_invocation_with_diagnostic(
recorded_outcome,
Some(&timing.crate_name),
timing.duration_ns,
current_diagnostic,
);
if output.status.success()
&& let Err(error) = validate_compiler_inputs(
&invocation,
&outputs,
&working_dir,
&portable,
compilation_started,
&input_snapshots,
)
{
if discard_modified_compiler_result(&outputs, &input_snapshots, &error) {
if !forwarded {
let _ = replay_bytes(&[], &output.stderr);
}
return Ok(ExitCode::FAILURE);
}
session::report_shim_warning(&format!(
"verification inputs were not validated: {error:#}"
));
}
let divergence = verification_divergence(&cached, &output);
record_verification(divergence.is_none(), cached.restore);
if let Some(divergence) = divergence {
session::report_shim_warning(&crate::materialize::verification_warning(
"rustc",
invocation.crate_name(),
&cached.action,
&divergence,
));
}
if !forwarded {
let _ = replay_output(&output);
}
return Ok(exit_code(output.status));
}
let mut compiler_input_invalid = false;
if output.status.success() {
let current_manifest_inputs =
if private_outputs && cargo_build_script_executable(&outputs, &invocation).is_none() {
current_manifest_inputs(&compilation, &outputs)
} else {
None
};
let publication: Result<Option<ActionDiagnostic>> =
if let Some(discovered) = current_manifest_inputs {
(|| {
let input_snapshots = input_snapshots
.as_ref()
.map_err(|error| eyre::eyre!(error.to_string()))?;
discovered.verify_not_modified_since_with_snapshots(
compilation_started,
input_snapshots,
)?;
discovered.verify()?;
learned.record_compiled();
Ok(None)
})()
} else {
(|| {
let input_snapshots = input_snapshots
.as_ref()
.map_err(|error| eyre::eyre!(error.to_string()))?;
let (candidates, discovered) =
action_from_dep_info(&compilation, &outputs.dep_info)?;
discovered.verify_not_modified_since_with_snapshots(
compilation_started,
input_snapshots,
)?;
discovered.verify()?;
learned.record_compiled();
if learned_enabled
&& learned.record.is_none()
&& source_is_in_workspace(&compilation)
{
record_learned_baseline(&compilation, &discovered);
}
let action = if private_outputs {
&candidates.literal
} else {
publish_result(&candidates, &outputs, &output, &portable.mappings)?
};
install_build_script_shim(&invocation, &outputs, &action.digest);
record_prediction(
&compilation,
&action.digest,
&discovered,
&timing,
flight
.as_ref()
.filter(|_| !private_outputs)
.map(|flight| &flight.flight),
remote_claim.as_deref().filter(|_| !private_outputs),
);
Ok(compilation_action_diagnostic(&compilation, action).ok())
})()
};
match publication {
Ok(diagnostic) => current_diagnostic = diagnostic,
Err(error) if discard_modified_compiler_result(&outputs, &input_snapshots, &error) => {
compiler_input_invalid = true;
}
Err(error) => {
session::report_shim_warning(&format!("result was not stored: {error:#}"));
}
}
}
session::record_compiler_invocation_with_diagnostic(
recorded_outcome,
Some(&timing.crate_name),
timing.duration_ns,
current_diagnostic,
);
if compiler_input_invalid {
if !forwarded {
let _ = replay_bytes(&[], &output.stderr);
}
Ok(ExitCode::FAILURE)
} else {
if !forwarded {
let _ = replay_output(&output);
}
Ok(exit_code(output.status))
}
}
fn run_compiler(command: &mut Command, forward: bool, eligible: bool) -> std::io::Result<Output> {
if !forward {
return crate::supervision::output(command, eligible);
}
let action = crate::supervision::prepare(command, eligible);
run_compiler_forwarding(command, std::io::stdout(), std::io::stderr(), action)
}
fn run_compiler_forwarding(
command: &mut Command,
stdout_sink: impl Write + Send + 'static,
stderr_sink: impl Write,
mut action: Option<crate::supervision::Action>,
) -> std::io::Result<Output> {
let mut child = command
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
if let Some(action) = &mut action {
action.started();
}
let stdout = child.stdout.take().expect("stdout was piped");
let stderr = child.stderr.take().expect("stderr was piped");
let stdout = std::thread::spawn(move || forward_stream(stdout, stdout_sink));
let stderr = forward_stream(stderr, stderr_sink);
let status = child.wait()?;
let stdout = stdout
.join()
.map_err(|_| std::io::Error::other("the compiler's stdout reader panicked"))??;
Ok(Output {
status,
stdout,
stderr: stderr?,
})
}
fn forward_stream(source: impl Read, mut sink: impl Write) -> std::io::Result<Vec<u8>> {
let mut reader = BufReader::new(source);
let mut captured = Vec::new();
let mut forwarding = true;
loop {
let start = captured.len();
if reader.read_until(b'\n', &mut captured)? == 0 {
break;
}
if forwarding {
forwarding = sink
.write_all(&captured[start..])
.and_then(|()| sink.flush())
.is_ok();
}
}
Ok(captured)
}
fn compiler_input_was_modified(
error: &eyre::Report,
input_snapshots: &std::io::Result<BTreeMap<PathBuf, FileSnapshot>>,
) -> bool {
match error.downcast_ref::<BypassReason>() {
Some(BypassReason::InputChanged(_)) => true,
Some(BypassReason::InputModifiedDuringCompilation(path)) => input_snapshots
.as_ref()
.ok()
.and_then(|snapshots| snapshots.get(path))
.is_some_and(FileSnapshot::proves_content_change),
_ => false,
}
}
fn discard_modified_compiler_result(
outputs: &RustcOutputs,
input_snapshots: &std::io::Result<BTreeMap<PathBuf, FileSnapshot>>,
error: &eyre::Report,
) -> bool {
if !compiler_input_was_modified(error, input_snapshots) {
return false;
}
if let Err(discard_error) = discard_compiler_outputs(outputs) {
session::report_shim_warning(&format!(
"some invalid compiler outputs could not be removed: {discard_error:#}"
));
}
session::report_shim_error(&format!("compilation result was discarded: {error:#}"));
true
}
fn validate_compiler_inputs(
invocation: &RustcInvocation,
outputs: &RustcOutputs,
working_dir: &Path,
portable: &Portable,
compilation_started: SystemTime,
input_snapshots: &std::io::Result<BTreeMap<PathBuf, FileSnapshot>>,
) -> Result<()> {
let input_snapshots = input_snapshots
.as_ref()
.map_err(|error| eyre::eyre!(error.to_string()))?;
let dep_info = RustcDepInfo::read(&outputs.dep_info)?;
let discovered = invocation.discover_inputs_with_mappings(
&dep_info,
working_dir,
&portable.mappings,
session::file_digest_cache(),
)?;
discovered.verify_not_modified_since_with_snapshots(compilation_started, input_snapshots)?;
discovered.verify()?;
Ok(())
}
fn discard_compiler_outputs(outputs: &RustcOutputs) -> Result<()> {
let mut failures = Vec::new();
for path in outputs
.files
.iter()
.chain(std::iter::once(&outputs.dep_info))
{
match std::fs::remove_file(path) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => failures.push(format!("{}: {error}", path.display())),
}
}
if !failures.is_empty() {
bail!("{}", failures.join("; "));
}
Ok(())
}
fn compile_execution_only_build_script(
rustc: &OsStr,
wrapper_argument: Option<&OsStr>,
arguments: &[OsString],
working_dir: &Path,
invocation: &RustcInvocation,
outputs: &RustcOutputs,
portable: &Portable,
) -> Result<ExitCode> {
let demand = crate::scheduler::Demand::new(invocation.crate_name(), true);
let permit = crate::scheduler::pool().and_then(|pool| pool.admit(&demand));
let required_inputs = invocation.required_inputs_in(working_dir);
let source = working_dir.join(invocation.source());
let input_snapshots = crate::util::snapshot_compiler_inputs(
required_inputs.iter().map(PathBuf::as_path),
Some(&source),
);
let compilation_started = SystemTime::now();
let started = Instant::now();
let forwarded = session::forward_compiler_notifications_requested();
let mut command = compiler_command(rustc, wrapper_argument);
command.args(arguments).current_dir(working_dir);
crate::materialize::clear_linked_outputs(
outputs
.files
.iter()
.chain(std::iter::once(&outputs.dep_info))
.map(PathBuf::as_path),
);
let output =
run_compiler(&mut command, forwarded, false).wrap_err("failed to execute rustc")?;
drop(permit);
crate::scheduler::record_compiler_memory(&demand, &output.status);
session::record_compiler_invocation(
"bypass",
Some(invocation.crate_name()),
started.elapsed().as_nanos().try_into().unwrap_or(u64::MAX),
);
if output.status.success()
&& let Err(error) = validate_compiler_inputs(
invocation,
outputs,
working_dir,
portable,
compilation_started,
&input_snapshots,
)
{
if discard_modified_compiler_result(outputs, &input_snapshots, &error) {
if !forwarded {
let _ = replay_bytes(&[], &output.stderr);
}
return Ok(ExitCode::FAILURE);
}
session::report_shim_warning(&format!(
"build-script inputs were not validated: {error:#}"
));
}
if output.status.success()
&& let Some(executable) = cargo_build_script_executable(outputs, invocation)
{
let installed = (|| -> Result<()> {
let modeled = (|| -> Result<CacheDigest> {
let compilation = Compilation {
rustc,
invocation,
working_dir,
portable,
linker: linker_for(invocation)?,
};
Ok(
action_from_current_dep_info(&compilation, &outputs.dep_info)?
.0
.literal
.digest,
)
})();
let output_name = executable
.file_name()
.and_then(OsStr::to_str)
.ok_or_else(|| eyre::eyre!("build-script output name is not UTF-8"))?;
let binary = match modeled {
Ok(action) => action,
Err(_) => {
let digest = CacheDigest::blake3_file(executable)
.wrap_err("failed to identify the build-script binary")?;
CacheDigest::blake3(&canonical_json(&BuildScriptBinaryIdentity {
digest: &digest,
kind: "build-script-binary",
output_name,
version: 1,
})?)
}
};
crate::build_script::install(executable, &binary)
})();
if let Err(error) = installed {
session::report_shim_warning(&format!(
"build-script shim was not installed: {error:#}"
));
}
}
if !forwarded {
let _ = replay_output(&output);
}
Ok(exit_code(output.status))
}
fn custom_target_may_resolve(rustc: &OsStr, arguments: &[OsString]) -> bool {
let Some(target) = flag_value(arguments, "--target") else {
return false;
};
if target.ends_with(".json") || target.contains(['/', '\\']) {
return false;
}
let under_target_path = std::env::var_os("RUST_TARGET_PATH").is_some_and(|paths| {
std::env::split_paths(&paths)
.any(|directory| directory.join(format!("{target}.json")).is_file())
});
if under_target_path {
return true;
}
let Some(sysroot) = flag_value(arguments, "--sysroot")
.map(PathBuf::from)
.or_else(|| compiler_sysroot(rustc))
else {
return true;
};
sysroot
.join("lib/rustlib")
.join(&target)
.join("target.json")
.is_file()
}
fn compiler_sysroot(rustc: &OsStr) -> Option<PathBuf> {
if let Ok(executable) = resolve_executable(rustc)
&& let Some(root) = executable.parent().and_then(Path::parent)
&& root.join("lib/rustlib").is_dir()
{
return Some(root.to_path_buf());
}
let output = Command::new(rustc)
.args(["--print", "sysroot"])
.output()
.ok()?;
output
.status
.success()
.then(|| PathBuf::from(String::from_utf8_lossy(&output.stdout).trim()))
}
fn flag_value(arguments: &[OsString], flag: &str) -> Option<String> {
let mut arguments = arguments.iter();
while let Some(argument) = arguments.next() {
if argument == flag {
return arguments.next()?.to_str().map(str::to_string);
}
if let Some(value) = argument
.to_str()
.and_then(|argument| argument.strip_prefix(flag))
.and_then(|value| value.strip_prefix('='))
{
return Some(value.to_string());
}
}
None
}
fn compiler_command(rustc: &OsStr, wrapper_argument: Option<&OsStr>) -> Command {
let mut command = Command::new(rustc);
if let Some(argument) = wrapper_argument {
command.arg(argument);
}
command
}
fn cargo_build_script_executable<'a>(
outputs: &'a RustcOutputs,
invocation: &RustcInvocation,
) -> Option<&'a Path> {
outputs
.build_script_executable(invocation.crate_name())
.filter(|_| session::is_cargo_build_script(invocation.crate_name()))
}
fn install_build_script_shim(
invocation: &RustcInvocation,
outputs: &RustcOutputs,
binary_action: &CacheDigest,
) {
if !session::build_script_execution_requested() {
return;
}
let Some(executable) = cargo_build_script_executable(outputs, invocation) else {
return;
};
if let Err(error) = crate::build_script::install(executable, binary_action) {
session::report_shim_warning(&format!("build-script shim was not installed: {error:#}"));
}
}
fn learned_plan(
recorded: Option<&ChurnState>,
sources: &CacheDigest,
enabled: bool,
threshold: u32,
) -> LearnedPlan {
let streak = match recorded {
Some(recorded) if recorded.sources == sources.key() => 0,
Some(recorded) => recorded.streak.saturating_add(1).min(threshold),
None => 0,
};
LearnedPlan {
hot: enabled && streak >= threshold,
streak,
directory: None,
record: None,
}
}
fn incremental_directory(invocation: &CacheDigest, crate_name: &str) -> Option<PathBuf> {
let root = incremental_root()?;
let key = invocation.key();
let shard = key.get(..16)?;
let directory = root.join(shard);
match prepare_incremental_directory(&directory, session::learned_incremental_max_size()) {
Ok(discarded) => {
if let Some(bytes) = discarded {
session::report_shim_warning(&format!(
"discarded {} of incremental state for {crate_name}: it passed learned_incremental_max_size, which can be raised to keep it",
bytesize::ByteSize::b(bytes).display().iec()
));
}
Some(directory)
}
Err(error) => {
session::report_shim_warning(&format!("incremental state was not prepared: {error:#}"));
None
}
}
}
fn prepare_incremental_directory(directory: &Path, budget: Option<u64>) -> Result<Option<u64>> {
let discarded = budget.and_then(|budget| {
let bytes = directory_bytes(directory);
(bytes > budget).then_some(bytes)
});
if discarded.is_some() {
std::fs::remove_dir_all(directory)
.wrap_err("failed to discard oversized incremental state")?;
}
std::fs::create_dir_all(directory).wrap_err("failed to create the incremental directory")?;
Ok(discarded)
}
fn directory_bytes(directory: &Path) -> u64 {
let mut total = 0;
let mut pending = vec![directory.to_path_buf()];
while let Some(next) = pending.pop() {
let Ok(listing) = std::fs::read_dir(&next) else {
continue;
};
for entry in listing.flatten() {
let Ok(metadata) = entry.metadata() else {
continue;
};
if metadata.is_dir() {
pending.push(entry.path());
} else if metadata.is_file() {
total += metadata.len();
}
}
}
total
}
fn plan_learned_reuse(
compilation: &Compilation<'_>,
discovered: &DiscoveredInputs,
enabled: bool,
) -> LearnedPlan {
let planned = (|| {
let sources = compilation.invocation.source_fingerprint(discovered);
let context = base_action_context(
compilation.rustc,
compilation.working_dir,
compilation.portable,
)?;
let unit = compilation.invocation.invocation_digest(&context)?;
let Some(state_path) = churn_state_path(&unit) else {
return Result::<LearnedPlan>::Ok(LearnedPlan::default());
};
let threshold = if source_is_in_workspace(compilation) {
WORKSPACE_HOT_STREAK_THRESHOLD
} else {
HOT_STREAK_THRESHOLD
};
let plan = learned_plan(
read_churn_state(&state_path).as_ref(),
&sources,
enabled,
threshold,
);
let hot = plan.hot || (enabled && links_private_artifact(compilation));
Ok(LearnedPlan {
hot,
record: Some((state_path, sources)),
..plan
}
.resolved(&unit, compilation.invocation.crate_name()))
})();
match planned {
Ok(plan) => plan,
Err(error) => {
session::report_shim_warning(&format!(
"churn was not tracked for this crate: {error:#}"
));
LearnedPlan::default()
}
}
}
fn eager_incremental_plan(compilation: &Compilation<'_>) -> LearnedPlan {
if !source_is_in_workspace(compilation) && !links_private_artifact(compilation) {
return LearnedPlan::default();
}
let planned = (|| {
let context = base_action_context(
compilation.rustc,
compilation.working_dir,
compilation.portable,
)?;
let unit = compilation.invocation.invocation_digest(&context)?;
Ok::<_, eyre::Report>(
LearnedPlan {
hot: true,
..LearnedPlan::default()
}
.resolved(&unit, compilation.invocation.crate_name()),
)
})();
planned.unwrap_or_else(|error| {
session::report_shim_warning(&format!(
"eager incremental state was not prepared: {error:#}"
));
LearnedPlan::default()
})
}
fn reuse_hot_workspace_plan(
compilation: &Compilation<'_>,
outputs: &RustcOutputs,
enabled: bool,
) -> LearnedPlan {
if !enabled || !outputs.dep_info.is_file() || !source_is_in_workspace(compilation) {
return LearnedPlan::default();
}
let planned = (|| {
let context = base_action_context(
compilation.rustc,
compilation.working_dir,
compilation.portable,
)?;
let unit = compilation.invocation.invocation_digest(&context)?;
let Some(state_path) = churn_state_path(&unit) else {
return Result::<LearnedPlan>::Ok(LearnedPlan::default());
};
let Some(recorded) = read_churn_state(&state_path) else {
return Ok(LearnedPlan::default());
};
let dep_info = RustcDepInfo::read(&outputs.dep_info)?;
verify_environment(&dep_info.environment)?;
let discovered = compilation.invocation.discover_inputs_with_mappings(
&dep_info,
compilation.working_dir,
&compilation.portable.mappings,
session::file_digest_cache(),
)?;
let sources = compilation.invocation.source_fingerprint(&discovered);
let changed = recorded.sources != sources.key();
let edited = changed && recorded.streak >= WORKSPACE_HOT_STREAK_THRESHOLD;
if !edited && !links_private_artifact(compilation) {
return Ok(LearnedPlan::default());
}
let streak = if changed {
recorded
.streak
.saturating_add(1)
.min(WORKSPACE_HOT_STREAK_THRESHOLD)
} else {
0
};
Ok(LearnedPlan {
hot: true,
streak,
directory: None,
record: Some((state_path, sources)),
}
.resolved(&unit, compilation.invocation.crate_name()))
})();
match planned {
Ok(plan) => plan,
Err(error) => {
session::report_shim_warning(&format!("incremental state was not reused: {error:#}"));
LearnedPlan::default()
}
}
}
fn current_manifest_inputs(
compilation: &Compilation<'_>,
outputs: &RustcOutputs,
) -> Option<DiscoveredInputs> {
let current = (|| -> Result<Option<DiscoveredInputs>> {
let context = base_action_context(
compilation.rustc,
compilation.working_dir,
compilation.portable,
)?;
let invocation_digest = compilation.invocation.invocation_digest(&context)?;
let responses = session::request_agent(&[AgentRequest::FindActionPrediction {
task: prediction_task(&invocation_digest),
invocation: invocation_digest.clone(),
}])?;
let Some(AgentResponse::ActionPrediction {
prediction: Some(prediction),
}) = responses.into_iter().next()
else {
return Ok(None);
};
if prediction.adapter != "rustc" || prediction.invocation != invocation_digest {
return Ok(None);
}
let recorded: RustcInputPrediction = serde_json::from_str(&prediction.payload)?;
let dep_info = RustcDepInfo::read(&outputs.dep_info)?;
let discovered = compilation.invocation.discover_inputs_with_mappings(
&dep_info,
compilation.working_dir,
&compilation.portable.mappings,
session::file_digest_cache(),
)?;
let now = compilation.invocation.prediction(&context, &discovered)?;
Ok(
(now.inputs == recorded.inputs && now.environment == recorded.environment)
.then_some(discovered),
)
})();
current.ok().flatten()
}
fn source_is_in_workspace(compilation: &Compilation<'_>) -> bool {
let Some(root) = std::env::var_os(session::WORKSPACE_ROOT_ENV).map(PathBuf::from) else {
return false;
};
let source = compilation.invocation.source();
let source = if source.is_absolute() {
source.to_path_buf()
} else {
compilation.working_dir.join(source)
};
let root = std::fs::canonicalize(&root).unwrap_or(root);
let source = std::fs::canonicalize(&source).unwrap_or(source);
source.starts_with(root)
}
fn record_learned_baseline(compilation: &Compilation<'_>, discovered: &DiscoveredInputs) {
let recorded = (|| {
let context = base_action_context(
compilation.rustc,
compilation.working_dir,
compilation.portable,
)?;
let unit = compilation.invocation.invocation_digest(&context)?;
let Some(path) = churn_state_path(&unit) else {
return Result::<()>::Ok(());
};
let sources = compilation.invocation.source_fingerprint(discovered);
if read_churn_state(&path)
.is_some_and(|recorded| recorded.sources == sources.key() && recorded.streak == 0)
{
return Ok(());
}
write_churn_state(&path, &sources, 0)
})();
if let Err(error) = recorded {
session::report_shim_warning(&format!("initial churn state was not recorded: {error:#}"));
}
}
fn churn_state_path(unit: &CacheDigest) -> Option<PathBuf> {
let root = incremental_root()?;
let key = unit.key();
let shard = key.get(..16)?;
Some(root.join(format!("{shard}.json")))
}
fn incremental_root() -> Option<PathBuf> {
std::env::var_os(session::INCREMENTAL_ROOT_ENV)
.or_else(|| {
std::env::var_os(session::TARGET_DIR_ENV).map(|target| {
PathBuf::from(target)
.join("mbx-incremental")
.into_os_string()
})
})
.map(PathBuf::from)
}
fn private_artifact_path(root: &Path, artifact: &Path) -> Option<PathBuf> {
let key = CacheDigest::blake3(artifact.as_os_str().as_encoded_bytes()).key();
let shard = key.get(..16)?;
Some(root.join("private").join(format!("{shard}.json")))
}
fn record_private_artifacts(root: &Path, outputs: &RustcOutputs) -> Result<()> {
for artifact in &outputs.files {
let Some(path) = private_artifact_path(root, artifact) else {
continue;
};
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.wrap_err("failed to create the private artifact directory")?;
}
let marker = PrivateArtifact {
version: PRIVATE_ARTIFACT_VERSION,
path: artifact.clone(),
};
crate::util::write_advisory(&path, &serde_json::to_vec(&marker)?)
.wrap_err_with(|| format!("failed to mark {} as private", artifact.display()))?;
}
Ok(())
}
fn forget_private_artifacts(root: &Path, outputs: &RustcOutputs) {
for artifact in &outputs.files {
if let Some(path) = private_artifact_path(root, artifact)
&& let Err(error) = std::fs::remove_file(&path)
&& error.kind() != std::io::ErrorKind::NotFound
{
session::report_shim_warning(&format!(
"a private artifact marker was not removed for {}: {error}",
artifact.display()
));
}
}
}
fn links_private_artifact(compilation: &Compilation<'_>) -> bool {
let Some(root) = incremental_root() else {
return false;
};
let inputs = compilation
.invocation
.required_inputs_in(compilation.working_dir);
links_private_artifact_in(&root, &inputs)
}
fn links_private_artifact_in(root: &Path, inputs: &[PathBuf]) -> bool {
inputs.iter().any(|input| {
private_artifact_path(root, input)
.and_then(|path| std::fs::read(path).ok())
.and_then(|bytes| serde_json::from_slice::<PrivateArtifact>(&bytes).ok())
.is_some_and(|marker| {
marker.version == PRIVATE_ARTIFACT_VERSION && marker.path == *input
})
})
}
fn read_churn_state(path: &Path) -> Option<ChurnState> {
let bytes = std::fs::read(path).ok()?;
serde_json::from_slice::<ChurnState>(&bytes)
.ok()
.filter(|state| state.version == CHURN_STATE_VERSION)
}
fn write_churn_state(path: &Path, sources: &CacheDigest, streak: u32) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.wrap_err("failed to create the incremental state directory")?;
}
let state = ChurnState {
version: CHURN_STATE_VERSION,
sources: sources.key(),
streak,
};
crate::util::write_advisory(path, &serde_json::to_vec(&state)?)
.wrap_err("failed to record what this crate last compiled")
}
fn restore_predicted_result(
compilation: &Compilation<'_>,
outputs: &RustcOutputs,
restore_outputs: bool,
action_lookup_attempted: &mut bool,
learned_enabled: bool,
learned: &mut LearnedPlan,
diagnostic: Option<&mut Option<ActionDiagnostic>>,
) -> Result<Option<CachedCompilation>> {
let Compilation {
invocation,
working_dir,
portable,
..
} = compilation;
let context = base_action_context(compilation.rustc, working_dir, portable)?;
let invocation_digest = invocation.invocation_digest(&context)?;
let task = prediction_task(&invocation_digest);
let responses = session::request_agent(&[AgentRequest::FindActionPrediction {
task,
invocation: invocation_digest.clone(),
}])?;
let Some(response) = responses.into_iter().next() else {
bail!("cache agent did not return an action prediction response");
};
let prediction = match response {
AgentResponse::ActionPrediction {
prediction: Some(prediction),
} => prediction,
AgentResponse::ActionPrediction { prediction: None } => {
return Ok(None);
}
AgentResponse::Error { message } => bail!(message),
_ => bail!("cache agent returned an unexpected action prediction response"),
};
if prediction.adapter != "rustc" || prediction.invocation != invocation_digest {
bail!("cache agent returned an incompatible rustc action prediction");
}
restore_prediction_payload(
compilation,
outputs,
context,
&invocation_digest,
&prediction.payload,
None,
restore_outputs,
action_lookup_attempted,
learned_enabled,
learned,
diagnostic,
)
}
#[allow(clippy::too_many_arguments)]
fn restore_prediction_payload(
compilation: &Compilation<'_>,
outputs: &RustcOutputs,
mut context: ActionContext,
invocation_digest: &CacheDigest,
payload: &str,
expected_action: Option<&CacheDigest>,
restore_outputs: bool,
action_lookup_attempted: &mut bool,
learned_enabled: bool,
learned: &mut LearnedPlan,
diagnostic: Option<&mut Option<ActionDiagnostic>>,
) -> Result<Option<CachedCompilation>> {
let _phase = crate::phase_timing::phase("key");
let Compilation {
invocation,
working_dir,
portable,
..
} = compilation;
let input_prediction: RustcInputPrediction = serde_json::from_str(payload)?;
if String::from_utf8(canonical_json(&input_prediction)?)? != payload {
bail!("the rustc action prediction is not canonical");
}
let discovered = input_prediction.discover(
working_dir,
&context.path_mappings,
session::file_digest_cache(),
)?;
discovered.clone().apply_to(&mut context)?;
let candidates = ActionCandidates::build(invocation, context, compilation.linker.clone())?;
if let Some(diagnostic) = diagnostic {
*diagnostic = candidates
.ordered()
.next()
.and_then(|action| compilation_action_diagnostic(compilation, action).ok());
}
if expected_action.is_some_and(|expected| !candidates.contains(expected)) {
bail!("the action prediction no longer matches its predicted inputs");
}
*action_lookup_attempted = true;
let restored = restore_candidates(
&candidates,
outputs,
&discovered,
restore_outputs,
&portable.mappings,
)?;
match restored {
Some((action, mut cached)) => {
cached.restore.avoided_compiler_duration_ns = input_prediction.compiler_duration_ns;
if learned_enabled && source_is_in_workspace(compilation) {
record_learned_baseline(compilation, &discovered);
}
if restore_outputs {
let diagnostic = candidates
.ordered()
.find(|candidate| candidate.digest == action)
.and_then(|candidate| {
compilation_action_diagnostic(compilation, candidate).ok()
});
record_action_hit_with_diagnostic(
&action,
cached.restore,
invocation.crate_name(),
diagnostic,
);
}
record_prediction_value(
invocation_digest.clone(),
action,
payload.to_string(),
invocation.crate_name(),
);
Ok(Some(cached))
}
None => {
if learned.record.is_none() {
*learned = plan_learned_reuse(compilation, &discovered, learned_enabled);
}
Ok(None)
}
}
}
struct ActionCandidates {
literal: RustcAction,
}
impl ActionCandidates {
fn build(
invocation: &RustcInvocation,
context: ActionContext,
linker: Option<LinkerIdentity>,
) -> Result<Self> {
Ok(Self {
literal: invocation.action_linked_by(context, linker)?,
})
}
fn contains(&self, digest: &CacheDigest) -> bool {
&self.literal.digest == digest
}
fn publishable(&self) -> &RustcAction {
&self.literal
}
fn ordered(&self) -> impl Iterator<Item = &RustcAction> {
std::iter::once(&self.literal)
}
}
fn compilation_action_diagnostic(
compilation: &Compilation<'_>,
action: &RustcAction,
) -> Result<ActionDiagnostic> {
let source = normalize_mapped_path(
compilation.invocation.source(),
compilation.working_dir,
&PathMapping::ordered(&compilation.portable.mappings),
)?;
action_diagnostic(action, &source)
}
fn action_diagnostic(action: &RustcAction, source: &str) -> Result<ActionDiagnostic> {
let mut descriptor: serde_json::Map<String, serde_json::Value> =
serde_json::from_slice(&action.bytes)?;
let mut components = BTreeMap::new();
if let Some(serde_json::Value::Object(compiler)) = descriptor.remove("compiler") {
for (name, value) in compiler {
components.insert(
format!("compiler {}", name.replace('_', " ")),
CacheDigest::blake3(&canonical_json(&value)?),
);
}
}
if let Some(serde_json::Value::Array(arguments)) = descriptor.remove("arguments") {
let unit_arguments = arguments
.iter()
.filter(|value| {
value.as_str().is_some_and(|argument| {
argument == "--test"
|| ["--crate-name=", "--crate-type=", "--target="]
.iter()
.any(|prefix| argument.starts_with(prefix))
})
})
.collect::<Vec<_>>();
components.insert(
"compilation unit".into(),
CacheDigest::blake3(&canonical_json(&(source, unit_arguments))?),
);
let mut occurrences = BTreeMap::<String, usize>::new();
for (index, value) in arguments.into_iter().enumerate() {
let base = value
.as_str()
.filter(|argument| argument.starts_with('-'))
.map(|argument| {
let argument = argument.trim_start_matches('-');
let (flag, value) = argument.split_once('=').unwrap_or((argument, ""));
let nested = matches!(flag, "C" | "Z" | "codegen")
.then(|| value.split_once('=').map_or(value, |(name, _)| name))
.filter(|name| !name.is_empty());
nested.map_or_else(
|| format!("argument --{flag}"),
|name| format!("argument --{flag} {name}"),
)
})
.unwrap_or_else(|| format!("argument #{}", index + 1));
let occurrence = occurrences.entry(base.clone()).or_default();
*occurrence += 1;
let name = if *occurrence == 1 {
base
} else {
format!("{base} #{}", *occurrence)
};
components.insert(name, CacheDigest::blake3(&canonical_json(&value)?));
}
}
if let Some(serde_json::Value::Object(linker)) = descriptor.remove("linker") {
for (name, value) in linker {
components.insert(
format!("linker {}", name.replace('_', " ")),
CacheDigest::blake3(&canonical_json(&value)?),
);
}
}
if let Some(serde_json::Value::Object(environment)) = descriptor.remove("environment") {
for (name, value) in environment {
components.insert(
format!("environment {name}"),
CacheDigest::blake3(&canonical_json(&value)?),
);
}
}
let mut inputs = BTreeMap::new();
if let Some(serde_json::Value::Array(entries)) = descriptor.remove("inputs") {
for entry in entries {
let Some(entry) = entry.as_object() else {
continue;
};
let Some(path) = entry.get("path").and_then(serde_json::Value::as_str) else {
continue;
};
let Some(digest) = entry.get("digest") else {
continue;
};
inputs.insert(path.to_string(), serde_json::from_value(digest.clone())?);
}
}
components.insert(
"action model".into(),
CacheDigest::blake3(&canonical_json(&descriptor)?),
);
Ok(ActionDiagnostic {
action: action.digest.clone(),
components,
inputs,
})
}
fn restore_candidates(
candidates: &ActionCandidates,
outputs: &RustcOutputs,
discovered: &DiscoveredInputs,
restore_outputs: bool,
mappings: &[PathMapping],
) -> Result<Option<(CacheDigest, CachedCompilation)>> {
for action in candidates.ordered() {
if let Some(cached) =
restore_result(action, outputs, discovered, restore_outputs, mappings)?
{
return Ok(Some((action.digest.clone(), cached)));
}
}
Ok(None)
}
fn action_from_dep_info(
compilation: &Compilation<'_>,
dep_info: &Path,
) -> Result<(ActionCandidates, DiscoveredInputs)> {
let dep_info = RustcDepInfo::read(dep_info)?;
action_from_parsed_dep_info(compilation, &dep_info)
}
fn action_from_current_dep_info(
compilation: &Compilation<'_>,
dep_info: &Path,
) -> Result<(ActionCandidates, DiscoveredInputs)> {
let dep_info = RustcDepInfo::read(dep_info)?;
verify_environment(&dep_info.environment)?;
action_from_parsed_dep_info(compilation, &dep_info)
}
fn action_from_parsed_dep_info(
compilation: &Compilation<'_>,
dep_info: &RustcDepInfo,
) -> Result<(ActionCandidates, DiscoveredInputs)> {
let _phase = crate::phase_timing::phase("key");
let Compilation {
invocation,
working_dir,
portable,
..
} = compilation;
let discovered = invocation.discover_inputs_with_mappings(
dep_info,
working_dir,
&portable.mappings,
session::file_digest_cache(),
)?;
let mut context = base_action_context(compilation.rustc, working_dir, portable)?;
discovered.clone().apply_to(&mut context)?;
let candidates = ActionCandidates::build(invocation, context, compilation.linker.clone())?;
Ok((candidates, discovered))
}
fn base_action_context(
rustc: &OsStr,
working_dir: &Path,
portable: &Portable,
) -> Result<ActionContext> {
let _phase = crate::phase_timing::phase("key");
let compiler = compiler_identity(rustc)?;
let mut context = ActionContext {
compiler,
working_dir: working_dir.to_path_buf(),
path_mappings: portable.mappings.clone(),
environment: compiler_environment(|name| std::env::var(name).ok()),
portable_environment: BTreeSet::new(),
inputs: Vec::new(),
};
if Path::new(rustc).file_stem() == Some(OsStr::new("clippy-driver"))
&& let Some(path) = selected_clippy_config(working_dir)
{
context.inputs.push(ActionInput {
digest: CacheDigest::blake3_file(&path)?,
path,
});
}
Ok(context)
}
const COMPILER_ENVIRONMENT: [&str; 1] = ["RUSTC_BOOTSTRAP"];
fn compiler_environment(
lookup: impl Fn(&str) -> Option<String>,
) -> BTreeMap<String, Option<String>> {
COMPILER_ENVIRONMENT
.into_iter()
.filter_map(|name| lookup(name).map(|value| (name.into(), Some(value))))
.collect()
}
fn selected_clippy_config(working_dir: &Path) -> Option<PathBuf> {
let start = std::env::var_os("CLIPPY_CONF_DIR")
.or_else(|| std::env::var_os("CARGO_MANIFEST_DIR"))
.map_or_else(|| working_dir.to_path_buf(), PathBuf::from);
let mut directory = std::fs::canonicalize(start).ok()?;
loop {
for name in [".clippy.toml", "clippy.toml"] {
let path = directory.join(name);
if path.is_file() {
return Some(path);
}
}
if !directory.pop() {
return None;
}
}
}
fn with_oso_prefix(arguments: &[OsString], cache_links: bool) -> Cow<'_, [OsString]> {
if !cfg!(target_os = "macos") || !cache_links {
return Cow::Borrowed(arguments);
}
let mut out_dir = None;
for (index, argument) in arguments.iter().enumerate() {
let Some(argument) = argument.to_str() else {
continue;
};
if argument.contains("-oso_prefix,") {
return Cow::Borrowed(arguments);
}
if argument == "--target" || argument.starts_with("--target=") {
return Cow::Borrowed(arguments);
}
if argument == "--out-dir" {
out_dir = arguments.get(index + 1).map(PathBuf::from);
} else if let Some(value) = argument.strip_prefix("--out-dir=") {
out_dir = Some(PathBuf::from(value));
}
}
let Some(out_dir) = out_dir.filter(|out_dir| out_dir.is_absolute()) else {
return Cow::Borrowed(arguments);
};
let prefix = std::env::var_os(session::TARGET_DIR_ENV)
.map(PathBuf::from)
.filter(|root| root.is_absolute() && out_dir.starts_with(root))
.unwrap_or_else(|| standalone_target_root(&out_dir, None));
let prefix = std::fs::canonicalize(&prefix).unwrap_or(prefix);
let mut extended = arguments.to_vec();
let native_output = arguments.iter().enumerate().any(|(index, argument)| {
argument == "--test"
|| matches!(
argument.to_str(),
Some("--crate-type=bin" | "--crate-type=proc-macro")
)
|| (argument == "--crate-type"
&& arguments
.get(index + 1)
.is_some_and(|value| matches!(value.to_str(), Some("bin" | "proc-macro"))))
});
if native_output && let Ok(output) = std::fs::canonicalize(&out_dir) {
for index in 0..extended.len() {
if extended[index] == "--out-dir" {
if let Some(value) = extended.get_mut(index + 1) {
*value = output.as_os_str().to_owned();
}
} else if extended[index]
.to_str()
.is_some_and(|arg| arg.starts_with("--out-dir="))
{
let mut value = OsString::from("--out-dir=");
value.push(&output);
extended[index] = value;
}
}
}
extended.push(format!("-Clink-arg=-Wl,-oso_prefix,{}/", prefix.display()).into());
Cow::Owned(extended)
}
fn linker_for(invocation: &RustcInvocation) -> Result<Option<LinkerIdentity>> {
if !invocation.links_natively() {
return Ok(None);
}
match crate::linker::identity_for(invocation.linker_override(), invocation.fuse_ld()) {
Ok(identity) => Ok(Some(identity)),
Err(error) => Err(BypassReason::UnportableNativeLink(format!(
"the linker could not be identified: {error:#}"
))
.into()),
}
}
fn record_prediction(
compilation: &Compilation<'_>,
action: &CacheDigest,
discovered: &DiscoveredInputs,
timing: &CompileTiming,
flight: Option<&crate::scheduler::Flight>,
remote_claim: Option<&str>,
) {
let _phase = crate::phase_timing::phase("predict");
let result = (|| {
let invocation = compilation.invocation;
let context = base_action_context(
compilation.rustc,
compilation.working_dir,
compilation.portable,
)?;
let invocation_digest = invocation.invocation_digest(&context)?;
let mut prediction = invocation.prediction(&context, discovered)?;
prediction.compiler_duration_ns = timing.duration_ns;
prediction.crate_name.clone_from(&timing.crate_name);
let payload = String::from_utf8(canonical_json(&prediction)?)?;
if let Some(flight) = flight {
flight.leave(&payload);
}
record_prediction_value(
invocation_digest.clone(),
action.clone(),
payload.clone(),
invocation.crate_name(),
);
if let Some(claim) = remote_claim {
let prediction = ActionPrediction {
invocation: invocation_digest,
action: action.clone(),
adapter: "rustc".into(),
payload,
};
let _ = session::request_agent(&[AgentRequest::CompleteActionPromise {
claim: claim.to_string(),
prediction,
}]);
}
Result::<()>::Ok(())
})();
if let Err(error) = result {
warn_prediction_not_recorded(compilation.invocation.crate_name(), &error);
}
}
struct InvocationFlight {
flight: crate::scheduler::Flight,
invocation: CacheDigest,
}
fn join_flight(compilation: &Compilation<'_>) -> Option<InvocationFlight> {
let invocation = (|| -> Result<CacheDigest> {
let context = base_action_context(
compilation.rustc,
compilation.working_dir,
compilation.portable,
)?;
Ok(compilation.invocation.invocation_digest(&context)?)
})()
.ok()?;
let flight = crate::scheduler::flight("rustc", &invocation.hash)?;
Some(InvocationFlight { flight, invocation })
}
#[allow(clippy::too_many_arguments)]
fn restore_flight_prediction(
compilation: &Compilation<'_>,
outputs: &RustcOutputs,
invocation_digest: &CacheDigest,
payload: &str,
action_lookup_attempted: &mut bool,
learned_enabled: bool,
learned: &mut LearnedPlan,
diagnostic: Option<&mut Option<ActionDiagnostic>>,
) -> Result<Option<CachedCompilation>> {
let context = base_action_context(
compilation.rustc,
compilation.working_dir,
compilation.portable,
)?;
restore_prediction_payload(
compilation,
outputs,
context,
invocation_digest,
payload,
None,
true,
action_lookup_attempted,
learned_enabled,
learned,
diagnostic,
)
}
fn refresh_prediction(
compilation: &Compilation<'_>,
action: &CacheDigest,
discovered: &DiscoveredInputs,
) -> Result<CompileTiming> {
let context = base_action_context(
compilation.rustc,
compilation.working_dir,
compilation.portable,
)?;
let invocation_digest = compilation.invocation.invocation_digest(&context)?;
let task = prediction_task(&invocation_digest);
let responses = session::request_agent(&[AgentRequest::FindActionPrediction {
task,
invocation: invocation_digest.clone(),
}])?;
let Some(response) = responses.into_iter().next() else {
bail!("cache agent did not return an action prediction response");
};
let stored = match response {
AgentResponse::ActionPrediction { prediction } => prediction,
AgentResponse::Error { message } => bail!(message),
_ => bail!("cache agent returned an unexpected action prediction response"),
};
let timing = match &stored {
Some(stored) => decode_prediction_timing(stored, &invocation_digest)?,
None => CompileTiming::default(),
};
let recorded = (|| {
let mut prediction = compilation.invocation.prediction(&context, discovered)?;
prediction.compiler_duration_ns = timing.duration_ns;
prediction.crate_name.clone_from(&timing.crate_name);
let payload = String::from_utf8(canonical_json(&prediction)?)?;
record_prediction_value(
invocation_digest,
action.clone(),
payload,
compilation.invocation.crate_name(),
);
Result::<()>::Ok(())
})();
if let Err(error) = recorded {
warn_prediction_not_recorded(compilation.invocation.crate_name(), &error);
}
Ok(timing)
}
fn decode_prediction_timing(
prediction: &ActionPrediction,
invocation: &CacheDigest,
) -> Result<CompileTiming> {
if prediction.adapter != "rustc" || prediction.invocation != *invocation {
bail!("cache agent returned an incompatible rustc timing prediction");
}
let timing: RustcInputPrediction = serde_json::from_str(&prediction.payload)?;
if !matches!(timing.version, 2..=4)
|| timing.crate_name.len() > 256
|| timing.crate_name.contains(['\0', '\n', '\r'])
|| String::from_utf8(canonical_json(&timing)?)? != prediction.payload
{
bail!("cache agent returned an invalid rustc timing prediction");
}
Ok(CompileTiming {
crate_name: timing.crate_name,
duration_ns: timing.compiler_duration_ns,
})
}
fn record_prediction_value(
invocation: CacheDigest,
action: CacheDigest,
payload: String,
crate_name: &str,
) {
let result = (|| {
let task = prediction_task(&invocation);
let responses = session::request_agent(&[AgentRequest::RecordActionPrediction {
task,
prediction: ActionPrediction {
invocation,
action,
adapter: "rustc".into(),
payload,
},
}])?;
match responses.into_iter().next() {
Some(AgentResponse::ActionPredictionRecorded) => Ok(()),
Some(AgentResponse::Error { message }) => bail!(message),
_ => bail!("cache agent returned an unexpected prediction response"),
}
})();
if let Err(error) = result {
warn_prediction_not_recorded(crate_name, &error);
}
}
fn warn_prediction_not_recorded(crate_name: &str, error: &eyre::Report) {
session::report_shim_warning(&format!(
"action prediction for {crate_name} was not recorded: {error:#}"
));
}
fn prediction_task(invocation: &CacheDigest) -> String {
std::env::var(session::BUILD_ENV).unwrap_or_else(|_| {
let shard = invocation.hash.get(..2).unwrap_or(&invocation.hash);
CacheDigest::blake3(format!("standalone-predictions-v1\0{shard}").as_bytes()).hash
})
}
fn verify_environment(environment: &BTreeMap<String, Option<String>>) -> Result<()> {
for (name, expected) in environment {
let actual = std::env::var_os(name)
.map(|value| {
value.into_string().map_err(|_| {
eyre::eyre!("compiler environment input is not valid UTF-8: {name}")
})
})
.transpose()?;
if &actual != expected {
bail!("compiler environment input changed: {name}");
}
}
Ok(())
}
fn restore_result(
action: &RustcAction,
outputs: &RustcOutputs,
discovered: &DiscoveredInputs,
restore_outputs: bool,
mappings: &[PathMapping],
) -> Result<Option<CachedCompilation>> {
let _phase = crate::phase_timing::phase("restore");
let responses = session::request_agent(&[AgentRequest::FindActionResult {
action: action.digest.clone(),
}])?;
let Some(response) = responses.into_iter().next() else {
bail!("cache agent did not return an action lookup response");
};
let result = match response {
AgentResponse::ActionResult { result } => match result {
Some(result) => result,
None => return Ok(None),
},
AgentResponse::Error { message } => bail!(message),
_ => bail!("cache agent returned an unexpected action lookup response"),
};
if result.version != 1 || result.action != action.digest {
bail!("cached rustc action result has an invalid identity");
}
let metadata_digest = result
.metadata
.ok_or_else(|| eyre::eyre!("cached rustc action result has no metadata"))?;
let output_root_digest = result
.output_root
.ok_or_else(|| eyre::eyre!("cached rustc action result has no output root"))?;
let roots = find_blobs(&[
action.digest.clone(),
metadata_digest.clone(),
output_root_digest.clone(),
])?;
let cached_action = read_verified_blob(&roots[0], &action.digest, "action descriptor")?;
if cached_action != action.bytes {
bail!("cached rustc action descriptor does not match the invocation");
}
let metadata: RustcMetadata =
read_canonical_blob(&roots[1], &metadata_digest, "rustc metadata")?;
if metadata.version != 1 || metadata.kind != "rustc" {
bail!("cached rustc metadata is unsupported");
}
let directory: CacheDirectory =
read_canonical_blob(&roots[2], &output_root_digest, "output directory")?;
let files = validated_outputs(directory, outputs)?;
let restored_output_files = files.len().try_into().unwrap_or(u64::MAX);
let restored_output_bytes = files.iter().fold(0_u64, |total, (node, _)| {
total.saturating_add(node.digest.size)
});
let mut digests = vec![metadata.stdout.clone(), metadata.stderr.clone()];
digests.extend(files.iter().map(|(node, _)| node.digest.clone()));
let blobs = find_blobs(&digests)?;
let stdout = denormalize_output_text(
&read_verified_blob(&blobs[0], &metadata.stdout, "stdout")?,
mappings,
);
let stderr = denormalize_output_text(
&read_verified_blob(&blobs[1], &metadata.stderr, "stderr")?,
mappings,
);
let materialization_started = Instant::now();
std::fs::create_dir_all(&outputs.directory)?;
let staging = tempfile::tempdir_in(&outputs.directory)?;
let mut staged = Vec::with_capacity(files.len());
let mut restore = RestoreStats {
output_files: restored_output_files,
output_bytes: restored_output_bytes,
..RestoreStats::default()
};
let mut cached_outputs = Vec::with_capacity(files.len());
for (index, ((node, destination), source)) in files.into_iter().zip(&blobs[2..]).enumerate() {
if destination == outputs.dep_info {
let bytes = denormalize_output_text(
&read_verified_blob(source, &node.digest, "dep-info")?,
mappings,
);
let digest = CacheDigest::blake3(&bytes);
cached_outputs.push(CachedOutput {
path: destination.clone(),
digest: digest.clone(),
executable: node.executable,
mode: node.mode,
});
if restore_outputs
&& std::fs::read(&destination).is_ok_and(|existing| existing == bytes)
{
mark_output_restored(&destination)?;
restore.reused_output_files = restore.reused_output_files.saturating_add(1);
restore.reused_output_bytes = restore
.reused_output_bytes
.saturating_add(bytes.len().try_into().unwrap_or(u64::MAX));
continue;
}
let temporary = staging.path().join(format!("output-{index}"));
std::fs::write(&temporary, &bytes)?;
let temporary = tempfile::TempPath::try_from_path(temporary)?;
apply_file_mode(&temporary, node.mode, node.executable)?;
restore.copied_output_files = restore.copied_output_files.saturating_add(1);
restore.copied_output_bytes = restore
.copied_output_bytes
.saturating_add(bytes.len().try_into().unwrap_or(u64::MAX));
staged.push((temporary, destination));
continue;
}
cached_outputs.push(CachedOutput {
path: destination.clone(),
digest: node.digest.clone(),
executable: node.executable,
mode: node.mode,
});
if restore_outputs
&& output_already_in_place(&node, &destination, session::file_digest_cache())
{
mark_output_restored(&destination)?;
restore.reused_output_files = restore.reused_output_files.saturating_add(1);
restore.reused_output_bytes =
restore.reused_output_bytes.saturating_add(node.digest.size);
continue;
}
let (temporary, materialization) =
stage_verified_cached_output(staging.path(), index, source, &node)?;
match materialization {
Materialization::Reflink => {
restore.reflinked_output_files = restore.reflinked_output_files.saturating_add(1);
restore.reflinked_output_bytes = restore
.reflinked_output_bytes
.saturating_add(node.digest.size);
}
Materialization::Hardlink => {
restore.hardlinked_output_files = restore.hardlinked_output_files.saturating_add(1);
restore.hardlinked_output_bytes = restore
.hardlinked_output_bytes
.saturating_add(node.digest.size);
}
Materialization::Copy => {
restore.copied_output_files = restore.copied_output_files.saturating_add(1);
restore.copied_output_bytes =
restore.copied_output_bytes.saturating_add(node.digest.size);
}
}
staged.push((temporary, destination));
}
let staged = StagedOutputs {
directory: staging,
files: staged,
};
verify_environment(&discovered.environment)?;
finalize_restored_outputs(staged, restore_outputs)?;
if restore_outputs {
record_output_digests(&cached_outputs);
}
restore.duration_ns = materialization_started
.elapsed()
.as_nanos()
.try_into()
.unwrap_or(u64::MAX);
Ok(Some(CachedCompilation {
action: action.digest.clone(),
stdout,
stderr,
outputs: cached_outputs,
restore,
}))
}
fn mark_output_restored(path: &Path) -> Result<()> {
crate::materialize::set_modified_now(path)
}
pub(crate) fn output_already_in_place(
node: &CacheFileNode,
destination: &Path,
digests: &dyn mbx_cache_core::FileDigestCache,
) -> bool {
let Ok(metadata) = std::fs::metadata(destination) else {
return false;
};
if !metadata.is_file()
|| metadata.len() != node.digest.size
|| !executable_mode_matches(&metadata, node.executable)
{
return false;
}
if let Ok(Some(identity)) = FileIdentity::for_digest_cache(destination, &metadata) {
match digests.resolve(FileDigestScope::Content, &[identity]).pop() {
Some(FileDigestResolution::Digest(recorded)) => return recorded == node.digest,
Some(FileDigestResolution::EmbeddedTimestampMacro) => return false,
Some(FileDigestResolution::Unresolved) | None => {}
}
}
CacheDigest::blake3_file(destination).is_ok_and(|digest| digest == node.digest)
}
fn record_output_digests(outputs: &[CachedOutput]) {
let entries = outputs
.iter()
.filter_map(|output| {
let metadata = std::fs::metadata(&output.path).ok()?;
if metadata.len() != output.digest.size {
return None;
}
Some(RecordedFileDigest {
file: FileIdentity::for_digest_cache(&output.path, &metadata).ok()??,
digest: output.digest.clone(),
})
})
.collect::<Vec<_>>();
session::record_file_digests(FileDigestScope::Content, entries);
}
fn finalize_restored_outputs(staged: StagedOutputs, restore_outputs: bool) -> Result<()> {
if restore_outputs {
persist_outputs(staged)?;
}
Ok(())
}
fn verification_divergence(cached: &CachedCompilation, output: &Output) -> Option<String> {
if !output.status.success() {
return Some("the shadow compilation failed".into());
}
if let Some(difference) =
crate::materialize::stream_divergence("standard output", &cached.stdout, &output.stdout)
.or_else(|| {
crate::materialize::stream_divergence(
"standard error",
&cached.stderr,
&output.stderr,
)
})
{
return Some(difference);
}
for expected in &cached.outputs {
let name = expected.path.display();
let Ok(metadata) = std::fs::metadata(&expected.path) else {
return Some(format!("{name} is missing"));
};
if file_mode(&metadata) != expected.mode {
return Some(format!("{name} has a different file mode"));
}
if !executable_mode_matches(&metadata, expected.executable) {
return Some(format!("{name} has a different executable bit"));
}
if !expected
.digest
.matches_file(&expected.path)
.unwrap_or(false)
{
return Some(format!("{name} has different contents"));
}
}
None
}
fn validated_outputs(
directory: CacheDirectory,
outputs: &RustcOutputs,
) -> Result<Vec<(CacheFileNode, PathBuf)>> {
if directory.version != 1 || !directory.directories.is_empty() || !directory.symlinks.is_empty()
{
bail!("cached rustc output directory has unsupported entries");
}
let mut expected = outputs
.files
.iter()
.chain(std::iter::once(&outputs.dep_info))
.map(|path| {
let name = path
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| eyre::eyre!("expected rustc output name is not UTF-8"))?;
if path.parent() != Some(outputs.directory.as_path()) {
bail!("expected rustc output escapes its output directory");
}
Ok((
name.to_string(),
(path.clone(), outputs.is_executable(path)),
))
})
.collect::<Result<BTreeMap<_, _>>>()?;
if directory.files.len() != expected.len() {
bail!("cached rustc output set does not match the invocation");
}
let mut files = Vec::with_capacity(directory.files.len());
for node in directory.files {
let (destination, executable) = expected
.remove(&node.name)
.ok_or_else(|| eyre::eyre!("cached rustc output is unexpected: {}", node.name))?;
validate_file_mode(&node, executable)?;
files.push((node, destination));
}
if !expected.is_empty() {
bail!("cached rustc output set is incomplete");
}
Ok(files)
}
fn compiler_identity(rustc: &OsStr) -> Result<CompilerIdentity> {
let _phase = crate::phase_timing::phase("key");
static IDENTITY: OnceLock<(OsString, CompilerIdentity)> = OnceLock::new();
if let Some((known, identity)) = IDENTITY.get()
&& known.as_os_str() == rustc
{
return Ok(identity.clone());
}
let identity = query_compiler_identity(rustc)?;
let _ = IDENTITY.set((rustc.to_os_string(), identity.clone()));
Ok(identity)
}
fn query_compiler_identity(rustc: &OsStr) -> Result<CompilerIdentity> {
let executable = resolve_executable(rustc)?;
let clippy = Path::new(rustc).file_stem() == Some(OsStr::new("clippy-driver"));
let environment = ["RUSTUP_HOME", "RUSTUP_TOOLCHAIN"]
.into_iter()
.map(|name| (name.into(), std::env::var(name).ok()))
.collect::<BTreeMap<_, _>>();
let responses = session::request_agent(&[AgentRequest::FindExecutableIdentity {
executable: executable.clone(),
environment: environment.clone(),
}])?;
let Some(AgentResponse::ExecutableIdentity { stdout }) = responses.into_iter().next() else {
bail!("cache agent did not return the rustc identity");
};
let stdout = if let Some(stdout) = stdout {
stdout
} else {
let mut pins = compiler_identity_pins(&executable);
let mut command = Command::new(&executable);
command.arg("-vV");
for (name, value) in &environment {
if let Some(value) = value {
command.env(name, value);
} else {
command.env_remove(name);
}
}
let output = crate::phase_timing::measure("compiler", || command.output())
.wrap_err("failed to query the rustc identity")?;
if !output.status.success() {
bail!(
"rustc identity command failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let mut stdout = output.stdout;
let host = identity_field(&String::from_utf8_lossy(&stdout), "host")?.to_string();
if clippy {
let mut command = Command::new(&executable);
command.arg("--version");
for (name, value) in &environment {
if let Some(value) = value {
command.env(name, value);
} else {
command.env_remove(name);
}
}
let output = crate::phase_timing::measure("compiler", || command.output())
.wrap_err("failed to query the clippy-driver identity")?;
if !output.status.success() {
bail!(
"clippy-driver identity command failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
stdout.extend_from_slice(b"\nmbx-driver: ");
stdout.extend_from_slice(output.stdout.trim_ascii());
stdout.push(b'\n');
}
let (backend_pins, backends) = codegen_backends(rustc, &host)?;
if !pins.is_empty() {
pins.extend(backend_pins);
}
stdout.extend_from_slice(backends.as_bytes());
let responses = session::request_agent(&[AgentRequest::StoreExecutableIdentity {
executable,
environment,
stdout,
pins,
}])?;
let Some(AgentResponse::ExecutableIdentity {
stdout: Some(stdout),
}) = responses.into_iter().next()
else {
bail!("cache agent did not store the rustc identity");
};
stdout
};
let verbose = std::str::from_utf8(&stdout).wrap_err("rustc identity is not UTF-8")?;
let release = identity_field(verbose, "release")?;
let host = identity_field(verbose, "host")?;
let rustc_version = verbose
.lines()
.filter(|line| {
line.starts_with("rustc ")
|| line.starts_with("commit-hash:")
|| line.starts_with("commit-date:")
|| line.starts_with("LLVM version:")
|| line.starts_with("mbx-codegen-backend:")
})
.collect::<Vec<_>>()
.join("; ");
if rustc_version.is_empty() {
bail!("rustc identity is missing its version");
}
let toolchain = std::env::var("RUSTUP_TOOLCHAIN")
.ok()
.filter(|value| !value.is_empty())
.unwrap_or_else(|| release.to_string());
Ok(CompilerIdentity {
toolchain,
rustc_version,
host: host.to_string(),
driver: verbose
.lines()
.find_map(|line| line.strip_prefix("mbx-driver: "))
.filter(|value| !value.is_empty())
.map(str::to_string),
})
}
fn compiler_identity_pins(executable: &Path) -> Vec<PinnedFile> {
let Some(driver) = compiler_driver_library(executable) else {
return Vec::new();
};
match (
PinnedFile::describe(executable),
PinnedFile::describe(&driver),
) {
(Some(executable), Some(driver)) => vec![executable, driver],
_ => Vec::new(),
}
}
fn codegen_backends(rustc: &OsStr, host: &str) -> Result<(Vec<PinnedFile>, String)> {
let mut pins = Vec::new();
let mut identity = String::new();
let Some(sysroot) = compiler_sysroot(rustc) else {
return Ok((pins, identity));
};
let directory = sysroot
.join("lib/rustlib")
.join(host)
.join("codegen-backends");
pins.extend(PinnedFile::describe(directory.clone()));
for (path, metadata) in resolved_entries(&directory)? {
if !metadata.is_file() {
continue;
}
pins.extend(PinnedFile::describe(path.clone()));
let digest = CacheDigest::blake3_file(&path)?;
let name = path.file_name().unwrap_or_default();
identity += &format!("\nmbx-codegen-backend: {:?} {}\n", name, digest.hash);
}
Ok((pins, identity))
}
fn resolved_entries(directory: &Path) -> Result<Vec<(PathBuf, std::fs::Metadata)>> {
let entries = match std::fs::read_dir(directory) {
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
entries => entries?,
};
let mut resolved = Vec::new();
for entry in entries {
let path = entry?.path();
match std::fs::metadata(&path) {
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
metadata => resolved.push((path, metadata?)),
}
}
resolved.sort_by(|(left, _), (right, _)| left.cmp(right));
Ok(resolved)
}
fn compiler_driver_library(executable: &Path) -> Option<PathBuf> {
let bin = executable.parent()?;
let mut directories = vec![bin.to_path_buf()];
if let Some(root) = bin.parent() {
directories.push(root.join("lib"));
}
directories.into_iter().find_map(|directory| {
std::fs::read_dir(directory)
.ok()?
.flatten()
.find_map(|entry| {
let name = entry.file_name();
let name = name.to_string_lossy();
let library =
name.starts_with("librustc_driver") || name.starts_with("rustc_driver");
(library && entry.file_type().is_ok_and(|kind| kind.is_file()))
.then(|| entry.path())
})
})
}
fn identity_field<'a>(verbose: &'a str, field: &str) -> Result<&'a str> {
verbose
.lines()
.find_map(|line| line.strip_prefix(&format!("{field}: ")))
.filter(|value| !value.is_empty())
.ok_or_else(|| eyre::eyre!("rustc identity is missing {field}"))
}
const PORTABLE_ENVIRONMENT: &[&str] = &["OUT_DIR"];
struct Portable {
mappings: Vec<PathMapping>,
arguments: Vec<OsString>,
}
impl Portable {
fn map_external_native_paths(&mut self, invocation: &RustcInvocation, working_dir: &Path) {
for path in invocation.native_search_paths() {
if normalize_mapped_path(path, working_dir, &self.mappings).is_ok() {
continue;
}
let path = working_dir.join(path);
let mapping = mbx_cache_core::PathMapping::new(&path, "");
let identity = mbx_cache_core::resolve_path_mappings(&[mapping])
.remove(0)
.root;
let Some(text) = identity.to_str().filter(|_| path.is_absolute()) else {
continue;
};
let placeholder = format!("native_{}", CacheDigest::blake3(text.as_bytes()).hash);
self.mappings.push(PathMapping::new(path, placeholder));
}
self.mappings = PathMapping::ordered(&self.mappings);
}
fn detect(working_dir: &Path, target_output: Option<&Path>, target: Option<&str>) -> Self {
let mut portable = Self {
mappings: PathMapping::ordered(&path_mappings(working_dir, target_output, target)),
arguments: Vec::new(),
};
portable.map_workspace_root();
if !session::share_out_dir_requested() {
return portable;
}
for name in PORTABLE_ENVIRONMENT {
let Some(value) = std::env::var(name)
.ok()
.filter(|value| Path::new(value).is_absolute())
else {
continue;
};
let Ok(placeholder) =
normalize_mapped_path(Path::new(&value), working_dir, &portable.mappings)
else {
continue;
};
let mut flag = OsString::from("--remap-path-prefix=");
flag.push(&value);
flag.push("=");
flag.push(&placeholder);
portable.arguments.push(flag);
}
portable
}
fn map_workspace_root(&mut self) {
if !session::share_workspace_root_requested() {
return;
}
let Some(mapping) = self
.mappings
.iter()
.find(|mapping| mapping.placeholder == "workspace")
else {
return;
};
let mut flag = OsString::from("--remap-path-prefix=");
flag.push(&mapping.root);
flag.push("=");
flag.push(format!("${{{}}}", mapping.placeholder));
self.arguments.push(flag);
}
fn applied_to(&self, arguments: &[OsString]) -> Vec<OsString> {
let mut applied = arguments.to_vec();
applied.extend(self.arguments.iter().cloned());
applied
}
}
fn path_mappings(
working_dir: &Path,
target_output: Option<&Path>,
target: Option<&str>,
) -> Vec<PathMapping> {
path_mappings_with_env(working_dir, target_output, target, |name| {
std::env::var_os(name)
})
}
fn path_mappings_with_env(
working_dir: &Path,
target_output: Option<&Path>,
target: Option<&str>,
environment: impl Fn(&str) -> Option<OsString>,
) -> Vec<PathMapping> {
let mut mappings = Vec::new();
let mut roots = BTreeSet::new();
if let (Some(root), Some(out_dir)) = (
environment(crate::out_dir::ROOT_ENV).map(PathBuf::from),
environment("OUT_DIR").map(PathBuf::from),
) && out_dir.is_absolute()
&& out_dir.starts_with(&root)
&& out_dir != root
{
add_mapping(
&mut mappings,
&mut roots,
out_dir,
crate::out_dir::PLACEHOLDER,
);
}
let home_roots = ["HOME", "USERPROFILE"]
.into_iter()
.filter_map(|name| environment(name).map(PathBuf::from))
.filter(|root| root.is_absolute())
.collect::<Vec<_>>();
let configured_target = environment(session::TARGET_DIR_ENV)
.map(PathBuf::from)
.filter(|root| root.is_absolute())
.map(|root| {
let physical = std::fs::canonicalize(&root).ok();
physical
.filter(|physical| {
target_output.is_some_and(|output| {
output.starts_with(physical) && !output.starts_with(&root)
})
})
.unwrap_or(root)
});
if let Some(root) = configured_target.or_else(|| {
target_output
.filter(|root| root.is_absolute())
.map(|output| standalone_target_root(output, target))
}) {
add_mapping(&mut mappings, &mut roots, root, "target");
}
if let Some(root) = environment(session::WORKSPACE_ROOT_ENV)
.map(PathBuf::from)
.filter(|root| root.is_absolute())
{
add_mapping(&mut mappings, &mut roots, root, "workspace");
}
let cargo_home = environment("CARGO_HOME")
.map(PathBuf::from)
.filter(|root| root.is_absolute())
.or_else(|| home_roots.first().map(|home| home.join(".cargo")));
if let Some(root) = cargo_home {
add_mapping(
&mut mappings,
&mut roots,
root.join("registry"),
"cargo_registry",
);
add_mapping(&mut mappings, &mut roots, root, "cargo_home");
}
if let Some(root) = environment("RUSTUP_HOME")
.map(PathBuf::from)
.filter(|root| root.is_absolute())
{
add_mapping(&mut mappings, &mut roots, root, "rustup_home");
}
if let Some(home) = home_roots.first()
&& !mappings
.iter()
.any(|mapping| mapping.placeholder == "rustup_home")
{
add_mapping(
&mut mappings,
&mut roots,
home.join(".rustup"),
"rustup_home",
);
}
if !mappings
.iter()
.any(|mapping| mapping.placeholder == "workspace")
&& !roots.iter().any(|root| working_dir.starts_with(root))
{
add_mapping(
&mut mappings,
&mut roots,
workspace_root(working_dir),
"workspace",
);
}
for root in home_roots {
add_mapping(&mut mappings, &mut roots, root, "home");
}
mappings
}
fn standalone_target_root(output: &Path, target: Option<&str>) -> PathBuf {
let profile = if matches!(
output.file_name().and_then(OsStr::to_str),
Some("deps" | "examples")
) {
output.parent()
} else if output.parent().and_then(Path::file_name) == Some(OsStr::new("build")) {
output.parent().and_then(Path::parent)
} else if output.file_name() == Some(OsStr::new("out"))
&& output.ancestors().nth(3).and_then(Path::file_name) == Some(OsStr::new("build"))
{
output.ancestors().nth(4)
} else {
None
};
if let Some(profile_root) = profile.and_then(Path::parent) {
let target_component = target.and_then(|target| Path::new(target).file_stem());
if target_component.is_some_and(|target| profile_root.file_name() == Some(target))
&& let Some(root) = profile_root.parent()
{
return root.to_path_buf();
}
return profile_root.to_path_buf();
}
output.to_path_buf()
}
fn add_mapping(
mappings: &mut Vec<PathMapping>,
roots: &mut BTreeSet<PathBuf>,
root: PathBuf,
placeholder: &str,
) {
if roots.insert(root.clone())
&& !mappings
.iter()
.any(|mapping| mapping.placeholder == placeholder)
{
mappings.push(PathMapping::new(root, placeholder));
}
}
fn replay_output(output: &Output) -> Result<()> {
replay_bytes(&output.stdout, &output.stderr)
}
fn publish_result<'a>(
candidates: &'a ActionCandidates,
outputs: &RustcOutputs,
output: &Output,
mappings: &[PathMapping],
) -> Result<&'a RustcAction> {
let _phase = crate::phase_timing::phase("store");
if outputs.files.is_empty() {
bail!("rustc produced no cacheable outputs");
}
let staging = staging_directory()?;
let mut blobs = Vec::new();
let stdout = staged_bytes(
staging.path(),
"stdout",
&normalize_output_text(&output.stdout, mappings),
)?;
let stderr = staged_bytes(
staging.path(),
"stderr",
&normalize_output_text(&output.stderr, mappings),
)?;
blobs.extend([stdout.clone(), stderr.clone()]);
let output_paths = outputs
.files
.iter()
.chain(std::iter::once(&outputs.dep_info));
let mut files = Vec::with_capacity(outputs.files.len() + 1);
let mut hashed_outputs = Vec::with_capacity(outputs.files.len());
for path in output_paths {
let metadata = std::fs::metadata(path)
.wrap_err_with(|| format!("failed to inspect rustc output {}", path.display()))?;
if !metadata.is_file() {
bail!("rustc output is not a regular file: {}", path.display());
}
let digest = if path == &outputs.dep_info {
let normalized = normalize_output_text(&std::fs::read(path)?, mappings);
let staged = staged_bytes(staging.path(), "dep-info", &normalized)?;
blobs.push(staged.clone());
staged.0
} else {
let digest = CacheDigest::blake3_file(path)?;
blobs.push((digest.clone(), path.clone()));
if metadata.len() == digest.size
&& let Ok(Some(file)) = FileIdentity::for_digest_cache(path, &metadata)
{
hashed_outputs.push(RecordedFileDigest {
file,
digest: digest.clone(),
});
}
digest
};
files.push(CacheFileNode {
digest,
executable: outputs.is_executable(path),
mode: file_mode(&metadata),
name: path
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| eyre::eyre!("rustc output name is not UTF-8"))?
.to_string(),
});
}
files.sort_by(|left, right| left.name.cmp(&right.name));
session::record_file_digests(FileDigestScope::Content, hashed_outputs);
let action = candidates.publishable();
blobs.push(staged_bytes(staging.path(), "action.json", &action.bytes)?);
let metadata = canonical_json(&RustcMetadata {
version: 1,
kind: "rustc".into(),
stdout: stdout.0,
stderr: stderr.0,
})?;
let metadata = staged_bytes(staging.path(), "metadata.json", &metadata)?;
blobs.push(metadata.clone());
let directory = canonical_json(&CacheDirectory {
directories: Vec::new(),
files,
symlinks: Vec::new(),
version: 1,
})?;
let directory = staged_bytes(staging.path(), "directory.json", &directory)?;
blobs.push(directory.clone());
let mut requests = Vec::new();
let mut published = BTreeSet::new();
for (digest, source) in blobs {
if published.insert(digest.clone()) {
requests.push(AgentRequest::StoreBlob { digest, source });
}
}
requests.push(AgentRequest::StoreActionResult {
result: RemoteActionResult {
action: action.digest.clone(),
metadata: Some(metadata.0),
output_root: Some(directory.0),
version: 1,
},
});
for response in session::request_agent(&requests)? {
match response {
AgentResponse::Stored { .. } | AgentResponse::ActionStored { .. } => {}
AgentResponse::Error { message } => bail!(message),
_ => bail!("cache agent returned an unexpected publish response"),
}
}
Ok(action)
}
fn staged_bytes(directory: &Path, name: &str, bytes: &[u8]) -> Result<(CacheDigest, PathBuf)> {
let path = directory.join(name);
std::fs::write(&path, bytes)?;
Ok((CacheDigest::blake3(bytes), path))
}
#[cfg(test)]
#[path = "rustc_tests.rs"]
mod tests;