use crate::materialize::{
CachedCompilation, CachedOutput, Materialization, StagedOutputs, 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,
record_verification, replay_bytes, resolve_executable, stage_verified_cached_output,
staging_directory, validate_file_mode,
};
use crate::session;
use eyre::{Context, Result, bail};
use mbx_cache_cc::{
CcAction, CcActionContext, CcBypassReason, CcCompilerFamily, CcCompilerIdentity, CcDepfile,
CcDiscoveredInputs, CcInputPrediction, CcInvocation, CcLanguage, environment_inputs_for,
is_system_path, manifest_snapshot,
};
use mbx_cache_core::{
ActionPrediction, AgentRequest, AgentResponse, CacheDigest, CacheDirectory, CacheFileNode,
CcMetadata, FileDigestScope, FileIdentity, FileSnapshot, PathMapping, RecordedFileDigest,
RemoteActionResult, RestoreStats, canonical_json, normalize_mapped_path,
};
use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet};
use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode, Output};
use std::time::{Instant, SystemTime};
const ADAPTER: &str = "cc";
const PREDICTION_ADAPTER: &str = "cc-path-binding-v1";
pub fn compile(compiler: &OsStr, arguments: &[OsString], language: CcLanguage) -> Result<ExitCode> {
let _timing = crate::phase_timing::start("cc", None);
let setup = crate::phase_timing::phase("key");
let working_dir = std::env::current_dir()?;
let mappings = path_mappings(&working_dir);
let identity = compiler_identity(compiler, language)?;
let preprocessing = CcInvocation::parse_for(arguments, identity.family)?.is_preprocessing();
let portable = if preprocessing {
Portable {
arguments: Vec::new(),
values: Vec::new(),
}
} else {
Portable::detect(&mappings, identity.family, &working_dir)
};
let arguments = portable.applied_to(arguments);
let arguments = arguments.as_ref();
let invocation = CcInvocation::parse_for(arguments, identity.family)?;
let compiler_arguments = invocation.compiler_arguments(arguments);
let mut environment = environment_inputs_for(
|name| std::env::var(name).ok(),
invocation.sysroot(),
identity.family,
)?;
if preprocessing {
environment.insert("PWD".into(), std::env::var("PWD").ok());
}
let mut context = CcActionContext {
compiler: identity,
working_dir: working_dir.clone(),
path_mappings: mappings,
environment,
inputs: Vec::new(),
};
drop(setup);
let invocation_digest = prediction_invocation(&invocation.invocation_digest(&context)?);
let _verification = session::verification::select(|| Ok(invocation_digest.clone()))?;
let verify = session::verify_requested();
let task = prediction_task(&invocation_digest);
let mut verification = None;
let usable = find_prediction(&task, &invocation_digest)?.and_then(|prediction| {
let _phase = crate::phase_timing::phase("key");
let discovered = prediction
.discover(
&working_dir,
&context.path_mappings,
session::file_digest_cache(),
)
.ok()?;
Some((prediction, discovered))
});
let mut lookup = LookupState::default();
if let Some((prediction, discovered)) = usable {
let mut candidate = context.clone();
discovered.clone().apply_to(&mut candidate)?;
let action = crate::phase_timing::measure("key", || {
invocation.action_with_path_binding(candidate, prediction.path_specific)
})?;
lookup.record(&action, &invocation_digest, prediction.path_specific);
let restored = match restore_result(
&action,
&invocation,
&discovered,
!verify,
&context.path_mappings,
&context.working_dir,
) {
Ok(restored) => restored,
Err(error) => {
session::report_shim_warning(&format!("cc result was not restored: {error:#}"));
None
}
};
if let Some(cached) = restored {
record_prediction(
&task,
&invocation_digest,
&action.digest,
&prediction,
None,
None,
);
if !verify {
write_caller_depfile(
&invocation,
discovered.files().map(|input| input.path.as_path()),
);
replay_bytes(&cached.stdout, &cached.stderr)?;
record_action_hit(
&action.digest,
cached.restore,
&compilation_name(&invocation),
);
return Ok(ExitCode::SUCCESS);
}
verification = Some(cached);
}
}
let flight = if verify {
None
} else {
crate::scheduler::flight(PREDICTION_ADAPTER, &invocation_digest.hash)
};
if let Some(flight) = &flight
&& (flight.waited() || !lookup.attempted)
&& let Some(payload) = flight.inherited()
{
match restore_flight_prediction(
payload,
&invocation,
&context,
&task,
&invocation_digest,
&mut lookup,
None,
) {
Ok(Some((action, cached, discovered))) => {
write_caller_depfile(
&invocation,
discovered.files().map(|input| input.path.as_path()),
);
replay_bytes(&cached.stdout, &cached.stderr)?;
record_action_hit(&action, cached.restore, &compilation_name(&invocation));
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 flight.is_some() {
match session::request_agent(&[AgentRequest::JoinActionPromise {
adapter: PREDICTION_ADAPTER.into(),
invocation: invocation_digest.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 == PREDICTION_ADAPTER
&& prediction.invocation == invocation_digest =>
{
match restore_flight_prediction(
&prediction.payload,
&invocation,
&context,
&task,
&invocation_digest,
&mut lookup,
Some(&prediction.action),
) {
Ok(Some((action, cached, discovered))) => {
if let Some(flight) = &flight {
flight.leave(&prediction.payload);
}
write_caller_depfile(
&invocation,
discovered.files().map(|input| input.path.as_path()),
);
replay_bytes(&cached.stdout, &cached.stderr)?;
record_action_hit(
&action,
cached.restore,
&compilation_name(&invocation),
);
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 !lookup.attempted {
session::record_unconsulted();
}
let depfile_staging = staging_directory()?;
let depfile = depfile_staging.path().join("compile.d");
let searchable = searchable_directories(&invocation, &working_dir);
let before = crate::phase_timing::measure("include_scan", || manifest_snapshot(&searchable));
let required_inputs = invocation
.required_inputs()
.iter()
.map(|path| absolute(path, &working_dir))
.collect::<Vec<_>>();
let input_snapshots =
crate::util::snapshot_compiler_inputs(required_inputs.iter().map(PathBuf::as_path));
let demand = crate::scheduler::Demand::new(&compilation_name(&invocation), false);
let permit = crate::scheduler::pool().and_then(|pool| pool.admit(&demand));
let started = Instant::now();
let compilation_started = SystemTime::now();
let mut command = Command::new(compiler);
command.args(&compiler_arguments);
command.args(invocation.dependency_arguments_for(&depfile, context.compiler.family));
let output = crate::phase_timing::measure("compiler", || command.output())
.wrap_err_with(|| format!("failed to run {}", Path::new(compiler).display()))?;
drop(permit);
crate::scheduler::record_compiler_memory(&demand, &output.status);
let duration_ns = duration_ns(started.elapsed());
session::record_compiler_invocation_with_diagnostic(
if verification.is_some() {
"verification"
} else if lookup.attempted {
"miss"
} else {
"unconsulted"
},
Some(&compilation_name(&invocation)),
duration_ns,
lookup.diagnostic,
);
let verified = verification.is_some();
if let Some(cached) = verification {
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(
"cc",
&compilation_name(&invocation),
&cached.action,
&divergence,
));
}
}
replay_bytes(&output.stdout, &output.stderr)?;
if !output.status.success() {
return Ok(exit_code(output.status));
}
if !verified
&& let Err(error) = publish(
&invocation,
&mut context,
&depfile,
&output,
compilation_started,
&input_snapshots,
&task,
&invocation_digest,
duration_ns,
&searchable,
before,
flight.as_ref(),
remote_claim.as_deref(),
&portable,
)
{
session::report_cc_publication_failure(&compilation_name(&invocation), &error);
}
if invocation.caller_depfile().is_some() {
match mbx_cache_cc::CcDepfile::read_for(&depfile, context.compiler.family) {
Ok(listed) => {
write_caller_depfile(&invocation, listed.files.iter().map(PathBuf::as_path))
}
Err(error) => session::report_shim_warning(&format!(
"the caller's dependency list was not written: {error}"
)),
}
}
Ok(exit_code(output.status))
}
#[allow(clippy::too_many_arguments)]
fn publish(
invocation: &CcInvocation,
context: &mut CcActionContext,
depfile: &Path,
output: &Output,
compilation_started: SystemTime,
input_snapshots: &std::io::Result<BTreeMap<PathBuf, FileSnapshot>>,
task: &str,
invocation_digest: &CacheDigest,
duration_ns: u64,
searchable: &BTreeSet<PathBuf>,
before: Result<BTreeMap<PathBuf, CacheDigest>, CcBypassReason>,
flight: Option<&crate::scheduler::Flight>,
remote_claim: Option<&str>,
portable: &Portable,
) -> Result<()> {
let _phase = crate::phase_timing::phase("store");
let input_snapshots = input_snapshots
.as_ref()
.map_err(|error| eyre::eyre!(error.to_string()))?;
let discovered = discover(invocation, context, depfile)?;
discovered.verify_not_modified_since_with_snapshots(compilation_started, input_snapshots)?;
verify_search_path_unchanged(searchable, before)?;
discovered.verify()?;
discovered.clone().apply_to(context)?;
let object = invocation.output_in(&context.working_dir);
let mut prediction = invocation.prediction(context, duration_ns)?;
prediction.path_specific = !portable.outputs_are_clean(&object, &context.path_mappings);
if prediction.path_specific
&& std::env::var_os("MBX_CC_STORE_PATH_SPECIFIC")
.is_some_and(|value| value == "0" || value.is_empty())
{
return Err(CcBypassReason::PathSpecificStorageDisabled.into());
}
let action = invocation.action_with_path_binding(context.clone(), prediction.path_specific)?;
if let Err(error) = publish_result(&action, &object, output, &context.path_mappings) {
let existing = restore_result(
&action,
invocation,
&discovered,
false,
&context.path_mappings,
&context.working_dir,
);
if !matches!(existing, Ok(Some(ref cached)) if publication_outputs_match(cached, output)) {
return Err(error);
}
}
record_prediction(
task,
invocation_digest,
&action.digest,
&prediction,
flight,
remote_claim,
);
Ok(())
}
#[derive(Default)]
struct LookupState {
attempted: bool,
diagnostic: Option<mbx_cache_core::ActionDiagnostic>,
}
impl LookupState {
fn record(&mut self, action: &CcAction, invocation: &CacheDigest, path_specific: bool) {
self.attempted = true;
self.diagnostic = path_specific.then(|| mbx_cache_core::ActionDiagnostic {
action: action.digest.clone(),
components: BTreeMap::from([
("compilation unit".into(), invocation.clone()),
(
"path-specific C object".into(),
CacheDigest::blake3(b"path-specific"),
),
]),
inputs: Default::default(),
});
}
}
fn restore_flight_prediction(
payload: &str,
invocation: &CcInvocation,
context: &CcActionContext,
task: &str,
invocation_digest: &CacheDigest,
lookup: &mut LookupState,
recorded_action: Option<&CacheDigest>,
) -> Result<Option<(CacheDigest, CachedCompilation, CcDiscoveredInputs)>> {
let _phase = crate::phase_timing::phase("key");
let prediction: CcInputPrediction = serde_json::from_str(payload)?;
let discovered = prediction.discover(
&context.working_dir,
&context.path_mappings,
session::file_digest_cache(),
)?;
let mut candidate = context.clone();
discovered.clone().apply_to(&mut candidate)?;
let action = crate::phase_timing::measure("key", || {
invocation.action_with_path_binding(candidate, prediction.path_specific)
})?;
if recorded_action.is_some_and(|recorded| recorded != &action.digest) {
bail!("the action promise no longer matches its predicted inputs");
}
lookup.record(&action, invocation_digest, prediction.path_specific);
let restored = restore_result(
&action,
invocation,
&discovered,
true,
&context.path_mappings,
&context.working_dir,
)?;
let Some(cached) = restored else {
return Ok(None);
};
record_prediction(
task,
invocation_digest,
&action.digest,
&prediction,
None,
None,
);
Ok(Some((action.digest, cached, discovered)))
}
fn write_caller_depfile<'a>(invocation: &CcInvocation, files: impl Iterator<Item = &'a Path>) {
let Some(caller) = invocation.caller_depfile() else {
return;
};
let files = files
.filter(|path| !caller.user_headers_only || !mbx_cache_cc::is_system_path(path))
.map(Path::to_path_buf)
.collect::<Vec<_>>();
let rendered = mbx_cache_cc::CcDepfile::render(
&caller.targets,
&files,
invocation.source(),
caller.phony_targets,
);
let written = (|| {
if let Some(parent) = caller
.path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
{
std::fs::create_dir_all(parent)?;
}
std::fs::write(&caller.path, rendered)
})();
if let Err(error) = written {
session::report_shim_warning(&format!(
"the caller's dependency list {} was not written: {error}",
caller.path.display()
));
}
}
struct Portable {
arguments: Vec<OsString>,
values: Vec<String>,
}
impl Portable {
fn detect(mappings: &[PathMapping], family: CcCompilerFamily, working_dir: &Path) -> Self {
let physical_dir =
std::fs::canonicalize(working_dir).unwrap_or_else(|_| working_dir.to_path_buf());
let logical_dir = std::env::var("PWD").ok().filter(|value| {
Path::new(value).is_absolute()
&& std::fs::canonicalize(value).is_ok_and(|path| path == physical_dir)
});
let values = working_dir
.to_str()
.map(str::to_owned)
.into_iter()
.chain(logical_dir)
.filter(|_| !family.is_msvc())
.chain(
PORTABLE_ENVIRONMENT
.iter()
.filter(|_| session::share_out_dir_requested())
.filter_map(|name| std::env::var(name).ok()),
);
Self::from_values(mappings, family, values)
}
fn from_values(
mappings: &[PathMapping],
family: CcCompilerFamily,
values: impl IntoIterator<Item = String>,
) -> Self {
let mut portable = Self {
arguments: Vec::new(),
values: Vec::new(),
};
let mut seen = BTreeSet::new();
for value in values
.into_iter()
.filter(|value| Path::new(value).is_absolute())
{
if !seen.insert(value.clone()) {
continue;
}
let canonical = std::fs::canonicalize(&value).unwrap_or_else(|_| PathBuf::from(&value));
let Ok(placeholder) =
normalize_mapped_path(Path::new(&value), Path::new("/"), mappings)
.or_else(|_| normalize_mapped_path(&canonical, Path::new("/"), mappings))
else {
continue;
};
let mut flag = OsString::from(if family.is_msvc() {
"/pathmap:"
} else {
"-fdebug-prefix-map="
});
flag.push(&value);
flag.push("=");
flag.push(&placeholder);
portable.arguments.push(flag);
portable.values.push(value);
}
portable
}
fn applied_to<'a>(&self, arguments: &'a [OsString]) -> Cow<'a, [OsString]> {
if self.arguments.is_empty() {
return Cow::Borrowed(arguments);
}
let mut applied = arguments.to_vec();
applied.extend(self.arguments.iter().cloned());
Cow::Owned(applied)
}
fn outputs_are_clean(&self, path: &Path, mappings: &[PathMapping]) -> bool {
let values = self
.values
.iter()
.cloned()
.chain(mappings.iter().flat_map(|mapping| {
[
Some(mapping.root.clone()),
std::fs::canonicalize(&mapping.root).ok(),
]
.into_iter()
.flatten()
.flat_map(mapping_root_spellings)
}))
.collect::<BTreeSet<_>>();
if values.is_empty() {
return true;
}
let Ok(contents) = std::fs::read(path) else {
return false;
};
!values.iter().any(|value| {
let slashes = value.replace('\\', "/");
let plain = if let Some(unc) = slashes.strip_prefix("//?/UNC/") {
format!("//{unc}")
} else {
slashes.strip_prefix("//?/").unwrap_or(&slashes).to_owned()
};
[value.as_str(), &slashes, &plain, &plain.replace('/', "\\")]
.iter()
.any(|spelling| memchr::memmem::find(&contents, spelling.as_bytes()).is_some())
})
}
}
fn mapping_root_spellings(path: PathBuf) -> Vec<String> {
let mut spellings = Vec::new();
if let Some(value) = path.to_str() {
spellings.push(value.to_owned());
}
#[cfg(target_os = "macos")]
if let Ok(relative) = path.strip_prefix("/private") {
let logical = Path::new("/").join(relative);
if let (Ok(physical), Ok(alias)) = (
std::fs::canonicalize(&path),
std::fs::canonicalize(&logical),
) && physical == alias
&& let Some(value) = logical.to_str()
{
spellings.push(value.to_owned());
}
}
spellings
}
const PORTABLE_ENVIRONMENT: &[&str] = &["OUT_DIR"];
fn discover(
invocation: &CcInvocation,
context: &CcActionContext,
depfile: &Path,
) -> Result<CcDiscoveredInputs> {
let dependencies = CcDepfile::read_for(depfile, context.compiler.family)?;
let mut files = BTreeSet::new();
for path in dependencies
.files
.into_iter()
.chain(invocation.required_inputs().iter().map(ToOwned::to_owned))
{
files.insert(absolute(&path, &context.working_dir));
}
invocation.validate_discovered_inputs(files.iter().map(PathBuf::as_path))?;
Ok(CcDiscoveredInputs::collect(
&context.working_dir,
files.clone(),
manifest_directories(invocation, context, &files),
session::file_digest_cache(),
)?)
}
fn searchable_directories(invocation: &CcInvocation, working_dir: &Path) -> BTreeSet<PathBuf> {
invocation
.include_dirs()
.iter()
.map(|directory| absolute(directory, working_dir))
.chain(
invocation
.source()
.parent()
.map(|parent| absolute(parent, working_dir)),
)
.filter(|directory| !is_system_include_path(directory))
.collect()
}
fn verify_search_path_unchanged(
searchable: &BTreeSet<PathBuf>,
before: Result<BTreeMap<PathBuf, CacheDigest>, CcBypassReason>,
) -> Result<()> {
let before = before?;
let after = crate::phase_timing::measure("include_scan", || manifest_snapshot(searchable))?;
for (directory, digest) in &before {
if after.get(directory) != Some(digest) {
return Err(
CcBypassReason::SearchPathModifiedDuringCompilation(directory.clone()).into(),
);
}
}
Ok(())
}
fn manifest_directories(
invocation: &CcInvocation,
context: &CcActionContext,
files: &BTreeSet<PathBuf>,
) -> BTreeSet<PathBuf> {
invocation
.include_dirs()
.iter()
.map(|directory| absolute(directory, &context.working_dir))
.chain(
files
.iter()
.filter_map(|file| file.parent().map(Path::to_path_buf)),
)
.filter(|directory| !is_system_include_path(directory))
.collect()
}
fn absolute(path: &Path, working_dir: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
working_dir.join(path)
}
}
fn path_mappings(working_dir: &Path) -> Vec<PathMapping> {
let mut mappings = Vec::new();
let mut add = |root: Option<PathBuf>, placeholder: &str| {
if let Some(root) = root.filter(|root| root.is_absolute())
&& !mappings
.iter()
.any(|existing: &PathMapping| existing.root == root)
{
mappings.push(PathMapping::new(root, placeholder));
}
};
add(session_path(session::TARGET_DIR_ENV), "target");
add(session_path(session::WORKSPACE_ROOT_ENV), "workspace");
let cargo_home = std::env::var_os("CARGO_HOME")
.map(PathBuf::from)
.or_else(|| dirs::home_dir().map(|home| home.join(".cargo")));
add(
cargo_home.as_ref().map(|home| home.join("registry")),
"cargo_registry",
);
add(cargo_home, "cargo_home");
add(
std::env::var_os("RUSTUP_HOME")
.map(PathBuf::from)
.or_else(|| dirs::home_dir().map(|home| home.join(".rustup"))),
"rustup_home",
);
add(dirs::home_dir(), "home");
add(std::env::var_os("VCINSTALLDIR").map(PathBuf::from), "msvc");
add(
std::env::var_os("WindowsSdkDir").map(PathBuf::from),
"windows_sdk",
);
add(
std::env::var_os("UniversalCRTSdkDir").map(PathBuf::from),
"ucrt_sdk",
);
let _ = working_dir;
mappings
}
fn is_system_include_path(path: &Path) -> bool {
is_system_path(path)
|| ["VCINSTALLDIR", "WindowsSdkDir", "UniversalCRTSdkDir"]
.iter()
.filter_map(std::env::var_os)
.map(PathBuf::from)
.any(|root| path.starts_with(root))
}
fn rustc_path_mappings(mappings: &[PathMapping]) -> Vec<mbx_cache_rustc::PathMapping> {
mappings
.iter()
.map(|mapping| mbx_cache_rustc::PathMapping::new(&mapping.root, &mapping.placeholder))
.collect()
}
fn session_path(name: &str) -> Option<PathBuf> {
std::env::var_os(name)
.filter(|value| !value.is_empty())
.map(PathBuf::from)
}
fn compiler_identity(compiler: &OsStr, language: CcLanguage) -> Result<CcCompilerIdentity> {
let _phase = crate::phase_timing::phase("key");
let executable = resolve_executable(compiler)?;
let is_cl = executable
.file_stem()
.and_then(OsStr::to_str)
.is_some_and(|stem| stem.eq_ignore_ascii_case("cl"));
let probe = if is_cl {
probe_msvc(&executable)?
} else {
probe_executable(&executable, &["-v"], &executable)?
};
let family = CcCompilerFamily::classify(&probe)?;
let target = if family.is_msvc() {
std::env::var("VSCMD_ARG_TGT_ARCH").unwrap_or_default()
} else {
probe
.lines()
.find_map(|line| line.strip_prefix("Target: "))
.unwrap_or_default()
.to_string()
};
let assembler = if family.uses_external_assembler() {
assembler_identity(&executable)
} else {
String::new()
};
let _ = language;
Ok(CcCompilerIdentity {
family,
version_text: probe,
target,
assembler,
})
}
fn probe_msvc(executable: &Path) -> Result<String> {
let environment = BTreeMap::new();
if let Ok(responses) = session::request_agent(&[AgentRequest::FindExecutableIdentity {
executable: executable.to_path_buf(),
environment: environment.clone(),
}]) && let Some(AgentResponse::ExecutableIdentity { stdout: Some(text) }) =
responses.into_iter().next()
{
return Ok(String::from_utf8_lossy(&text).into_owned());
}
let output = Command::new(executable)
.arg("/Bv")
.output()
.map_err(|error| CcBypassReason::CompilerIdentityUnavailable(error.to_string()))?;
let mut text = String::from_utf8_lossy(&output.stderr).into_owned();
text.push_str(&String::from_utf8_lossy(&output.stdout));
if !text.contains("Microsoft (R) C/C++ Optimizing Compiler") {
return Err(CcBypassReason::CompilerIdentityUnavailable(format!(
"{} did not report an MSVC compiler identity",
executable.display()
))
.into());
}
let _ = session::request_agent(&[AgentRequest::StoreExecutableIdentity {
executable: executable.to_path_buf(),
environment,
stdout: text.clone().into_bytes(),
pins: Vec::new(),
}]);
Ok(text)
}
fn named_assembler(printed: &str) -> Option<PathBuf> {
let trimmed = printed.trim();
Path::new(trimmed)
.is_absolute()
.then(|| PathBuf::from(trimmed))
}
fn assembler_identity(compiler: &Path) -> String {
let named = probe_executable(compiler, &["-print-prog-name=as"], &compiler.join("as"))
.unwrap_or_default();
let assembler = match named_assembler(&named) {
Some(assembler) => assembler,
None => match resolve_executable(OsStr::new("as")) {
Ok(found) => found,
Err(_) => return "unresolved".into(),
},
};
let version = probe_executable(&assembler, &["--version"], &assembler)
.ok()
.and_then(|probe| probe.lines().next().map(ToOwned::to_owned))
.unwrap_or_default();
format!("{}; {version}", assembler.display())
}
fn probe_executable(executable: &Path, arguments: &[&str], memo: &Path) -> Result<String> {
let key = memo.to_path_buf();
let environment = BTreeMap::new();
let responses = session::request_agent(&[AgentRequest::FindExecutableIdentity {
executable: key.clone(),
environment: environment.clone(),
}]);
if let Ok(responses) = responses
&& let Some(AgentResponse::ExecutableIdentity { stdout: Some(text) }) =
responses.into_iter().next()
{
return Ok(String::from_utf8_lossy(&text).into_owned());
}
let output = Command::new(executable)
.args(arguments)
.output()
.map_err(|error| CcBypassReason::CompilerIdentityUnavailable(error.to_string()))?;
if !output.status.success() {
return Err(CcBypassReason::CompilerIdentityUnavailable(format!(
"{} exited with {}",
executable.display(),
output.status
))
.into());
}
let mut text = String::from_utf8_lossy(&output.stderr).into_owned();
if text.trim().is_empty() {
text = String::from_utf8_lossy(&output.stdout).into_owned();
}
let _ = session::request_agent(&[AgentRequest::StoreExecutableIdentity {
executable: key,
environment,
stdout: text.clone().into_bytes(),
pins: Vec::new(),
}]);
Ok(text)
}
fn prediction_invocation(invocation: &CacheDigest) -> CacheDigest {
CacheDigest::blake3(format!("{PREDICTION_ADAPTER}\0{}", invocation.hash).as_bytes())
}
fn find_prediction(task: &str, invocation: &CacheDigest) -> Result<Option<CcInputPrediction>> {
let responses = session::request_agent(&[AgentRequest::FindActionPrediction {
task: task.to_string(),
invocation: invocation.clone(),
}])?;
let Some(AgentResponse::ActionPrediction { prediction }) = responses.into_iter().next() else {
return Ok(None);
};
let Some(prediction) = prediction else {
return Ok(None);
};
if prediction.adapter != PREDICTION_ADAPTER {
return Ok(None);
}
let payload: CcInputPrediction = serde_json::from_str(&prediction.payload)?;
Ok(Some(payload))
}
fn record_prediction(
task: &str,
invocation: &CacheDigest,
action: &CacheDigest,
prediction: &CcInputPrediction,
flight: Option<&crate::scheduler::Flight>,
remote_claim: Option<&str>,
) {
let Ok(payload) = serde_json::to_string(prediction) else {
return;
};
if let Some(flight) = flight {
flight.leave(&payload);
}
let wire_prediction = ActionPrediction {
invocation: invocation.clone(),
action: action.clone(),
adapter: PREDICTION_ADAPTER.into(),
payload,
};
let recorded = session::request_agent(&[AgentRequest::RecordActionPrediction {
task: task.to_string(),
prediction: wire_prediction.clone(),
}]);
#[cfg(debug_assertions)]
match recorded.map(|responses| responses.into_iter().next()) {
Ok(Some(AgentResponse::ActionPredictionRecorded)) => {}
Ok(other) => session::report_shim_warning(&format!(
"cc prediction was not recorded: unexpected agent response {other:?}"
)),
Err(error) => {
session::report_shim_warning(&format!("cc prediction was not recorded: {error:#}"))
}
}
#[cfg(not(debug_assertions))]
let _ = recorded;
if let Some(claim) = remote_claim {
let _ = session::request_agent(&[AgentRequest::CompleteActionPromise {
claim: claim.to_string(),
prediction: wire_prediction,
}]);
}
}
fn prediction_task(invocation: &CacheDigest) -> String {
std::env::var(session::BUILD_ENV).unwrap_or_else(|_| standalone_prediction_task(invocation))
}
fn standalone_prediction_task(invocation: &CacheDigest) -> String {
let shard = invocation.hash.get(..2).unwrap_or(&invocation.hash);
CacheDigest::blake3(format!("cc-standalone-predictions-v1\0{shard}").as_bytes()).hash
}
fn restore_result(
action: &CcAction,
invocation: &CcInvocation,
discovered: &CcDiscoveredInputs,
restore_outputs: bool,
mappings: &[PathMapping],
working_dir: &Path,
) -> Result<Option<CachedCompilation>> {
let _phase = crate::phase_timing::phase("restore");
let text_mappings = rustc_path_mappings(mappings);
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 cc action result has an invalid identity");
}
let metadata_digest = result
.metadata
.ok_or_else(|| eyre::eyre!("cached cc action result has no metadata"))?;
let output_root_digest = result
.output_root
.ok_or_else(|| eyre::eyre!("cached cc 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 cc action descriptor does not match the invocation");
}
let metadata: CcMetadata = read_canonical_blob(&roots[1], &metadata_digest, "cc metadata")?;
if !metadata.validate() {
bail!("cached cc metadata is unsupported");
}
let directory: CacheDirectory =
read_canonical_blob(&roots[2], &output_root_digest, "output directory")?;
let node = validated_object(directory, invocation)?;
let destination = invocation.output_in(working_dir);
let blobs = find_blobs(&[
metadata.stdout.clone(),
metadata.stderr.clone(),
node.digest.clone(),
])?;
let stdout = denormalize_output_text(
&read_verified_blob(&blobs[0], &metadata.stdout, "stdout")?,
&text_mappings,
);
let stderr = denormalize_output_text(
&read_verified_blob(&blobs[1], &metadata.stderr, "stderr")?,
&text_mappings,
);
let materialization_started = Instant::now();
let parent = destination
.parent()
.ok_or_else(|| eyre::eyre!("cc output has no parent directory"))?;
std::fs::create_dir_all(parent)?;
let staging = tempfile::tempdir_in(parent)?;
let mut restore = RestoreStats {
output_files: 1,
output_bytes: node.digest.size,
..RestoreStats::default()
};
let mut staged_files = Vec::new();
if restore_outputs
&& crate::rustc::output_already_in_place(&node, &destination, session::file_digest_cache())
{
restore.reused_output_files = 1;
restore.reused_output_bytes = node.digest.size;
} else {
let (temporary, materialization) =
stage_verified_cached_output(staging.path(), 0, &blobs[2], &node)?;
match materialization {
Materialization::Reflink => {
restore.reflinked_output_files = 1;
restore.reflinked_output_bytes = node.digest.size;
}
Materialization::Copy => {
restore.copied_output_files = 1;
restore.copied_output_bytes = node.digest.size;
}
}
staged_files.push((temporary, destination.clone()));
}
let staged = StagedOutputs {
directory: staging,
files: staged_files,
};
discovered.verify()?;
if restore_outputs {
persist_outputs(staged)?;
if let Ok(metadata) = std::fs::metadata(&destination)
&& metadata.len() == node.digest.size
&& let Ok(Some(file)) = FileIdentity::for_digest_cache(&destination, &metadata)
{
session::record_file_digests(
FileDigestScope::Content,
vec![RecordedFileDigest {
file,
digest: node.digest.clone(),
}],
);
}
}
restore.duration_ns = duration_ns(materialization_started.elapsed());
Ok(Some(CachedCompilation {
action: action.digest.clone(),
stdout,
stderr,
outputs: vec![CachedOutput {
path: destination,
digest: node.digest,
executable: node.executable,
mode: node.mode,
}],
restore,
}))
}
fn validated_object(directory: CacheDirectory, invocation: &CcInvocation) -> Result<CacheFileNode> {
if directory.version != 1 || !directory.directories.is_empty() || !directory.symlinks.is_empty()
{
bail!("cached cc output directory has unsupported entries");
}
let name = invocation
.output()
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| eyre::eyre!("cc output name is not UTF-8"))?;
let [node] = <[CacheFileNode; 1]>::try_from(directory.files)
.map_err(|_| eyre::eyre!("cached cc output set does not match the invocation"))?;
if node.name != name {
bail!("cached cc output is unexpected: {}", node.name);
}
validate_file_mode(&node, false)?;
Ok(node)
}
fn publish_result(
action: &CcAction,
object: &Path,
output: &Output,
mappings: &[PathMapping],
) -> Result<()> {
let _phase = crate::phase_timing::phase("store");
let text_mappings = rustc_path_mappings(mappings);
let metadata = std::fs::metadata(object)
.wrap_err_with(|| format!("failed to inspect cc output {}", object.display()))?;
if !metadata.is_file() {
bail!("cc output is not a regular file: {}", object.display());
}
let staging = staging_directory()?;
let mut blobs = vec![staged_bytes(staging.path(), "action.json", &action.bytes)?];
let stdout = staged_bytes(
staging.path(),
"stdout",
&normalize_output_text(&output.stdout, &text_mappings),
)?;
let stderr = staged_bytes(
staging.path(),
"stderr",
&normalize_output_text(&output.stderr, &text_mappings),
)?;
blobs.extend([stdout.clone(), stderr.clone()]);
let digest = CacheDigest::blake3_file(object)?;
blobs.push((digest.clone(), object.to_path_buf()));
if metadata.len() == digest.size
&& let Ok(Some(file)) = FileIdentity::for_digest_cache(object, &metadata)
{
session::record_file_digests(
FileDigestScope::Content,
vec![RecordedFileDigest {
file,
digest: digest.clone(),
}],
);
}
let files = vec![CacheFileNode {
digest,
executable: false,
mode: file_mode(&metadata),
name: object
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| eyre::eyre!("cc output name is not UTF-8"))?
.to_string(),
}];
let metadata_bytes = canonical_json(&CcMetadata {
version: 1,
kind: ADAPTER.into(),
stdout: stdout.0,
stderr: stderr.0,
})?;
let metadata = staged_bytes(staging.path(), "metadata.json", &metadata_bytes)?;
blobs.push(metadata.clone());
let directory_bytes = canonical_json(&CacheDirectory {
directories: Vec::new(),
files,
symlinks: Vec::new(),
version: 1,
})?;
let directory = staged_bytes(staging.path(), "directory.json", &directory_bytes)?;
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(())
}
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))
}
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);
}
output_divergence(cached)
}
fn publication_outputs_match(cached: &CachedCompilation, output: &Output) -> bool {
output.status.success() && cached.stdout == output.stdout && output_divergence(cached).is_none()
}
fn output_divergence(cached: &CachedCompilation) -> Option<String> {
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 compilation_name(invocation: &CcInvocation) -> String {
format!("{ADAPTER}:{}", invocation.source_name())
}
fn duration_ns(duration: std::time::Duration) -> u64 {
duration.as_nanos().try_into().unwrap_or(u64::MAX)
}
#[cfg(test)]
#[path = "cc_tests.rs"]
mod tests;