use crate::store::StoreHashExt;
use anyhow::{Context, Result};
use bytesize::ByteSize;
use chrono::Utc;
pub(crate) use kache_store::markers::*;
use std::path::{Component, Path, PathBuf};
use crate::args::{ExternDep, RustcArgs};
use crate::cache_key::FileHashStats;
use crate::cache_key::FileHasher;
use crate::compile;
use crate::compiler::cc::CcCompiler;
use crate::compiler::nvcc::NvccCompiler;
use crate::compiler::rustc::RustcCompiler;
use crate::compiler::{
ArtifactKind, ArtifactSet, Compiler, KeyCtx, classify_by_filename, plan_post_restore, platform,
};
use crate::config::Config;
use crate::events::{self, BuildEvent, EventResult};
use crate::incremental_policy::{AdaptiveUnit, CompileOutcome, Lease, compile_outcome};
use crate::key_env::KeyEnv;
use crate::link;
use crate::maintenance::unix_now_secs;
use crate::scheduler::{self, FlightIdentity, MissGuard};
use crate::store::{BuildClaim, EntryMeta, KeyLock, Store, StorePutResult};
mod remote;
use remote::{
NegativeReply, acquire_entry, compiler_remote_enabled, compiler_upload_enabled,
maybe_enqueue_upload,
};
mod hit;
use hit::HitCompletion;
mod cc_diagnostics;
mod rustc_hit;
use rustc_hit::RustcHitContext;
fn progress_level() -> u8 {
match std::env::var("KACHE_PROGRESS").as_deref() {
Ok("1" | "hits") => 1,
Ok("verbose" | "all") => 2,
_ => 0,
}
}
fn heartbeat_lines_enabled(level: u8) -> bool {
level >= 2
}
fn progress_label(result: EventResult, level: u8) -> Option<&'static str> {
match result {
EventResult::LocalHit => Some("local hit"),
EventResult::PrefetchHit => Some("prefetch hit"),
EventResult::RemoteHit => Some("remote hit"),
EventResult::Dup if level < 2 => None,
EventResult::Dup => Some("dup"),
EventResult::Miss if level < 2 => None,
EventResult::Miss => Some("miss"),
EventResult::Error => Some("error"),
EventResult::Passthrough => None,
EventResult::Skipped => None,
}
}
fn print_progress(crate_name: &str, result: EventResult, elapsed_ms: u64, size: u64) {
let level = progress_level();
if level == 0 {
return;
}
let Some(label) = progress_label(result, level) else {
return;
};
let size_str = if size > 0 {
format!(", {}", ByteSize(size))
} else {
String::new()
};
let elapsed_str = if elapsed_ms >= 1000 {
format!("{:.1}s", elapsed_ms as f64 / 1000.0)
} else {
format!("{}ms", elapsed_ms)
};
crate::notice::show_requested(&format!(
"[kache] {crate_name}: {label} ({elapsed_str}{size_str})"
));
}
fn store_unavailable_message(err: &anyhow::Error) -> String {
format!(
"[kache] the cache index could not be opened after retries ({err:#}).\n\
[kache] Caching is disabled for this build — compilation still succeeds,\n\
[kache] just without cache hits or stores (everything builds uncached).\n\
[kache] This is usually a storage issue: the cache directory is on a\n\
[kache] filesystem that doesn't support reliable file locking, or it is\n\
[kache] being accessed from more than one machine at the same time.\n\
[kache] → set KACHE_CACHE_DIR to a fast, local, single-machine path"
)
}
pub(crate) use kache_store::markers::WARN_SESSION_SECS;
fn adaptive_policy_guard(config: &Config) -> [u8; 32] {
fn fold(hasher: &mut blake3::Hasher, label: &[u8], value: &[u8]) {
hasher.update(&(label.len() as u64).to_le_bytes());
hasher.update(label);
hasher.update(&(value.len() as u64).to_le_bytes());
hasher.update(value);
}
let mut hasher = blake3::Hasher::new();
fold(&mut hasher, b"policy", b"adaptive-incremental-v1");
if let Some(salt) = config.key_salt.as_deref() {
fold(&mut hasher, b"key-salt", salt.as_bytes());
}
if let Some(env_guard) = crate::cache_key::key_env_guard(&config.key_env_vars) {
fold(&mut hasher, b"key-env", env_guard.as_bytes());
}
for base_dir in &config.base_dirs {
fold(&mut hasher, b"base-dir", base_dir.as_bytes());
}
*hasher.finalize().as_bytes()
}
fn adaptive_mode_enabled(config: &Config) -> bool {
config.adaptive_incremental && !config.preserve_incremental
}
fn preserve_incremental_requested(config: &Config, args: &RustcArgs) -> bool {
config.preserve_incremental && args.incremental.is_some()
}
fn force_incremental_requested(config: &Config, args: &RustcArgs) -> bool {
args.incremental.is_some()
&& args
.crate_name
.as_deref()
.is_some_and(|crate_name| config.incremental_crate_forced(crate_name))
}
fn adaptive_seed_allowed(config: &Config, args: &RustcArgs) -> bool {
adaptive_mode_enabled(config) && !force_incremental_requested(config, args)
}
fn managed_unit_keys_first(managed: bool, force_listed: bool, policy_state: bool) -> bool {
managed && (force_listed || policy_state)
}
fn managed_incremental_unit<F>(
config: &Config,
args: &RustcArgs,
extra_inputs_declared: F,
) -> Option<AdaptiveUnit>
where
F: FnOnce() -> bool,
{
if !adaptive_mode_enabled(config) && !force_incremental_requested(config, args) {
return None;
}
let guard = adaptive_policy_guard(config);
let unit = AdaptiveUnit::eligible(args, &guard)?;
if extra_inputs_declared() {
let _ = unit.reset();
return None;
}
Some(unit)
}
fn incremental_fast_path_allowed(
has_refuse_reasons: bool,
source_excluded: bool,
skip_user_facing: bool,
reentered: bool,
) -> bool {
!has_refuse_reasons && !source_excluded && !skip_user_facing && !reentered
}
fn reentered_after_deferred_compile(precompiled: bool, emitted_closure_waiting: bool) -> bool {
precompiled || emitted_closure_waiting
}
fn adaptive_lane_takes_permit(kind: crate::incremental_policy::LeaseKind) -> bool {
kind == crate::incremental_policy::LeaseKind::Seed
}
fn unit_refuses_caching(has_refuse_reasons: bool, untrusted_codegen_backend: bool) -> bool {
has_refuse_reasons || untrusted_codegen_backend
}
fn incremental_cleanup_enabled(config: &Config) -> bool {
config.clean_incremental && !config.preserve_incremental
}
fn disable_incremental_env(incremental_preserved: bool) -> bool {
!incremental_preserved
}
pub(crate) fn warn_marker_path(kind: &str, cache_dir: &Path) -> PathBuf {
let hash = blake3::hash(cache_dir.as_os_str().as_encoded_bytes()).to_hex();
std::env::temp_dir().join(format!("kache-{kind}-warn-{}", &hash[..16]))
}
fn warn_store_unavailable_once(config: &Config, err: &anyhow::Error) {
tracing::warn!("failed to open store: {:#}", err);
let marker = warn_marker_path("store", &config.cache_dir);
warn_once_per_session(&marker, WARN_SESSION_SECS, &store_unavailable_message(err));
}
pub(crate) fn warn_nonlocal_cache_fs_once(config: &Config) {
let probe = crate::cache_fs::probe(&config.cache_dir);
let Some(message) = crate::cache_fs::advisory_for(&probe, &config.cache_dir) else {
return;
};
tracing::warn!(
cache_dir = %config.cache_dir.display(),
filesystem = ?probe.name,
"cache directory is not on host-local storage; the WAL index can corrupt"
);
let marker = warn_marker_path("cachefs", &config.cache_dir);
warn_once_per_session(&marker, WARN_SESSION_SECS, &message);
}
const AUTO_GC_CHECK_INTERVAL: std::time::Duration = std::time::Duration::from_secs(300);
const AUTO_GC_SLACK_PERCENT: u64 = 10;
const AUTO_GC_MAX_BACKOFF: std::time::Duration = std::time::Duration::from_secs(2 * 3600);
fn auto_gc_backoff_path(cache_dir: &Path) -> PathBuf {
cache_dir.join("auto-gc-backoff.json")
}
fn auto_gc_threshold(max_size: u64) -> u64 {
max_size.saturating_add(max_size / 100 * AUTO_GC_SLACK_PERCENT)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
struct AutoGcBackoff {
since: u64,
interval_secs: u64,
size_after: u64,
#[serde(default)]
held: u64,
}
const AUTO_GC_HELD_TTL: std::time::Duration = std::time::Duration::from_secs(6 * 3600);
fn recent_held_bytes(backoff: Option<AutoGcBackoff>, now: u64) -> u64 {
backoff
.filter(|b| now < b.since.saturating_add(AUTO_GC_HELD_TTL.as_secs()))
.map_or(0, |b| b.held)
}
fn read_auto_gc_backoff(cache_dir: &Path) -> Option<AutoGcBackoff> {
let json = std::fs::read(auto_gc_backoff_path(cache_dir)).ok()?;
serde_json::from_slice(&json).ok()
}
fn next_auto_gc_backoff(
previous: Option<AutoGcBackoff>,
now: u64,
size_after: u64,
held: u64,
max_size: u64,
) -> Option<AutoGcBackoff> {
if size_after.saturating_sub(held) <= auto_gc_threshold(max_size) {
return (held > 0).then_some(AutoGcBackoff {
since: now,
interval_secs: 0,
size_after,
held,
});
}
let interval_secs = previous
.map_or(AUTO_GC_CHECK_INTERVAL.as_secs(), |b| b.interval_secs)
.max(AUTO_GC_CHECK_INTERVAL.as_secs())
.saturating_mul(2)
.min(AUTO_GC_MAX_BACKOFF.as_secs());
Some(AutoGcBackoff {
since: now,
interval_secs,
size_after,
held,
})
}
fn auto_gc_backoff_holds(
backoff: Option<AutoGcBackoff>,
now: u64,
total: u64,
max_size: u64,
) -> bool {
let Some(backoff) = backoff else {
return false;
};
let grown = total.saturating_sub(backoff.size_after);
now < backoff.since.saturating_add(backoff.interval_secs)
&& grown <= max_size / 100 * AUTO_GC_SLACK_PERCENT
}
pub(crate) fn auto_gc_sweep_due(config: &Config, total: u64) -> bool {
auto_gc_due_at(
read_auto_gc_backoff(&config.cache_dir),
unix_now_secs(),
total,
config.max_size,
)
}
fn auto_gc_due_at(backoff: Option<AutoGcBackoff>, now: u64, total: u64, max_size: u64) -> bool {
total.saturating_sub(recent_held_bytes(backoff, now)) > auto_gc_threshold(max_size)
&& !auto_gc_backoff_holds(backoff, now, total, max_size)
}
pub(crate) fn record_auto_gc_outcome(config: &Config, size_after: u64, held: u64) {
let path = auto_gc_backoff_path(&config.cache_dir);
let next = next_auto_gc_backoff(
read_auto_gc_backoff(&config.cache_dir),
unix_now_secs(),
size_after,
held,
config.max_size,
);
let Some(next) = next else {
let _ = std::fs::remove_file(&path);
return;
};
tracing::info!("{}", auto_gc_outcome_line(&next, config.max_size));
if let Ok(json) = serde_json::to_vec(&next)
&& let Err(e) = crate::atomic::atomic_replace(&path, &json)
{
tracing::debug!("auto-gc: could not write {}: {e:#}", path.display());
}
}
fn auto_gc_outcome_line(outcome: &AutoGcBackoff, max_size: u64) -> String {
if outcome.interval_secs > 0 {
format!(
"auto-gc: store still at {} after the sweep, {} of it held by target directories \
(max {max_size}); next automatic sweep in {}s at the earliest",
outcome.size_after, outcome.held, outcome.interval_secs
)
} else {
format!(
"auto-gc: store at {} after the sweep, {} of it held by target directories \
that no sweep can free (max {max_size})",
outcome.size_after, outcome.held
)
}
}
#[cfg(test)]
pub(crate) fn age_auto_gc_record_for_test(cache_dir: &Path, secs: u64) {
let mut record = read_auto_gc_backoff(cache_dir).expect("a recorded sweep");
record.since -= secs;
let json = serde_json::to_vec(&record).unwrap();
std::fs::write(auto_gc_backoff_path(cache_dir), json).unwrap();
}
#[cfg(test)]
pub(crate) fn auto_gc_backoff_interval_for_test(cache_dir: &Path) -> Option<u64> {
read_auto_gc_backoff(cache_dir).map(|backoff| backoff.interval_secs)
}
#[cfg(test)]
pub(crate) fn expire_auto_gc_backoff_for_test(cache_dir: &Path) {
let mut backoff = read_auto_gc_backoff(cache_dir).expect("a recorded backoff");
backoff.since -= backoff.interval_secs;
let json = serde_json::to_vec(&backoff).unwrap();
std::fs::write(auto_gc_backoff_path(cache_dir), json).unwrap();
}
fn auto_gc_stamp_path(cache_dir: &Path) -> PathBuf {
cache_dir.join("auto-gc-check.stamp")
}
fn auto_gc_wanted(config: &Config, store: &Store) -> bool {
if !config.auto_gc {
return false;
}
let stamp = auto_gc_stamp_path(&config.cache_dir);
if !auto_gc_check_due(&stamp) {
return false;
}
let total = match store.physical_size() {
Ok(total) => total,
Err(e) => {
tracing::debug!("auto-gc: store size query failed: {e:#}");
return false;
}
};
let swept = config.for_store_dir(store.cache_dir(), crate::volume_gc::filesystem_bytes);
if !auto_gc_sweep_due(&swept, total) && !crate::disk_recovery::wanted(&swept) {
let now_str = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs().to_string())
.unwrap_or_default();
let _ = std::fs::write(&stamp, now_str);
return false;
}
if !claim_auto_gc_check(config, &stamp) {
return false;
}
tracing::info!(
"auto-gc: cache or disk pressure, triggering background GC (store {}, max {}, slack {}%)",
total,
swept.max_size,
AUTO_GC_SLACK_PERCENT
);
true
}
fn auto_gc_check_due(stamp: &Path) -> bool {
if let Ok(meta) = std::fs::metadata(stamp) {
match meta.modified().ok().and_then(|m| m.elapsed().ok()) {
Some(age) if age < AUTO_GC_CHECK_INTERVAL => return false,
None => return false,
_ => {}
}
}
true
}
fn claim_auto_gc_check(config: &Config, stamp: &Path) -> bool {
let now_str = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs().to_string())
.unwrap_or_default();
if std::fs::write(stamp, now_str).is_ok() {
return true;
}
if config.auto_recover_min_free_bytes == 0 {
return false;
}
if filetime::set_file_mtime(stamp, filetime::FileTime::now()).is_ok() {
return true;
}
let _ = std::fs::remove_file(stamp);
match std::fs::File::create_new(stamp) {
Ok(_) => true,
Err(e) => {
tracing::debug!("auto-gc: could not claim {}: {e}", stamp.display());
false
}
}
}
pub(crate) fn after_store(config: &Config, store: &Store, failure: Option<&anyhow::Error>) {
after_store_with(
config,
store,
failure,
crate::daemon::send_gc_hint,
spawn_auto_gc_worker,
);
}
fn after_store_with(
config: &Config,
store: &Store,
failure: Option<&anyhow::Error>,
hint_daemon: impl FnOnce(&Config) -> bool,
spawn_worker: impl FnOnce(&Config),
) {
let _trace = crate::phase_trace::phase("auto_gc_check");
let Some(error) = failure else {
run_auto_gc_check(config, store, hint_daemon, spawn_worker);
return;
};
if !is_storage_full(error) || !space_recovery_wanted(config, store) {
return;
}
tracing::info!(
"auto-gc: the volume of {} is full, requesting recovery",
store.cache_dir().display()
);
request_auto_gc(config, hint_daemon, spawn_worker);
}
fn is_storage_full(error: &anyhow::Error) -> bool {
error.chain().any(|cause| {
cause
.downcast_ref::<std::io::Error>()
.is_some_and(|io| io.kind() == std::io::ErrorKind::StorageFull)
|| cause
.downcast_ref::<rusqlite::Error>()
.and_then(rusqlite::Error::sqlite_error_code)
== Some(rusqlite::ErrorCode::DiskFull)
})
}
fn space_recovery_wanted(config: &Config, store: &Store) -> bool {
let swept = config.for_store_dir(store.cache_dir(), crate::volume_gc::filesystem_bytes);
let stamp = auto_gc_stamp_path(&config.cache_dir);
if !crate::disk_recovery::wanted(&swept) || !auto_gc_check_due(&stamp) {
return false;
}
claim_auto_gc_check(config, &stamp)
}
fn run_auto_gc_check(
config: &Config,
store: &Store,
hint_daemon: impl FnOnce(&Config) -> bool,
spawn_worker: impl FnOnce(&Config),
) {
if !auto_gc_wanted(config, store) {
return;
}
request_auto_gc(config, hint_daemon, spawn_worker);
}
fn request_auto_gc(
config: &Config,
hint_daemon: impl FnOnce(&Config) -> bool,
spawn_worker: impl FnOnce(&Config),
) {
if hint_daemon(config) {
tracing::info!("auto-gc: handed the sweep to the daemon");
return;
}
spawn_worker(config);
}
fn auto_gc_worker_command(exe: &Path) -> std::process::Command {
let mut cmd = crate::platform::self_command(exe, "gc");
cmd.env("KACHE_AUTO_GC_WORKER", "1")
.stdin(std::process::Stdio::null());
cmd
}
fn spawn_auto_gc_worker(config: &Config) {
let exe = match std::env::current_exe() {
Ok(exe) => exe,
Err(e) => {
tracing::warn!("auto-gc: cannot resolve current executable: {e}");
return;
}
};
let log_file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(config.cache_dir.join("auto-gc.log"));
let mut cmd = auto_gc_worker_command(&exe);
match log_file {
Ok(f) => {
if let Ok(dup) = f.try_clone() {
cmd.stdout(dup);
} else {
cmd.stdout(std::process::Stdio::null());
}
cmd.stderr(f);
}
Err(_) => {
cmd.stdout(std::process::Stdio::null());
cmd.stderr(std::process::Stdio::null());
}
}
crate::platform::configure_detached_process(&mut cmd);
match cmd.spawn() {
Ok(_) => tracing::info!("auto-gc: spawned background `kache gc`"),
Err(e) => tracing::warn!("auto-gc: failed to spawn `kache gc`: {e}"),
}
}
fn flush_or_hand_off_durability(config: &Config, store: &Store, cache_key: &str) {
let _trace = crate::phase_trace::phase("durability_flush");
if !config.deferred_durability {
return;
}
if crate::transport::is_reachable(&config.socket_path()) {
return;
}
if let Err(error) = store.flush_entry_durability(cache_key) {
tracing::debug!("durability flush failed for {cache_key}: {error:#}");
}
}
fn event_result_for_store_put(put: StorePutResult) -> EventResult {
if put.is_full_dup() {
EventResult::Dup
} else {
EventResult::Miss
}
}
fn compiled_without_lock_reason(peer_committed: bool) -> &'static str {
if peer_committed {
"peer-already-committed"
} else {
"build-lock-unavailable"
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct CcStoreDecision {
admission_skipped: bool,
should_store: bool,
}
fn cc_store_decision(store_candidate: bool, admitted: bool) -> CcStoreDecision {
CcStoreDecision {
admission_skipped: store_candidate && !admitted,
should_store: admitted && store_candidate,
}
}
fn event_result_for_store_admission(
store_candidate: bool,
admitted: bool,
put: StorePutResult,
) -> EventResult {
if cc_store_decision(store_candidate, admitted).admission_skipped {
EventResult::Skipped
} else {
event_result_for_store_put(put)
}
}
fn store_admits_compile(config: &Config, compile_time_ms: u64, publishes_to_remote: bool) -> bool {
let writable_remote = config.remote.is_some() && !config.remote_readonly;
(publishes_to_remote && writable_remote)
|| config.min_store_compile_ms == 0
|| compile_time_ms >= config.min_store_compile_ms
}
fn cc_publishes_to_remote(parsed: &crate::compiler::cc::CcArgs) -> bool {
!parsed.embeds_codeview_debug()
}
fn should_store_cc_result(exit_code: i32, has_artifacts: bool) -> bool {
exit_code == 0 && has_artifacts
}
fn cc_peer_committed_precompile(precompiled: bool, committed: bool) -> bool {
precompiled && committed
}
fn cc_restore_committed(precompiled: bool, entry_ok: bool) -> bool {
!precompiled && entry_ok
}
fn cc_memo_publishable(
clean: bool,
inputs_changed: bool,
include_dirs_hold: impl FnOnce() -> bool,
) -> bool {
clean && !inputs_changed && include_dirs_hold()
}
fn cc_store_candidate(clean: bool, inputs_changed: bool, peer_committed: bool) -> bool {
clean && !inputs_changed && !peer_committed
}
fn cc_output_path_requires_passthrough(path: &Path) -> bool {
crate::compiler::cc::output_path_requires_compiler_semantics(path)
}
pub fn run_cc_probe(args: &[String]) -> Result<i32> {
let program = probe_forward_compiler();
let status = std::process::Command::new(&program)
.args(args)
.status()
.with_context(|| {
format!("spawning `{program}` to forward cc-crate compiler-family probe")
})?;
Ok(status.code().unwrap_or(1))
}
fn probe_forward_compiler() -> String {
let self_stem = std::env::current_exe()
.ok()
.as_deref()
.and_then(Path::file_stem)
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "kache".to_string());
let env_vars = std::env::vars_os()
.filter_map(|(k, v)| Some((k.into_string().ok()?, v.into_string().ok()?)));
let target = std::env::var("TARGET").ok();
crate::compiler::cc::resolve_probe_compiler(&self_stem, target.as_deref(), env_vars)
.unwrap_or_else(|| "cc".to_string())
}
fn take_recheck_hit(
store: &Store,
cache_key: &str,
entry_ok: &impl Fn(&EntryMeta) -> bool,
) -> Option<EntryMeta> {
match store.get(cache_key) {
Ok(Some(meta)) if entry_ok(&meta) => Some(meta),
_ => None,
}
}
fn volume_route_path_rustc(args: &RustcArgs) -> PathBuf {
if let Some(dir) = &args.out_dir {
return dir.clone();
}
if let Some(out) = &args.output {
if let Some(parent) = out.parent().filter(|p| !p.as_os_str().is_empty()) {
return parent.to_path_buf();
}
return out.clone();
}
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
}
fn volume_route_path_cc(parsed: &crate::compiler::cc::CcArgs) -> PathBuf {
if let Some(out) = &parsed.output {
if let Some(parent) = out.parent().filter(|p| !p.as_os_str().is_empty()) {
return parent.to_path_buf();
}
return out.clone();
}
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
}
fn volume_cache_dirs_match(routed: &Path, main: &Path) -> bool {
routed == main
}
fn open_primary_and_fallback(config: &Config, route: &Path) -> Result<(Store, Vec<Store>)> {
crate::link::set_mapped_target(config.volume_store_for(route).is_some());
let routed = config.routed_for_path(route);
let primary = Store::open(&routed)?;
let mut fallbacks = Vec::new();
if !volume_cache_dirs_match(&routed.cache_dir, &config.cache_dir) {
match Store::open(config) {
Ok(store) => fallbacks.push(store),
Err(e) => tracing::warn!(
"main store unavailable for volume-shard fallback ({}): {e:#}",
config.cache_dir.display()
),
}
}
fallbacks.extend(open_readonly_store(config));
Ok((primary, fallbacks))
}
const LOOKUP_BUSY_BUDGET: std::time::Duration = std::time::Duration::from_secs(30);
const LOOKUP_BUSY_PAUSE: std::time::Duration = std::time::Duration::from_millis(20);
const LOOKUP_BUSY_PAUSE_MAX: std::time::Duration = std::time::Duration::from_millis(500);
fn lookup_or_miss<T>(
crate_name: &str,
budget: std::time::Duration,
pause: impl FnMut(std::time::Duration),
lookup: impl FnMut() -> Result<Option<T>>,
) -> Option<T> {
let start = std::time::Instant::now();
lookup_or_miss_with_clock(crate_name, budget, || start.elapsed(), pause, lookup)
}
fn lookup_or_miss_with_clock<T>(
crate_name: &str,
budget: std::time::Duration,
mut elapsed: impl FnMut() -> std::time::Duration,
mut pause: impl FnMut(std::time::Duration),
mut lookup: impl FnMut() -> Result<Option<T>>,
) -> Option<T> {
let mut step = LOOKUP_BUSY_PAUSE;
let error = loop {
let error = match lookup() {
Ok(found) => return found,
Err(error) => error,
};
let left = budget.saturating_sub(elapsed());
if left.is_zero() || !crate::blob_heal::is_index_busy(&error) {
break error;
}
pause(step.min(left));
if elapsed() >= budget {
break error;
}
step = (step * 2).min(LOOKUP_BUSY_PAUSE_MAX);
};
tracing::warn!(
"local store lookup failed for {crate_name} after {:?}: {error:#}; compiling it as a miss",
elapsed()
);
None
}
fn open_readonly_store(config: &Config) -> Option<Store> {
let dir = config.readonly_store.as_ref()?;
if *dir == config.cache_dir {
return None;
}
let mut readonly = config.clone();
readonly.cache_dir = dir.clone();
match Store::open_read_only(&readonly) {
Ok(store) => Some(store),
Err(e) => {
tracing::warn!("read-only store {} unavailable: {e:#}", dir.display());
None
}
}
}
fn lookup_local_entry<'a>(
primary: &'a Store,
fallbacks: &'a [Store],
cache_key: &str,
) -> Result<Option<(&'a Store, crate::store::EntryMeta)>> {
crate::demand::record(cache_key);
let _trace = crate::phase_trace::phase("lookup");
for store in std::iter::once(primary).chain(fallbacks) {
if let Some(meta) = store.get(cache_key)? {
return Ok(Some((store, meta)));
}
}
Ok(None)
}
fn admit_scheduler_miss(
config: &Config,
store: &Store,
cache_key: &str,
identity: FlightIdentity,
crate_name: &str,
is_link: bool,
entry_ok: impl Fn(&EntryMeta) -> bool,
) -> (MissGuard, Option<EntryMeta>) {
if !config.scheduler {
return (MissGuard::empty(), None);
}
let identity = identity.with_key(cache_key);
loop {
match scheduler::begin_miss(
&config.cache_dir,
true,
&identity,
crate_name,
is_link,
config.test_lease.as_deref(),
) {
scheduler::BeginMiss::Recheck => {
if let Some(meta) = take_recheck_hit(store, cache_key, &entry_ok) {
return (MissGuard::empty(), Some(meta));
}
}
scheduler::BeginMiss::Compile(guard) => return (guard, None),
}
}
}
fn wrapper_entry() -> std::time::Instant {
match crate::opcounts::process_start() {
Some(start) => {
crate::opcounts::record_startup(start.elapsed());
start
}
None => std::time::Instant::now(),
}
}
pub fn run_nvcc(config: &Config, wrapper_args: &[String]) -> Result<i32> {
let rebuilt_package = KeyEnv::capture().var("CARGO_PKG_NAME");
let _trace = crate::phase_trace::start("nvcc", wrapper_args);
let start = wrapper_entry();
let invocation_start_ns = crate::cache_key::stamp_clock_ns();
crate::link::set_windows_hardlink_restore(config.windows_hardlink);
crate::link::set_shared_hardlink_restores(config.shared_hardlink_restores);
crate::link::set_storage_layout_advice(config.storage_layout_advice);
crate::link::set_layout_advice_to_log(true);
crate::link::set_cow_warn_marker(warn_marker_path("cow", &config.cache_dir));
warn_nonlocal_cache_fs_once(config);
let compiler =
NvccCompiler::with_extra_allowlist_flags(config.cc_extra_allowlist_flags.clone())
.with_base_dirs(config.base_dirs.clone());
let parsed = compiler
.parse(wrapper_args)
.context("parsing nvcc arguments")?;
let event_root = nvcc_event_root();
let crate_name = parsed
.sources
.first()
.and_then(|s| s.file_name())
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "unknown".to_string());
let refuse = compiler.refuse_reasons(&parsed);
if !refuse.is_empty() {
let reasons: Vec<&str> = refuse.iter().map(|r| r.description()).collect();
tracing::debug!("nvcc: passthrough ({})", reasons.join("; "));
let reason = refuse_reason_string(&refuse);
return nvcc_passthrough_with_event(
config,
&parsed,
&crate_name,
&event_root,
start,
reason,
);
}
if let Some(reason) = config
.project_rules
.user_bypass_reason(&crate_name, &parsed.rest)
{
tracing::debug!("nvcc invocation bypassed by user rule: {reason}");
return nvcc_passthrough_with_event(
config,
&parsed,
&crate_name,
&event_root,
start,
reason,
);
}
let current_dir = std::env::current_dir().ok();
let exclude_roots: Vec<_> = current_dir.iter().cloned().collect();
if let Some(source) = parsed.sources.first()
&& config.project_rules.source_excluded(source, &exclude_roots)
{
tracing::debug!("nvcc source excluded from cache: {}", source.display());
return nvcc_passthrough_with_event(
config,
&parsed,
&crate_name,
&event_root,
start,
format!("source excluded: {}", source.display()),
);
}
nvcc_legacy_blob_check(config, &parsed)?;
let store = match Store::open(config) {
Ok(store) => store,
Err(e) => {
warn_store_unavailable_once(config, &e);
return nvcc_passthrough_with_event(
config,
&parsed,
&crate_name,
&event_root,
start,
format!("store unavailable: {e}"),
);
}
};
let key_start = std::time::Instant::now();
let mut file_hasher = store.file_hasher();
file_hasher.arm_too_new_guard(invocation_start_ns, 0);
let path_normalizer = crate::path_normalizer::PathNormalizer::empty();
let key_ctx = KeyCtx {
file_hasher: &file_hasher,
path_normalizer: &path_normalizer,
cache_dir: &config.cache_dir,
key_salt: config.key_salt.as_deref(),
key_env_vars: &config.key_env_vars,
extra_inputs_digest: None,
build_script_inputs_digest: None,
};
let cache_key = match compiler.cache_key(&parsed, &key_ctx) {
Ok(k) => k,
Err(e) => {
tracing::debug!("nvcc cache key failed for {crate_name}: {e} — passthrough");
return nvcc_passthrough_with_event(
config,
&parsed,
&crate_name,
&event_root,
start,
format!("uncacheable|{e}"),
);
}
};
let key_ms = key_start.elapsed().as_millis() as u64;
tracing::debug!("nvcc cache key for {}: {}", crate_name, &cache_key[..16]);
let lookup_start = std::time::Instant::now();
let lookup = lookup_or_miss(&crate_name, LOOKUP_BUSY_BUDGET, std::thread::sleep, || {
lookup_local_entry(&store, &[], &cache_key)
});
let lookup_ms = lookup_start.elapsed().as_millis() as u64;
let mut lookup_rejection = String::new();
if let Some((hit_store, meta)) = lookup {
if meta.files.is_empty() {
tracing::warn!("nvcc cache entry for {crate_name} has no files, evicting");
lookup_rejection = "matching entry has no cached artifacts".to_string();
let _ = hit_store.remove_entry(&cache_key);
} else if let Some(reason) = nvcc_cache_entry_rejection_reason(&parsed, &meta) {
tracing::warn!(
"nvcc cache entry for {crate_name} lacks artifacts required by this invocation ({reason}), evicting"
);
lookup_rejection = reason.to_string();
let _ = hit_store.remove_entry(&cache_key);
} else {
let restore_start = std::time::Instant::now();
if let Err(e) = restore_nvcc_from_cache(hit_store, &parsed, &meta) {
tracing::warn!(
"restoring nvcc cache hit for {crate_name} failed: {e} — recompiling"
);
return nvcc_passthrough_with_event(
config,
&parsed,
&crate_name,
&event_root,
start,
format!("restore failed: {e}"),
);
}
let restore_ms = restore_start.elapsed().as_millis() as u64;
tracing::debug!(
"nvcc local cache hit for {crate_name} ({})",
&cache_key[..16]
);
HitCompletion {
event_root: &event_root,
crate_name: &crate_name,
result: EventResult::LocalHit,
cache_key: &cache_key,
start,
key_ms,
key_hash_stats: FileHashStats::default(),
lookup_ms,
restore_ms,
key_record: KeyEventRecord::default(),
object_output: recorded_object_output(parsed.object_output_path()),
}
.report(config, &meta);
return Ok(0);
}
}
if let Some(exit) = nvcc_try_remote_hit(
config,
&store,
&parsed,
&cache_key,
&crate_name,
&event_root,
start,
key_ms,
lookup_ms,
)? {
return Ok(exit);
}
nvcc_legacy_blob_check(config, &parsed)?;
let mut committed = None;
let mut _build_lock = None;
match store.claim_build(&cache_key) {
Ok(BuildClaim::Acquired(lock)) => _build_lock = Some(lock),
Ok(BuildClaim::Committed(meta)) => committed = Some(*meta),
Ok(BuildClaim::Contended) => {
tracing::debug!("waiting for nvcc {crate_name} to be built by another process");
committed = store
.wait_for_committed(&cache_key)
.unwrap_or(false)
.then(|| store.get(&cache_key).ok().flatten())
.flatten()
.filter(|meta| nvcc_cache_entry_rejection_reason(&parsed, meta).is_none());
}
Err(e) => {
tracing::debug!("nvcc claim_build failed ({e:#}); compiling without a key lock");
}
}
if let Some(meta) =
committed.filter(|meta| nvcc_cache_entry_rejection_reason(&parsed, meta).is_none())
{
let restore_start = std::time::Instant::now();
if let Err(e) = restore_nvcc_from_cache(&store, &parsed, &meta) {
tracing::warn!(
"restoring nvcc coalesced hit for {crate_name} failed: {e} — recompiling"
);
return nvcc_passthrough_with_event(
config,
&parsed,
&crate_name,
&event_root,
start,
format!("restore failed: {e}"),
);
}
let restore_ms = restore_start.elapsed().as_millis() as u64;
HitCompletion {
event_root: &event_root,
crate_name: &crate_name,
result: EventResult::LocalHit,
cache_key: &cache_key,
start,
key_ms,
key_hash_stats: FileHashStats::default(),
lookup_ms,
restore_ms,
key_record: KeyEventRecord::default(),
object_output: recorded_object_output(parsed.object_output_path()),
}
.report(config, &meta);
return Ok(0);
}
let compile_start = std::time::Instant::now();
let result = match compiler.execute(&parsed) {
Ok(r) => r,
Err(e) => {
return nvcc_passthrough_with_event(
config,
&parsed,
&crate_name,
&event_root,
start,
format!("compiler spawn failed: {e}"),
);
}
};
let compile_time_ms = compile_start.elapsed().as_millis() as u64;
replay_diagnostics(
&result.stdout,
result.pending_stderr(),
std::io::stdout(),
std::io::stderr(),
);
let inputs_changed = file_hasher.inputs_changed_since_keyed();
if inputs_changed {
tracing::debug!(
"nvcc: {crate_name} read an input modified during the build; not storing it"
);
}
let store_start = std::time::Instant::now();
let mut store_put = StorePutResult::default();
let mut store_error = String::new();
let store_candidate = cc_store_candidate(
should_store_cc_result(result.exit_code, !result.artifacts.is_empty()),
inputs_changed,
false,
);
let admitted = store_admits_compile(config, compile_time_ms, true);
let store_decision = cc_store_decision(store_candidate, admitted);
if store_decision.admission_skipped {
tracing::debug!(
crate_name = %crate_name,
compile_time_ms,
min_store_compile_ms = config.min_store_compile_ms,
"admission: compile too cheap to store"
);
}
if store_decision.should_store {
let depinfo_anchor = nvcc_depinfo_rewrite_root(&parsed);
let target = crate::compiler::nvcc::nvcc_target_label(&parsed.deferred_flags);
match prepare_cc_store_files(&result.artifacts, depinfo_anchor.as_deref()) {
Ok(prepared) => {
let put = store.put_with_compile_time_independent(
&cache_key,
&crate_name,
&[], &[], &target,
"", &prepared.files,
&result.stdout,
&result.stderr,
compile_time_ms,
);
after_store(config, &store, put.as_ref().err());
match put {
Ok(put) => {
store_put = put;
flush_or_hand_off_durability(config, &store, &cache_key);
maybe_enqueue_upload(config, &store, &cache_key, &crate_name, true);
}
Err(e) => {
store_error = store_error_for_event(&e);
tracing::warn!(
"failed to store nvcc cache entry for {crate_name}: {store_error}"
);
}
}
}
Err(e) => {
store_error = store_error_for_event(&e);
tracing::warn!(
"failed to prepare nvcc cache entry for {crate_name}: {store_error}"
);
after_store(config, &store, Some(&e));
}
}
}
let store_ms = store_start.elapsed().as_millis() as u64;
let elapsed = start.elapsed().as_millis() as u64;
let size = result.artifacts.total_size();
let event_result = event_result_for_store_admission(store_candidate, admitted, store_put);
log_event(
config,
EventInputs::new(&event_root, &crate_name, event_result, elapsed)
.rebuilt(result.exit_code == 0, &result.artifacts, rebuilt_package)
.compile_time_ms(compile_time_ms)
.size(size)
.keyed(&cache_key, key_ms, FileHashStats::default())
.lookup_ms(lookup_ms)
.store_ms(store_ms)
.store_put(store_put)
.store_error(store_error)
.lookup_rejection(lookup_rejection)
.skip_reason(if event_result == EventResult::Skipped {
"admission-threshold"
} else {
""
})
.object_output(&recorded_object_output(parsed.object_output_path())),
);
print_progress(&crate_name, event_result, elapsed, size);
Ok(result.exit_code)
}
fn nvcc_passthrough(parsed: &crate::compiler::nvcc::NvccArgs) -> Result<PassthroughOutput> {
crate::opcounts::record_compiler_run();
let status = std::process::Command::new(&parsed.program)
.args(&parsed.rest)
.status()
.with_context(|| format!("executing {}", parsed.program))?;
Ok(PassthroughOutput {
exit_code: status.code().unwrap_or(1),
fallback: false,
fallback_attempt: None,
})
}
fn nvcc_passthrough_with_event<R: Into<String>>(
config: &Config,
parsed: &crate::compiler::nvcc::NvccArgs,
crate_name: &str,
root: &str,
start: std::time::Instant,
reason: R,
) -> Result<i32> {
let output = nvcc_passthrough(parsed)?;
log_event(
config,
EventInputs::passthrough(
root,
crate_name,
start.elapsed().as_millis() as u64,
reason.into(),
&output,
)
.object_output(&recorded_object_output(parsed.object_output_path())),
);
Ok(output.exit_code)
}
fn nvcc_event_root() -> String {
nvcc_event_root_in(std::env::var_os("OUT_DIR"))
}
fn nvcc_event_root_in(out_dir: Option<std::ffi::OsString>) -> String {
event_root_string(
event_root_override()
.or_else(|| out_dir_workspace(out_dir))
.or_else(|| std::env::current_dir().ok()),
)
}
fn nvcc_legacy_blob_check(config: &Config, parsed: &crate::compiler::nvcc::NvccArgs) -> Result<()> {
let store_dir = config.store_dir();
let mut outputs = Vec::new();
if let Some(object) = parsed.object_output_path() {
outputs.push(object);
}
if let Some(depfile) = parsed.depinfo_output_path() {
outputs.push(depfile);
}
for output in outputs {
if let Some(blob) = Store::matching_readonly_blob_inode(&store_dir, &output)? {
anyhow::bail!(
"refusing to invoke the compiler because {} still shares the \
read-only cache blob {}; remove the build output and retry",
output.display(),
blob.display()
);
}
}
Ok(())
}
fn nvcc_cache_entry_rejection_reason(
parsed: &crate::compiler::nvcc::NvccArgs,
meta: &crate::store::EntryMeta,
) -> Option<&'static str> {
let has_object = meta
.files
.iter()
.any(|file| classify_by_filename(&file.name) == ArtifactKind::Object);
let has_depinfo = meta
.files
.iter()
.any(|file| classify_by_filename(&file.name) == ArtifactKind::DepInfo);
if !has_object {
Some("matching entry lacks the object artifact required by this invocation")
} else if parsed.depinfo_output_path().is_some() && !has_depinfo {
Some("matching entry lacks dep-info required by this invocation")
} else {
None
}
}
fn nvcc_depinfo_rewrite_root(
parsed: &crate::compiler::nvcc::NvccArgs,
) -> Option<std::path::PathBuf> {
let cwd = std::env::current_dir().ok()?;
nvcc_depinfo_rewrite_root_from_cwd(parsed, &cwd)
}
fn nvcc_depinfo_rewrite_root_from_cwd(
parsed: &crate::compiler::nvcc::NvccArgs,
cwd: &Path,
) -> Option<std::path::PathBuf> {
use std::path::Component;
parsed.depinfo_output_path()?;
let object_anchor = parsed.object_output_path().and_then(|object| {
absolute_clean_path(&object, cwd)
.parent()
.map(Path::to_path_buf)
})?;
let source_anchor = parsed
.sources
.first()
.map(|source| absolute_clean_path(source, cwd))
.and_then(|source| source.parent().map(Path::to_path_buf));
source_anchor
.and_then(|source| common_path_prefix(&source, &object_anchor))
.filter(|root| root.components().any(|c| matches!(c, Component::Normal(_))))
.or(Some(object_anchor))
}
fn restore_nvcc_from_cache(
store: &Store,
parsed: &crate::compiler::nvcc::NvccArgs,
meta: &crate::store::EntryMeta,
) -> Result<()> {
let depinfo_anchor =
nvcc_depinfo_rewrite_root(parsed).unwrap_or_else(|| Path::new(".").to_path_buf());
let mut prepared = Vec::new();
let mut targets = std::collections::HashSet::new();
for cached in &meta.files {
let kind = classify_by_filename(&cached.name);
let target = match kind {
ArtifactKind::Object => parsed
.object_output_path()
.context("nvcc restore: cannot determine object output path")?,
ArtifactKind::DepInfo => match parsed.depinfo_output_path() {
Some(path) => path,
None => {
tracing::debug!(
"nvcc restore: cached dep-info {} not requested by invocation; skipping",
cached.name
);
continue;
}
},
_ => {
tracing::debug!(
"nvcc restore: cached artifact {} has unsupported kind {:?}; skipping",
cached.name,
kind
);
continue;
}
};
if cc_output_path_requires_passthrough(&target) {
anyhow::bail!(
"nvcc restore: output path changed and now requires compiler passthrough semantics"
);
}
anyhow::ensure!(
targets.insert(target.clone()),
"nvcc restore: cache entry maps multiple artifacts to {}",
target.display()
);
prepared.push(prepare_cc_cached_artifact(
store,
cached,
&target,
kind,
&depinfo_anchor,
&[],
)?);
}
publish_prepared_cc_artifacts(prepared)
}
fn nvcc_try_remote_hit(
config: &Config,
store: &Store,
parsed: &crate::compiler::nvcc::NvccArgs,
cache_key: &str,
crate_name: &str,
event_root: &str,
start: std::time::Instant,
key_ms: u64,
lookup_ms: u64,
) -> Result<Option<i32>> {
let Some((meta, event_result)) = acquire_entry(
config,
store,
cache_key,
crate_name,
NegativeReply::ContinueCompile,
) else {
return Ok(None);
};
let restore_start = std::time::Instant::now();
if let Err(e) = restore_nvcc_from_cache(store, parsed, &meta) {
tracing::warn!(
"restoring nvcc remote cache hit for {crate_name} failed: {e} — recompiling"
);
return Ok(Some(nvcc_passthrough_with_event(
config,
parsed,
crate_name,
event_root,
start,
format!("restore failed: {e}"),
)?));
}
let restore_ms = restore_start.elapsed().as_millis() as u64;
HitCompletion {
event_root,
crate_name,
result: event_result,
cache_key,
start,
key_ms,
key_hash_stats: FileHashStats::default(),
lookup_ms,
restore_ms,
key_record: KeyEventRecord::default(),
object_output: recorded_object_output(parsed.object_output_path()),
}
.report(config, &meta);
Ok(Some(0))
}
pub fn run_cc(config: &Config, wrapper_args: &[String]) -> Result<i32> {
let _trace = crate::phase_trace::start("cc", wrapper_args);
let start = wrapper_entry();
let invocation_start_ns = crate::cache_key::stamp_clock_ns();
run_cc_inner(config, wrapper_args, start, invocation_start_ns)
}
struct CcPrecompiled {
result: crate::compile::CompileResult,
compile_time_ms: u64,
inputs: Option<crate::compiler::cc::CcCapturedInputs>,
flight: Option<crate::store::StoreLock>,
}
thread_local! {
static CC_PRECOMPILED_EXIT: std::cell::Cell<Option<i32>> = const { std::cell::Cell::new(None) };
static CC_PRECOMPILED_REBUILT: std::cell::RefCell<RebuiltArtifacts> = std::cell::RefCell::new(RebuiltArtifacts::default());
}
fn run_cc_inner(
config: &Config,
wrapper_args: &[String],
start: std::time::Instant,
invocation_start_ns: i64,
) -> Result<i32> {
crate::link::set_windows_hardlink_restore(config.windows_hardlink);
crate::link::set_shared_hardlink_restores(config.shared_hardlink_restores);
crate::link::set_storage_layout_advice(config.storage_layout_advice);
crate::link::set_layout_advice_to_log(true);
crate::link::set_cow_warn_marker(warn_marker_path("cow", &config.cache_dir));
warn_nonlocal_cache_fs_once(config);
let compiler = CcCompiler::with_extra_allowlist_flags(config.cc_extra_allowlist_flags.clone())
.with_cache_cc_links(config.cache_cc_links)
.with_base_dirs(config.base_dirs.clone());
let trace_parse = crate::phase_trace::phase("cc_parse");
let parsed = compiler
.parse(wrapper_args)
.context("parsing cc-family arguments")?;
drop(trace_parse);
if crate::compiler::cc::cc_is_internal_key_probe() {
let crate_name = parsed
.sources
.first()
.and_then(|s| s.file_name())
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "unknown".to_string());
return cc_direct_passthrough_with_event(
config,
&parsed,
&crate_name,
&cc_event_root(&parsed),
start,
"cc key probe".to_string(),
);
}
let event_root = cc_event_root(&parsed);
let crate_name = parsed
.sources
.first()
.and_then(|s| s.file_name())
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "unknown".to_string());
let trace_preflight = crate::phase_trace::phase("cc_preflight");
let refuse = compiler.refuse_reasons(&parsed);
if !refuse.is_empty() {
let reasons: Vec<&str> = refuse.iter().map(|r| r.description()).collect();
tracing::debug!(
"{}: passthrough ({})",
compiler.id().as_str(),
reasons.join("; ")
);
let reason = refuse_reason_string(&refuse);
return if parsed.requires_compiler_output_semantics() {
cc_direct_passthrough_with_event(
config,
&parsed,
&crate_name,
&event_root,
start,
reason,
)
} else {
cc_passthrough_with_event(config, &parsed, &crate_name, &event_root, start, reason)
};
}
if let Some(reason) = config
.project_rules
.user_bypass_reason(&crate_name, &parsed.rest)
{
tracing::debug!("cc invocation bypassed by user rule: {reason}");
return cc_passthrough_with_event(config, &parsed, &crate_name, &event_root, start, reason);
}
let current_dir = std::env::current_dir().ok();
let exclude_roots: Vec<_> = current_dir.iter().cloned().collect();
if let Some(source) = parsed.sources.first()
&& config.project_rules.source_excluded(source, &exclude_roots)
{
tracing::debug!("cc source excluded from cache: {}", source.display());
return cc_passthrough_with_event(
config,
&parsed,
&crate_name,
&event_root,
start,
format!("source excluded: {}", source.display()),
);
}
drop(trace_preflight);
let trace_store_open = crate::phase_trace::phase("store_open");
let (store, fallback_stores) =
match open_primary_and_fallback(config, &volume_route_path_cc(&parsed)) {
Ok(pair) => {
drop(trace_store_open);
pair
}
Err(e) => {
warn_store_unavailable_once(config, &e);
return cc_passthrough_with_event(
config,
&parsed,
&crate_name,
&event_root,
start,
format!("store unavailable: {e}"),
);
}
};
let invocation = CcStoreInvocation {
compiler,
parsed,
store,
fallback_stores,
crate_name,
event_root,
start,
invocation_start_ns,
};
run_cc_with_store(config, &invocation, None)
}
struct CcStoreInvocation {
compiler: CcCompiler,
parsed: crate::compiler::cc::CcArgs,
store: Store,
fallback_stores: Vec<Store>,
crate_name: String,
event_root: String,
start: std::time::Instant,
invocation_start_ns: i64,
}
fn run_cc_with_store(
config: &Config,
invocation: &CcStoreInvocation,
mut precompiled: Option<CcPrecompiled>,
) -> Result<i32> {
let CcStoreInvocation {
compiler,
parsed,
store,
fallback_stores,
crate_name,
event_root,
start,
invocation_start_ns,
} = invocation;
let start = *start;
let invocation_start_ns = *invocation_start_ns;
let rebuilt_package = KeyEnv::capture().var("CARGO_PKG_NAME");
let key_start = std::time::Instant::now();
let mut file_hasher = store.file_hasher();
file_hasher.arm_too_new_guard(invocation_start_ns, 0);
let path_normalizer = crate::path_normalizer::PathNormalizer::empty();
let key_ctx = KeyCtx {
file_hasher: &file_hasher,
path_normalizer: &path_normalizer,
cache_dir: &config.cache_dir,
key_salt: config.key_salt.as_deref(),
key_env_vars: &config.key_env_vars,
extra_inputs_digest: None,
build_script_inputs_digest: None,
};
let mut captured_inputs_changed = false;
let discovery = match precompiled.as_mut().and_then(|pre| pre.inputs.take()) {
Some(inputs) => {
captured_inputs_changed = inputs.inputs_changed();
crate::compiler::cc::CcKeyDiscovery::Captured(inputs)
}
None if precompiled.is_none()
&& config.deferred_discovery
&& config.remote.is_none()
&& config.fallback.is_none()
&& config.readonly_store.is_none()
&& crate::compiler::cc::cc_direct_key_eligible(parsed) =>
{
crate::compiler::cc::CcKeyDiscovery::Deferrable
}
None => crate::compiler::cc::CcKeyDiscovery::Expansion,
};
let keyed = compiler.cache_key_with(parsed, &key_ctx, discovery);
let keyed = match keyed {
Ok(crate::compiler::cc::CcKeyOutcome::Deferred(deferred)) => {
let flight = crate::scheduler::join_discovery_flight(
&config.cache_dir,
&format!("cc:{}", deferred.memo_key),
);
let keyed = if flight.waited {
compiler.cache_key_with(
parsed,
&key_ctx,
crate::compiler::cc::CcKeyDiscovery::Deferrable,
)
} else {
Ok(crate::compiler::cc::CcKeyOutcome::Deferred(deferred))
};
let flight = flight.lock;
match keyed {
Ok(crate::compiler::cc::CcKeyOutcome::Deferred(_)) => {
return cc_compile_before_key(config, invocation, &file_hasher, flight);
}
other => other,
}
}
other => other,
};
let cache_key = match keyed {
Ok(crate::compiler::cc::CcKeyOutcome::Key(k)) => k,
Ok(crate::compiler::cc::CcKeyOutcome::Deferred(_)) => {
unreachable!("a deferred cc key is resolved above")
}
Err(e) => {
tracing::debug!(
"cc cache key failed for {}: {} — passthrough",
crate_name,
e
);
let reason = format!("uncacheable|{e}");
return if cc_key_error_skips_fallback(&e) {
cc_direct_passthrough_with_event(
config, parsed, crate_name, event_root, start, reason,
)
} else {
cc_passthrough_with_event(config, parsed, crate_name, event_root, start, reason)
};
}
};
let key_ms = key_start.elapsed().as_millis() as u64;
tracing::debug!("cc cache key for {}: {}", crate_name, &cache_key[..16]);
let lookup_start = std::time::Instant::now();
let trace_lookup = crate::phase_trace::phase("lookup");
let lookup = if precompiled.is_some() {
None
} else {
let lookup = lookup_or_miss(crate_name, LOOKUP_BUSY_BUDGET, std::thread::sleep, || {
lookup_local_entry(store, fallback_stores, &cache_key)
});
drop(trace_lookup);
lookup
};
let lookup_ms = lookup_start.elapsed().as_millis() as u64;
let mut lookup_rejection = String::new();
if let Some((hit_store, meta)) = lookup {
if meta.files.is_empty() {
tracing::warn!("cc cache entry for {} has no files, evicting", crate_name);
lookup_rejection = "matching entry has no cached artifacts".to_string();
let _ = hit_store.remove_entry(&cache_key);
} else if let Some(reason) = cc_cache_entry_rejection_reason(parsed, &meta) {
tracing::warn!(
"cc cache entry for {} lacks artifacts required by this invocation ({reason}), evicting",
crate_name,
);
lookup_rejection = reason.to_string();
let _ = hit_store.remove_entry(&cache_key);
} else {
let restore_start = std::time::Instant::now();
let trace_restore = crate::phase_trace::phase("restore");
let restored = restore_cc_hit(hit_store, parsed, meta);
drop(trace_restore);
if let Err(e) = &restored {
if e.downcast_ref::<PartialCcRestore>().is_some() {
return restored.map(|_| 0);
}
tracing::warn!(
"restoring cc cache hit for {} failed: {} — recompiling",
crate_name,
e
);
if !hit_store.is_read_only() {
return cc_passthrough_with_event(
config,
parsed,
crate_name,
event_root,
start,
format!("restore failed: {e}"),
);
}
lookup_rejection = format!("read-only restore failed: {e}");
}
if let Ok(meta) = restored {
let restore_ms = restore_start.elapsed().as_millis() as u64;
tracing::debug!(
"cc local cache hit for {} ({})",
crate_name,
&cache_key[..16]
);
let trace_report = crate::phase_trace::phase("event_report");
HitCompletion {
event_root,
crate_name,
result: EventResult::LocalHit,
cache_key: &cache_key,
start,
key_ms,
key_hash_stats: FileHashStats::default(),
lookup_ms,
restore_ms,
key_record: KeyEventRecord::default(),
object_output: recorded_object_output(parsed.object_output_path()),
}
.report(config, &meta);
drop(trace_report);
let _trace = crate::phase_trace::phase("memo_commit");
compiler.commit_preprocess_memo(&file_hasher);
return Ok(0);
}
}
}
if precompiled.is_none()
&& let Some(exit) = cc_try_remote_hit(
config,
store,
compiler,
parsed,
&file_hasher,
&cache_key,
crate_name,
event_root,
start,
key_ms,
lookup_ms,
)?
{
return Ok(exit);
}
if precompiled.is_none() && parsed.requires_compiler_output_semantics() {
return cc_direct_passthrough_with_event(
config,
parsed,
crate_name,
event_root,
start,
"output appeared before compiler execution",
);
}
let (miss_guard, scheduled_hit) = if precompiled.is_none() {
admit_scheduler_miss(
config,
store,
&cache_key,
FlightIdentity::cc(crate_name),
crate_name,
false,
|meta| cc_scheduled_hit_ok(parsed, meta),
)
} else {
(MissGuard::empty(), None)
};
let mut committed = scheduled_hit;
let mut _build_lock = None;
if committed.is_none() {
match store.claim_build(&cache_key) {
Ok(BuildClaim::Acquired(lock)) => _build_lock = Some(lock),
Ok(BuildClaim::Committed(meta)) => committed = Some(*meta),
Ok(BuildClaim::Contended) => {
tracing::debug!(
"waiting for cc {} to be built by another process",
crate_name
);
committed = store
.wait_for_committed(&cache_key)
.unwrap_or(false)
.then(|| store.get(&cache_key).ok().flatten())
.flatten()
.filter(|meta| cc_scheduled_hit_ok(parsed, meta));
}
Err(e) => {
tracing::debug!("cc claim_build failed ({e:#}); compiling without a key lock");
}
}
}
let peer_committed = cc_peer_committed_precompile(precompiled.is_some(), committed.is_some());
if let Some(meta) = committed.filter(|meta| {
cc_restore_committed(precompiled.is_some(), cc_scheduled_hit_ok(parsed, meta))
}) {
let restore_start = std::time::Instant::now();
let restored = restore_cc_hit(store, parsed, meta);
if let Err(e) = restored {
if e.downcast_ref::<PartialCcRestore>().is_some() {
return Err(e);
}
tracing::warn!(
"restoring cc coalesced hit for {} failed: {} — recompiling",
crate_name,
e
);
return cc_passthrough_with_event(
config,
parsed,
crate_name,
event_root,
start,
format!("restore failed: {e}"),
);
}
let meta = restored?;
let restore_ms = restore_start.elapsed().as_millis() as u64;
HitCompletion {
event_root,
crate_name,
result: EventResult::LocalHit,
cache_key: &cache_key,
start,
key_ms,
key_hash_stats: FileHashStats::default(),
lookup_ms,
restore_ms,
key_record: KeyEventRecord::default(),
object_output: recorded_object_output(parsed.object_output_path()),
}
.report(config, &meta);
compiler.commit_preprocess_memo(&file_hasher);
return Ok(0);
}
let _flight = precompiled.as_mut().and_then(|pre| pre.flight.take());
let (result, compile_time_ms, inputs_changed) = match precompiled.take() {
Some(pre) => {
let changed = captured_inputs_changed || file_hasher.too_new();
(pre.result, pre.compile_time_ms, changed)
}
None => {
let compile_start = std::time::Instant::now();
let result = match compiler.execute(parsed) {
Ok(r) => r,
Err(e) => {
return cc_passthrough_with_event(
config,
parsed,
crate_name,
event_root,
start,
format!("compiler spawn failed: {e}"),
);
}
};
miss_guard.record_compile_rss(crate_name);
let compile_time_ms = compile_start.elapsed().as_millis() as u64;
replay_diagnostics(
&result.stdout,
result.pending_stderr(),
std::io::stdout(),
std::io::stderr(),
);
let changed = file_hasher.inputs_changed_since_keyed();
(result, compile_time_ms, changed)
}
};
if inputs_changed {
tracing::debug!(
"cc: {} read an input modified during the build; not storing it",
crate_name
);
}
let store_start = std::time::Instant::now();
let mut store_put = StorePutResult::default();
let mut store_error = String::new();
let clean = should_store_cc_result(result.exit_code, !result.artifacts.is_empty());
let store_candidate = cc_store_candidate(clean, inputs_changed, peer_committed);
let memo_publishable = cc_memo_publishable(clean, inputs_changed, || {
cc_include_dirs_hold(parsed.mode, || {
compiler.include_dir_names_still_match(parsed)
})
});
let daemon_publish = config.daemon_publish
&& crate::daemon::existing_daemon_run_lock_is_held(&config.socket_path()).unwrap_or(false);
let handoff_memo = daemon_publish
.then(|| compiler.captured_preprocess_memo())
.flatten();
if memo_publishable && handoff_memo.is_none() {
compiler.commit_preprocess_memo(&file_hasher);
}
let publishes_to_remote = cc_publishes_to_remote(parsed);
let admitted = store_admits_compile(config, compile_time_ms, publishes_to_remote);
let store_decision = cc_store_decision(store_candidate, admitted);
if store_decision.admission_skipped {
tracing::debug!(
crate_name = %crate_name,
compile_time_ms,
min_store_compile_ms = config.min_store_compile_ms,
"admission: compile too cheap to store"
);
}
if store_decision.should_store && !memo_publishable {
tracing::debug!(
crate_name = %crate_name,
"cc include-dir names changed during compile; skipping store"
);
} else if store_decision.should_store {
let _trace = crate::phase_trace::phase("store");
let depinfo_anchor = cc_depinfo_rewrite_root(parsed);
let target = parsed.cache_target_arch();
let staging_dir = daemon_publish.then(|| crate::daemon_publish::handoff_dir(config));
match prepare_cc_store_files_in(
&result.artifacts,
depinfo_anchor.as_deref(),
staging_dir.as_deref(),
&parsed.depinfo_roots,
) {
Ok(prepared) => {
let stdout = if crate::compiler::cc::cc_expansion_is_stdout(parsed) {
""
} else {
&result.stdout
};
let stdout = cc_diagnostics::store(stdout, &parsed.depinfo_roots);
let stderr = cc_diagnostics::store(&result.stderr, &parsed.depinfo_roots);
if daemon_publish {
let handoff = CcHandoff {
cache_key: &cache_key,
crate_name,
target: &target,
files: &prepared.files,
stdout: &stdout,
stderr: &stderr,
compile_time_ms,
publishes_to_remote,
event_root,
start,
size: result.artifacts.total_size(),
key_ms,
lookup_ms,
lookup_rejection: &lookup_rejection,
store_start,
rebuilt: RebuiltArtifacts::observed(
store_candidate,
&result.artifacts,
rebuilt_package.clone(),
),
memo: handoff_memo,
};
match hand_off_cc_store(config, store, &mut _build_lock, handoff) {
CcHandoffOutcome::Accepted => {
compiler.discard_preprocess_memo();
return Ok(result.exit_code);
}
CcHandoffOutcome::Done => {
compiler.commit_preprocess_memo(&file_hasher);
return Ok(result.exit_code);
}
CcHandoffOutcome::Publish => {
compiler.commit_preprocess_memo(&file_hasher);
}
}
}
let put = store.put_with_compile_time_independent(
&cache_key,
crate_name,
&[], &[], &target,
"", &prepared.files,
&stdout,
&stderr,
compile_time_ms,
);
after_store(config, store, put.as_ref().err());
match put {
Ok(result) => {
store_put = result;
flush_or_hand_off_durability(config, store, &cache_key);
maybe_enqueue_upload(
config,
store,
&cache_key,
crate_name,
publishes_to_remote,
);
}
Err(e) => {
store_error = store_error_for_event(&e);
tracing::warn!(
"failed to store cc cache entry for {}: {}",
crate_name,
store_error
);
}
}
}
Err(e) => {
store_error = store_error_for_event(&e);
tracing::warn!(
"failed to prepare cc cache entry for {}: {}",
crate_name,
store_error
);
after_store(config, store, Some(&e));
}
}
}
if memo_publishable {
compiler.commit_preprocess_memo(&file_hasher);
}
let store_ms = store_start.elapsed().as_millis() as u64;
let elapsed = start.elapsed().as_millis() as u64;
let size = result.artifacts.total_size();
let event_result = event_result_for_store_admission(store_candidate, admitted, store_put);
log_event(
config,
EventInputs::new(event_root, crate_name, event_result, elapsed)
.rebuilt(result.exit_code == 0, &result.artifacts, rebuilt_package)
.compile_time_ms(compile_time_ms)
.size(size)
.keyed(&cache_key, key_ms, FileHashStats::default())
.lookup_ms(lookup_ms)
.store_ms(store_ms)
.store_put(store_put)
.store_error(store_error)
.lookup_rejection(lookup_rejection)
.skip_reason(if event_result == EventResult::Skipped {
"admission-threshold"
} else {
""
})
.object_output(&recorded_object_output(parsed.object_output_path())),
);
print_progress(crate_name, event_result, elapsed, size);
Ok(result.exit_code)
}
fn cc_key_error_skips_fallback(error: &anyhow::Error) -> bool {
error
.downcast_ref::<crate::compiler::cc::CcHiddenInput>()
.is_some()
}
fn refuse_reason_string(refuse: &[crate::compiler::RefuseReason]) -> String {
let category = refuse.first().map_or("unsupported", |r| r.category());
let detail = refuse
.iter()
.map(|r| r.description())
.collect::<Vec<_>>()
.join("; ");
format!("{category}|{detail}")
}
fn cc_passthrough(
config: &Config,
parsed: &crate::compiler::cc::CcArgs,
) -> Result<PassthroughOutput> {
cc_passthrough_impl(config, parsed, false)
}
fn cc_direct_passthrough(
config: &Config,
parsed: &crate::compiler::cc::CcArgs,
) -> Result<PassthroughOutput> {
cc_passthrough_impl(config, parsed, true)
}
fn cc_passthrough_impl(
config: &Config,
parsed: &crate::compiler::cc::CcArgs,
force_direct: bool,
) -> Result<PassthroughOutput> {
let mut fallback_attempt = None;
if let Some(fb) = config.fallback.as_deref()
&& !force_direct
&& !parsed.requires_compiler_output_semantics()
{
let mut cmd = std::process::Command::new(fb);
cmd.arg(&parsed.program);
cmd.args(&parsed.rest);
let outputs: Vec<&Path> = parsed
.output
.as_deref()
.map(Path::new)
.into_iter()
.collect();
let attempt = crate::fallback::run(cmd, fb, &outputs, &parsed.rest);
if let Some(exit_code) = attempt.terminal_code() {
return Ok(PassthroughOutput {
exit_code,
fallback: true,
fallback_attempt: Some(attempt),
});
}
fallback_attempt = Some(attempt);
}
refuse_legacy_cc_blob_outputs(config, parsed)?;
crate::opcounts::record_compiler_run();
let status = std::process::Command::new(&parsed.program)
.args(&parsed.rest)
.status()
.with_context(|| format!("executing {}", parsed.program))?;
Ok(PassthroughOutput {
exit_code: status.code().unwrap_or(1),
fallback: false,
fallback_attempt,
})
}
pub(crate) fn refuse_legacy_cc_blob_outputs(
config: &Config,
parsed: &crate::compiler::cc::CcArgs,
) -> Result<()> {
let store_dir = config.store_dir();
for output in parsed.compiler_output_paths() {
if let Some(blob) = Store::matching_readonly_blob_inode(&store_dir, &output)? {
anyhow::bail!(
"refusing to invoke the compiler because {} still shares the \
read-only cache blob {}; remove the build output and retry",
output.display(),
blob.display()
);
}
}
Ok(())
}
fn cache_entry_has_files(meta: &crate::store::EntryMeta) -> bool {
!meta.files.is_empty()
}
fn cc_scheduled_hit_ok(
parsed: &crate::compiler::cc::CcArgs,
meta: &crate::store::EntryMeta,
) -> bool {
cache_entry_has_files(meta) && cc_cache_entry_rejection_reason(parsed, meta).is_none()
}
#[cfg(test)]
fn cc_cache_entry_satisfies_invocation(
parsed: &crate::compiler::cc::CcArgs,
meta: &crate::store::EntryMeta,
) -> bool {
cc_cache_entry_rejection_reason(parsed, meta).is_none()
}
fn cc_preprocess_restore_target(
parsed: &crate::compiler::cc::CcArgs,
cached_name: &str,
) -> Option<std::path::PathBuf> {
if parsed.mode != crate::compiler::cc::CompileMode::Preprocess {
return None;
}
let target = parsed.object_output_path()?;
let names_match = target
.file_name()
.is_some_and(|name| name.to_string_lossy() == cached_name);
names_match.then_some(target)
}
fn cc_store_revalidates_include_dirs(mode: crate::compiler::cc::CompileMode) -> bool {
mode == crate::compiler::cc::CompileMode::Compile
}
fn cc_include_dirs_hold(
mode: crate::compiler::cc::CompileMode,
names_still_match: impl FnOnce() -> bool,
) -> bool {
!cc_store_revalidates_include_dirs(mode) || names_still_match()
}
fn cc_cache_entry_rejection_reason(
parsed: &crate::compiler::cc::CcArgs,
meta: &crate::store::EntryMeta,
) -> Option<&'static str> {
let has_object = meta
.files
.iter()
.any(|file| classify_by_filename(&file.name) == ArtifactKind::Object);
let has_depinfo = meta
.files
.iter()
.any(|file| classify_by_filename(&file.name) == ArtifactKind::DepInfo);
let has_named_output = meta
.files
.iter()
.any(|file| cc_preprocess_restore_target(parsed, &file.name).is_some());
let has_stdout = meta
.files
.iter()
.any(|file| file.name == crate::compiler::cc::CC_STDOUT_STORE_NAME);
match parsed.mode {
crate::compiler::cc::CompileMode::Compile if !has_object => {
Some("matching entry lacks the object artifact required by this invocation")
}
crate::compiler::cc::CompileMode::Preprocess
if crate::compiler::cc::cc_expansion_is_stdout(parsed) && !has_stdout =>
{
Some(
"matching entry lacks the preprocessor stdout artifact required by this invocation",
)
}
crate::compiler::cc::CompileMode::Preprocess
if parsed.output.is_some() && !has_named_output =>
{
Some("matching entry lacks the preprocessed output required by this invocation")
}
crate::compiler::cc::CompileMode::Link if meta.files.is_empty() => {
Some("matching entry lacks the link artifact required by this invocation")
}
_ if parsed.depinfo_output_path().is_some() && !has_depinfo => {
Some("matching entry lacks dep-info required by this invocation")
}
_ => None,
}
}
fn cc_depinfo_rewrite_root(parsed: &crate::compiler::cc::CcArgs) -> Option<std::path::PathBuf> {
let cwd = std::env::current_dir().ok()?;
cc_depinfo_rewrite_root_from_cwd(parsed, &cwd)
}
fn rustc_event_root(args: &RustcArgs) -> String {
let written_to = args.out_dir.clone().or_else(|| {
args.output
.as_deref()
.and_then(Path::parent)
.map(Path::to_path_buf)
});
event_root_string(event_root_override().or_else(|| {
written_to
.as_deref()
.and_then(cargo_workspace_of)
.or_else(|| args.workspace_root())
.or_else(|| std::env::current_dir().ok())
}))
}
fn cc_event_root(parsed: &crate::compiler::cc::CcArgs) -> String {
cc_event_root_in(parsed, std::env::var_os("OUT_DIR"))
}
fn cc_event_root_in(
parsed: &crate::compiler::cc::CcArgs,
out_dir: Option<std::ffi::OsString>,
) -> String {
event_root_string(
event_root_override()
.or_else(|| out_dir_workspace(out_dir))
.or_else(|| cc_depinfo_rewrite_root(parsed).or_else(|| std::env::current_dir().ok())),
)
}
fn cargo_workspace_of(dir: &Path) -> Option<PathBuf> {
dir.ancestors()
.find(|ancestor| crate::tree_stamp::is_cargo_build_tag(&ancestor.join("CACHEDIR.TAG")))
.and_then(Path::parent)
.map(Path::to_path_buf)
}
fn out_dir_workspace(out_dir: Option<std::ffi::OsString>) -> Option<PathBuf> {
let out_dir = out_dir.filter(|value| !value.is_empty())?;
cargo_workspace_of(Path::new(&out_dir))
}
fn event_root_override() -> Option<PathBuf> {
std::env::var_os("KACHE_EVENT_ROOT")
.filter(|value| !value.is_empty())
.map(PathBuf::from)
}
fn event_root_string(root: Option<PathBuf>) -> String {
let Some(root) = root else {
return String::new();
};
let abs = if root.is_absolute() {
root
} else {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(root)
};
std::fs::canonicalize(&abs)
.unwrap_or(abs)
.to_string_lossy()
.into_owned()
}
fn cc_depinfo_rewrite_root_from_cwd(
parsed: &crate::compiler::cc::CcArgs,
cwd: &Path,
) -> Option<std::path::PathBuf> {
parsed.depinfo_output_path()?;
let object_anchor = parsed
.depinfo_anchor()
.map(|anchor| absolute_clean_path(&anchor, cwd))?;
let source_anchor = parsed
.sources
.first()
.map(|source| absolute_clean_path(source, cwd))
.and_then(|source| source.parent().map(Path::to_path_buf));
source_anchor
.and_then(|source| common_path_prefix(&source, &object_anchor))
.filter(|root| root.components().any(|c| matches!(c, Component::Normal(_))))
.or(Some(object_anchor))
}
fn absolute_clean_path(path: &Path, cwd: &Path) -> std::path::PathBuf {
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
cwd.join(path)
};
clean_path(&absolute)
}
fn clean_path(path: &Path) -> std::path::PathBuf {
let mut cleaned = std::path::PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
if !cleaned.pop() {
cleaned.push(component.as_os_str());
}
}
Component::Prefix(_) | Component::RootDir | Component::Normal(_) => {
cleaned.push(component.as_os_str());
}
}
}
if cleaned.as_os_str().is_empty() {
Path::new(".").to_path_buf()
} else {
cleaned
}
}
fn common_path_prefix(left: &Path, right: &Path) -> Option<std::path::PathBuf> {
let mut prefix = std::path::PathBuf::new();
let mut matched = false;
for (left_component, right_component) in left.components().zip(right.components()) {
if left_component != right_component {
break;
}
prefix.push(left_component.as_os_str());
matched = true;
}
matched.then_some(prefix)
}
#[derive(Debug)]
struct PartialCcRestore;
impl std::fmt::Display for PartialCcRestore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("cc cache restore published only part of the output set")
}
}
impl std::error::Error for PartialCcRestore {}
fn prepare_cc_cached_artifact(
store: &Store,
cached: &crate::store::CachedFile,
target: &Path,
kind: ArtifactKind,
depinfo_anchor: &Path,
depinfo_roots: &[(PathBuf, String, u8)],
) -> Result<link::PreparedWritableTarget> {
let transforms: Vec<_> = plan_post_restore(kind)
.into_iter()
.filter(|action| action.is_content_transform())
.collect();
let blob = store.blob_path(&cached.hash);
if !blob.exists() {
anyhow::bail!(
"cc restore: blob for {} (hash {}) was evicted before restore: {}",
cached.name,
&cached.hash[..16.min(cached.hash.len())],
blob.display()
);
}
if transforms.is_empty() {
return link::prepare_writable_target_from_file(&blob, target).with_context(|| {
format!(
"cc restore: staging {} -> {}",
blob.display(),
target.display()
)
});
}
let mut content = std::fs::read(&blob)
.with_context(|| format!("cc restore: reading blob {}", blob.display()))?;
if kind == ArtifactKind::DepInfo {
let text = std::str::from_utf8(&content).context("cc restore: dep-info is not UTF-8")?;
content = link::rewrite_rustc_depinfo_content_with_configured_roots(
text,
depinfo_anchor,
depinfo_anchor,
None,
depinfo_roots,
link::DepInfoMode::Expand,
)
.into_bytes();
}
for action in transforms {
content = action.transform(content, depinfo_anchor);
}
link::prepare_writable_target_from_bytes(target, &content)
.with_context(|| format!("cc restore: staging transformed {}", target.display()))
}
fn apply_cc_post_publish_actions(published: &[(PathBuf, ArtifactKind)]) -> Result<()> {
let host = platform::current();
for (path, kind) in published {
link::touch_mtime_write_clock(path)
.with_context(|| format!("cc restore: stamping {}", path.display()))?;
for action in plan_post_restore(*kind) {
if action.is_content_transform() {
continue;
}
action.apply(path, &*host).with_context(|| {
format!("cc restore: applying {action:?} to {}", path.display())
})?;
}
}
Ok(())
}
fn finalize_cc_restore(
published: &[(PathBuf, ArtifactKind)],
parsed: &crate::compiler::cc::CcArgs,
) -> Result<()> {
let finish = || -> Result<()> {
apply_cc_post_publish_actions(published)?;
#[cfg(unix)]
if parsed.mode == crate::compiler::cc::CompileMode::Link
&& let Some(output) = parsed.object_output_path()
{
use std::os::unix::fs::PermissionsExt;
let mut permissions = std::fs::metadata(&output)
.with_context(|| format!("cc restore: stat {}", output.display()))?
.permissions();
permissions.set_mode(0o755);
std::fs::set_permissions(&output, permissions)
.with_context(|| format!("cc restore: chmod +x {}", output.display()))?;
}
#[cfg(not(unix))]
let _ = parsed;
Ok(())
};
finish().context(PartialCcRestore)
}
fn publish_prepared_cc_artifacts(prepared: Vec<link::PreparedWritableTarget>) -> Result<()> {
publish_prepared_cc_artifacts_with(prepared, |_, _| Ok(()))
}
fn publish_prepared_cc_artifacts_with(
prepared: Vec<link::PreparedWritableTarget>,
mut before_publish: impl FnMut(usize, &Path) -> Result<()>,
) -> Result<()> {
let mut replace_existing = Vec::with_capacity(prepared.len());
for artifact in &prepared {
if cc_output_path_requires_passthrough(artifact.target()) {
anyhow::bail!(
"cc restore: output path changed and now requires compiler passthrough semantics"
);
}
replace_existing.push(std::fs::symlink_metadata(artifact.target()).is_ok());
}
for (index, (artifact, replace_existing)) in
prepared.into_iter().zip(replace_existing).enumerate()
{
if let Err(error) = before_publish(index, artifact.target()) {
return if index == 0 {
Err(error)
} else {
Err(error.context(PartialCcRestore))
};
}
let publish = if replace_existing {
if cc_output_path_requires_passthrough(artifact.target()) {
Err(anyhow::anyhow!(
"cc restore: output path changed and now requires compiler passthrough semantics"
))
} else {
artifact.publish_replacing()
}
} else {
artifact.publish()
};
if let Err(error) = publish {
return if index == 0 {
Err(error)
} else {
Err(error.context(PartialCcRestore))
};
}
}
Ok(())
}
fn restore_cc_stdout_from_cache(
store: &Store,
meta: &crate::store::EntryMeta,
writer: &mut impl std::io::Write,
) -> Result<()> {
let cached = meta
.files
.iter()
.find(|file| file.name == crate::compiler::cc::CC_STDOUT_STORE_NAME)
.context("cc restore: -E stdout entry has no stdout.i blob")?;
let blob = store.blob_path(&cached.hash);
let mut file = std::fs::File::open(&blob)
.with_context(|| format!("cc restore: opening {}", blob.display()))?;
std::io::copy(&mut file, writer).context("cc restore: writing -E stdout")?;
writer.flush().context("cc restore: flushing -E stdout")?;
Ok(())
}
fn restore_cc_hit(
store: &Store,
parsed: &crate::compiler::cc::CcArgs,
mut meta: EntryMeta,
) -> Result<EntryMeta> {
if let std::borrow::Cow::Owned(stdout) =
cc_diagnostics::restore(&meta.stdout, &parsed.depinfo_roots)?
{
meta.stdout = stdout;
}
if let std::borrow::Cow::Owned(stderr) =
cc_diagnostics::restore(&meta.stderr, &parsed.depinfo_roots)?
{
meta.stderr = stderr;
}
restore_cc_from_cache(store, parsed, &meta)?;
Ok(meta)
}
fn restore_cc_from_cache(
store: &Store,
parsed: &crate::compiler::cc::CcArgs,
meta: &crate::store::EntryMeta,
) -> Result<()> {
if crate::compiler::cc::cc_expansion_is_stdout(parsed) {
return restore_cc_stdout_from_cache(store, meta, &mut std::io::stdout());
}
if parsed.requires_compiler_output_semantics() {
anyhow::bail!("cc restore: existing output requires compiler passthrough semantics");
}
let depinfo_anchor =
cc_depinfo_rewrite_root(parsed).unwrap_or_else(|| Path::new(".").to_path_buf());
let mut prepared = Vec::new();
let mut published_kinds = Vec::new();
let mut targets = std::collections::HashSet::new();
for cached in &meta.files {
let kind = classify_by_filename(&cached.name);
let target = match kind {
ArtifactKind::Object => parsed
.object_output_path()
.context("cc restore: cannot determine object output path")?,
ArtifactKind::DepInfo => match parsed.depinfo_output_path() {
Some(path) => path,
None => {
tracing::debug!(
"cc restore: cached dep-info {} not requested by invocation; skipping",
cached.name
);
continue;
}
},
ArtifactKind::Executable
| ArtifactKind::DynamicLibrary
| ArtifactKind::WasmModule
| ArtifactKind::Other("extensionless")
if parsed.mode == crate::compiler::cc::CompileMode::Link =>
{
parsed
.object_output_path()
.context("cc restore: cannot determine link output path")?
}
ArtifactKind::DebugSidecar
| ArtifactKind::DebugBundle
| ArtifactKind::Library
| ArtifactKind::Other(_)
if parsed.mode == crate::compiler::cc::CompileMode::Link =>
{
let parent = parsed
.object_output_path()
.and_then(|path| path.parent().map(PathBuf::from))
.unwrap_or_else(|| PathBuf::from("."));
parent.join(&cached.name)
}
_ => match cc_preprocess_restore_target(parsed, &cached.name) {
Some(target) => target,
None => {
tracing::debug!(
"cc restore: cached artifact {} has unsupported kind {:?}; skipping",
cached.name,
kind
);
continue;
}
},
};
if cc_output_path_requires_passthrough(&target) {
anyhow::bail!(
"cc restore: output path changed and now requires compiler passthrough semantics"
);
}
anyhow::ensure!(
targets.insert(target.clone()),
"cc restore: cache entry maps multiple artifacts to {}",
target.display()
);
prepared.push(prepare_cc_cached_artifact(
store,
cached,
&target,
kind,
&depinfo_anchor,
&parsed.depinfo_roots,
)?);
published_kinds.push((target, kind));
}
publish_prepared_cc_artifacts(prepared)?;
finalize_cc_restore(&published_kinds, parsed)
}
fn cc_try_remote_hit(
config: &Config,
store: &Store,
compiler: &CcCompiler,
parsed: &crate::compiler::cc::CcArgs,
file_hasher: &crate::cache_key::FileHasher<'_>,
cache_key: &str,
crate_name: &str,
event_root: &str,
start: std::time::Instant,
key_ms: u64,
lookup_ms: u64,
) -> Result<Option<i32>> {
if !compiler_remote_enabled(config, cc_publishes_to_remote(parsed)) {
return Ok(None);
}
let Some((meta, event_result)) = acquire_entry(
config,
store,
cache_key,
crate_name,
NegativeReply::ContinueCompile,
) else {
return Ok(None);
};
let restore_start = std::time::Instant::now();
let restored = restore_cc_hit(store, parsed, meta);
if let Err(e) = restored {
if e.downcast_ref::<PartialCcRestore>().is_some() {
return Err(e);
}
tracing::warn!(
"restoring cc remote cache hit for {} failed: {} — recompiling",
crate_name,
e
);
return Ok(Some(cc_passthrough_with_event(
config,
parsed,
crate_name,
event_root,
start,
format!("restore failed: {e}"),
)?));
}
let meta = restored?;
let restore_ms = restore_start.elapsed().as_millis() as u64;
HitCompletion {
event_root,
crate_name,
result: event_result,
cache_key,
start,
key_ms,
key_hash_stats: FileHashStats::default(),
lookup_ms,
restore_ms,
key_record: KeyEventRecord::default(),
object_output: recorded_object_output(parsed.object_output_path()),
}
.report(config, &meta);
compiler.commit_preprocess_memo(file_hasher);
Ok(Some(0))
}
pub fn run(config: &Config, wrapper_args: &[String]) -> Result<i32> {
let _trace = crate::phase_trace::start("rustc", wrapper_args);
let start = wrapper_entry();
crate::link::set_windows_hardlink_restore(config.windows_hardlink);
crate::link::set_shared_hardlink_restores(config.shared_hardlink_restores);
crate::link::set_storage_layout_advice(config.storage_layout_advice);
crate::link::set_layout_advice_to_log(true);
crate::link::set_cow_warn_marker(warn_marker_path("cow", &config.cache_dir));
warn_nonlocal_cache_fs_once(config);
let invocation_start_ns = crate::cache_key::stamp_clock_ns();
let compiler = RustcCompiler::new().with_base_dirs(config.base_dirs.clone());
let args = compiler
.parse(wrapper_args)
.context("parsing rustc arguments")?;
let extra_inputs_key_start = std::time::Instant::now();
let mut extra_inputs_hasher =
crate::cache_key::FileHasher::new().with_daemon(config.socket_path());
if config.modified_input_guard {
extra_inputs_hasher.arm_too_new_guard(invocation_start_ns, 0);
}
let crate_name = args.crate_name.as_deref().unwrap_or("unknown");
let trace_extra = crate::phase_trace::phase("extra_inputs_resolve");
let extra_inputs =
crate::extra_inputs::ExtraInputsSnapshot::resolve_for_rustc(&args, &extra_inputs_hasher)
.with_context(|| format!("resolving extra_inputs for {crate_name}"))?;
drop(trace_extra);
validate_extra_inputs_freshness_mode(&args, extra_inputs.is_some())?;
let extra_inputs_hash_stats = extra_inputs_hasher.stats();
let extra_inputs_too_new = extra_inputs_hasher.too_new();
let extra_inputs_guard_inputs = extra_inputs_hasher.take_guarded_inputs();
let extra_inputs_key_ms = extra_inputs_key_start.elapsed().as_millis() as u64;
let drop_fallback = config.fallback.is_some()
&& (extra_inputs.is_some() || build_script_declares_inputs(&args));
let drop_incremental = extra_inputs.is_some() && config.preserve_incremental;
let mut safe_extra_inputs_config = None;
if drop_fallback || drop_incremental {
let mut safe = config.clone();
if drop_fallback {
tracing::debug!(
"disabling fallback cache for {crate_name}: its key holds inputs the fallback does not know"
);
safe.fallback = None;
}
if drop_incremental {
tracing::debug!(
"disabling preserved incremental state for active extra_inputs crate {crate_name}"
);
safe.preserve_incremental = false;
}
safe_extra_inputs_config = Some(safe);
}
let effective_config = safe_extra_inputs_config.as_ref().unwrap_or(config);
let exit = run_parsed_rustc(
effective_config,
&compiler,
&args,
start,
invocation_start_ns,
extra_inputs.as_ref(),
extra_inputs_hash_stats,
extra_inputs_too_new,
extra_inputs_key_ms,
extra_inputs_guard_inputs,
None,
None,
)?;
if exit == 0 {
complete_current_extra_inputs_after_success(
effective_config,
&args,
extra_inputs.as_ref(),
)?;
crate::build_script::install_shim(&args);
}
Ok(exit)
}
fn build_script_declares_inputs(args: &RustcArgs) -> bool {
let cwd = std::env::current_dir().ok();
crate::build_script_inputs::locate(
args,
&|name: &str| std::env::var_os(name),
cwd.as_deref(),
crate::out_dir_alias::active_alias().is_some(),
)
.is_some_and(|located| crate::build_script_inputs::declares_inputs(&located))
}
pub(crate) fn build_script_event_root(out_dir: &Path, manifest_dir: &Path) -> String {
event_root_string(
event_root_override()
.or_else(|| cargo_workspace_of(out_dir))
.or_else(|| Some(manifest_dir.to_path_buf())),
)
}
pub(crate) fn resolve_extra_inputs_for_passthrough(
config: &Config,
args: &RustcArgs,
) -> Result<Option<crate::extra_inputs::ExtraInputsSnapshot>> {
let crate_name = args.crate_name.as_deref().unwrap_or("unknown");
let hasher = crate::cache_key::FileHasher::new().with_daemon(config.socket_path());
let snapshot = crate::extra_inputs::ExtraInputsSnapshot::resolve_for_rustc(args, &hasher)
.with_context(|| format!("resolving extra_inputs for {crate_name}"))?;
validate_extra_inputs_freshness_mode(args, snapshot.is_some())?;
Ok(snapshot)
}
fn validate_extra_inputs_freshness_mode(args: &RustcArgs, active: bool) -> Result<()> {
if active && args.checksum_freshness_enabled() {
anyhow::bail!(
"extra_inputs cannot safely complete Cargo checksum-freshness dep-info yet; \
disable -Z checksum-freshness, or run the whole Cargo command with \
KACHE_DISABLED=1 while retaining matching cargo:rerun-if-changed directives"
);
}
Ok(())
}
pub(crate) fn complete_current_extra_inputs_after_success(
config: &Config,
args: &RustcArgs,
original: Option<&crate::extra_inputs::ExtraInputsSnapshot>,
) -> Result<()> {
let current = resolve_extra_inputs_for_passthrough(config, args)?;
if current.as_ref() != original {
anyhow::bail!(
"extra_inputs declaration changed while the compiler wrapper was running; retry the build"
);
}
match current.as_ref() {
Some(snapshot) => complete_extra_inputs_dep_info(args, snapshot),
None => Ok(()),
}
}
pub(crate) fn complete_extra_inputs_dep_info(
args: &RustcArgs,
snapshot: &crate::extra_inputs::ExtraInputsSnapshot,
) -> Result<()> {
let crate_name = args.crate_name.as_deref().unwrap_or("unknown");
let Some(dep_info_path) = args.dep_info_path() else {
tracing::debug!(
"extra_inputs dep-info completion skipped because rustc did not request a supported \
dep-info path for {crate_name}; non-Cargo callers retain their own freshness mechanism"
);
return Ok(());
};
snapshot
.merge_into_dep_info(&dep_info_path)
.with_context(|| {
format!(
"completing Cargo dep-info {} for extra_inputs",
dep_info_path.display()
)
})
}
#[derive(Default)]
struct BuildScriptInputs {
located: Option<crate::build_script_inputs::Located>,
snapshot: Option<crate::build_script_inputs::Snapshot>,
key_ms: u64,
hash_stats: FileHashStats,
}
impl BuildScriptInputs {
fn digest(&self) -> Option<&str> {
self.snapshot
.as_ref()
.map(|snapshot| snapshot.digest.as_str())
}
}
fn resolve_build_script_inputs(
config: &Config,
args: &RustcArgs,
store: &Store,
invocation_start_ns: i64,
workspace_root: Option<&Path>,
cwd: Option<&Path>,
) -> Result<BuildScriptInputs> {
let _trace = crate::phase_trace::phase("build_script_inputs");
let start = std::time::Instant::now();
let Some(located) = crate::build_script_inputs::locate(
args,
&|name: &str| std::env::var_os(name),
cwd,
crate::out_dir_alias::active_alias().is_some(),
) else {
return Ok(BuildScriptInputs::default());
};
let mut file_hasher = store.file_hasher_with_daemon(config.socket_path());
file_hasher.arm_too_new_guard(invocation_start_ns, 0);
let resolved = resolve_located_build_script_inputs(
config,
args,
store,
&located,
&file_hasher,
workspace_root,
)?;
let crate_name = args.crate_name.as_deref().unwrap_or("unknown");
let snapshot = match resolved {
crate::build_script_inputs::Resolved::Folded(snapshot) => {
tracing::trace!(
"[key:{crate_name}] build_script_inputs package_mode={} paths={} vars={} -> {}",
snapshot.package_mode,
snapshot.paths,
snapshot.vars,
&snapshot.digest[..16],
);
Some(snapshot)
}
crate::build_script_inputs::Resolved::Unrecorded => None,
crate::build_script_inputs::Resolved::PackageUnkeyed(why) => {
warn_build_script_package_unkeyed(config, &located, &why);
None
}
};
Ok(BuildScriptInputs {
located: Some(located),
snapshot,
key_ms: start.elapsed().as_millis() as u64,
hash_stats: file_hasher.stats(),
})
}
fn resolve_located_build_script_inputs(
config: &Config,
args: &RustcArgs,
store: &Store,
located: &crate::build_script_inputs::Located,
file_hasher: &FileHasher<'_>,
workspace_root: Option<&Path>,
) -> Result<crate::build_script_inputs::Resolved> {
let mut excluded = crate::build_script_inputs::excluded_roots(args.target_dir(), []);
excluded.extend(written_while_building(config, Some(store)));
let normalizer = std::cell::OnceCell::new();
let outside = |path: &Path| {
let normalizer = normalizer.get_or_init(|| {
crate::path_normalizer::PathNormalizer::from_env(workspace_root)
.with_target_dir(args.target_dir().as_deref())
.with_base_dirs(&config.base_dirs)
});
crate::cache_key::source_path_identity(path, normalizer)
};
crate::build_script_inputs::resolve(
located,
&crate::build_script_inputs::Resolver {
file_hasher,
var: &|name: &str| std::env::var_os(name),
cache_dir: &config.cache_dir,
excluded: &excluded,
outside: &outside,
max_entries: crate::build_script_inputs::MAX_ENTRIES,
},
)
}
fn build_script_inputs_moved(
config: &Config,
args: &RustcArgs,
store: &Store,
before: &BuildScriptInputs,
workspace_root: Option<&Path>,
) -> bool {
let (Some(located), Some(snapshot)) = (&before.located, &before.snapshot) else {
return false;
};
let _trace = crate::phase_trace::phase("build_script_inputs_verify");
let crate_name = args.crate_name.as_deref().unwrap_or("unknown");
let file_hasher = store.file_hasher_with_daemon(config.socket_path());
match resolve_located_build_script_inputs(
config,
args,
store,
located,
&file_hasher,
workspace_root,
) {
Ok(crate::build_script_inputs::Resolved::Folded(now)) if !snapshot.moved_since(&now) => {
false
}
Ok(_) => {
tracing::warn!(
"not caching {crate_name}: inputs its build script declared changed while it compiled"
);
true
}
Err(error) => {
tracing::warn!(
"not caching {crate_name}: inputs its build script declared could not be read after the compile: {error:#}"
);
true
}
}
}
fn warn_build_script_package_unkeyed(
config: &Config,
located: &crate::build_script_inputs::Located,
why: &str,
) {
let package = blake3::hash(located.manifest_dir.as_os_str().as_encoded_bytes()).to_hex();
let marker = warn_marker_path(
&format!("build-script-package-{}", &package[..16]),
&config.cache_dir,
);
warn_once_per_session_to(
&marker,
WARN_SESSION_SECS,
&format!(
"[kache] {}: its build script declares no inputs and {why}, so its units are keyed \
without the package's files. Print cargo:rerun-if-changed for the files its macros \
read.",
located.package,
),
WarnSink::Log,
);
}
fn extra_inputs_changed_during_compile(
config: &Config,
args: &RustcArgs,
before: Option<&crate::extra_inputs::ExtraInputsSnapshot>,
invocation_start_ns: i64,
) -> bool {
let crate_name = args.crate_name.as_deref().unwrap_or("unknown");
let mut hasher = crate::cache_key::FileHasher::new().with_daemon(config.socket_path());
hasher.arm_too_new_guard(invocation_start_ns, 0);
let after = match crate::extra_inputs::ExtraInputsSnapshot::resolve_for_rustc(args, &hasher) {
Ok(snapshot) => snapshot,
Err(error) => {
tracing::warn!(
"not caching {crate_name}: extra_inputs could not be revalidated after compile: {error:#}"
);
return true;
}
};
if before != after.as_ref() {
tracing::warn!(
"not caching {crate_name}: extra_inputs changed while the compiler was running"
);
return true;
}
if key_inputs_changed_during_compile(hasher.too_new(), &hasher.take_guarded_inputs()) {
tracing::warn!(
"not caching {crate_name}: extra_inputs may have changed while the compiler was running"
);
return true;
}
false
}
fn run_parsed_rustc(
config: &Config,
compiler: &RustcCompiler,
args: &RustcArgs,
start: std::time::Instant,
invocation_start_ns: i64,
extra_inputs: Option<&crate::extra_inputs::ExtraInputsSnapshot>,
extra_inputs_hash_stats: FileHashStats,
extra_inputs_too_new: bool,
extra_inputs_key_ms: u64,
extra_inputs_guard_inputs: Vec<crate::cache_key::ObservedFingerprint>,
build_script_inputs: Option<&BuildScriptInputs>,
mut precompiled: Option<Precompiled>,
) -> Result<i32> {
let crate_name = args.crate_name.as_deref().unwrap_or("unknown");
let event_root = rustc_event_root(args);
let mut key_record = precompiled
.as_mut()
.map(|pre| std::mem::take(&mut pre.key_record))
.unwrap_or_default();
crate::heartbeat::set_heartbeat_ctx(
config.heartbeat_secs,
config.event_log_path(),
config.socket_path(),
event_root.clone(),
heartbeat_lines_enabled(progress_level()),
);
maybe_notice_unknown_layout(config, args, &event_root, now_epoch_secs());
crate::cache_key::set_remote_rows(remote_prediction_rows(config));
let preserve_incremental = preserve_incremental_requested(config, args);
if preserve_incremental && compile::isolate_incremental_flags(&args.all_args).is_some() {
tracing::debug!("preserving incremental compilation for {crate_name}");
return preserved_incremental_with_event(
config,
args,
crate_name,
&event_root,
start,
key_record,
);
}
if preserve_incremental {
tracing::warn!(
"[kache] incremental directory for {crate_name} has no safe sibling path; stripping incremental flags"
);
}
let force_incremental = force_incremental_requested(config, args);
let adaptive_policy_for_invocation = adaptive_seed_allowed(config, args);
let trace_adaptive = crate::phase_trace::phase("adaptive_unit");
let adaptive_unit = managed_incremental_unit(config, args, || extra_inputs.is_some());
drop(trace_adaptive);
let refuse = compiler.refuse_reasons(args);
let untrusted_codegen_backend =
untrusted_codegen_backend(args.codegen_backend_dylib(), config.trust_codegen_backends);
let key_env = KeyEnv::capture();
let current_dir = key_env.cwd().map(Path::to_path_buf);
let workspace_root = args.path_normalization_root().map(Path::to_path_buf);
let exclude_roots: Vec<_> = workspace_root
.iter()
.chain(current_dir.iter())
.cloned()
.collect();
let excluded_source = args
.source_file
.as_ref()
.filter(|source| config.project_rules.source_excluded(source, &exclude_roots));
let user_bypass = config
.project_rules
.user_bypass_reason(crate_name, &args.all_args);
let skip_user_facing = args.is_user_facing_executable() && !config.cache_executables;
let reentered = reentered_after_deferred_compile(
precompiled.is_some(),
crate::cache_key::dep_info_provided(),
);
if incremental_fast_path_allowed(
unit_refuses_caching(!refuse.is_empty(), untrusted_codegen_backend.is_some()),
excluded_source.is_some() || user_bypass.is_some(),
skip_user_facing,
reentered,
) {
if force_incremental {
if let Some(lease) = adaptive_unit.as_ref().and_then(AdaptiveUnit::try_immediate) {
return adaptive_incremental_with_event(
config,
args,
crate_name,
&event_root,
start,
lease,
format!("incremental force-list: {crate_name}"),
None,
key_record,
);
}
} else if let Some(lease) = adaptive_unit.as_ref().and_then(AdaptiveUnit::try_active) {
return adaptive_incremental_with_event(
config,
args,
crate_name,
&event_root,
start,
lease,
"adaptive active",
None,
key_record,
);
}
}
let rustc_route = volume_route_path_rustc(args);
let mut fallback_stores = Vec::new();
let trace_store_open = crate::phase_trace::phase("store_open");
let store = if args.is_primary || (config.clean_incremental && args.incremental.is_some()) {
match open_primary_and_fallback(config, &rustc_route) {
Ok((primary, fallbacks)) => {
fallback_stores = fallbacks;
Some(primary)
}
Err(e) => {
warn_store_unavailable_once(config, &e);
None
}
}
} else {
None
};
if incremental_cleanup_enabled(config)
&& let Some(incr_dir) = &args.incremental
&& let Some(store) = &store
&& let Err(e) = store.remember_incremental_dir(incr_dir)
{
tracing::warn!(
"failed to register incremental dir {}: {}",
incr_dir.display(),
e
);
}
if untrusted_codegen_backend.is_some() {
tracing::debug!("rustc codegen backend dylib not trusted; running rustc directly");
reset_adaptive_unit(adaptive_unit.as_ref());
return rustc_direct_passthrough_with_event(
config,
args,
crate_name,
&event_root,
start,
UNTRUSTED_CODEGEN_BACKEND_REASON,
key_record,
);
}
if !refuse.is_empty() {
let reasons: Vec<&str> = refuse.iter().map(|r| r.description()).collect();
tracing::debug!(
"{}: bypassing cache ({})",
compiler.id().as_str(),
reasons.join("; ")
);
reset_adaptive_unit(adaptive_unit.as_ref());
return passthrough_with_event(
config,
args,
crate_name,
&event_root,
start,
refuse_reason_string(&refuse),
key_record,
);
}
if let Some(source) = excluded_source {
tracing::debug!("rustc source excluded from cache: {}", source.display());
reset_adaptive_unit(adaptive_unit.as_ref());
return passthrough_with_event(
config,
args,
crate_name,
&event_root,
start,
format!("source excluded: {}", source.display()),
key_record,
);
}
if let Some(reason) = user_bypass {
tracing::debug!("rustc invocation bypassed by user rule: {reason}");
reset_adaptive_unit(adaptive_unit.as_ref());
return passthrough_with_event(
config,
args,
crate_name,
&event_root,
start,
reason,
key_record,
);
}
if skip_user_facing {
tracing::debug!("skipping cache for user-facing executable: {}", crate_name);
return intentional_passthrough_with_event(
config,
args,
crate_name,
&event_root,
start,
adaptive_unit.as_ref(),
"user-facing executable (cache_executables=false)",
key_record,
);
}
let Some(store) = store else {
return passthrough_with_event(
config,
args,
crate_name,
&event_root,
start,
"store unavailable",
key_record,
);
};
let resolved_build_script_inputs;
let build_script_inputs = match build_script_inputs {
Some(carried) => carried,
None => {
resolved_build_script_inputs = match resolve_build_script_inputs(
config,
args,
&store,
invocation_start_ns,
workspace_root.as_deref(),
current_dir.as_deref(),
) {
Ok(resolved) => resolved,
Err(e) => {
tracing::warn!("not caching {crate_name}: {e:#}");
return rustc_direct_passthrough_with_event(
config,
args,
crate_name,
&event_root,
start,
&format!("{UNCACHEABLE_REASON}build-script inputs: {e:#}"),
key_record,
);
}
};
&resolved_build_script_inputs
}
};
let (inputs_key_ms, inputs_hash_stats, inputs_too_new) = combine_key_measurements(
extra_inputs_key_ms,
build_script_inputs.key_ms,
extra_inputs_hash_stats,
build_script_inputs.hash_stats,
extra_inputs_too_new,
false,
);
let mut deferred_seed = if adaptive_policy_for_invocation
&& precompiled.is_none()
&& stop_on_hit_allowed(args)
&& deferral_allowed(config, args, false, extra_inputs)
{
adaptive_unit
.as_ref()
.and_then(AdaptiveUnit::try_deferred_seed)
} else {
None
};
let keys_first = deferred_seed.is_none()
&& managed_unit_keys_first(
adaptive_unit.is_some(),
force_incremental,
adaptive_unit
.as_ref()
.is_some_and(AdaptiveUnit::has_policy_state),
);
let keyed = match compute_rustc_cache_key(
config,
compiler,
args,
workspace_root.as_deref(),
invocation_start_ns,
Some(&store),
&key_env,
ExtraInputsKey {
digest: extra_inputs.and_then(crate::extra_inputs::ExtraInputsSnapshot::digest),
build_script_inputs: build_script_inputs.digest(),
hash_stats: inputs_hash_stats,
too_new: inputs_too_new,
key_ms: inputs_key_ms,
guard_inputs: extra_inputs_guard_inputs,
},
match precompiled
.as_mut()
.and_then(|pre| Some((pre.dep_info.take()?, pre.tree_guard.take())))
{
Some((dep_info, tree)) => KeyDiscovery::Emitted(dep_info, tree),
None if deferral_allowed(config, args, keys_first, extra_inputs) => {
KeyDiscovery::Deferrable {
stop_on_hit: stop_on_hit_allowed(args),
}
}
None => KeyDiscovery::Immediate,
},
&mut key_record,
) {
Ok(keyed) => keyed,
Err(e) => {
if let Some(lease) = deferred_seed.take() {
let _ = lease.finish(CompileOutcome::Abnormal);
}
tracing::warn!("failed to compute cache key for {}: {:#}", crate_name, e);
return passthrough_with_event(
config,
args,
crate_name,
&event_root,
start,
uncacheable_reason(&e),
key_record,
);
}
};
let ComputedKey {
mut cache_key,
deferred,
stop_on_hit,
discovery_flight,
predicted,
outputs: mut key_outputs,
mut key_ms,
mut key_hash_stats,
mut key_too_new,
mut guard_inputs,
hashed_after_compile,
written_sources,
} = keyed;
let compile_first = CompileFirst {
config,
compiler,
args,
start,
invocation_start_ns,
extra_inputs,
extra_inputs_hash_stats,
extra_inputs_too_new,
extra_inputs_key_ms,
build_script_inputs,
workspace_root: workspace_root.as_deref(),
store: &store,
key_env: &key_env,
crate_name,
event_root: &event_root,
};
if deferred {
if let Some(lease) = deferred_seed.take() {
drop(discovery_flight);
return adaptive_compile_before_key(
&compile_first,
lease,
key_outputs.tree_guard,
guard_inputs,
key_record,
key_ms,
key_hash_stats,
);
}
tracing::debug!("no closure record for {crate_name}; compiling before keying");
return compile_before_key(
&compile_first,
stop_on_hit,
key_outputs.tree_guard,
guard_inputs,
key_record,
key_ms,
key_hash_stats,
);
}
if let Some(lease) = deferred_seed.take() {
let _ = lease.finish(CompileOutcome::Abnormal);
}
crate::out_dir_alias::register_after_key(key_outputs.bakes_out_dir);
let adaptive_key_fields = if adaptive_policy_for_invocation {
adaptive_unit
.as_ref()
.and_then(|_| key_outputs.fields.clone())
} else {
None
};
let hit_context = RustcHitContext {
config,
compiler,
memo_store: &store,
args,
crate_name,
event_root: &event_root,
start,
extra_inputs,
};
drop(trace_store_open);
let trace_remember = crate::phase_trace::phase("remember_target_root");
if let Some(target_dir) = prestage_target_dir(args) {
crate::prestage::maybe_hint(config, &target_dir);
}
if args.is_primary
&& let Some(target_dir) = args.target_dir()
&& let Some(workspace_root) = workspace_root.as_deref()
&& owns_its_target(
workspace_root,
crate::build_script_inputs::cargo_home(&|name| std::env::var_os(name)).as_deref(),
)
&& let Err(e) = store.remember_target_root_built_by(
&target_dir,
workspace_root,
crate::cache_key::rustc_version_text(&args.rustc).as_deref(),
)
{
tracing::warn!(
"failed to register target root {}: {}",
target_dir.display(),
e
);
}
drop(trace_remember);
tracing::debug!("cache key for {}: {}", crate_name, &cache_key[..16]);
let mut lookup_ms = 0_u64;
let mut record_closure = should_record_closure(predicted, false);
let mut rederived = false;
let keyed_first = keys_first && !reentered;
let may_defer = deferral_allowed(config, args, false, extra_inputs);
let mut compile_then_key = None;
while precompiled.is_none() {
let lookup_start = std::time::Instant::now();
let lookup_result =
lookup_or_miss(crate_name, LOOKUP_BUSY_BUDGET, std::thread::sleep, || {
lookup_local_entry(&store, &fallback_stores, &cache_key)
});
lookup_ms = lookup_ms.saturating_add(lookup_start.elapsed().as_millis() as u64);
if let Some((hit_store, meta)) = lookup_result {
if meta.files.is_empty() {
tracing::warn!(
"cache entry for {} has no files, evicting and recompiling",
crate_name
);
let _ = hit_store.remove_entry(&cache_key);
} else {
tracing::debug!("local cache hit for {} ({})", crate_name, &cache_key[..16]);
match hit_context.restore_and_finish(
hit_store,
&meta,
EventResult::LocalHit,
&cache_key,
key_ms,
key_hash_stats,
lookup_ms,
record_closure.then_some(&store),
&key_outputs,
&key_record,
) {
Ok(()) => {
observe_adaptive_hit(
adaptive_unit.as_ref(),
adaptive_key_fields.as_ref(),
&cache_key,
);
return Ok(0);
}
Err(e) => {
tracing::warn!(
"restoring local cache hit for {} failed: {} — recompiling",
crate_name,
e
);
if !hit_store.is_read_only() {
return passthrough_with_event(
config,
args,
crate_name,
&event_root,
start,
format!("restore failed: {e}"),
key_record,
);
}
}
}
}
}
maybe_trigger_prefetch(config, args);
if let Some(restored) = try_rustc_remote_hit(
&hit_context,
&store,
&cache_key,
key_ms,
key_hash_stats,
lookup_ms,
record_closure,
&key_outputs,
&key_record,
) {
if let Err(e) = restored {
tracing::warn!(
"restoring cache hit for {} failed: {} — recompiling",
crate_name,
e
);
return passthrough_with_event(
config,
args,
crate_name,
&event_root,
start,
format!("restore failed: {e}"),
key_record,
);
}
observe_adaptive_hit(
adaptive_unit.as_ref(),
adaptive_key_fields.as_ref(),
&cache_key,
);
return Ok(0);
}
if let (Some(unit), Some(fields)) = (adaptive_unit.as_ref(), adaptive_key_fields.as_ref())
&& let Some(lease) = unit.try_seed(&cache_key, fields)
{
drop(discovery_flight);
return adaptive_incremental_with_event(
config,
args,
crate_name,
&event_root,
start,
lease,
"adaptive seed",
Some((&cache_key, key_ms, key_hash_stats, lookup_ms)),
key_record,
);
}
let owes = owes_rederivation(predicted, rederived);
compile_then_key =
compile_before_key_after_miss(may_defer, stop_on_hit_allowed(args), owes, keyed_first);
if compile_then_key.is_some() || !owes {
break;
}
rederived = true;
record_closure = should_record_closure(predicted, rederived);
let previous_key = cache_key.clone();
match recompute_key_without_prediction(
config,
compiler,
args,
workspace_root.as_deref(),
invocation_start_ns,
Some(&store),
&key_env,
extra_inputs.and_then(crate::extra_inputs::ExtraInputsSnapshot::digest),
build_script_inputs.digest(),
&mut key_record,
) {
Ok(recomputed) => {
cache_key = recomputed.cache_key;
let first_guard = key_outputs.tree_guard.take();
key_outputs = recomputed.outputs;
key_outputs.tree_guard = key_outputs.tree_guard.take().or(first_guard);
(key_ms, key_hash_stats, key_too_new) = combine_key_measurements(
key_ms,
recomputed.key_ms,
key_hash_stats,
recomputed.key_hash_stats,
key_too_new,
recomputed.key_too_new,
);
guard_inputs.extend(recomputed.guard_inputs);
}
Err(e) => {
return passthrough_with_event(
config,
args,
crate_name,
&event_root,
start,
uncacheable_reason(&e),
key_record,
);
}
}
if cache_key == previous_key {
compile_then_key = compile_before_key_after_miss(
may_defer,
stop_on_hit_allowed(args),
false,
keyed_first,
);
break;
}
}
if let Some(stop_on_hit) = compile_then_key {
tracing::debug!("{crate_name}: no lane took the miss; compiling before keying again");
return compile_before_key(
&compile_first,
stop_on_hit,
key_outputs.tree_guard,
guard_inputs,
key_record,
key_ms,
key_hash_stats,
);
}
let (miss_guard, scheduled_hit) = admit_scheduler_miss(
config,
&store,
&cache_key,
FlightIdentity::rustc(crate_name, &args.crate_types, args.emits_link()),
crate_name,
args.invokes_linker(),
cache_entry_has_files,
);
let (lock, committed) = if let Some(meta) = scheduled_hit {
(None, Some(meta))
} else {
match store.claim_build(&cache_key) {
Ok(BuildClaim::Acquired(lock)) => (Some(lock), None),
Ok(BuildClaim::Committed(meta)) => (None, Some(*meta)),
Err(e) => {
tracing::warn!(
"claiming build for {} failed: {} — recompiling",
crate_name,
e
);
(None, None)
}
Ok(BuildClaim::Contended) => {
tracing::debug!("waiting for {} to be built by another process", crate_name);
let committed = store
.wait_for_committed(&cache_key)
.unwrap_or(false)
.then(|| store.get(&cache_key).ok().flatten())
.flatten();
(None, committed)
}
}
};
let peer_committed = committed.is_some();
if let Some(meta) = committed.filter(|_| precompiled.is_none()) {
if let Err(e) = hit_context.restore_and_finish(
&store,
&meta,
EventResult::LocalHit,
&cache_key,
key_ms,
key_hash_stats,
lookup_ms,
record_closure.then_some(&store),
&key_outputs,
&key_record,
) {
tracing::warn!(
"restoring cache hit for {} failed: {} — recompiling",
crate_name,
e
);
return passthrough_with_event(
config,
args,
crate_name,
&event_root,
start,
format!("restore failed: {e}"),
key_record,
);
}
observe_adaptive_hit(
adaptive_unit.as_ref(),
adaptive_key_fields.as_ref(),
&cache_key,
);
return Ok(0);
}
tracing::debug!(
"cache miss for {}, compiling ({})",
crate_name,
&cache_key[..16]
);
let compile_start = std::time::Instant::now();
let precompiled_time = precompiled.as_ref().map(|pre| pre.compile_time_ms);
let mut result = match precompiled.take() {
Some(pre) => pre.result,
None => match compiler.execute_streaming(args) {
Ok(r) => r,
Err(e) => {
return passthrough_with_event(
config,
args,
crate_name,
&event_root,
start,
format!("compiler spawn failed: {e}"),
key_record,
);
}
},
};
miss_guard.record_compile_rss(crate_name);
let compile_time_ms =
precompiled_time.unwrap_or_else(|| compile_start.elapsed().as_millis() as u64);
if precompiled_time.is_none() {
replay_diagnostics(
&result.stdout,
result.pending_stderr(),
std::io::stdout(),
std::io::stderr(),
);
}
after_rustc_exit(result.exit_code, &result.stderr, &args.externs);
if result.exit_code != 0 {
let elapsed = start.elapsed().as_millis() as u64;
log_event(
config,
EventInputs::new(&event_root, crate_name, EventResult::Error, elapsed)
.keyed(&cache_key, key_ms, key_hash_stats)
.lookup_ms(lookup_ms)
.key_record(key_record),
);
print_progress(crate_name, EventResult::Error, elapsed, 0);
drop(lock);
return Ok(result.exit_code);
}
let rebuilt =
RebuiltArtifacts::observed(true, &result.artifacts, key_env.var("CARGO_PKG_NAME"));
let extra_inputs_racy = args.is_primary
&& extra_inputs_changed_during_compile(config, args, extra_inputs, invocation_start_ns);
let sources_changed = emitted_sources_changed_during_compile(&written_sources, &guard_inputs);
let key_inputs_changed = config.modified_input_guard && {
let hashed: Vec<_> = guard_inputs
.iter()
.chain(hashed_after_compile.iter().flatten())
.cloned()
.collect();
key_inputs_changed_during_compile(key_too_new, &hashed)
};
if should_skip_cache_store_for_input_race(
extra_inputs_racy,
sources_changed,
config.modified_input_guard,
key_inputs_changed,
) {
let elapsed = start.elapsed().as_millis() as u64;
log_event(
config,
EventInputs::new(&event_root, crate_name, EventResult::Skipped, elapsed)
.rebuilt_observed(rebuilt.clone())
.skip_reason("inputs-changed")
.keyed(&cache_key, key_ms, key_hash_stats)
.lookup_ms(lookup_ms)
.key_record(key_record),
);
print_progress(crate_name, EventResult::Skipped, elapsed, 0);
drop(lock);
return Ok(result.exit_code);
}
if build_script_inputs_moved(
config,
args,
&store,
build_script_inputs,
workspace_root.as_deref(),
) {
let elapsed = start.elapsed().as_millis() as u64;
log_event(
config,
EventInputs::new(&event_root, crate_name, EventResult::Skipped, elapsed)
.rebuilt_observed(rebuilt.clone())
.skip_reason("build-script-inputs-changed")
.compile_time_ms(compile_time_ms)
.keyed(&cache_key, key_ms, key_hash_stats)
.lookup_ms(lookup_ms)
.key_record(key_record),
);
print_progress(crate_name, EventResult::Skipped, elapsed, 0);
drop(lock);
return Ok(result.exit_code);
}
if !crate::out_dir_alias::store_gate(args) {
let elapsed = start.elapsed().as_millis() as u64;
log_event(
config,
EventInputs::new(&event_root, crate_name, EventResult::Skipped, elapsed)
.rebuilt_observed(rebuilt.clone())
.skip_reason("out-dir-alias-changed")
.compile_time_ms(compile_time_ms)
.keyed(&cache_key, key_ms, key_hash_stats)
.lookup_ms(lookup_ms)
.key_record(key_record),
);
print_progress(crate_name, EventResult::Skipped, elapsed, 0);
drop(lock);
return Ok(result.exit_code);
}
if let Some(missing) = missing_requested_emit(args, &result.artifacts) {
tracing::warn!(
"not caching {}: discovered outputs do not cover requested --emit {} \
(have {:?}) — refusing to store a partial entry",
crate_name,
missing,
result
.artifacts
.outputs()
.iter()
.map(|a| a.store_name.as_str())
.collect::<Vec<_>>()
);
let elapsed = start.elapsed().as_millis() as u64;
log_event(
config,
EventInputs::new(&event_root, crate_name, EventResult::Skipped, elapsed)
.rebuilt_observed(rebuilt.clone())
.skip_reason(format!("missing-output: {missing}"))
.compile_time_ms(compile_time_ms)
.keyed(&cache_key, key_ms, key_hash_stats)
.lookup_ms(lookup_ms)
.key_record(key_record),
);
print_progress(crate_name, EventResult::Skipped, elapsed, 0);
drop(lock);
return Ok(result.exit_code);
}
let native_archives = key_outputs.native_archives.take().unwrap_or_default();
let unaudited = match unaudited_native_bundle(args, &result.artifacts, &native_archives) {
Ok(None) => None,
Ok(Some(member)) => Some(format!(
"its rlib bundles `{member}` from an archive the key does not hash"
)),
Err(error) => Some(format!("its native bundle audit failed: {error:#}")),
};
let degraded = kache_format::reports_debug_info_tool_failure(&result.stderr)
.then(|| "rustc could not post-process its debug info".to_string());
if let Some(reason) = unaudited.or(degraded) {
tracing::warn!("not caching {crate_name}: {reason}");
let elapsed = start.elapsed().as_millis() as u64;
log_event(
config,
EventInputs::new(&event_root, crate_name, EventResult::Skipped, elapsed)
.rebuilt_observed(rebuilt.clone())
.skip_reason(reason)
.compile_time_ms(compile_time_ms)
.keyed(&cache_key, key_ms, key_hash_stats)
.lookup_ms(lookup_ms)
.key_record(key_record),
);
print_progress(crate_name, EventResult::Skipped, elapsed, 0);
drop(lock);
return Ok(result.exit_code);
}
if !store_admits_compile(config, compile_time_ms, true) {
tracing::debug!(
crate_name = %crate_name,
compile_time_ms,
min_store_compile_ms = config.min_store_compile_ms,
"admission: compile too cheap to store"
);
let elapsed = start.elapsed().as_millis() as u64;
log_event(
config,
EventInputs::new(&event_root, crate_name, EventResult::Skipped, elapsed)
.rebuilt_observed(rebuilt.clone())
.skip_reason("admission-threshold")
.compile_time_ms(compile_time_ms)
.keyed(&cache_key, key_ms, key_hash_stats)
.lookup_ms(lookup_ms)
.key_record(key_record),
);
print_progress(crate_name, EventResult::Skipped, elapsed, 0);
clean_incremental_dir(config, args);
drop(lock);
return Ok(result.exit_code);
}
if let (Some(unit), Some(fields)) = (adaptive_unit.as_ref(), adaptive_key_fields.as_ref()) {
let _ = unit.observe_build(&cache_key, fields);
}
let target = args.target.as_deref().unwrap_or("host");
let profile = match args.get_codegen_opt("opt-level") {
Some("0") | None => "dev",
Some("s") | Some("z") => "release-size",
_ => "release",
};
let depinfo_anchor = args.target_dir();
let depinfo_working_dir = current_dir.as_deref().unwrap_or_else(|| Path::new("."));
let depinfo_workspace_dir = args.path_normalization_root();
let depinfo_configured_roots =
configured_rustc_depinfo_roots(config, depinfo_workspace_dir, depinfo_anchor.as_deref());
if let Some(snapshot) = extra_inputs
&& let Err(error) =
validate_extra_inputs_dep_info_before_store(args, &result.artifacts, snapshot)
{
return Err(error).context("validating extra_inputs dep-info before cache commit");
}
let mut _debug_bundle_staging: Option<tempfile::TempDir> = None;
if wants_debug_bundle(args)
&& let Some((exec_path, exec_name)) =
find_executable_output(compiler, args, &result.artifacts)
{
match tempfile::tempdir() {
Ok(staging) => {
match platform::current().package_debug_bundle(&exec_path, staging.path()) {
Ok(Some(tar_path)) => {
result.artifacts.push(crate::compiler::Artifact {
path: tar_path,
store_name: format!("{exec_name}.dsym.tar"),
kind: ArtifactKind::DebugBundle,
required: false,
});
_debug_bundle_staging = Some(staging);
}
Ok(None) => {}
Err(e) => {
tracing::warn!(
"failed to package debug bundle for {}: {e:#}",
exec_path.display()
);
}
}
}
Err(e) => {
tracing::warn!("failed to create debug bundle staging dir: {e}");
}
}
}
let prepared_store = match prepare_rustc_store_files(
&result.artifacts,
depinfo_anchor.as_deref(),
depinfo_working_dir,
depinfo_workspace_dir,
&depinfo_configured_roots,
) {
Ok(prepared) => prepared,
Err(error) => {
tracing::warn!(
"not caching {}: dep-info could not be staged safely: {error:#}",
crate_name
);
after_store(config, &store, Some(&error));
let elapsed = start.elapsed().as_millis() as u64;
log_event(
config,
EventInputs::new(&event_root, crate_name, EventResult::Skipped, elapsed)
.rebuilt_observed(rebuilt.clone())
.skip_reason(format!("dep-info-staging: {error:#}"))
.compile_time_ms(compile_time_ms)
.keyed(&cache_key, key_ms, key_hash_stats)
.lookup_ms(lookup_ms)
.key_record(key_record),
);
print_progress(crate_name, EventResult::Skipped, elapsed, 0);
clean_incremental_dir(config, args);
drop(lock);
return Ok(result.exit_code);
}
};
if let Some(snapshot) = extra_inputs {
complete_extra_inputs_dep_info(args, snapshot)
.context("completing extra_inputs dep-info before cache publication")?;
}
let Some(lock) = lock else {
let elapsed = start.elapsed().as_millis() as u64;
log_event(
config,
EventInputs::new(&event_root, crate_name, EventResult::Skipped, elapsed)
.rebuilt_observed(rebuilt.clone())
.skip_reason(compiled_without_lock_reason(peer_committed))
.compile_time_ms(compile_time_ms)
.keyed(&cache_key, key_ms, key_hash_stats)
.lookup_ms(lookup_ms)
.key_record(key_record),
);
print_progress(crate_name, EventResult::Skipped, elapsed, 0);
clean_incremental_dir(config, args);
return Ok(result.exit_code);
};
let store_start = std::time::Instant::now();
let trace_store = crate::phase_trace::phase("store");
let mut store_put = StorePutResult::default();
let mut store_error = String::new();
let put = store.put_with_compile_time(
&cache_key,
crate_name,
&args.crate_types,
&args.features,
target,
profile,
&prepared_store.files,
&result.stdout,
&result.stderr,
compile_time_ms,
);
after_store(config, &store, put.as_ref().err());
match put {
Ok(result) => {
store_put = result;
if let Some(unit) = args.get_codegen_opt("metadata")
&& let Err(e) = store.record_entry_unit(&cache_key, unit)
{
tracing::debug!("recording the unit of {crate_name}'s entry failed: {e}");
}
if stores_prestage_candidate(&prepared_store.files)
&& let Some(output_dir) = rustc_output_dir(args)
&& let Some(meta) = store.stored_meta(&cache_key)
{
let shared =
shared_inode_loadable(args, platform::current().may_share_restored_loadables());
remember_prestaged_executables(config, compiler, args, &output_dir, shared, &meta);
}
flush_or_hand_off_durability(config, &store, &cache_key);
}
Err(e) => {
store_error = store_error_for_event(&e);
tracing::warn!(
"failed to store cache entry for {}: {}",
crate_name,
store_error
);
}
}
drop(trace_store);
let store_ms = store_start.elapsed().as_millis() as u64;
maybe_enqueue_upload(config, &store, &cache_key, crate_name, true);
record_input_prediction(config, Some(&store), args, record_closure, &key_outputs);
clean_incremental_dir(config, args);
let elapsed = start.elapsed().as_millis() as u64;
let size = result.artifacts.total_size();
let event_result = event_result_for_store_put(store_put);
log_event(
config,
EventInputs::new(&event_root, crate_name, event_result, elapsed)
.rebuilt_observed(rebuilt)
.compile_time_ms(compile_time_ms)
.size(size)
.keyed(&cache_key, key_ms, key_hash_stats)
.lookup_ms(lookup_ms)
.store_ms(store_ms)
.store_put(store_put)
.store_error(store_error)
.key_record(key_record),
);
print_progress(crate_name, event_result, elapsed, size);
drop(lock);
Ok(result.exit_code)
}
struct PreparedCcStoreFiles {
files: Vec<(PathBuf, String)>,
_temporary_files: Vec<tempfile::TempPath>,
}
struct CcHandoff<'a> {
cache_key: &'a str,
crate_name: &'a str,
target: &'a str,
files: &'a [(PathBuf, String)],
stdout: &'a str,
stderr: &'a str,
compile_time_ms: u64,
publishes_to_remote: bool,
event_root: &'a str,
start: std::time::Instant,
size: u64,
key_ms: u64,
lookup_ms: u64,
lookup_rejection: &'a str,
store_start: std::time::Instant,
rebuilt: RebuiltArtifacts,
memo: Option<crate::daemon_publish::CcMemoHandoff>,
}
enum CcHandoffOutcome {
Accepted,
Done,
Publish,
}
fn hand_off_cc_store(
config: &Config,
store: &Store,
build_lock: &mut Option<KeyLock>,
handoff: CcHandoff<'_>,
) -> CcHandoffOutcome {
use crate::daemon_publish::{Handoff, PublishCcRequest};
if !volume_cache_dirs_match(store.cache_dir(), &config.cache_dir) {
return CcHandoffOutcome::Publish;
}
let snapshots = match crate::daemon_publish::snapshot_for_handoff(config, handoff.files) {
Ok(snapshots) => snapshots,
Err(error) => {
tracing::debug!("cc hand-off: could not snapshot outputs: {error:#}");
return CcHandoffOutcome::Publish;
}
};
let elapsed = handoff.start.elapsed().as_millis() as u64;
let trace_event = crate::phase_trace::phase("handoff_event");
let event = build_event_details(
config,
EventInputs::new(
handoff.event_root,
handoff.crate_name,
EventResult::Miss,
elapsed,
)
.rebuilt_observed(handoff.rebuilt.clone())
.compile_time_ms(handoff.compile_time_ms)
.size(handoff.size)
.keyed(handoff.cache_key, handoff.key_ms, FileHashStats::default())
.lookup_ms(handoff.lookup_ms)
.store_ms(handoff.store_start.elapsed().as_millis() as u64)
.lookup_rejection(handoff.lookup_rejection.to_string()),
);
drop(trace_event);
let request = PublishCcRequest {
client_epoch: crate::daemon::build_epoch(),
client_version: Some(crate::VERSION.to_owned()),
cache_key: handoff.cache_key.to_string(),
crate_name: handoff.crate_name.to_string(),
target: handoff.target.to_string(),
files: snapshots,
stdout: handoff.stdout.to_string(),
stderr: handoff.stderr.to_string(),
compile_time_ms: handoff.compile_time_ms,
publishes_to_remote: compiler_upload_enabled(config, handoff.publishes_to_remote),
event,
memo: handoff.memo,
};
*build_lock = None;
match crate::daemon_publish::hand_off_cc_publish(config, &request) {
Handoff::Accepted => {
print_progress(handoff.crate_name, EventResult::Miss, elapsed, handoff.size);
CcHandoffOutcome::Accepted
}
Handoff::Declined(reason) => {
tracing::debug!("cc hand-off declined for {}: {reason}", handoff.crate_name);
crate::daemon_publish::remove_handoff_files(&request.files);
match reclaim_after_declined_handoff(store, handoff.cache_key, HANDOFF_HOLDER_WAIT) {
Ok(BuildClaim::Acquired(lock)) => {
*build_lock = Some(lock);
CcHandoffOutcome::Publish
}
Ok(BuildClaim::Committed(_)) | Ok(BuildClaim::Contended) => {
let mut event = request.event;
event.elapsed_ms = handoff.start.elapsed().as_millis() as u64;
write_event(config, &event);
print_progress(
handoff.crate_name,
EventResult::Miss,
event.elapsed_ms,
handoff.size,
);
CcHandoffOutcome::Done
}
Err(error) => {
tracing::warn!(
"reclaiming {} after a declined hand-off failed: {error:#}; storing without a lock",
handoff.crate_name
);
CcHandoffOutcome::Publish
}
}
}
}
}
const HANDOFF_HOLDER_WAIT: std::time::Duration = std::time::Duration::from_secs(5);
fn reclaim_after_declined_handoff(
store: &Store,
cache_key: &str,
wait: std::time::Duration,
) -> anyhow::Result<BuildClaim> {
match store.claim_build(cache_key)? {
BuildClaim::Contended => {
store.wait_for_committed_with_timeout(cache_key, wait)?;
store.claim_build(cache_key)
}
claim => Ok(claim),
}
}
fn prepare_cc_store_files(
artifacts: &ArtifactSet,
depinfo_anchor: Option<&Path>,
) -> Result<PreparedCcStoreFiles> {
prepare_cc_store_files_in(artifacts, depinfo_anchor, None, &[])
}
fn prepare_cc_store_files_in(
artifacts: &ArtifactSet,
depinfo_anchor: Option<&Path>,
staging_dir: Option<&Path>,
depinfo_roots: &[(PathBuf, String, u8)],
) -> Result<PreparedCcStoreFiles> {
use std::io::{Read, Write};
let staging_dir = staging_dir.filter(|dir| std::fs::create_dir_all(dir).is_ok());
let mut files = Vec::with_capacity(artifacts.outputs().len());
let mut temporary_files = Vec::with_capacity(artifacts.outputs().len());
for artifact in artifacts.outputs() {
let mut builder = tempfile::Builder::new();
builder.prefix("kache-cc-artifact-");
let staged = match staging_dir {
Some(dir) => builder.tempfile_in(dir).or_else(|_| builder.tempfile()),
None => builder.tempfile(),
}
.context("cc store: creating private artifact staging file")?;
let staged = staged.into_temp_path();
if artifact.kind == ArtifactKind::DepInfo {
let anchor = depinfo_anchor.context("cc store: missing dep-info rewrite anchor")?;
let mut content = String::new();
std::fs::File::open(&artifact.path)
.with_context(|| format!("cc store: opening dep-info {}", artifact.path.display()))?
.read_to_string(&mut content)
.with_context(|| {
format!("cc store: reading dep-info {}", artifact.path.display())
})?;
let normalized =
link::rewrite_depinfo_content(&content, anchor, link::DepInfoMode::Relativize);
let normalized = link::rewrite_rustc_depinfo_content_with_configured_roots(
&normalized,
anchor,
anchor,
None,
depinfo_roots,
link::DepInfoMode::Relativize,
);
std::fs::write(&staged, normalized.as_bytes())
.context("cc store: writing normalized dep-info staging file")?;
} else if artifact.kind == ArtifactKind::DebugBundle
&& std::fs::metadata(&artifact.path).is_ok_and(|meta| meta.is_dir())
{
platform::build_deterministic_tar(&artifact.path, &staged).with_context(|| {
format!(
"cc store: packaging debug bundle {}",
artifact.path.display()
)
})?;
} else {
let mut source = std::fs::File::open(&artifact.path).with_context(|| {
format!("cc store: opening artifact {}", artifact.path.display())
})?;
let mut dest = std::fs::File::create(&staged).with_context(|| {
format!(
"cc store: creating staging file for {}",
artifact.path.display()
)
})?;
std::io::copy(&mut source, &mut dest).with_context(|| {
format!("cc store: copying artifact {}", artifact.path.display())
})?;
dest.flush()
.context("cc store: flushing private artifact staging file")?;
}
files.push((staged.to_path_buf(), artifact.store_name.clone()));
temporary_files.push(staged);
}
Ok(PreparedCcStoreFiles {
files,
_temporary_files: temporary_files,
})
}
#[derive(Debug)]
struct PreparedRustcStoreFiles {
files: Vec<(PathBuf, String)>,
_temporary_files: Vec<tempfile::TempPath>,
}
fn prepare_rustc_store_files(
artifacts: &ArtifactSet,
target_dir: Option<&Path>,
working_dir: &Path,
workspace_dir: Option<&Path>,
configured_roots: &[(PathBuf, String, u8)],
) -> Result<PreparedRustcStoreFiles> {
use std::io::{Read, Write};
let mut files = Vec::with_capacity(artifacts.outputs().len());
let mut temporary_files = Vec::with_capacity(artifacts.outputs().len());
for artifact in artifacts.outputs() {
if artifact.kind != ArtifactKind::DepInfo {
files.push((artifact.path.clone(), artifact.store_name.clone()));
continue;
}
let mut staged = tempfile::Builder::new()
.prefix("kache-rustc-artifact-")
.tempfile()
.context("rustc store: creating private artifact staging file")?;
let anchor = target_dir.context("rustc store: missing dep-info rewrite anchor")?;
let mut content = String::new();
std::fs::File::open(&artifact.path)
.with_context(|| format!("rustc store: opening dep-info {}", artifact.path.display()))?
.read_to_string(&mut content)
.with_context(|| {
format!("rustc store: reading dep-info {}", artifact.path.display())
})?;
let normalized = link::rewrite_rustc_depinfo_content_with_configured_roots(
&content,
anchor,
working_dir,
workspace_dir,
configured_roots,
link::DepInfoMode::Relativize,
);
staged
.write_all(normalized.as_bytes())
.context("rustc store: writing normalized dep-info staging file")?;
staged
.flush()
.context("rustc store: flushing private artifact staging file")?;
let staged = staged.into_temp_path();
files.push((staged.to_path_buf(), artifact.store_name.clone()));
temporary_files.push(staged);
}
Ok(PreparedRustcStoreFiles {
files,
_temporary_files: temporary_files,
})
}
fn configured_rustc_depinfo_roots(
config: &Config,
workspace_root: Option<&Path>,
target_dir: Option<&Path>,
) -> Vec<(PathBuf, String, u8)> {
crate::path_normalizer::PathNormalizer::from_env(workspace_root)
.with_target_dir(target_dir)
.with_base_dirs(&config.base_dirs)
.depinfo_source_roots()
.into_iter()
.map(|root| (root.root, root.depinfo_sentinel, root.priority))
.collect()
}
fn validate_extra_inputs_dep_info_before_store(
args: &RustcArgs,
artifacts: &ArtifactSet,
snapshot: &crate::extra_inputs::ExtraInputsSnapshot,
) -> Result<()> {
let expected_name = args
.dep_info_path()
.and_then(|path| path.file_name().map(std::ffi::OsStr::to_os_string));
let mut saw_dep_info = false;
for artifact in artifacts.outputs() {
if artifact.kind != ArtifactKind::DepInfo {
continue;
}
if expected_name
.as_ref()
.is_some_and(|expected| artifact.path.file_name() != Some(expected.as_os_str()))
{
continue;
}
saw_dep_info = true;
let raw = std::fs::read_to_string(&artifact.path)
.with_context(|| format!("reading producer dep-info {}", artifact.path.display()))?;
snapshot
.merge_dep_info_content(&raw)
.with_context(|| format!("completing producer dep-info {}", artifact.path.display()))?;
}
anyhow::ensure!(
expected_name.is_none() || saw_dep_info,
"successful rustc invocation produced no expected dep-info artifact required by active extra_inputs"
);
Ok(())
}
fn rustc_debuginfo_enabled(args: &RustcArgs) -> bool {
args.debuginfo_enabled()
}
fn find_executable_output(
compiler: &RustcCompiler,
args: &RustcArgs,
artifacts: &crate::compiler::ArtifactSet,
) -> Option<(std::path::PathBuf, String)> {
artifacts
.outputs()
.iter()
.find(|a| compiler.classify_output(args, &a.store_name) == ArtifactKind::Executable)
.map(|a| (a.path.clone(), a.store_name.clone()))
}
fn wants_debug_bundle(args: &RustcArgs) -> bool {
args.is_user_facing_executable() && rustc_debuginfo_enabled(args)
}
fn stores_prestage_candidate(files: &[(PathBuf, String)]) -> bool {
files.iter().any(|(path, _)| {
std::fs::metadata(path).is_ok_and(|m| m.len() >= crate::prestage::MIN_BYTES)
})
}
fn take_prestaged(
strategy: link::LinkStrategy,
cached_file: &crate::store::CachedFile,
target_path: &Path,
) -> bool {
strategy == link::LinkStrategy::Copy
&& cached_file.size >= crate::prestage::MIN_BYTES
&& crate::prestage::take(target_path, &cached_file.hash)
}
fn owns_its_target(workspace_root: &Path, cargo_home: Option<&Path>) -> bool {
!crate::cache_key::is_registry_package(workspace_root)
&& !cargo_home
.is_some_and(|home| crate::build_script_inputs::under_cargo_home(home, workspace_root))
}
fn prestage_target_dir(args: &RustcArgs) -> Option<PathBuf> {
if let Some(out_dir) = &args.out_dir
&& let Some(profile) = crate::cargo_layout::build_script_dir_profile(out_dir)
{
return profile.parent().map(Path::to_path_buf);
}
args.target_dir()
}
fn rustc_output_dir(args: &RustcArgs) -> Option<PathBuf> {
match (&args.output, &args.out_dir) {
(Some(output), _) => Some(output.parent().unwrap_or(Path::new(".")).to_path_buf()),
(None, Some(dir)) => Some(dir.clone()),
(None, None) => None,
}
}
fn artifact_target_path(args: &RustcArgs, output_dir: &Path, name: &str) -> PathBuf {
match &args.output {
Some(output) if name == output.file_name().unwrap_or_default().to_string_lossy() => {
output.clone()
}
_ => output_dir.join(name),
}
}
fn remember_prestaged_executables(
config: &Config,
compiler: &RustcCompiler,
args: &RustcArgs,
output_dir: &Path,
shared_loadable: Option<ArtifactKind>,
meta: &crate::store::EntryMeta,
) {
let Some(target_dir) = prestage_target_dir(args) else {
return;
};
for file in &meta.files {
let kind = compiler.classify_output(args, &file.name);
if file.size >= crate::prestage::MIN_BYTES
&& restore_link_strategy(kind, file.executable, shared_loadable)
== link::LinkStrategy::Copy
{
crate::prestage::remember(
&config.cache_dir,
&target_dir,
&artifact_target_path(args, output_dir, &file.name),
&file.hash,
file.size,
);
}
}
}
fn restore_link_strategy(
kind: ArtifactKind,
executable: bool,
shared_loadable: Option<ArtifactKind>,
) -> link::LinkStrategy {
if shared_loadable == Some(kind) {
link::LinkStrategy::ExecutableHardlink
} else if executable {
link::LinkStrategy::Copy
} else {
kind.link_strategy()
}
}
fn shared_inode_loadable(args: &RustcArgs, platform_allows: bool) -> Option<ArtifactKind> {
if !platform_allows {
None
} else if args.crate_types == ["proc-macro"] {
Some(ArtifactKind::DynamicLibrary)
} else if crate::build_script::build_script_output(args).is_some() {
Some(ArtifactKind::Executable)
} else {
None
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum RestoredBytes {
ExactBlobCopy(crate::cache_key::FileFingerprint),
Rewritten,
}
fn materialize_cached_artifact(
store: &Store,
cached_file: &crate::store::CachedFile,
target_path: &Path,
kind: ArtifactKind,
shared_loadable: Option<ArtifactKind>,
depinfo_anchor: &Path,
depinfo_working_dir: &Path,
depinfo_workspace_dir: Option<&Path>,
depinfo_configured_roots: &[(PathBuf, String, u8)],
platform: &dyn crate::compiler::Platform,
context: &str,
extra_inputs: Option<&crate::extra_inputs::ExtraInputsSnapshot>,
) -> Result<RestoredBytes> {
let store_path = store.blob_path(&cached_file.hash);
if !store_path.exists() {
anyhow::bail!(
"{context}: blob for {} (hash {}) was evicted before restore — \
treating as a cache miss: {}",
cached_file.name,
&cached_file.hash[..16.min(cached_file.hash.len())],
store_path.display()
);
}
let plan = plan_post_restore(kind);
let transforms: Vec<_> = plan
.iter()
.copied()
.filter(|action| action.is_content_transform())
.collect();
let complete_extra_inputs = extra_inputs.filter(|_| kind == ArtifactKind::DepInfo);
let transformed = if transforms.is_empty() && complete_extra_inputs.is_none() {
None
} else {
let original = std::fs::read(&store_path)
.with_context(|| format!("{context}: reading blob {}", store_path.display()))?;
let mut content = original.clone();
for action in &transforms {
content = action.transform(content, depinfo_anchor);
}
if kind == ArtifactKind::DepInfo {
content = match String::from_utf8(content) {
Ok(text) => crate::link::rewrite_rustc_depinfo_content_with_configured_roots(
&text,
depinfo_anchor,
depinfo_working_dir,
depinfo_workspace_dir,
depinfo_configured_roots,
link::DepInfoMode::Expand,
)
.into_bytes(),
Err(error) => error.into_bytes(),
};
}
if let Some(snapshot) = complete_extra_inputs {
let text = String::from_utf8(content)
.with_context(|| format!("{context}: dep-info is not valid UTF-8"))?;
content = snapshot
.merge_dep_info_content(&text)
.with_context(|| format!("{context}: completing extra_inputs dep-info"))?
.into_bytes();
}
if content == original {
None
} else {
Some(content)
}
};
let strategy = restore_link_strategy(kind, cached_file.executable, shared_loadable);
let blob_strategy =
if store.is_read_only() && strategy == link::LinkStrategy::ExecutableHardlink {
link::LinkStrategy::Copy
} else {
strategy
};
let rewrote_content = transformed.is_some();
match transformed {
Some(content) => {
link::write_restored(target_path, &content, strategy)
.with_context(|| format!("{context}: writing {}", target_path.display()))?;
}
None => {
if store.is_read_only() && blob_strategy == link::LinkStrategy::Hardlink {
link::prepare_writable_target_from_file(&store_path, target_path)?
.publish_replacing()
.with_context(|| format!("{context}: copying {}", target_path.display()))?;
} else {
let staged = take_prestaged(blob_strategy, cached_file, target_path);
if !staged {
link::link_to_target(&store_path, target_path, blob_strategy).with_context(
|| {
format!(
"{context}: linking {} -> {}",
store_path.display(),
target_path.display()
)
},
)?;
}
}
link::touch_mtime_write_clock(target_path)
.with_context(|| format!("{context}: touching {}", target_path.display()))?;
}
}
let external: Vec<_> = plan
.iter()
.copied()
.filter(|action| !action.is_content_transform())
.collect();
let before = (!external.is_empty())
.then(|| crate::cache_key::FileFingerprint::from_path(target_path).ok())
.flatten();
let verified = (!rewrote_content)
.then(|| platform.verified_loadable_dir())
.flatten()
.map(|dir| VerifiedLoadable::new(&dir, &cached_file.hash));
let known_loadable = verified.as_ref().is_some_and(VerifiedLoadable::is_recorded);
let mut verified_now = false;
for action in &external {
if known_loadable && matches!(action, crate::compiler::PostRestoreAction::Sign(_)) {
continue;
}
let loadability = action
.apply(target_path, platform)
.with_context(|| format!("{context}: applying {action:?}"))?;
if loadability == crate::compiler::Loadability::Verified {
verified_now = true;
}
}
if rewrote_content {
return Ok(RestoredBytes::Rewritten);
}
let Ok(after) = crate::cache_key::FileFingerprint::from_path(target_path) else {
return Ok(RestoredBytes::Rewritten);
};
let untouched = external.is_empty() || before.is_some_and(|before| before == after);
if untouched
&& verified_now
&& let Some(verified) = &verified
{
verified.record();
}
Ok(if untouched {
RestoredBytes::ExactBlobCopy(after)
} else {
RestoredBytes::Rewritten
})
}
struct VerifiedLoadable(PathBuf);
impl VerifiedLoadable {
fn new(dir: &Path, hash: &str) -> Self {
Self(dir.join(hash))
}
fn is_recorded(&self) -> bool {
self.0.is_file()
}
fn record(&self) {
let written = self
.0
.parent()
.map_or(Ok(()), std::fs::create_dir_all)
.and_then(|()| std::fs::write(&self.0, b""));
if let Err(error) = written {
tracing::debug!(
"could not remember {} as loadable: {error}",
self.0.display()
);
}
}
}
fn missing_requested_emit(args: &RustcArgs, artifacts: &ArtifactSet) -> Option<String> {
let present: std::collections::HashSet<&str> = artifacts
.outputs()
.iter()
.filter_map(|a| crate::compiler::emit_kind_for_filename(&a.store_name))
.collect();
args.emit
.iter()
.find(|kind| {
crate::compiler::GATED_EMIT_KINDS.contains(&kind.as_str())
&& !present.contains(kind.as_str())
})
.cloned()
}
fn unaudited_native_bundle(
args: &RustcArgs,
artifacts: &ArtifactSet,
native: &crate::cache_key::KeyedNativeArchives,
) -> Result<Option<String>> {
if !crate::cache_key::needs_native_bundle_audit(args, &native.dirs) {
return Ok(None);
}
let Some(rlib) = artifacts
.outputs()
.iter()
.find(|artifact| artifact.store_name.ends_with(".rlib"))
else {
return Ok(None);
};
let members = without_import_members(&crate::native_archive::members(&rlib.path)?);
let keyed = bundle_credits(&native.bundled)?;
if unkeyed_rlib_members(&members, &keyed).is_empty() {
return Ok(None);
}
let mut candidates = Vec::new();
for dir in &native.dirs {
for archive in crate::cache_key::native_dir_archives(dir)? {
if !native.archives.contains(&archive)
&& crate::native_archive::has_archive_magic(&archive)?
{
candidates.extend(archive_names(&archive)?);
}
}
}
Ok(unaudited_bundled_member(&members, &keyed, &candidates))
}
fn bundle_credits(bundled: &[crate::cache_key::BundledArchive]) -> Result<Vec<String>> {
let mut credits = Vec::new();
let mut seen = std::collections::HashSet::new();
for archive in bundled {
if !seen.insert(std::fs::canonicalize(&archive.path)?) {
continue;
}
if archive.packed {
credits.extend(
archive
.path
.file_name()
.map(|name| name.to_string_lossy().into_owned()),
);
} else {
credits.extend(crate::native_archive::member_names(&archive.path)?);
}
}
Ok(credits)
}
fn without_import_members(members: &[crate::native_archive::ArchiveMember]) -> Vec<String> {
let dlls: std::collections::HashSet<&str> = members
.iter()
.filter(|member| member.short_import)
.map(|member| member.name.as_str())
.collect();
members
.iter()
.filter(|member| !dlls.contains(member.name.as_str()))
.map(|member| member.name.clone())
.collect()
}
fn archive_names(archive: &Path) -> Result<Vec<String>> {
let mut names = crate::native_archive::member_names(archive)?;
names.extend(
archive
.file_name()
.map(|name| name.to_string_lossy().into_owned()),
);
Ok(names)
}
fn is_rustc_rlib_member(name: &str) -> bool {
matches!(name, "/" | "//" | "/SYM64/")
|| name.starts_with("__.SYMDEF")
|| name.starts_with("lib.rmeta")
|| name.ends_with(".rcgu.o")
|| name.ends_with(".rcgu.dwo")
}
fn unkeyed_rlib_members<'a>(rlib_members: &'a [String], keyed: &[String]) -> Vec<&'a str> {
let mut remaining: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
for name in keyed {
*remaining.entry(name).or_default() += 1;
}
rlib_members
.iter()
.map(String::as_str)
.filter(|member| !is_rustc_rlib_member(member))
.filter(|member| match remaining.get_mut(member) {
Some(count) if *count > 0 => {
*count -= 1;
false
}
_ => true,
})
.collect()
}
fn unaudited_bundled_member(
rlib_members: &[String],
keyed: &[String],
candidates: &[String],
) -> Option<String> {
unkeyed_rlib_members(rlib_members, keyed)
.into_iter()
.find(|member| candidates.iter().any(|candidate| candidate == member))
.map(str::to_string)
}
struct ComputedKey {
cache_key: String,
deferred: bool,
stop_on_hit: bool,
discovery_flight: Option<crate::store::StoreLock>,
predicted: bool,
outputs: crate::cache_key::KeyOutputs,
key_ms: u64,
key_hash_stats: FileHashStats,
key_too_new: bool,
guard_inputs: Vec<crate::cache_key::ObservedFingerprint>,
hashed_after_compile: Option<Vec<crate::cache_key::ObservedFingerprint>>,
written_sources: Vec<crate::cache_key::FileFingerprint>,
}
struct Precompiled {
result: crate::compile::CompileResult,
compile_time_ms: u64,
dep_info: Option<crate::cache_key::DepInfo>,
tree_guard: Option<crate::cache_key::TreeGuard>,
key_record: KeyEventRecord,
}
struct CompileFirst<'a> {
config: &'a Config,
compiler: &'a RustcCompiler,
args: &'a RustcArgs,
start: std::time::Instant,
invocation_start_ns: i64,
extra_inputs: Option<&'a crate::extra_inputs::ExtraInputsSnapshot>,
extra_inputs_hash_stats: FileHashStats,
extra_inputs_too_new: bool,
extra_inputs_key_ms: u64,
build_script_inputs: &'a BuildScriptInputs,
workspace_root: Option<&'a Path>,
store: &'a Store,
key_env: &'a KeyEnv,
crate_name: &'a str,
event_root: &'a str,
}
fn stop_on_hit_key(
ctx: &CompileFirst<'_>,
dep_info: crate::cache_key::DepInfo,
tree: Option<crate::cache_key::TreeGuard>,
) -> Result<ComputedKey> {
emitted_compile_key(ctx, dep_info, tree, &mut KeyEventRecord::default())
}
fn emitted_compile_key(
ctx: &CompileFirst<'_>,
dep_info: crate::cache_key::DepInfo,
tree: Option<crate::cache_key::TreeGuard>,
key_record: &mut KeyEventRecord,
) -> Result<ComputedKey> {
compute_rustc_cache_key(
ctx.config,
ctx.compiler,
ctx.args,
ctx.workspace_root,
ctx.invocation_start_ns,
Some(ctx.store),
ctx.key_env,
ExtraInputsKey {
build_script_inputs: ctx.build_script_inputs.digest(),
..ExtraInputsKey::default()
},
KeyDiscovery::Emitted(dep_info, tree),
key_record,
)
}
#[allow(clippy::too_many_arguments)]
fn adaptive_compile_before_key(
ctx: &CompileFirst<'_>,
mut lease: Lease,
tree_guard: Option<crate::cache_key::TreeGuard>,
guard_inputs: Vec<crate::cache_key::ObservedFingerprint>,
mut key_record: KeyEventRecord,
mut key_ms: u64,
mut key_hash_stats: FileHashStats,
) -> Result<i32> {
crate::out_dir_alias::register_before_compile();
let permit = scheduler::begin_keyless_compile(
&ctx.config.cache_dir,
ctx.config.scheduler,
ctx.crate_name,
ctx.args.invokes_linker(),
ctx.config.test_lease.as_deref(),
);
let compiler_args = lease.compiler_args(ctx.args);
let mut hit_closure = None;
let mut observed = None;
let mut lookup_ms = 0_u64;
let compile_start = std::time::Instant::now();
let mut on_dep_info = || {
let Some(dep_info) = emitted_dep_info(ctx.args) else {
return true;
};
let Ok(keyed) =
emitted_compile_key(ctx, dep_info.clone(), tree_guard.clone(), &mut key_record)
else {
return true;
};
let lookup_start = std::time::Instant::now();
let stored = ctx.store.get(&keyed.cache_key);
lookup_ms = lookup_ms.saturating_add(lookup_start.elapsed().as_millis() as u64);
(key_ms, key_hash_stats, _) = combine_key_measurements(
key_ms,
keyed.key_ms,
key_hash_stats,
keyed.key_hash_stats,
false,
false,
);
match stored {
Ok(Some(meta)) if cache_entry_has_files(&meta) => {
hit_closure = Some((dep_info, tree_guard.clone()));
false
}
Ok(_) => {
if let Some(fields) = &keyed.outputs.fields {
let _ = lease.qualify_deferred_seed(&keyed.cache_key, fields);
observed = Some((keyed.cache_key, fields.clone()));
}
true
}
Err(_) => true,
}
};
let executed = ctx.compiler.execute_preserving_incremental_until_dep_info(
ctx.args,
&compiler_args,
&mut on_dep_info,
);
if hit_closure.is_none() && executed.is_ok() {
permit.record_compile_rss(ctx.crate_name);
}
drop(permit);
if let Some((dep_info, tree)) = hit_closure {
let _ = lease.finish(CompileOutcome::Abnormal);
crate::cache_key::provide_dep_info(dep_info, tree);
return run_parsed_rustc(
ctx.config,
ctx.compiler,
ctx.args,
ctx.start,
ctx.invocation_start_ns,
ctx.extra_inputs,
ctx.extra_inputs_hash_stats,
ctx.extra_inputs_too_new,
ctx.extra_inputs_key_ms,
guard_inputs,
Some(ctx.build_script_inputs),
None,
);
}
let result = match executed {
Ok(result) => result,
Err(error) => {
let _ = lease.finish(CompileOutcome::Abnormal);
return passthrough_with_event(
ctx.config,
ctx.args,
ctx.crate_name,
ctx.event_root,
ctx.start,
format!("adaptive compiler spawn failed: {error}"),
key_record,
);
}
};
let compile_time_ms = compile_start.elapsed().as_millis() as u64;
replay_diagnostics(
&result.stdout,
result.pending_stderr(),
std::io::stdout(),
std::io::stderr(),
);
after_rustc_exit(result.exit_code, &result.stderr, &ctx.args.externs);
let outcome = compile_outcome(result.exit_code, result.signaled, &result.stderr);
let _ = lease.finish(outcome);
let cache_key = observed.as_ref().map_or("", |(key, _)| key.as_str());
log_event(
ctx.config,
EventInputs::new(
ctx.event_root,
ctx.crate_name,
EventResult::Passthrough,
ctx.start.elapsed().as_millis() as u64,
)
.rebuilt(
result.exit_code == 0,
&result.artifacts,
ctx.key_env.var("CARGO_PKG_NAME"),
)
.compile_time_ms(compile_time_ms)
.keyed(cache_key, key_ms, key_hash_stats)
.lookup_ms(lookup_ms)
.passthrough_reason("adaptive seed".to_owned())
.exit_code(result.exit_code)
.key_record(key_record),
);
Ok(result.exit_code)
}
fn compile_before_key(
ctx: &CompileFirst<'_>,
stop_on_hit: bool,
tree_guard: Option<crate::cache_key::TreeGuard>,
guard_inputs: Vec<crate::cache_key::ObservedFingerprint>,
key_record: KeyEventRecord,
key_ms: u64,
key_hash_stats: FileHashStats,
) -> Result<i32> {
let CompileFirst {
config,
compiler,
args,
start,
invocation_start_ns,
extra_inputs,
extra_inputs_hash_stats,
extra_inputs_too_new,
extra_inputs_key_ms,
crate_name,
..
} = *ctx;
crate::out_dir_alias::register_before_compile();
let miss_guard = scheduler::begin_keyless_compile(
&config.cache_dir,
config.scheduler,
crate_name,
args.invokes_linker(),
config.test_lease.as_deref(),
);
let compile_start = std::time::Instant::now();
let mut hit_closure = None;
let executed = if stop_on_hit {
let tree = tree_guard.clone();
let mut on_dep_info = || {
let Some(dep_info) = emitted_dep_info(args) else {
return true;
};
let keyed = stop_on_hit_key(ctx, dep_info.clone(), tree.clone());
let stored = keyed
.ok()
.is_some_and(|keyed| ctx.store.get(&keyed.cache_key).ok().flatten().is_some());
if stored {
hit_closure = Some((dep_info, tree.clone()));
}
!stored
};
compiler.execute_streaming_until_dep_info(args, &mut on_dep_info)
} else {
compiler.execute_streaming(args)
};
if hit_closure.is_none() {
miss_guard.record_compile_rss(crate_name);
}
drop(miss_guard);
if let Some((dep_info, tree)) = hit_closure {
tracing::debug!("{crate_name}: stored entry found while compiling; restoring it");
crate::cache_key::provide_dep_info(dep_info, tree);
return run_parsed_rustc(
config,
compiler,
args,
start,
invocation_start_ns,
extra_inputs,
extra_inputs_hash_stats,
extra_inputs_too_new,
extra_inputs_key_ms,
guard_inputs,
Some(ctx.build_script_inputs),
None,
);
}
let result = match executed {
Ok(result) => result,
Err(e) => {
return passthrough_with_event(
config,
args,
crate_name,
ctx.event_root,
start,
format!("compiler spawn failed: {e}"),
key_record,
);
}
};
let compile_time_ms = compile_start.elapsed().as_millis() as u64;
replay_diagnostics(
&result.stdout,
result.pending_stderr(),
std::io::stdout(),
std::io::stderr(),
);
after_rustc_exit(result.exit_code, &result.stderr, &args.externs);
if result.exit_code != 0 {
let elapsed = start.elapsed().as_millis() as u64;
log_event(
config,
EventInputs::new(ctx.event_root, crate_name, EventResult::Error, elapsed)
.compile_time_ms(compile_time_ms)
.keyed("", key_ms, key_hash_stats)
.key_record(key_record),
);
print_progress(crate_name, EventResult::Error, elapsed, 0);
return Ok(result.exit_code);
}
let emitted = args
.dep_info_path()
.zip(args.source_file.as_deref())
.map(|(path, source)| crate::cache_key::dep_info_from_emitted(&path, source));
let dep_info = match emitted {
Some(Ok(dep_info)) => dep_info,
other => {
tracing::debug!(
"not caching {crate_name}: the compile left no readable dep-info ({:?})",
other.map(|r| r.map(|_| ()))
);
let elapsed = start.elapsed().as_millis() as u64;
log_event(
config,
EventInputs::new(ctx.event_root, crate_name, EventResult::Skipped, elapsed)
.rebuilt(true, &result.artifacts, ctx.key_env.var("CARGO_PKG_NAME"))
.skip_reason("missing-dep-info")
.compile_time_ms(compile_time_ms)
.keyed("", key_ms, key_hash_stats)
.key_record(key_record),
);
print_progress(crate_name, EventResult::Skipped, elapsed, 0);
return Ok(result.exit_code);
}
};
let exit_code = result.exit_code;
PRECOMPILED_REBUILT.with(|cell| {
*cell.borrow_mut() =
RebuiltArtifacts::observed(true, &result.artifacts, ctx.key_env.var("CARGO_PKG_NAME"))
});
PRECOMPILED_EXIT.with(|cell| cell.set(Some(exit_code)));
let stored = run_parsed_rustc(
config,
compiler,
args,
start,
invocation_start_ns,
extra_inputs,
extra_inputs_hash_stats,
extra_inputs_too_new,
extra_inputs_key_ms,
guard_inputs,
Some(ctx.build_script_inputs),
Some(Precompiled {
result,
compile_time_ms,
dep_info: Some(dep_info),
tree_guard,
key_record,
}),
);
PRECOMPILED_EXIT.with(|cell| cell.set(None));
PRECOMPILED_REBUILT.with(|cell| *cell.borrow_mut() = RebuiltArtifacts::default());
stored.or(Ok(exit_code))
}
fn stop_on_hit_allowed(args: &RustcArgs) -> bool {
args.reports_artifacts() && !args.invokes_linker()
}
fn emitted_dep_info(args: &RustcArgs) -> Option<crate::cache_key::DepInfo> {
let path = args.dep_info_path()?;
crate::cache_key::dep_info_from_emitted(&path, args.source_file.as_deref()?).ok()
}
fn deferral_allowed(
config: &Config,
args: &RustcArgs,
keys_first: bool,
extra_inputs: Option<&crate::extra_inputs::ExtraInputsSnapshot>,
) -> bool {
args.dep_info_path().is_some()
&& config.deferred_discovery
&& config.remote.is_none()
&& config.fallback.is_none()
&& config.readonly_store.is_none()
&& !keys_first
&& extra_inputs.is_none()
}
fn remote_prediction_rows(config: &Config) -> Option<crate::cache_key::RemoteRows> {
if !fetches_remote_predictions(config) {
return None;
}
let config = config.clone();
Some(Box::new(move |identity: &str| {
crate::daemon::send_prediction_fetch(&config, identity)
}))
}
fn fetches_remote_predictions(config: &Config) -> bool {
config.input_predictions && config.remote.is_some()
}
fn record_input_prediction(
config: &Config,
store: Option<&Store>,
args: &RustcArgs,
wanted: bool,
key: &crate::cache_key::KeyOutputs,
) {
let _trace = crate::phase_trace::phase("prediction_record");
if !config.input_predictions || !wanted {
return;
}
let Some(dep_info) = &key.dep_info else {
return;
};
let Some(store) = store else {
return;
};
let file_hasher = store.file_hasher();
if !file_hasher.supports_input_predictions() {
return;
}
let Some(identity) = crate::cache_key::rustc_prediction_identity(args) else {
return;
};
let tree = key
.tree_guard
.as_ref()
.filter(|guard| guard.held())
.map(|guard| guard.digest.clone());
let registry = crate::cache_key::registry_src_of(&std::env::vars_os().collect::<Vec<_>>());
let workspace = crate::cache_key::workspace_record(args, dep_info, tree.as_deref());
let guard = crate::cache_key::same_checkout_guard(args, dep_info, tree.clone());
file_hasher.record_input_prediction(
&identity,
args.crate_name.as_deref(),
dep_info,
guard.clone(),
);
if let Some((identity, record)) = workspace {
file_hasher.record_portable_prediction(&identity, args.crate_name.as_deref(), &record);
publish_prediction(
config,
&identity,
crate::prediction_share::for_remote_portable(&record, registry.as_deref()),
);
}
if crate::cache_key::shared_prediction_can_record(args, dep_info) {
if let Some(identity) = crate::cache_key::rustc_shared_prediction_identity(args) {
file_hasher.record_input_prediction(
&identity,
args.crate_name.as_deref(),
dep_info,
guard.clone(),
);
if let Some(registry) = ®istry {
let row = crate::cache_key::InputPrediction::from_dep_info(dep_info, guard);
publish_prediction(
config,
&identity,
crate::prediction_share::for_remote_plain(&row, registry),
);
}
}
} else if let Some((identity, record)) =
crate::cache_key::relocatable_record(args, dep_info, tree.as_deref())
{
file_hasher.record_portable_prediction(&identity, args.crate_name.as_deref(), &record);
publish_prediction(
config,
&identity,
crate::prediction_share::for_remote_portable(&record, registry.as_deref()),
);
}
}
fn publish_prediction(
config: &Config,
identity: &str,
row: Option<crate::prediction_share::SharedPrediction>,
) {
if let Some(row) = row {
crate::daemon::send_prediction_publish(config, identity, row);
}
}
fn should_skip_cache_store_for_input_race(
extra_inputs_racy: bool,
sources_changed: bool,
modified_input_guard: bool,
key_inputs_changed: bool,
) -> bool {
extra_inputs_racy || sources_changed || (modified_input_guard && key_inputs_changed)
}
fn sources_written_since(
hashed: &[crate::cache_key::ObservedFingerprint],
sources: &std::collections::HashSet<String>,
invocation_start_ns: i64,
) -> Vec<crate::cache_key::FileFingerprint> {
if invocation_start_ns <= 0 {
return Vec::new();
}
let mut written: Vec<_> = hashed
.iter()
.map(|input| &input.fingerprint)
.filter(|input| {
sources.contains(&input.path)
&& crate::cache_key::stamp_written_since(input, invocation_start_ns)
})
.cloned()
.collect();
let recorded: std::collections::HashSet<&str> = hashed
.iter()
.map(|input| input.fingerprint.path.as_str())
.collect();
written.extend(
sources
.iter()
.filter(|source| !recorded.contains(source.as_str()))
.map(|source| crate::cache_key::FileFingerprint {
path: source.clone(),
size: 0,
mtime_ns: invocation_start_ns,
ctime_ns: invocation_start_ns,
inode: 0,
}),
);
written
}
fn emitted_sources_changed_during_compile(
written: &[crate::cache_key::FileFingerprint],
before: &[crate::cache_key::ObservedFingerprint],
) -> bool {
written.iter().any(|input| {
!before.iter().any(|earlier| {
earlier.fingerprint.path == input.path
&& earlier.settled()
&& FileHasher::guarded_inputs_unchanged_since_hash(std::slice::from_ref(earlier))
})
})
}
fn key_inputs_changed_during_compile(
key_too_new: bool,
guard_inputs: &[crate::cache_key::ObservedFingerprint],
) -> bool {
FileHasher::guarded_inputs_moved_since_hash(guard_inputs)
|| (key_too_new && !FileHasher::guarded_inputs_unchanged_since_hash(guard_inputs))
}
fn combine_key_measurements(
key_ms: u64,
extra_inputs_key_ms: u64,
key_hash_stats: FileHashStats,
extra_inputs_hash_stats: FileHashStats,
key_too_new: bool,
extra_inputs_too_new: bool,
) -> (u64, FileHashStats, bool) {
(
key_ms + extra_inputs_key_ms,
FileHashStats {
cache_hits: key_hash_stats.cache_hits + extra_inputs_hash_stats.cache_hits,
cache_misses: key_hash_stats.cache_misses + extra_inputs_hash_stats.cache_misses,
bytes_hashed: key_hash_stats.bytes_hashed + extra_inputs_hash_stats.bytes_hashed,
},
key_too_new || extra_inputs_too_new,
)
}
enum KeyDiscovery {
Immediate,
Deferrable { stop_on_hit: bool },
Emitted(
crate::cache_key::DepInfo,
Option<crate::cache_key::TreeGuard>,
),
Rederived,
}
fn discovery_flight_dir(config: &Config, discovery: &KeyDiscovery) -> Option<PathBuf> {
(config.scheduler && !matches!(discovery, KeyDiscovery::Rederived))
.then(|| config.cache_dir.clone())
}
fn written_while_building(config: &Config, store: Option<&Store>) -> Vec<PathBuf> {
let probe_dir = crate::config::probe_memo_dir();
let mut written = crate::build_script_inputs::excluded_roots(
None,
[
config.cache_dir.as_path(),
config.runtime_dir.as_path(),
probe_dir.as_path(),
]
.into_iter()
.chain(store.map(Store::cache_dir)),
);
if let Some(home) = crate::build_script_inputs::cargo_home(&|name| std::env::var_os(name)) {
written.extend(crate::build_script_inputs::cargo_home_writes(&home));
}
written
}
#[derive(Default)]
struct ExtraInputsKey<'a> {
digest: Option<&'a str>,
build_script_inputs: Option<&'a str>,
hash_stats: FileHashStats,
too_new: bool,
key_ms: u64,
guard_inputs: Vec<crate::cache_key::ObservedFingerprint>,
}
#[allow(clippy::too_many_arguments)]
fn compute_rustc_cache_key(
config: &Config,
compiler: &RustcCompiler,
args: &RustcArgs,
workspace_root: Option<&Path>,
invocation_start_ns: i64,
store: Option<&Store>,
key_env: &KeyEnv,
extra_inputs: ExtraInputsKey<'_>,
discovery: KeyDiscovery,
key_record: &mut KeyEventRecord,
) -> Result<ComputedKey> {
let ExtraInputsKey {
digest: extra_inputs_digest,
build_script_inputs: build_script_inputs_digest,
hash_stats: extra_inputs_hash_stats,
too_new: extra_inputs_too_new,
key_ms: extra_inputs_key_ms,
guard_inputs: mut extra_inputs_guard_inputs,
} = extra_inputs;
let key_start = std::time::Instant::now();
let emitted =
matches!(discovery, KeyDiscovery::Emitted(..)) || crate::cache_key::dep_info_provided();
let flight_dir = discovery_flight_dir(config, &discovery);
crate::cache_key::set_defer_discovery(matches!(discovery, KeyDiscovery::Deferrable { .. }));
crate::cache_key::set_compile_while_keying(matches!(
discovery,
KeyDiscovery::Deferrable { stop_on_hit: true }
));
if let KeyDiscovery::Emitted(dep_info, tree) = discovery {
crate::cache_key::provide_dep_info(dep_info, tree);
}
let emitted_sources = crate::cache_key::provided_dep_info_sources();
let mut file_hasher = match store {
Some(store) => store.file_hasher_with_daemon(config.socket_path()),
None => crate::cache_key::FileHasher::new().with_daemon(config.socket_path()),
}
.with_input_predictions(config.input_predictions)
.with_prediction_flights(flight_dir)
.with_own_dirs(written_while_building(config, store));
if config.modified_input_guard || emitted {
file_hasher.arm_too_new_guard(invocation_start_ns, 0);
}
let path_normalizer = crate::path_normalizer::PathNormalizer::from_env(workspace_root)
.with_target_dir(args.target_dir().as_deref())
.with_base_dirs(&config.base_dirs)
.with_path_only_env_vars(config.path_only_env_vars.clone())
.with_rust_src_rule(
crate::cache_key::get_rustc_sysroot(args).as_deref(),
crate::cache_key::get_rustc_commit_hash(&args.rustc).as_deref(),
);
let key_ctx = KeyCtx {
file_hasher: &file_hasher,
path_normalizer: &path_normalizer,
cache_dir: &config.cache_dir,
key_salt: config.key_salt.as_deref(),
key_env_vars: &config.key_env_vars,
extra_inputs_digest,
build_script_inputs_digest,
};
let (cache_key, outputs) = compiler.cache_key_in(args, &key_ctx, key_env);
key_record.absorb(&outputs);
let deferral = cache_key.as_ref().err().and_then(|error| {
error
.downcast_ref::<crate::cache_key::DeferredDiscovery>()
.map(|deferred| deferred.miss_certain)
});
crate::cache_key::set_compile_while_keying(false);
let cache_key = match cache_key {
Ok(cache_key) => cache_key,
Err(_) if let Some(miss_certain) = deferral => {
crate::cache_key::set_defer_discovery(false);
return Ok(ComputedKey {
cache_key: String::new(),
deferred: true,
stop_on_hit: !miss_certain,
discovery_flight: file_hasher.take_discovery_flight(),
predicted: false,
outputs,
key_ms: key_start.elapsed().as_millis() as u64,
key_hash_stats: file_hasher.stats(),
key_too_new: false,
guard_inputs: extra_inputs_guard_inputs,
hashed_after_compile: None,
written_sources: Vec::new(),
});
}
Err(error) => {
crate::cache_key::set_defer_discovery(false);
return Err(error);
}
};
crate::cache_key::set_defer_discovery(false);
let key_hash_stats = file_hasher.stats();
let hashed = file_hasher.take_guarded_inputs();
let written_sources = emitted_sources
.map(|sources| sources_written_since(&hashed, &sources, invocation_start_ns))
.unwrap_or_default();
let hashed_after_compile = if emitted {
Some(hashed)
} else {
extra_inputs_guard_inputs.extend(hashed);
None
};
let (key_ms, key_hash_stats, key_too_new) = combine_key_measurements(
key_start.elapsed().as_millis() as u64,
extra_inputs_key_ms,
key_hash_stats,
extra_inputs_hash_stats,
file_hasher.too_new(),
extra_inputs_too_new,
);
Ok(ComputedKey {
cache_key,
deferred: false,
stop_on_hit: false,
discovery_flight: file_hasher.take_discovery_flight(),
predicted: outputs.used_prediction,
outputs,
key_ms,
key_hash_stats,
key_too_new,
guard_inputs: extra_inputs_guard_inputs,
hashed_after_compile,
written_sources,
})
}
fn owes_rederivation(predicted: bool, already_rederived: bool) -> bool {
predicted && !already_rederived
}
fn compile_before_key_after_miss(
deferral_allowed: bool,
stop_on_hit_allowed: bool,
owes_rederivation: bool,
keyed_first: bool,
) -> Option<bool> {
if !deferral_allowed {
return None;
}
if owes_rederivation {
return stop_on_hit_allowed.then_some(true);
}
keyed_first.then_some(stop_on_hit_allowed)
}
fn should_record_closure(predicted: bool, rederived: bool) -> bool {
!predicted || rederived
}
#[allow(clippy::too_many_arguments)]
fn recompute_key_without_prediction(
config: &Config,
compiler: &RustcCompiler,
args: &RustcArgs,
workspace_root: Option<&Path>,
invocation_start_ns: i64,
store: Option<&Store>,
key_env: &KeyEnv,
extra_inputs_digest: Option<&str>,
build_script_inputs_digest: Option<&str>,
key_record: &mut KeyEventRecord,
) -> Result<ComputedKey> {
let mut without = config.clone();
without.input_predictions = false;
compute_rustc_cache_key(
&without,
compiler,
args,
workspace_root,
invocation_start_ns,
store,
key_env,
ExtraInputsKey {
digest: extra_inputs_digest,
build_script_inputs: build_script_inputs_digest,
..ExtraInputsKey::default()
},
KeyDiscovery::Rederived,
key_record,
)
}
#[allow(clippy::too_many_arguments)]
fn try_rustc_remote_hit(
hit: &RustcHitContext<'_>,
store: &Store,
cache_key: &str,
key_ms: u64,
key_hash_stats: FileHashStats,
lookup_ms: u64,
record_closure: bool,
key: &crate::cache_key::KeyOutputs,
key_record: &KeyEventRecord,
) -> Option<Result<()>> {
let (meta, result) = acquire_entry(
hit.config,
store,
cache_key,
hit.crate_name,
NegativeReply::CheckConcurrentEntry,
)?;
Some(hit.restore_and_finish(
store,
&meta,
result,
cache_key,
key_ms,
key_hash_stats,
lookup_ms,
record_closure.then_some(store),
key,
key_record,
))
}
fn record_known_file_hashes(store: &Store, restored: &[(crate::cache_key::FileFingerprint, &str)]) {
let _trace = crate::phase_trace::phase("memo_restored");
let restored: Vec<_> = restored
.iter()
.map(|(fingerprint, hash)| (fingerprint.clone(), *hash))
.collect();
store.record_verified_file_hashes(&restored);
}
fn replay_diagnostics(
stdout: &str,
stderr: &str,
mut out: impl std::io::Write,
mut err: impl std::io::Write,
) {
if !stdout.is_empty() {
let _ = write!(out, "{stdout}");
}
if !stderr.is_empty() {
let _ = write!(err, "{stderr}");
}
}
fn replay_cached_diagnostics(
meta: &crate::store::EntryMeta,
out: impl std::io::Write,
err: impl std::io::Write,
) {
replay_diagnostics(&meta.stdout, &meta.stderr, out, err);
}
fn maybe_verify_restored_hit(
compiler: &RustcCompiler,
args: &RustcArgs,
restored: &[(String, PathBuf)],
) {
if !crate::verify_compare::enabled() {
return;
}
let crate_name = args.crate_name.as_deref().unwrap_or("unknown");
match run_verify_recompile(compiler, args, restored) {
Ok(report) => {
let summary = report.event_summary();
match report.worst() {
crate::verify_compare::DivergenceClass::Match => {
tracing::info!(
crate_name,
summary = %summary,
"KACHE_VERIFY: restored artifacts match a fresh compile"
);
}
crate::verify_compare::DivergenceClass::PathDebug => {
tracing::warn!(
crate_name,
summary = %summary,
"KACHE_VERIFY: path/debug divergence versus a fresh compile"
);
}
crate::verify_compare::DivergenceClass::Content => {
tracing::error!(
crate_name,
summary = %summary,
"KACHE_VERIFY: content mismatch versus a fresh compile; serving the restored hit"
);
}
}
crate::verify_compare::record_report(summary);
}
Err(error) => {
let summary = format!("recompile-failed: {error:#}");
tracing::error!(
crate_name,
error = %error,
"KACHE_VERIFY: recompile failed; serving the restored hit"
);
crate::verify_compare::record_report(summary);
}
}
}
fn run_verify_recompile(
compiler: &RustcCompiler,
args: &RustcArgs,
restored: &[(String, PathBuf)],
) -> Result<crate::verify_compare::CompareReport> {
let staging = tempfile::Builder::new()
.prefix("kache-verify-")
.tempdir()
.context("creating KACHE_VERIFY staging directory")?;
let staged_args = retarget_rustc_args_for_staging(args, staging.path());
let result = crate::opcounts::suspend_spawn_counts(|| compiler.execute(&staged_args))
.context("running KACHE_VERIFY recompile")?;
verify_recompile_exit_status(result.exit_code, &result.stderr)?;
let mut compiled_by_name = std::collections::BTreeMap::new();
for artifact in result.artifacts.outputs() {
compiled_by_name.insert(artifact.store_name.clone(), artifact.path.clone());
if let Some(file_name) = artifact.path.file_name() {
compiled_by_name
.entry(file_name.to_string_lossy().into_owned())
.or_insert_with(|| artifact.path.clone());
}
}
for (name, _) in restored {
let staged = staging.path().join(name);
if staged.is_file() {
compiled_by_name.entry(name.clone()).or_insert(staged);
}
}
Ok(crate::verify_compare::compare_named_artifacts(
restored,
&compiled_by_name,
))
}
fn verify_recompile_exit_status(exit_code: i32, stderr: &str) -> Result<()> {
if exit_code == 0 {
return Ok(());
}
anyhow::bail!(
"rustc exited {exit_code}{}",
verify_recompile_stderr_hint(stderr)
)
}
fn verify_recompile_stderr_hint(stderr: &str) -> String {
let line = stderr
.lines()
.map(str::trim)
.find(|line| !line.is_empty() && !line.starts_with('{'))
.unwrap_or("");
if line.is_empty() {
String::new()
} else {
format!(" ({line})")
}
}
fn retarget_rustc_args_for_staging(args: &RustcArgs, staging: &Path) -> RustcArgs {
let mut staged = args.clone();
staged.all_args = rewrite_output_argv(&args.all_args, staging);
staged.out_dir = args.out_dir.as_ref().map(|_| staging.to_path_buf());
if let Some(output) = &args.output {
let name = output.file_name().unwrap_or_default();
staged.output = Some(staging.join(name));
}
if let Some(dep) = &args.dep_info_output {
let name = dep.file_name().unwrap_or_default();
staged.dep_info_output = Some(staging.join(name));
}
staged
}
fn rewrite_output_argv(argv: &[String], staging: &Path) -> Vec<String> {
let staging_s = staging.display().to_string();
let mut out = Vec::with_capacity(argv.len());
let mut args = argv.iter();
while let Some(arg) = args.next() {
match arg.as_str() {
"--out-dir" => {
out.push(arg.clone());
if args.next().is_some() {
out.push(staging_s.clone());
}
}
"-o" => {
out.push(arg.clone());
if let Some(next) = args.next() {
let name = Path::new(next).file_name().unwrap_or_default();
out.push(staging.join(name).to_string_lossy().into_owned());
}
}
"--emit" => {
out.push(arg.clone());
if let Some(next) = args.next() {
out.push(rewrite_emit_value(next, staging));
}
}
_ if arg.starts_with("--out-dir=") => {
out.push(format!("--out-dir={staging_s}"));
}
_ => {
if let Some(value) = arg.strip_prefix("--emit=") {
out.push(format!("--emit={}", rewrite_emit_value(value, staging)));
} else {
out.push(arg.clone());
}
}
}
}
out
}
fn rewrite_emit_value(value: &str, staging: &Path) -> String {
value
.split(',')
.map(|part| match part.split_once('=') {
Some((kind, path)) if !path.is_empty() => {
let name = Path::new(path).file_name().unwrap_or_default();
format!("{kind}={}", staging.join(name).display())
}
_ => part.to_string(),
})
.collect::<Vec<_>>()
.join(",")
}
#[cfg(test)]
fn restore_from_cache(
config: &Config,
compiler: &RustcCompiler,
store: &Store,
args: &RustcArgs,
meta: &crate::store::EntryMeta,
extra_inputs: Option<&crate::extra_inputs::ExtraInputsSnapshot>,
) -> Result<()> {
restore_from_cache_with_memo_store(config, compiler, store, store, args, meta, extra_inputs)
}
#[allow(clippy::too_many_arguments)]
fn restore_from_cache_with_memo_store(
config: &Config,
compiler: &RustcCompiler,
store: &Store,
memo_store: &Store,
args: &RustcArgs,
meta: &crate::store::EntryMeta,
extra_inputs: Option<&crate::extra_inputs::ExtraInputsSnapshot>,
) -> Result<()> {
let _trace = crate::phase_trace::phase("restore");
let current = resolve_extra_inputs_for_passthrough(config, args)
.context("revalidating extra_inputs before cache-hit publication")?;
anyhow::ensure!(
current.as_ref() == extra_inputs,
"extra_inputs declaration changed during cache lookup; refusing the stale hit"
);
if !meta.covers_requested_emit(&args.emit) {
let _ = store.remove_entry(&meta.cache_key);
anyhow::bail!(
"cached entry for {} covers --emit {:?} but this invocation requested {:?} \
— evicting partial entry and recompiling",
meta.crate_name,
meta.emit_kinds,
args.emit
);
}
if kache_format::reports_debug_info_tool_failure(&meta.stderr) {
let _ = store.remove_entry(&meta.cache_key);
anyhow::bail!(
"cached entry for {} was built while rustc could not post-process its \
debug info — evicting degraded entry and recompiling",
meta.crate_name
);
}
let expected_dep_info_name = extra_inputs.and_then(|_| {
args.dep_info_path()
.and_then(|path| path.file_name().map(std::ffi::OsStr::to_os_string))
});
if let Some(expected) = &expected_dep_info_name
&& !meta.files.iter().any(|file| {
matches!(
crate::compiler::classify_by_filename(&file.name),
crate::compiler::ArtifactKind::DepInfo
) && Path::new(&file.name).file_name() == Some(expected.as_os_str())
})
{
let _ = store.remove_entry(&meta.cache_key);
anyhow::bail!(
"cached entry for {} has no dep-info artifact named {} required by active \
extra_inputs; evicting the legacy entry and recompiling",
meta.crate_name,
expected.to_string_lossy()
);
}
let Some(output_dir) = rustc_output_dir(args) else {
anyhow::bail!("no output path (-o) or output directory (--out-dir) in args");
};
std::fs::create_dir_all(&output_dir)
.with_context(|| format!("creating output directory {}", output_dir.display()))?;
let cargo_target_dir = args.target_dir();
let depinfo_anchor = cargo_target_dir
.clone()
.or_else(|| std::env::current_dir().ok())
.unwrap_or_else(|| Path::new(".").to_path_buf());
let depinfo_working_dir =
std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf());
let depinfo_workspace_dir = args.path_normalization_root();
let depinfo_configured_roots =
configured_rustc_depinfo_roots(config, depinfo_workspace_dir, cargo_target_dir.as_deref());
for cached_file in &meta.files {
if !matches!(
crate::compiler::classify_by_filename(&cached_file.name),
crate::compiler::ArtifactKind::DepInfo
) {
continue;
}
if expected_dep_info_name.as_ref().is_some_and(|expected| {
Path::new(&cached_file.name).file_name() != Some(expected.as_os_str())
}) {
continue;
}
let blob = store.blob_path(&cached_file.hash);
let raw = match read_cached_dep_info_blob(&blob, extra_inputs.is_some()) {
Ok(Some(raw)) => raw,
Ok(None) => continue,
Err(error) => {
let _ = store.remove_entry(&meta.cache_key);
return Err(error).with_context(|| {
format!(
"cached dep-info for {} is unreadable or not UTF-8; evicting the entry",
meta.crate_name
)
});
}
};
let expanded = crate::link::rewrite_rustc_depinfo_content_with_configured_roots(
&raw,
&depinfo_anchor,
&depinfo_working_dir,
depinfo_workspace_dir,
&depinfo_configured_roots,
link::DepInfoMode::Expand,
);
let expanded = if let Some(snapshot) = extra_inputs {
match snapshot.merge_dep_info_content(&expanded) {
Ok(completed) => completed,
Err(error) => {
let _ = store.remove_entry(&meta.cache_key);
return Err(error).with_context(|| {
format!(
"cached dep-info for {} cannot be completed safely; evicting the entry",
meta.crate_name
)
});
}
}
} else {
expanded
};
let dependencies = match crate::extra_inputs::parse_dep_info_dependencies(&expanded) {
Ok(dependencies) if !dependencies.is_empty() => dependencies,
Ok(_) => {
let _ = store.remove_entry(&meta.cache_key);
anyhow::bail!(
"cached dep-info for {} has no dependencies; evicting the entry and recompiling",
meta.crate_name
);
}
Err(error) => {
let _ = store.remove_entry(&meta.cache_key);
return Err(error).with_context(|| {
format!(
"cached dep-info for {} is malformed; evicting the entry",
meta.crate_name
)
});
}
};
for dep in dependencies {
if !dep.exists() {
let _ = store.remove_entry(&meta.cache_key);
anyhow::bail!(
"cached dep-info for {} references {} which does not resolve here — \
evicting the entry and recompiling (#330)",
meta.crate_name,
dep.display()
);
}
}
}
let platform = platform::current();
tracing::debug!(
"restoring {} files via platform={}",
meta.files.len(),
platform.name()
);
let shared_loadable = shared_inode_loadable(args, platform.may_share_restored_loadables());
remember_prestaged_executables(config, compiler, args, &output_dir, shared_loadable, meta);
let mut exact_restores: Vec<(crate::cache_key::FileFingerprint, &str)> = Vec::new();
let mut restored_paths: Vec<(String, PathBuf)> = Vec::with_capacity(meta.files.len());
for cached_file in &meta.files {
if !crate::remote_layout::is_safe_artifact_name(&cached_file.name) {
anyhow::bail!(
"refusing to restore cache entry with unsafe artifact name {:?}",
cached_file.name
);
}
let target_path = artifact_target_path(args, &output_dir, &cached_file.name);
let kind = compiler.classify_output(args, &cached_file.name);
let restored = materialize_cached_artifact(
store,
cached_file,
&target_path,
kind,
shared_loadable,
&depinfo_anchor,
&depinfo_working_dir,
depinfo_workspace_dir,
&depinfo_configured_roots,
&*platform,
"rustc restore",
extra_inputs,
)?;
if let RestoredBytes::ExactBlobCopy(fingerprint) = restored {
exact_restores.push((fingerprint, &cached_file.hash));
}
restored_paths.push((cached_file.name.clone(), target_path));
}
record_known_file_hashes(memo_store, &exact_restores);
maybe_verify_restored_hit(compiler, args, &restored_paths);
Ok(())
}
fn read_cached_dep_info_blob(
path: &Path,
extra_inputs_active: bool,
) -> std::io::Result<Option<String>> {
match std::fs::read_to_string(path) {
Ok(raw) => Ok(Some(raw)),
Err(error) if extra_inputs_active => Err(error),
Err(_) => Ok(None),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct PassthroughOutput {
exit_code: i32,
fallback: bool,
fallback_attempt: Option<crate::fallback::Attempt>,
}
fn passthrough(
args: &RustcArgs,
fallback: Option<&str>,
preserve_incremental: bool,
) -> Result<PassthroughOutput> {
let isolated_args = preserve_incremental
.then(|| compile::isolate_incremental_flags(&args.all_args))
.flatten();
let incremental_preserved = args.incremental.is_some() && isolated_args.is_some();
let compiler_args = if let Some(isolated_args) = isolated_args {
isolated_args
} else {
compile::strip_incremental_flags(&args.all_args)
.into_iter()
.cloned()
.collect()
};
passthrough_args(args, fallback, &compiler_args, incremental_preserved)
}
fn passthrough_direct_args<'a>(
args: &'a RustcArgs,
compiler_args: &'a [String],
compiler_args_changed: bool,
) -> Vec<&'a String> {
if args.has_expanded_argfiles() && !compiler_args_changed {
args.raw_args().iter().collect()
} else {
compiler_args.iter().collect()
}
}
fn compiler_args_changed(args: &RustcArgs, compiler_args: &[String]) -> bool {
compiler_args != args.all_args.as_slice()
}
fn stripped_incremental_count(args: &RustcArgs, compiler_args: &[String]) -> Option<usize> {
let count = args.all_args.len().saturating_sub(compiler_args.len());
(count > 0).then_some(count)
}
fn handle_response_file_error(
error: anyhow::Error,
compiler_args_changed: bool,
) -> Result<Option<compile::RustcResponseFile>> {
if compiler_args_changed {
return Err(error)
.context("materializing rustc response file after rewriting incremental arguments");
}
tracing::warn!(
"failed to materialize expanded rustc response file; using unchanged original argv: {error:#}"
);
Ok(None)
}
fn passthrough_args(
args: &RustcArgs,
fallback: Option<&str>,
compiler_args: &[String],
incremental_preserved: bool,
) -> Result<PassthroughOutput> {
let compiler_args_changed = compiler_args_changed(args, compiler_args);
let stripped_incremental = stripped_incremental_count(args, compiler_args);
if incremental_preserved {
tracing::info!(
"[kache] passthrough: preserving isolated incremental state for {}",
args.crate_name.as_deref().unwrap_or("unknown")
);
} else if let Some(stripped) = stripped_incremental {
tracing::info!(
"[kache] passthrough: stripped {} incremental flag(s) for {}",
stripped,
args.crate_name.as_deref().unwrap_or("unknown")
);
}
let response_file = if args.has_expanded_argfiles() {
match compile::RustcResponseFile::new(compiler_args.iter().map(|arg| arg.as_str())) {
Ok(response) => Some(response),
Err(error) => handle_response_file_error(error, compiler_args_changed)?,
}
} else {
None
};
let direct_args = response_file
.is_none()
.then(|| passthrough_direct_args(args, compiler_args, compiler_args_changed));
compile::pre_clean_outputs(
args.output.as_deref(),
args.out_dir.as_deref(),
args.crate_name.as_deref(),
args.extra_filename.as_deref(),
&args.emit,
);
let mut fallback_attempt = None;
if let Some(fb) = fallback {
let mut cmd = std::process::Command::new(fb);
if disable_incremental_env(incremental_preserved) {
cmd.env("CARGO_INCREMENTAL", "0");
}
cmd.arg(&args.rustc);
if let Some(inner) = &args.inner_rustc {
cmd.arg(inner);
}
if let Some(response) = &response_file {
cmd.arg(response.argument());
} else if let Some(direct) = &direct_args {
cmd.args(direct);
}
let outputs: Vec<&Path> = args
.output
.as_deref()
.map(Path::new)
.into_iter()
.chain(args.out_dir.as_deref().map(Path::new))
.collect();
let attempt = crate::fallback::run(cmd, fb, &outputs, compiler_args);
if let Some(exit_code) = attempt.terminal_code() {
return Ok(PassthroughOutput {
exit_code,
fallback: true,
fallback_attempt: Some(attempt),
});
}
fallback_attempt = Some(attempt);
}
let mut cmd = std::process::Command::new(&args.rustc);
crate::toolchain_dylib::apply(&mut cmd, &args.rustc);
if disable_incremental_env(incremental_preserved) {
cmd.env("CARGO_INCREMENTAL", "0");
}
if let Some(inner) = &args.inner_rustc {
cmd.arg(inner);
}
if let Some(response) = &response_file {
cmd.arg(response.argument());
} else if let Some(direct) = &direct_args {
cmd.args(direct);
}
let status = cmd
.status()
.with_context(|| format!("executing {}", args.rustc.display()))?;
Ok(PassthroughOutput {
exit_code: status.code().unwrap_or(1),
fallback: false,
fallback_attempt,
})
}
fn observe_adaptive_hit(
unit: Option<&AdaptiveUnit>,
fields: Option<&std::collections::BTreeMap<String, String>>,
cache_key: &str,
) {
match (unit, fields) {
(Some(unit), Some(fields)) => {
let _ = unit.observe_build(cache_key, fields);
}
(unit, _) => reset_adaptive_unit(unit),
}
}
fn reset_adaptive_unit(unit: Option<&AdaptiveUnit>) {
if let Some(unit) = unit {
let _ = unit.reset();
}
}
#[allow(clippy::too_many_arguments)]
fn intentional_passthrough_with_event<R: Into<String>>(
config: &Config,
args: &RustcArgs,
crate_name: &str,
root: &str,
start: std::time::Instant,
adaptive_unit: Option<&AdaptiveUnit>,
reason: R,
key_record: KeyEventRecord,
) -> Result<i32> {
let reason = reason.into();
if config.fallback.is_none()
&& let Some(lease) = adaptive_unit.and_then(AdaptiveUnit::try_immediate)
{
return adaptive_incremental_with_event(
config,
args,
crate_name,
root,
start,
lease,
format!("adaptive passthrough: {reason}"),
None,
key_record,
);
}
passthrough_with_event(config, args, crate_name, root, start, reason, key_record)
}
#[allow(clippy::too_many_arguments)]
fn adaptive_incremental_with_event<R: Into<String>>(
config: &Config,
args: &RustcArgs,
crate_name: &str,
root: &str,
start: std::time::Instant,
lease: Lease,
reason: R,
keyed: Option<(&str, u64, FileHashStats, u64)>,
key_record: KeyEventRecord,
) -> Result<i32> {
let reason = reason.into();
let kind = lease.kind();
let compiler_args = lease.compiler_args(args);
let permit = adaptive_lane_takes_permit(kind).then(|| {
scheduler::begin_keyless_compile(
&config.cache_dir,
config.scheduler,
crate_name,
args.invokes_linker(),
config.test_lease.as_deref(),
)
});
let compile_start = std::time::Instant::now();
let compiler = RustcCompiler::new().with_base_dirs(config.base_dirs.clone());
let rebuilt_package = KeyEnv::capture().var("CARGO_PKG_NAME");
let compile = if kind == crate::incremental_policy::LeaseKind::Immediate {
compiler.execute_passthrough_preserving_incremental(args, &compiler_args)
} else {
compiler.execute_preserving_incremental(args, &compiler_args)
};
if let (Some(permit), Ok(_)) = (&permit, &compile) {
permit.record_compile_rss(crate_name);
}
drop(permit);
let result = match compile {
Ok(result) => result,
Err(error) => {
let _ = lease.finish(CompileOutcome::Abnormal);
tracing::warn!("adaptive incremental compiler spawn failed for {crate_name}: {error}");
return passthrough_with_event(
config,
args,
crate_name,
root,
start,
format!("adaptive compiler spawn failed: {error}"),
key_record,
);
}
};
let compile_time_ms = compile_start.elapsed().as_millis() as u64;
replay_diagnostics(
&result.stdout,
result.pending_stderr(),
std::io::stdout(),
std::io::stderr(),
);
after_rustc_exit(result.exit_code, &result.stderr, &args.externs);
let outcome = compile_outcome(result.exit_code, result.signaled, &result.stderr);
let kept = lease.finish(outcome);
tracing::debug!(
?kind,
?outcome,
kept,
"adaptive incremental compiler lease finished"
);
let (cache_key, key_ms, key_hash_stats, lookup_ms) =
keyed.unwrap_or(("", 0, FileHashStats::default(), 0));
log_event(
config,
EventInputs::new(
root,
crate_name,
EventResult::Passthrough,
start.elapsed().as_millis() as u64,
)
.rebuilt(result.exit_code == 0, &result.artifacts, rebuilt_package)
.compile_time_ms(compile_time_ms)
.keyed(cache_key, key_ms, key_hash_stats)
.lookup_ms(lookup_ms)
.passthrough_reason(reason)
.exit_code(result.exit_code)
.key_record(key_record),
);
Ok(result.exit_code)
}
thread_local! {
static PRECOMPILED_EXIT: std::cell::Cell<Option<i32>> = const { std::cell::Cell::new(None) };
static PRECOMPILED_REBUILT: std::cell::RefCell<RebuiltArtifacts> = std::cell::RefCell::new(RebuiltArtifacts::default());
}
const UNCACHEABLE_REASON: &str = "uncacheable|";
fn uncacheable_reason(error: &anyhow::Error) -> String {
format!("{UNCACHEABLE_REASON}{error:#}")
}
fn rustc_output_in(reason: &str) -> &str {
reason.strip_prefix(UNCACHEABLE_REASON).unwrap_or_default()
}
fn after_rustc_exit(exit_code: i32, rustc_output: &str, externs: &[ExternDep]) {
if exit_code != 0 {
crate::out_dir_alias::after_failed_compile(rustc_output, externs);
}
}
fn passthrough_with_event<R: Into<String>>(
config: &Config,
args: &RustcArgs,
crate_name: &str,
root: &str,
start: std::time::Instant,
reason: R,
key_record: KeyEventRecord,
) -> Result<i32> {
if let Some(exit_code) = PRECOMPILED_EXIT.with(std::cell::Cell::get) {
let reason = reason.into();
tracing::debug!("{crate_name}: compiled, not stored: {reason}");
let elapsed = start.elapsed().as_millis() as u64;
log_event(
config,
EventInputs::new(root, crate_name, EventResult::Skipped, elapsed)
.rebuilt_observed(PRECOMPILED_REBUILT.with(|cell| cell.borrow().clone()))
.skip_reason(reason)
.key_record(key_record),
);
print_progress(crate_name, EventResult::Skipped, elapsed, 0);
return Ok(exit_code);
}
let reason = reason.into();
let output = passthrough(
args,
config.fallback.as_deref(),
config.preserve_incremental,
)?;
let elapsed = start.elapsed().as_millis() as u64;
after_rustc_exit(output.exit_code, rustc_output_in(&reason), &args.externs);
log_event(
config,
EventInputs::passthrough(root, crate_name, elapsed, reason, &output).key_record(key_record),
);
Ok(output.exit_code)
}
fn untrusted_codegen_backend(backend: Option<&str>, trusted: bool) -> Option<&str> {
backend.filter(|backend| !trusted || !crate::args::codegen_backend_is_keyable(backend))
}
const UNTRUSTED_CODEGEN_BACKEND_REASON: &str = "unsupported|rustc codegen backend dylib (-Zcodegen-backend=<path>) may write files kache cannot restore; set cache.trust_codegen_backends and pass the backend as a path to cache it";
fn rustc_direct_passthrough_with_event(
config: &Config,
args: &RustcArgs,
crate_name: &str,
root: &str,
start: std::time::Instant,
reason: &str,
key_record: KeyEventRecord,
) -> Result<i32> {
if PRECOMPILED_EXIT.with(std::cell::Cell::get).is_some() {
return passthrough_with_event(config, args, crate_name, root, start, reason, key_record);
}
let output = passthrough(args, None, config.preserve_incremental)?;
log_event(
config,
EventInputs::passthrough(
root,
crate_name,
start.elapsed().as_millis() as u64,
reason.to_string(),
&output,
)
.key_record(key_record),
);
Ok(output.exit_code)
}
fn preserved_incremental_with_event(
config: &Config,
args: &RustcArgs,
crate_name: &str,
root: &str,
start: std::time::Instant,
key_record: KeyEventRecord,
) -> Result<i32> {
let output = passthrough(args, None, true)?;
log_event(
config,
EventInputs::passthrough(
root,
crate_name,
start.elapsed().as_millis() as u64,
"incremental preserved".to_string(),
&output,
)
.key_record(key_record),
);
Ok(output.exit_code)
}
fn cc_compile_before_key(
config: &Config,
invocation: &CcStoreInvocation,
file_hasher: &crate::cache_key::FileHasher<'_>,
flight: Option<crate::store::StoreLock>,
) -> Result<i32> {
let rebuilt_package = KeyEnv::capture().var("CARGO_PKG_NAME");
let CcStoreInvocation {
compiler,
parsed,
crate_name,
event_root,
start,
..
} = invocation;
let start = *start;
tracing::debug!("no read-set memo for {crate_name}; compiling before keying");
let miss_guard = scheduler::begin_keyless_compile(
&config.cache_dir,
config.scheduler,
crate_name,
false,
config.test_lease.as_deref(),
);
let compile_start = std::time::Instant::now();
let compiled = compiler.execute_capturing_inputs(parsed, file_hasher);
miss_guard.record_compile_rss(crate_name);
drop(miss_guard);
let (result, inputs) = match compiled {
Ok(pair) => pair,
Err(e) => {
return cc_passthrough_with_event(
config,
parsed,
crate_name,
event_root,
start,
format!("compiler spawn failed: {e}"),
);
}
};
let compile_time_ms = compile_start.elapsed().as_millis() as u64;
replay_diagnostics(
&result.stdout,
result.pending_stderr(),
std::io::stdout(),
std::io::stderr(),
);
if result.exit_code != 0 {
let elapsed = start.elapsed().as_millis() as u64;
log_event(
config,
EventInputs::new(event_root, crate_name, EventResult::Error, elapsed)
.compile_time_ms(compile_time_ms),
);
print_progress(crate_name, EventResult::Error, elapsed, 0);
return Ok(result.exit_code);
}
let exit_code = result.exit_code;
let rebuilt = RebuiltArtifacts::observed(true, &result.artifacts, rebuilt_package);
if inputs.is_none() {
return cc_precompiled_skipped(
config,
crate_name,
event_root,
start,
"the compile left no usable read set".to_string(),
exit_code,
rebuilt,
);
}
CC_PRECOMPILED_REBUILT.with(|cell| *cell.borrow_mut() = rebuilt);
CC_PRECOMPILED_EXIT.with(|cell| cell.set(Some(exit_code)));
let stored = run_cc_with_store(
config,
invocation,
Some(CcPrecompiled {
result,
compile_time_ms,
inputs,
flight,
}),
);
CC_PRECOMPILED_EXIT.with(|cell| cell.set(None));
CC_PRECOMPILED_REBUILT.with(|cell| *cell.borrow_mut() = RebuiltArtifacts::default());
stored.or(Ok(exit_code))
}
#[allow(clippy::too_many_arguments)]
fn cc_precompiled_skipped(
config: &Config,
crate_name: &str,
root: &str,
start: std::time::Instant,
reason: String,
exit_code: i32,
rebuilt: RebuiltArtifacts,
) -> Result<i32> {
tracing::debug!("{crate_name}: compiled, not stored: {reason}");
let elapsed = start.elapsed().as_millis() as u64;
log_event(
config,
EventInputs::new(root, crate_name, EventResult::Skipped, elapsed)
.rebuilt_observed(rebuilt)
.skip_reason(reason),
);
print_progress(crate_name, EventResult::Skipped, elapsed, 0);
Ok(exit_code)
}
fn cc_passthrough_with_event<R: Into<String>>(
config: &Config,
parsed: &crate::compiler::cc::CcArgs,
crate_name: &str,
root: &str,
start: std::time::Instant,
reason: R,
) -> Result<i32> {
if let Some(exit_code) = CC_PRECOMPILED_EXIT.with(std::cell::Cell::get) {
return cc_precompiled_skipped(
config,
crate_name,
root,
start,
reason.into(),
exit_code,
CC_PRECOMPILED_REBUILT.with(|cell| cell.borrow().clone()),
);
}
let output = cc_passthrough(config, parsed)?;
log_event(
config,
EventInputs::passthrough(
root,
crate_name,
start.elapsed().as_millis() as u64,
reason.into(),
&output,
)
.object_output(&recorded_object_output(parsed.object_output_path())),
);
Ok(output.exit_code)
}
fn cc_direct_passthrough_with_event<R: Into<String>>(
config: &Config,
parsed: &crate::compiler::cc::CcArgs,
crate_name: &str,
root: &str,
start: std::time::Instant,
reason: R,
) -> Result<i32> {
if let Some(exit_code) = CC_PRECOMPILED_EXIT.with(std::cell::Cell::get) {
return cc_precompiled_skipped(
config,
crate_name,
root,
start,
reason.into(),
exit_code,
CC_PRECOMPILED_REBUILT.with(|cell| cell.borrow().clone()),
);
}
let output = cc_direct_passthrough(config, parsed)?;
log_event(
config,
EventInputs::passthrough(
root,
crate_name,
start.elapsed().as_millis() as u64,
reason.into(),
&output,
)
.object_output(&recorded_object_output(parsed.object_output_path())),
);
Ok(output.exit_code)
}
#[derive(Clone, Debug, Default)]
struct RebuiltArtifacts {
package: Option<String>,
primary: Option<String>,
paths: Vec<String>,
}
impl RebuiltArtifacts {
fn observed(success: bool, artifacts: &ArtifactSet, package: Option<String>) -> Self {
if !success || artifacts.is_empty() {
return Self::default();
}
let outputs: Vec<_> = artifacts
.outputs()
.iter()
.filter(|artifact| artifact.kind != ArtifactKind::DebugBundle)
.collect();
if outputs.is_empty() {
return Self::default();
}
let primary = outputs
.iter()
.find(|artifact| {
matches!(
artifact.kind,
ArtifactKind::Executable
| ArtifactKind::Metadata
| ArtifactKind::Library
| ArtifactKind::DynamicLibrary
| ArtifactKind::WasmModule
| ArtifactKind::Object
)
})
.or_else(|| outputs.first())
.map(|artifact| artifact.path.to_string_lossy().into_owned());
Self {
package: package.filter(|package| !package.is_empty()),
primary,
paths: outputs
.iter()
.map(|artifact| artifact.path.to_string_lossy().into_owned())
.collect(),
}
}
}
pub(crate) struct EventInputs<'a> {
root: &'a str,
crate_name: &'a str,
result: EventResult,
elapsed_ms: u64,
compile_time_ms: u64,
size: u64,
cache_key: &'a str,
key_ms: u64,
key_hash_stats: FileHashStats,
lookup_ms: u64,
restore_ms: u64,
store_ms: u64,
store_put: StorePutResult,
passthrough_reason: String,
skip_reason: String,
object_output: String,
package: String,
store_error: String,
lookup_rejection: String,
fallback: bool,
exit_code: Option<i32>,
fallback_attempt: Option<crate::fallback::Attempt>,
key_record: KeyEventRecord,
rebuilt: RebuiltArtifacts,
}
impl<'a> EventInputs<'a> {
pub(crate) fn new(
root: &'a str,
crate_name: &'a str,
result: EventResult,
elapsed_ms: u64,
) -> Self {
Self {
root,
crate_name,
result,
elapsed_ms,
compile_time_ms: 0,
size: 0,
cache_key: "",
key_ms: 0,
key_hash_stats: FileHashStats::default(),
lookup_ms: 0,
restore_ms: 0,
store_ms: 0,
store_put: StorePutResult::default(),
passthrough_reason: String::new(),
skip_reason: String::new(),
object_output: String::new(),
package: String::new(),
store_error: String::new(),
lookup_rejection: String::new(),
fallback: false,
exit_code: None,
fallback_attempt: None,
key_record: KeyEventRecord::default(),
rebuilt: RebuiltArtifacts::default(),
}
}
pub(crate) fn rebuilt(
mut self,
success: bool,
artifacts: &ArtifactSet,
package: Option<String>,
) -> Self {
self.rebuilt = RebuiltArtifacts::observed(
success
&& !matches!(
self.result,
EventResult::LocalHit
| EventResult::PrefetchHit
| EventResult::RemoteHit
| EventResult::Error
),
artifacts,
package,
);
self
}
fn rebuilt_observed(mut self, rebuilt: RebuiltArtifacts) -> Self {
self.rebuilt = rebuilt;
self
}
fn passthrough(
root: &'a str,
crate_name: &'a str,
elapsed_ms: u64,
reason: String,
output: &PassthroughOutput,
) -> Self {
Self::new(root, crate_name, EventResult::Passthrough, elapsed_ms)
.passthrough_reason(reason)
.fallback(output.fallback, output.fallback_attempt.clone())
.exit_code(output.exit_code)
}
pub(crate) fn compile_time_ms(mut self, compile_time_ms: u64) -> Self {
self.compile_time_ms = compile_time_ms;
self
}
pub(crate) fn size(mut self, size: u64) -> Self {
self.size = size;
self
}
pub(crate) fn keyed(
mut self,
cache_key: &'a str,
key_ms: u64,
key_hash_stats: FileHashStats,
) -> Self {
self.cache_key = cache_key;
self.key_ms = key_ms;
self.key_hash_stats = key_hash_stats;
self
}
pub(crate) fn lookup_ms(mut self, lookup_ms: u64) -> Self {
self.lookup_ms = lookup_ms;
self
}
pub(crate) fn restore_ms(mut self, restore_ms: u64) -> Self {
self.restore_ms = restore_ms;
self
}
pub(crate) fn store_ms(mut self, store_ms: u64) -> Self {
self.store_ms = store_ms;
self
}
pub(crate) fn store_put(mut self, store_put: StorePutResult) -> Self {
self.store_put = store_put;
self
}
fn store_error(mut self, store_error: String) -> Self {
self.store_error = store_error;
self
}
fn lookup_rejection(mut self, lookup_rejection: String) -> Self {
self.lookup_rejection = lookup_rejection;
self
}
pub(crate) fn passthrough_reason(mut self, reason: String) -> Self {
self.passthrough_reason = reason;
self
}
pub(crate) fn skip_reason(mut self, reason: impl Into<String>) -> Self {
self.skip_reason = reason.into();
self
}
pub(crate) fn object_output(mut self, object_output: &str) -> Self {
self.object_output = object_output.to_string();
self
}
pub(crate) fn package(mut self, package: String) -> Self {
self.package = package;
self
}
fn fallback(mut self, fallback: bool, attempt: Option<crate::fallback::Attempt>) -> Self {
self.fallback = fallback;
self.fallback_attempt = attempt;
self
}
fn exit_code(mut self, exit_code: i32) -> Self {
self.exit_code = Some(exit_code);
self
}
fn key_record(mut self, key_record: KeyEventRecord) -> Self {
self.key_record = key_record;
self
}
}
pub(crate) fn log_event(config: &Config, inputs: EventInputs<'_>) {
let _trace = crate::phase_trace::phase("event_report");
let event = build_event_details(config, inputs);
write_event(config, &event);
}
pub(crate) fn store_error_for_event(error: &anyhow::Error) -> String {
const MAX_CHARS: usize = 2048;
let rendered = format!("{error:#}");
let mut chars = rendered.chars();
let mut bounded: String = chars
.by_ref()
.take(MAX_CHARS)
.map(|ch| if ch.is_control() { ' ' } else { ch })
.collect();
if chars.next().is_some() {
bounded.push_str("… [truncated]");
}
bounded
}
pub(crate) fn write_event(config: &Config, event: &BuildEvent) {
let _trace = crate::phase_trace::phase("event_log");
let _ = events::log_event(&config.event_log_path(), event);
let _ = events::rotate_events_if_needed(
&config.event_log_path(),
config.event_log_max_size,
config.event_log_keep_lines,
&config.cache_dir,
);
let _ = events::rotate_transfers_if_needed(
&config.transfer_log_path(),
config.event_log_max_size,
config.event_log_keep_lines,
);
}
#[derive(Clone, Default)]
pub(crate) struct KeyEventRecord {
fields: Option<std::collections::BTreeMap<String, String>>,
env_deps: Option<std::collections::BTreeMap<String, String>>,
externs: Option<std::collections::BTreeMap<String, String>>,
extern_units: Option<std::collections::BTreeMap<String, String>>,
unit_id: Option<String>,
}
impl KeyEventRecord {
fn absorb(&mut self, outputs: &crate::cache_key::KeyOutputs) {
if let Some(fields) = &outputs.fields {
self.fields = Some(fields.clone());
self.env_deps = outputs.env_deps.clone();
}
self.externs = outputs.externs.clone();
self.extern_units = outputs.extern_units.clone();
self.unit_id = outputs.unit_id.clone();
}
}
pub(crate) fn build_event_details(config: &Config, inputs: EventInputs<'_>) -> BuildEvent {
let EventInputs {
root,
crate_name,
result,
elapsed_ms,
compile_time_ms,
size,
cache_key,
key_ms,
key_hash_stats,
lookup_ms,
restore_ms,
store_ms,
store_put,
passthrough_reason,
skip_reason,
object_output,
package,
store_error,
lookup_rejection,
fallback,
exit_code,
fallback_attempt,
key_record: recorded,
rebuilt,
} = inputs;
let session_id = session_id_for_event(
config,
root,
invocation_started_secs(now_epoch_secs(), elapsed_ms),
);
refresh_session_marker(config, root, &session_id);
let key_env_deps = if config.explain_miss {
recorded.env_deps
} else {
None
};
let key_fields = recorded.fields.unwrap_or_default();
let recorded_externs = recorded.externs;
let key_externs_recorded = config.explain_miss && recorded_externs.is_some();
let key_externs = if key_externs_recorded {
recorded_externs.unwrap_or_default()
} else {
Default::default()
};
let recorded_extern_units = recorded.extern_units;
let recorded_unit_id = recorded.unit_id;
let (unit_id, extern_units) = if key_externs_recorded {
(
recorded_unit_id.unwrap_or_default(),
recorded_extern_units.unwrap_or_default(),
)
} else {
(String::new(), Default::default())
};
let key_diff = explain_miss_diff(config, root, crate_name, result, cache_key, &key_fields);
let miss_reason = events::MissReason::classify(&passthrough_reason);
BuildEvent {
ts: Utc::now(),
crate_name: crate_name.to_string(),
root: root.to_string(),
version: crate::VERSION.to_string(),
result,
elapsed_ms,
compile_time_ms,
size,
cache_key: cache_key.to_string(),
rebuilt_package: rebuilt.package,
rebuilt_fingerprint: (!rebuilt.paths.is_empty() && !cache_key.is_empty())
.then(|| cache_key.to_string()),
rebuilt_path: rebuilt.primary,
rebuilt_paths: rebuilt.paths,
schema: 26,
demands: crate::demand::take(),
session_id,
key_ms,
key_hash_hits: key_hash_stats.cache_hits,
key_hash_misses: key_hash_stats.cache_misses,
key_hash_bytes: key_hash_stats.bytes_hashed,
lookup_ms,
restore_ms,
store_ms,
startup_ms: crate::opcounts::startup_ms(),
dep_info_ms: crate::opcounts::dep_info_ms(),
dep_info_runs: crate::opcounts::dep_info_runs(),
prediction_mismatches: u32::try_from(crate::opcounts::prediction_mismatches())
.unwrap_or(u32::MAX),
flight_wait_ms: crate::opcounts::flight_wait_ms(),
permit_wait_ms: crate::opcounts::permit_wait_ms(),
store_output_blobs: store_put.output_blobs,
store_duplicate_blobs: store_put.duplicate_blobs,
store_new_blobs: store_put.new_blobs,
compiler_runs: crate::opcounts::compiler_runs(),
preprocessor_runs: crate::opcounts::preprocessor_runs(),
probe_runs: crate::opcounts::probe_runs(),
reflinked_bytes: crate::opcounts::reflinked_bytes(),
hardlinked_bytes: crate::opcounts::hardlinked_bytes(),
copied_bytes: crate::opcounts::copied_bytes(),
store_reflinked_bytes: crate::opcounts::store_reflinked_bytes(),
store_hardlinked_bytes: crate::opcounts::store_hardlinked_bytes(),
store_copied_bytes: crate::opcounts::store_copied_bytes(),
store_copy_cross_device_bytes: crate::opcounts::store_copy_cross_device_bytes(),
store_copy_permission_bytes: crate::opcounts::store_copy_permission_bytes(),
store_copy_ineligible_bytes: crate::opcounts::store_copy_ineligible_bytes(),
store_copy_other_bytes: crate::opcounts::store_copy_other_bytes(),
restore_copy_cross_device_bytes: crate::opcounts::restore_copy_cross_device_bytes(),
restore_copy_permission_bytes: crate::opcounts::restore_copy_permission_bytes(),
restore_copy_exclusive_bytes: crate::opcounts::restore_copy_exclusive_bytes(),
restore_copy_other_bytes: crate::opcounts::restore_copy_other_bytes(),
passthrough_reason,
skip_reason,
miss_reason,
object_output,
package,
store_error,
store_handed_off: false,
daemon_store_ms: 0,
lookup_rejection,
verify_compare: crate::verify_compare::take_last_report(),
fallback,
fallback_attempt,
exit_code,
key_fields,
key_env_deps,
key_diff,
key_externs,
key_externs_recorded,
unit_id,
extern_units,
}
}
pub(crate) fn recorded_object_output(path: Option<std::path::PathBuf>) -> String {
path.map(|path| path.to_string_lossy().into_owned())
.unwrap_or_default()
}
fn explain_miss_diff(
config: &Config,
root: &str,
crate_name: &str,
result: EventResult,
cache_key: &str,
key_fields: &std::collections::BTreeMap<String, String>,
) -> Vec<String> {
if !config.explain_miss
|| !matches!(result, EventResult::Miss | EventResult::Dup)
|| key_fields.is_empty()
{
return Vec::new();
}
let events = match events::read_events(&config.event_log_path()) {
Ok(events) => events,
Err(_) => return Vec::new(),
};
let Some(last_hit) = events.iter().rev().find(|e| {
e.crate_name == crate_name
&& e.root == root
&& !e.key_fields.is_empty()
&& matches!(
e.result,
EventResult::LocalHit | EventResult::PrefetchHit | EventResult::RemoteHit
)
}) else {
return Vec::new();
};
if last_hit.cache_key == cache_key {
crate::notice::show(&format!(
"[kache] miss: crate {crate_name} (key unchanged since last hit — entry evicted or store cleared?)"
));
return vec!["none:entry-evicted".to_string()];
}
let mut changed: Vec<String> = key_fields
.iter()
.filter(|(group, digest)| last_hit.key_fields.get(*group) != Some(digest))
.map(|(group, _)| group.clone())
.collect();
changed.extend(
last_hit
.key_fields
.keys()
.filter(|g| !key_fields.contains_key(*g))
.cloned(),
);
changed.sort();
changed.dedup();
if changed.is_empty() {
changed.push("salt_or_extra_inputs".to_string());
}
let ago = Utc::now()
.signed_duration_since(last_hit.ts)
.num_minutes()
.max(0);
crate::notice::show(&format!(
"[kache] miss: crate {} (last hit {}m ago; key changed in: {})",
crate_name,
ago,
changed.join(", ")
));
changed
}
fn maybe_trigger_prefetch(config: &Config, args: &RustcArgs) {
maybe_trigger_prefetch_with(config, args, now_epoch_secs(), || {
crate::build_intent::discover(Some(args))
});
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PrefetchTrigger {
NoRemote,
NoSession,
NotDue,
NoMarker,
Busy,
DiscoveryFailed,
Sent,
}
fn maybe_trigger_prefetch_with(
config: &Config,
args: &RustcArgs,
now: u64,
discover: impl FnOnce() -> Option<kache_core::BuildIntent>,
) -> PrefetchTrigger {
if config.remote.is_none() {
return PrefetchTrigger::NoRemote;
}
let root = rustc_event_root(args);
let session_id = session_id_for_event(config, &root, now);
if session_id.is_empty() {
return PrefetchTrigger::NoSession;
}
let marker = prefetch_marker_path(config, &root);
if !prefetch_due(
&parse_prefetch_marker(&std::fs::read_to_string(&marker).unwrap_or_default()),
&session_id,
now,
) {
return PrefetchTrigger::NotDue;
}
let Some(lock_file) = open_marker_for_lock(&marker) else {
return PrefetchTrigger::NoMarker;
};
if lock_file.try_lock().is_err() {
return PrefetchTrigger::Busy; }
let locked = read_locked_marker(&lock_file);
let state = parse_prefetch_marker(&locked);
if !prefetch_due(&state, &session_id, now) {
return PrefetchTrigger::NotDue;
}
let Some(build_intent) = discover() else {
let record = failed_prefetch_marker(&session_id, now, &state);
tracing::debug!("prefetch hint skipped, discovery failed; marker now {record}");
write_locked_marker(&lock_file, &record);
return PrefetchTrigger::DiscoveryFailed;
};
let shard_prefetch_enabled =
build_intent.namespace.is_some() && !build_intent.cargo_lock_deps.is_empty();
tracing::info!(
"build session detected, sending prefetch hint for {} crates (shard context: {})",
build_intent.crate_names.len(),
if shard_prefetch_enabled {
"available"
} else {
"fallback"
}
);
crate::daemon::send_build_started(
config,
crate::build_intent::into_build_started_request(build_intent, session_id.clone()),
);
write_locked_marker(&lock_file, &session_id);
PrefetchTrigger::Sent
}
fn unrecognized_cargo_layout<'a>(
args: &'a RustcArgs,
cargo_crate_name: Option<&std::ffi::OsStr>,
) -> Option<&'a Path> {
let out_dir = args.out_dir.as_deref()?;
let crate_name = args.crate_name.as_deref()?;
(cargo_crate_name == Some(std::ffi::OsStr::new(crate_name))
&& !crate::cargo_layout::is_cargo_unit_dir(out_dir))
.then_some(out_dir)
}
fn maybe_notice_unknown_layout(config: &Config, args: &RustcArgs, root: &str, now: u64) -> bool {
let cargo_crate_name = std::env::var_os("CARGO_CRATE_NAME");
let Some(out_dir) = unrecognized_cargo_layout(args, cargo_crate_name.as_deref()) else {
return false;
};
if cargo_workspace_of(out_dir).is_none() {
return false;
}
tracing::debug!("unrecognized Cargo layout: {}", out_dir.display());
let session_id = session_id_for_event(config, root, now);
if session_id.is_empty() {
return false;
}
let marker = session_marker_path(config, root).with_extension("layout");
if std::fs::read_to_string(&marker).is_ok_and(|seen| seen == session_id) {
return false;
}
let Some(lock_file) = open_marker_for_lock(&marker) else {
return false;
};
if lock_file.try_lock().is_err() || read_locked_marker(&lock_file) == session_id {
return false;
}
write_locked_marker(&lock_file, &session_id);
crate::notice::show(&format!(
"[kache] Cargo is writing to a directory layout kache does not recognize ({}). \
Builds still work, but build-script caching and cross-checkout reuse are reduced. \
Please report your `cargo -V` at https://github.com/kunobi-ninja/kache/issues",
out_dir.display()
));
true
}
fn prefetch_marker_path(config: &Config, root: &str) -> PathBuf {
session_marker_path(config, root).with_extension("prefetch")
}
const PREFETCH_RETRY_MIN_SECS: u64 = 30;
const PREFETCH_RETRY_MAX_SECS: u64 = 600;
#[derive(Debug, PartialEq, Eq)]
enum PrefetchMarker<'a> {
Unset,
Sent(&'a str),
Failed { until: u64, attempts: u32 },
}
fn parse_prefetch_marker(content: &str) -> PrefetchMarker<'_> {
let content = content.trim();
if content.is_empty() {
return PrefetchMarker::Unset;
}
let Some(record) = content.strip_prefix("fail:") else {
return PrefetchMarker::Sent(content);
};
let mut fields = record.rsplitn(3, ':');
let attempts = fields.next().and_then(|field| field.parse().ok());
let until = fields.next().and_then(|field| field.parse().ok());
match (until, attempts) {
(Some(until), Some(attempts)) => PrefetchMarker::Failed { until, attempts },
_ => PrefetchMarker::Unset,
}
}
fn prefetch_due(marker: &PrefetchMarker<'_>, session_id: &str, now: u64) -> bool {
match marker {
PrefetchMarker::Unset => true,
PrefetchMarker::Sent(sent) => *sent != session_id,
PrefetchMarker::Failed { until, .. } => now >= *until,
}
}
fn prefetch_retry_secs(attempts: u32) -> u64 {
let doublings = attempts.saturating_sub(1).min(16);
(PREFETCH_RETRY_MIN_SECS << doublings).min(PREFETCH_RETRY_MAX_SECS)
}
fn failed_prefetch_marker(session_id: &str, now: u64, previous: &PrefetchMarker<'_>) -> String {
let attempts = match previous {
PrefetchMarker::Failed { attempts, .. } => attempts.saturating_add(1),
_ => 1,
};
let until = now.saturating_add(prefetch_retry_secs(attempts));
format!("fail:{session_id}:{until}:{attempts}")
}
fn read_locked_marker(mut file: &std::fs::File) -> String {
use std::io::{Read, Seek, SeekFrom};
let mut content = String::new();
if file.seek(SeekFrom::Start(0)).is_ok() {
let _ = file.read_to_string(&mut content);
}
content
}
fn write_locked_marker(mut file: &std::fs::File, content: &str) {
use std::io::{Seek, SeekFrom, Write};
let _ = file.set_len(0);
let _ = file.seek(SeekFrom::Start(0));
let _ = file.write_all(content.as_bytes());
let _ = file.flush();
}
pub(crate) fn session_marker_path(config: &Config, root: &str) -> std::path::PathBuf {
let hash = blake3::hash(root.as_bytes()).to_hex();
config
.runtime_dir
.join(".build-sessions")
.join(&hash.as_str()[..16])
}
pub(crate) fn session_id_for_event(config: &Config, root: &str, started_secs: u64) -> String {
if root.is_empty() {
return String::new();
}
let marker = session_marker_path(config, root);
if let Some(id) = open_session_id(
&std::fs::read_to_string(&marker).unwrap_or_default(),
started_secs,
) {
return id;
}
if let Some(parent) = marker.parent() {
let _ = std::fs::create_dir_all(parent);
}
let Some(lock_file) = open_marker_for_lock(&marker) else {
return String::new();
};
if lock_file.lock().is_err() {
return String::new();
}
if let Some(id) = open_session_id(&read_locked_marker(&lock_file), started_secs) {
return id;
}
let id = mint_session_id(root);
write_session_marker(&lock_file, &id);
id
}
fn invocation_started_secs(now_secs: u64, elapsed_ms: u64) -> u64 {
now_secs.saturating_sub(elapsed_ms / 1000)
}
fn open_session_id(content: &str, at_secs: u64) -> Option<String> {
let (touched, id) = parse_session_marker(content)?;
(!id.is_empty() && timestamp_is_fresh_at(touched, BUILD_SESSION_SECS, at_secs)).then_some(id)
}
pub(crate) fn refresh_session_marker(config: &Config, root: &str, session_id: &str) {
if root.is_empty() || session_id.is_empty() {
return;
}
let marker = session_marker_path(config, root);
match std::fs::read_to_string(&marker) {
Ok(content) => match parse_session_marker(&content) {
Some((_, id)) if id == session_id => {}
_ => return, },
Err(_) => return,
}
let tmp = marker.with_extension(format!("tmp.{}", std::process::id()));
let record = format!("v1 {} {}", now_epoch_secs(), session_id);
if std::fs::write(&tmp, record).is_err() {
return;
}
if std::fs::rename(&tmp, &marker).is_err() {
let _ = std::fs::remove_file(&tmp);
}
}
fn write_session_marker(mut file: &std::fs::File, session_id: &str) {
use std::io::{Seek, SeekFrom, Write};
let record = format!("v1 {} {}", now_epoch_secs(), session_id);
let _ = file.set_len(0);
let _ = file.seek(SeekFrom::Start(0));
let _ = file.write_all(record.as_bytes());
let _ = file.flush();
}
fn mint_session_id(root: &str) -> String {
static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let seq = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let mut hasher = blake3::Hasher::new();
hasher.update(root.as_bytes());
hasher.update(&std::process::id().to_le_bytes());
hasher.update(&nanos.to_le_bytes());
hasher.update(&seq.to_le_bytes());
hasher.finalize().to_hex().as_str()[..16].to_string()
}
pub(crate) const SESSION_MARKER_RETENTION: std::time::Duration =
std::time::Duration::from_secs(86_400);
pub(crate) fn prune_session_markers(
config: &Config,
retention: std::time::Duration,
now: std::time::SystemTime,
) -> usize {
let Ok(entries) = std::fs::read_dir(config.runtime_dir.join(".build-sessions")) else {
return 0;
};
entries
.filter_map(Result::ok)
.filter(|entry| {
entry.file_type().is_ok_and(|kind| kind.is_file())
&& entry
.metadata()
.and_then(|meta| meta.modified())
.is_ok_and(|touched| {
now.duration_since(touched)
.is_ok_and(|age| age >= retention)
})
})
.filter(|entry| std::fs::remove_file(entry.path()).is_ok())
.count()
}
pub(crate) const BUILD_SESSION_SECS: u64 = 300;
fn clean_incremental_dir(config: &Config, args: &RustcArgs) {
if incremental_cleanup_enabled(config)
&& let Some(incr_dir) = &args.incremental
&& incr_dir.is_dir()
&& let Err(e) = std::fs::remove_dir_all(incr_dir)
{
tracing::debug!(
"failed to clean incremental dir {}: {}",
incr_dir.display(),
e
);
}
}
#[cfg(test)]
mod tests;