use super::session;
use eyre::{Context, Result, bail};
use mise_cache_core::{
ActionPrediction, AgentRequest, AgentResponse, CacheDigest, CacheDirectory, CacheFileNode,
RemoteActionResult, RestoreStats, RustcMetadata, canonical_json,
};
use mise_cache_rustc::{
ActionContext, CompilerIdentity, DiscoveredInputs, PathMapping, RustcAction, RustcDepInfo,
RustcInputPrediction, RustcInvocation, RustcOutputs,
};
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::collections::{BTreeMap, BTreeSet};
use std::ffi::{OsStr, OsString};
use std::io::{Read as _, Write as _};
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode, ExitStatus, Output};
use std::time::{Instant, SystemTime};
struct CachedCompilation {
stdout: Vec<u8>,
stderr: Vec<u8>,
outputs: Vec<CachedOutput>,
restore: RestoreStats,
}
struct CachedOutput {
path: PathBuf,
digest: CacheDigest,
mode: u32,
}
struct StagedOutputs {
directory: tempfile::TempDir,
files: Vec<(tempfile::TempPath, PathBuf)>,
}
pub(super) fn compile(rustc: &OsStr, arguments: &[OsString]) -> Result<ExitCode> {
let invocation = RustcInvocation::parse(arguments)?;
let working_dir = std::env::current_dir()?;
let outputs = invocation.outputs(&working_dir)?;
let verify = std::env::var_os(session::VERIFY_ENV).is_some();
let mut verification = None;
let mut action_lookup_attempted = false;
if outputs.dep_info.is_file()
&& let Ok((action, discovered)) =
action_from_current_dep_info(rustc, &invocation, &outputs.dep_info, &working_dir)
{
action_lookup_attempted = true;
match restore_result(&action, &outputs, &discovered, !verify) {
Ok(Some(cached)) => {
record_prediction(rustc, &invocation, &action, &discovered, &working_dir);
if verify {
verification = Some(cached);
} else {
let _ = replay_bytes(&cached.stdout, &cached.stderr);
return Ok(ExitCode::SUCCESS);
}
}
Ok(None) => {}
Err(error) => {
eprintln!("mise rustc cache warning: result was not restored: {error:#}");
}
}
}
if !action_lookup_attempted {
match restore_predicted_result(rustc, &invocation, &outputs, &working_dir, !verify) {
Ok(Some(cached)) => {
if verify {
verification = Some(cached);
} else {
let _ = replay_bytes(&cached.stdout, &cached.stderr);
return Ok(ExitCode::SUCCESS);
}
}
Ok(None) => {}
Err(error) => {
eprintln!("mise rustc cache warning: prediction was not restored: {error:#}");
}
}
}
let compilation_started = SystemTime::now();
let output = Command::new(rustc)
.args(arguments)
.current_dir(&working_dir)
.output()
.wrap_err("failed to execute rustc")?;
let _ = replay_output(&output);
if let Some(cached) = verification {
let matched = cached_matches(&cached, &output);
record_verification(matched, cached.restore);
if !matched {
eprintln!("mise rustc cache warning: shadow verification diverged from cached output");
}
return Ok(exit_code(output.status));
}
if output.status.success() {
let publication: Result<()> = (|| {
let (action, discovered) =
action_from_dep_info(rustc, &invocation, &outputs.dep_info, &working_dir)?;
discovered.verify_not_modified_since(compilation_started)?;
discovered.verify()?;
let mut cacheable_outputs = outputs.files.clone();
cacheable_outputs.push(outputs.dep_info.clone());
publish_result(&action.digest, &action.bytes, &cacheable_outputs, &output)?;
record_prediction(rustc, &invocation, &action, &discovered, &working_dir);
Ok(())
})();
if let Err(error) = publication {
eprintln!("mise rustc cache warning: result was not stored: {error:#}");
}
}
Ok(exit_code(output.status))
}
fn restore_predicted_result(
rustc: &OsStr,
invocation: &RustcInvocation,
outputs: &RustcOutputs,
working_dir: &Path,
restore_outputs: bool,
) -> Result<Option<CachedCompilation>> {
let task = std::env::var(session::TASK_ENV)
.wrap_err_with(|| format!("{} is not set", session::TASK_ENV))?;
let mut context = base_action_context(rustc, working_dir)?;
let invocation_digest = invocation.invocation_digest(&context)?;
let responses = session::request_agent(&[AgentRequest::FindActionPrediction {
task,
invocation: invocation_digest.clone(),
}])?;
let Some(response) = responses.into_iter().next() else {
bail!("cache agent did not return an action prediction response");
};
let prediction = match response {
AgentResponse::ActionPrediction {
prediction: Some(prediction),
} => prediction,
AgentResponse::ActionPrediction { prediction: None } => return Ok(None),
AgentResponse::Error { message } => bail!(message),
_ => bail!("cache agent returned an unexpected action prediction response"),
};
if prediction.adapter != "rustc" || prediction.invocation != invocation_digest {
bail!("cache agent returned an incompatible rustc action prediction");
}
let input_prediction: RustcInputPrediction = serde_json::from_str(&prediction.payload)?;
if String::from_utf8(canonical_json(&input_prediction)?)? != prediction.payload {
bail!("cache agent returned a non-canonical rustc action prediction");
}
let discovered = input_prediction.discover(working_dir, &context.path_mappings)?;
discovered.clone().apply_to(&mut context)?;
let action = invocation.action(context)?;
let restored = restore_result(&action, outputs, &discovered, restore_outputs)?;
if restored.is_some() {
record_prediction_value(invocation_digest, action.digest.clone(), prediction.payload);
}
Ok(restored)
}
fn action_from_dep_info(
rustc: &OsStr,
invocation: &RustcInvocation,
dep_info: &Path,
working_dir: &Path,
) -> Result<(RustcAction, DiscoveredInputs)> {
let dep_info = RustcDepInfo::read(dep_info)?;
action_from_parsed_dep_info(rustc, invocation, &dep_info, working_dir)
}
fn action_from_current_dep_info(
rustc: &OsStr,
invocation: &RustcInvocation,
dep_info: &Path,
working_dir: &Path,
) -> Result<(RustcAction, DiscoveredInputs)> {
let dep_info = RustcDepInfo::read(dep_info)?;
verify_environment(&dep_info.environment)?;
action_from_parsed_dep_info(rustc, invocation, &dep_info, working_dir)
}
fn action_from_parsed_dep_info(
rustc: &OsStr,
invocation: &RustcInvocation,
dep_info: &RustcDepInfo,
working_dir: &Path,
) -> Result<(RustcAction, DiscoveredInputs)> {
let discovered = invocation.discover_inputs(dep_info, working_dir)?;
let mut context = base_action_context(rustc, working_dir)?;
discovered.clone().apply_to(&mut context)?;
let action = invocation.action(context)?;
Ok((action, discovered))
}
fn base_action_context(rustc: &OsStr, working_dir: &Path) -> Result<ActionContext> {
Ok(ActionContext {
compiler: compiler_identity(rustc)?,
working_dir: working_dir.to_path_buf(),
path_mappings: path_mappings(working_dir),
environment: BTreeMap::new(),
inputs: Vec::new(),
})
}
fn record_prediction(
rustc: &OsStr,
invocation: &RustcInvocation,
action: &RustcAction,
discovered: &DiscoveredInputs,
working_dir: &Path,
) {
let result = (|| {
let context = base_action_context(rustc, working_dir)?;
let invocation_digest = invocation.invocation_digest(&context)?;
let prediction = invocation.prediction(&context, discovered)?;
let payload = String::from_utf8(canonical_json(&prediction)?)?;
record_prediction_value(invocation_digest, action.digest.clone(), payload);
Result::<()>::Ok(())
})();
if let Err(error) = result {
eprintln!("mise rustc cache warning: action prediction was not recorded: {error:#}");
}
}
fn record_prediction_value(invocation: CacheDigest, action: CacheDigest, payload: String) {
let result = (|| {
let task = std::env::var(session::TASK_ENV)
.wrap_err_with(|| format!("{} is not set", session::TASK_ENV))?;
let responses = session::request_agent(&[AgentRequest::RecordActionPrediction {
task,
prediction: ActionPrediction {
invocation,
action,
adapter: "rustc".into(),
payload,
},
}])?;
match responses.into_iter().next() {
Some(AgentResponse::ActionPredictionRecorded) => Ok(()),
Some(AgentResponse::Error { message }) => bail!(message),
_ => bail!("cache agent returned an unexpected prediction response"),
}
})();
if let Err(error) = result {
eprintln!("mise rustc cache warning: action prediction was not recorded: {error:#}");
}
}
fn verify_environment(environment: &BTreeMap<String, Option<String>>) -> Result<()> {
for (name, expected) in environment {
let actual = std::env::var_os(name)
.map(|value| {
value.into_string().map_err(|_| {
eyre::eyre!("compiler environment input is not valid UTF-8: {name}")
})
})
.transpose()?;
if &actual != expected {
bail!("compiler environment input changed: {name}");
}
}
Ok(())
}
fn restore_result(
action: &RustcAction,
outputs: &RustcOutputs,
discovered: &DiscoveredInputs,
restore_outputs: bool,
) -> Result<Option<CachedCompilation>> {
let responses = session::request_agent(&[AgentRequest::FindActionResult {
action: action.digest.clone(),
}])?;
let Some(response) = responses.into_iter().next() else {
bail!("cache agent did not return an action lookup response");
};
let result = match response {
AgentResponse::ActionResult { result } => match result {
Some(result) => result,
None => return Ok(None),
},
AgentResponse::Error { message } => bail!(message),
_ => bail!("cache agent returned an unexpected action lookup response"),
};
if result.version != 1 || result.action != action.digest {
bail!("cached rustc action result has an invalid identity");
}
let metadata_digest = result
.metadata
.ok_or_else(|| eyre::eyre!("cached rustc action result has no metadata"))?;
let output_root_digest = result
.output_root
.ok_or_else(|| eyre::eyre!("cached rustc action result has no output root"))?;
let roots = find_blobs(&[
action.digest.clone(),
metadata_digest.clone(),
output_root_digest.clone(),
])?;
let cached_action = read_verified_blob(&roots[0], &action.digest, "action descriptor")?;
if cached_action != action.bytes {
bail!("cached rustc action descriptor does not match the invocation");
}
let metadata: RustcMetadata =
read_canonical_blob(&roots[1], &metadata_digest, "rustc metadata")?;
if metadata.version != 1 || metadata.kind != "rustc" {
bail!("cached rustc metadata is unsupported");
}
let directory: CacheDirectory =
read_canonical_blob(&roots[2], &output_root_digest, "output directory")?;
let files = validated_outputs(directory, outputs)?;
let cached_outputs = files
.iter()
.map(|(node, destination)| CachedOutput {
path: destination.clone(),
digest: node.digest.clone(),
mode: node.mode,
})
.collect();
let restored_output_files = files.len().try_into().unwrap_or(u64::MAX);
let restored_output_bytes = files.iter().fold(0_u64, |total, (node, _)| {
total.saturating_add(node.digest.size)
});
let mut digests = vec![metadata.stdout.clone(), metadata.stderr.clone()];
digests.extend(files.iter().map(|(node, _)| node.digest.clone()));
let blobs = find_blobs(&digests)?;
let stdout = read_verified_blob(&blobs[0], &metadata.stdout, "stdout")?;
let stderr = read_verified_blob(&blobs[1], &metadata.stderr, "stderr")?;
let materialization_started = Instant::now();
std::fs::create_dir_all(&outputs.directory)?;
let staging = tempfile::tempdir_in(&outputs.directory)?;
let mut staged = Vec::with_capacity(files.len());
for (index, ((node, destination), source)) in files.into_iter().zip(&blobs[2..]).enumerate() {
let temporary = stage_cached_output(staging.path(), index, source, &node)?;
staged.push((temporary, destination));
}
let staged = StagedOutputs {
directory: staging,
files: staged,
};
discovered.verify()?;
verify_environment(&discovered.environment)?;
finalize_restored_outputs(staged, restore_outputs)?;
let restore = RestoreStats {
duration_ns: materialization_started
.elapsed()
.as_nanos()
.try_into()
.unwrap_or(u64::MAX),
output_files: restored_output_files,
output_bytes: restored_output_bytes,
};
if restore_outputs {
record_action_hit(&action.digest, restore);
}
Ok(Some(CachedCompilation {
stdout,
stderr,
outputs: cached_outputs,
restore,
}))
}
fn finalize_restored_outputs(staged: StagedOutputs, restore_outputs: bool) -> Result<()> {
if restore_outputs {
persist_outputs(staged)?;
}
Ok(())
}
fn stage_cached_output(
directory: &Path,
index: usize,
source: &Path,
node: &CacheFileNode,
) -> Result<tempfile::TempPath> {
let temporary = directory.join(format!("output-{index}"));
reflink_copy::reflink_or_copy(source, &temporary)
.wrap_err_with(|| format!("failed to materialize cached rustc output {}", node.name))?;
let temporary = tempfile::TempPath::try_from_path(temporary)?;
make_owner_writable(&temporary)?;
std::fs::OpenOptions::new()
.write(true)
.open(&temporary)?
.sync_all()?;
if !node.digest.matches_file(&temporary)? {
bail!(
"cached rustc output failed digest verification: {}",
node.name
);
}
apply_file_mode(&temporary, node.mode)?;
Ok(temporary)
}
fn cached_matches(cached: &CachedCompilation, output: &Output) -> bool {
output.status.success()
&& cached.stdout == output.stdout
&& cached.stderr == output.stderr
&& cached.outputs.iter().all(|expected| {
std::fs::metadata(&expected.path).is_ok_and(|metadata| {
file_mode(&metadata) == expected.mode
&& expected
.digest
.matches_file(&expected.path)
.unwrap_or(false)
})
})
}
fn record_verification(matched: bool, restore: RestoreStats) {
let responses =
session::request_agent(&[AgentRequest::RecordActionVerification { matched, restore }]);
match responses.map(|responses| responses.into_iter().next()) {
Ok(Some(AgentResponse::ActionVerificationRecorded)) => {}
Ok(Some(AgentResponse::Error { message })) => {
eprintln!("mise rustc cache warning: verification was not recorded: {message}");
}
Ok(_) => eprintln!("mise rustc cache warning: verification was not recorded"),
Err(error) => {
eprintln!("mise rustc cache warning: verification was not recorded: {error:#}");
}
}
}
fn persist_outputs(staged: StagedOutputs) -> Result<()> {
let StagedOutputs {
directory: _directory,
files,
} = staged;
let destinations = files
.iter()
.map(|(_, destination)| destination.clone())
.collect::<Vec<_>>();
for (temporary, destination) in files {
let persisted = temporary
.persist(&destination)
.map_err(|error| error.error)
.wrap_err_with(|| format!("failed to atomically restore {}", destination.display()));
if let Err(error) = persisted {
for destination in &destinations {
match std::fs::remove_file(destination) {
Ok(()) => {}
Err(remove_error) if remove_error.kind() == std::io::ErrorKind::NotFound => {}
Err(remove_error) => eprintln!(
"mise rustc cache warning: failed to roll back {}: {remove_error}",
destination.display()
),
}
}
return Err(error);
}
}
Ok(())
}
fn record_action_hit(action: &CacheDigest, restore: RestoreStats) {
let responses = session::request_agent(&[AgentRequest::RecordActionHit {
action: action.clone(),
restore,
}]);
match responses.map(|responses| responses.into_iter().next()) {
Ok(Some(AgentResponse::ActionHitRecorded)) => {}
Ok(Some(AgentResponse::Error { message })) => {
eprintln!("mise rustc cache warning: hit was not recorded: {message}");
}
Ok(_) => eprintln!("mise rustc cache warning: hit was not recorded"),
Err(error) => {
eprintln!("mise rustc cache warning: hit was not recorded: {error:#}");
}
}
}
fn find_blobs(digests: &[CacheDigest]) -> Result<Vec<PathBuf>> {
let requests = digests
.iter()
.cloned()
.map(|digest| AgentRequest::FindBlob { digest })
.collect::<Vec<_>>();
let responses = session::request_agent(&requests)?;
if responses.len() != digests.len() {
bail!("cache agent returned an incomplete blob lookup response");
}
responses
.into_iter()
.zip(digests)
.map(|(response, digest)| match response {
AgentResponse::Blob { path: Some(path) } => Ok(path),
AgentResponse::Blob { path: None } => {
bail!("cached rustc action is missing blob {}", digest.hash)
}
AgentResponse::Error { message } => bail!(message),
_ => bail!("cache agent returned an unexpected blob lookup response"),
})
.collect()
}
fn read_canonical_blob<T>(path: &Path, digest: &CacheDigest, description: &str) -> Result<T>
where
T: DeserializeOwned + Serialize,
{
let bytes = read_verified_blob(path, digest, description)?;
let value = serde_json::from_slice(&bytes)
.wrap_err_with(|| format!("cached {description} is not valid JSON"))?;
if canonical_json(&value)? != bytes {
bail!("cached {description} is not canonical JSON");
}
Ok(value)
}
fn read_verified_blob(path: &Path, digest: &CacheDigest, description: &str) -> Result<Vec<u8>> {
let mut bytes = Vec::new();
std::fs::File::open(path)?.read_to_end(&mut bytes)?;
if !digest.matches_bytes(&bytes)? {
bail!("cached {description} failed digest verification");
}
Ok(bytes)
}
fn validated_outputs(
directory: CacheDirectory,
outputs: &RustcOutputs,
) -> Result<Vec<(CacheFileNode, PathBuf)>> {
if directory.version != 1 || !directory.directories.is_empty() || !directory.symlinks.is_empty()
{
bail!("cached rustc output directory has unsupported entries");
}
let mut expected = outputs
.files
.iter()
.chain(std::iter::once(&outputs.dep_info))
.map(|path| {
let name = path
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| eyre::eyre!("expected rustc output name is not UTF-8"))?;
if path.parent() != Some(outputs.directory.as_path()) {
bail!("expected rustc output escapes its output directory");
}
Ok((name.to_string(), path.clone()))
})
.collect::<Result<BTreeMap<_, _>>>()?;
if directory.files.len() != expected.len() {
bail!("cached rustc output set does not match the invocation");
}
let mut files = Vec::with_capacity(directory.files.len());
for node in directory.files {
validate_file_mode(&node)?;
let destination = expected
.remove(&node.name)
.ok_or_else(|| eyre::eyre!("cached rustc output is unexpected: {}", node.name))?;
files.push((node, destination));
}
if !expected.is_empty() {
bail!("cached rustc output set is incomplete");
}
Ok(files)
}
fn compiler_identity(rustc: &OsStr) -> Result<CompilerIdentity> {
let executable = resolve_executable(rustc)?;
let environment = ["RUSTUP_HOME", "RUSTUP_TOOLCHAIN"]
.into_iter()
.map(|name| (name.into(), std::env::var(name).ok()))
.collect::<BTreeMap<_, _>>();
let responses = session::request_agent(&[AgentRequest::FindExecutableIdentity {
executable: executable.clone(),
environment: environment.clone(),
}])?;
let Some(AgentResponse::ExecutableIdentity { stdout }) = responses.into_iter().next() else {
bail!("cache agent did not return the rustc identity");
};
let stdout = if let Some(stdout) = stdout {
stdout
} else {
let mut command = Command::new(&executable);
command.arg("-vV");
for (name, value) in &environment {
if let Some(value) = value {
command.env(name, value);
} else {
command.env_remove(name);
}
}
let output = command
.output()
.wrap_err("failed to query the rustc identity")?;
if !output.status.success() {
bail!(
"rustc identity command failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let responses = session::request_agent(&[AgentRequest::StoreExecutableIdentity {
executable,
environment,
stdout: output.stdout,
}])?;
let Some(AgentResponse::ExecutableIdentity {
stdout: Some(stdout),
}) = responses.into_iter().next()
else {
bail!("cache agent did not store the rustc identity");
};
stdout
};
let verbose = std::str::from_utf8(&stdout).wrap_err("rustc identity is not UTF-8")?;
let release = identity_field(verbose, "release")?;
let host = identity_field(verbose, "host")?;
let rustc_version = verbose
.lines()
.filter(|line| {
line.starts_with("rustc ")
|| line.starts_with("commit-hash:")
|| line.starts_with("commit-date:")
|| line.starts_with("LLVM version:")
})
.collect::<Vec<_>>()
.join("; ");
if rustc_version.is_empty() {
bail!("rustc identity is missing its version");
}
let toolchain = std::env::var("RUSTUP_TOOLCHAIN")
.ok()
.filter(|value| !value.is_empty())
.unwrap_or_else(|| release.to_string());
Ok(CompilerIdentity {
toolchain,
rustc_version,
host: host.to_string(),
})
}
fn identity_field<'a>(verbose: &'a str, field: &str) -> Result<&'a str> {
verbose
.lines()
.find_map(|line| line.strip_prefix(&format!("{field}: ")))
.filter(|value| !value.is_empty())
.ok_or_else(|| eyre::eyre!("rustc identity is missing {field}"))
}
fn path_mappings(working_dir: &Path) -> Vec<PathMapping> {
let mut mappings = Vec::new();
let mut roots = BTreeSet::new();
add_mapping(
&mut mappings,
&mut roots,
working_dir.to_path_buf(),
"workspace",
);
for (name, placeholder) in [
("CARGO_HOME", "cargo_home"),
("RUSTUP_HOME", "rustup_home"),
("HOME", "home"),
("USERPROFILE", "home"),
] {
if let Some(root) = std::env::var_os(name).map(PathBuf::from)
&& root.is_absolute()
{
add_mapping(&mut mappings, &mut roots, root, placeholder);
}
}
mappings
}
fn add_mapping(
mappings: &mut Vec<PathMapping>,
roots: &mut BTreeSet<PathBuf>,
root: PathBuf,
placeholder: &str,
) {
if roots.insert(root.clone())
&& !mappings
.iter()
.any(|mapping| mapping.placeholder == placeholder)
{
mappings.push(PathMapping::new(root, placeholder));
}
}
fn replay_output(output: &Output) -> Result<()> {
replay_bytes(&output.stdout, &output.stderr)
}
fn replay_bytes(stdout_bytes: &[u8], stderr_bytes: &[u8]) -> Result<()> {
let mut stdout = std::io::stdout().lock();
stdout.write_all(stdout_bytes)?;
stdout.flush()?;
let mut stderr = std::io::stderr().lock();
stderr.write_all(stderr_bytes)?;
stderr.flush()?;
Ok(())
}
fn staging_directory() -> Result<tempfile::TempDir> {
let root = std::env::var_os(session::STAGING_ENV)
.map(PathBuf::from)
.ok_or_else(|| eyre::eyre!("{} is not set", session::STAGING_ENV))?;
Ok(tempfile::tempdir_in(root)?)
}
fn resolve_executable(executable: &OsStr) -> Result<PathBuf> {
let executable = PathBuf::from(executable);
if executable.is_absolute() {
return Ok(executable);
}
which::which(&executable).wrap_err_with(|| {
format!(
"failed to resolve compiler executable {}",
executable.display()
)
})
}
fn publish_result(
action: &CacheDigest,
action_bytes: &[u8],
outputs: &[PathBuf],
output: &Output,
) -> Result<()> {
if outputs.is_empty() {
bail!("rustc produced no cacheable outputs");
}
let staging = staging_directory()?;
let mut blobs = vec![staged_bytes(staging.path(), "action.json", action_bytes)?];
let stdout = staged_bytes(staging.path(), "stdout", &output.stdout)?;
let stderr = staged_bytes(staging.path(), "stderr", &output.stderr)?;
blobs.extend([stdout.clone(), stderr.clone()]);
let mut files = Vec::with_capacity(outputs.len());
for path in outputs {
let metadata = std::fs::metadata(path)
.wrap_err_with(|| format!("failed to inspect rustc output {}", path.display()))?;
if !metadata.is_file() {
bail!("rustc output is not a regular file: {}", path.display());
}
let digest = CacheDigest::blake3_file(path)?;
blobs.push((digest.clone(), path.clone()));
files.push(CacheFileNode {
digest,
executable: false,
mode: file_mode(&metadata),
name: path
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| eyre::eyre!("rustc output name is not UTF-8"))?
.to_string(),
});
}
files.sort_by(|left, right| left.name.cmp(&right.name));
let metadata = canonical_json(&RustcMetadata {
version: 1,
kind: "rustc".into(),
stdout: stdout.0,
stderr: stderr.0,
})?;
let metadata = staged_bytes(staging.path(), "metadata.json", &metadata)?;
blobs.push(metadata.clone());
let directory = canonical_json(&CacheDirectory {
directories: Vec::new(),
files,
symlinks: Vec::new(),
version: 1,
})?;
let directory = staged_bytes(staging.path(), "directory.json", &directory)?;
blobs.push(directory.clone());
let mut requests = Vec::new();
let mut published = BTreeSet::new();
for (digest, source) in blobs {
if published.insert(digest.clone()) {
requests.push(AgentRequest::StoreBlob { digest, source });
}
}
requests.push(AgentRequest::StoreActionResult {
result: RemoteActionResult {
action: action.clone(),
metadata: Some(metadata.0),
output_root: Some(directory.0),
version: 1,
},
});
for response in session::request_agent(&requests)? {
match response {
AgentResponse::Stored { .. } | AgentResponse::ActionStored { .. } => {}
AgentResponse::Error { message } => bail!(message),
_ => bail!("cache agent returned an unexpected publish response"),
}
}
Ok(())
}
fn staged_bytes(directory: &Path, name: &str, bytes: &[u8]) -> Result<(CacheDigest, PathBuf)> {
let path = directory.join(name);
std::fs::write(&path, bytes)?;
Ok((CacheDigest::blake3(bytes), path))
}
#[cfg(unix)]
fn file_mode(metadata: &std::fs::Metadata) -> u32 {
use std::os::unix::fs::PermissionsExt as _;
metadata.permissions().mode() & 0o644
}
#[cfg(windows)]
fn file_mode(_metadata: &std::fs::Metadata) -> u32 {
0
}
#[cfg(unix)]
fn validate_file_mode(node: &CacheFileNode) -> Result<()> {
if node.executable
|| node.mode & !0o777 != 0
|| node.mode & 0o111 != 0
|| node.mode & 0o022 != 0
{
bail!("cached rustc output has an unsafe file mode: {}", node.name);
}
Ok(())
}
#[cfg(windows)]
fn validate_file_mode(node: &CacheFileNode) -> Result<()> {
if node.executable || node.mode != 0 {
bail!("cached rustc output has an unsafe file mode: {}", node.name);
}
Ok(())
}
#[cfg(unix)]
fn apply_file_mode(temporary: &Path, mode: u32) -> Result<()> {
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(temporary, std::fs::Permissions::from_mode(mode))?;
Ok(())
}
#[cfg(windows)]
fn apply_file_mode(_temporary: &Path, _mode: u32) -> Result<()> {
Ok(())
}
#[cfg(unix)]
fn make_owner_writable(path: &Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt as _;
let mut permissions = std::fs::metadata(path)?.permissions();
permissions.set_mode(permissions.mode() | 0o200);
std::fs::set_permissions(path, permissions)?;
Ok(())
}
#[cfg(windows)]
fn make_owner_writable(path: &Path) -> Result<()> {
let mut permissions = std::fs::metadata(path)?.permissions();
permissions.set_readonly(false);
std::fs::set_permissions(path, permissions)?;
Ok(())
}
#[cfg(unix)]
fn exit_code(status: ExitStatus) -> ExitCode {
use std::os::unix::process::ExitStatusExt as _;
ExitCode::from(
status
.code()
.unwrap_or_else(|| 128 + status.signal().unwrap_or(1)) as u8,
)
}
#[cfg(windows)]
fn exit_code(status: ExitStatus) -> ExitCode {
unsafe { windows_sys::Win32::System::Threading::ExitProcess(status.code().unwrap_or(1) as u32) }
}
#[cfg(test)]
mod tests {
use super::*;
fn staged_outputs(root: &Path, entries: Vec<(&[u8], PathBuf)>) -> StagedOutputs {
let directory = tempfile::tempdir_in(root).unwrap();
let files = entries
.into_iter()
.enumerate()
.map(|(index, (contents, destination))| {
let path = directory.path().join(format!("output-{index}"));
std::fs::write(&path, contents).unwrap();
(
tempfile::TempPath::try_from_path(path).unwrap(),
destination,
)
})
.collect();
StagedOutputs { directory, files }
}
fn test_outputs(root: &Path) -> RustcOutputs {
let directory = root.join("out");
RustcOutputs {
files: vec![directory.join("libdemo.rlib")],
dep_info: directory.join("demo.d"),
directory,
}
}
fn test_file(name: &str) -> CacheFileNode {
CacheFileNode {
digest: CacheDigest::blake3(b"artifact"),
executable: false,
mode: if cfg!(unix) { 0o644 } else { 0 },
name: name.into(),
}
}
fn test_directory(files: Vec<CacheFileNode>) -> CacheDirectory {
CacheDirectory {
directories: Vec::new(),
files,
symlinks: Vec::new(),
version: 1,
}
}
fn test_output_directory(file: CacheFileNode) -> CacheDirectory {
test_directory(vec![file, test_file("demo.d")])
}
#[test]
fn parses_verbose_rustc_identity() {
let verbose = "rustc 1.97.0 (abc 2026-08-01)\n\
binary: rustc\n\
commit-hash: abc\n\
commit-date: 2026-08-01\n\
host: x86_64-unknown-linux-gnu\n\
release: 1.97.0\n\
LLVM version: 22.0.0\n";
assert_eq!(identity_field(verbose, "release").unwrap(), "1.97.0");
assert_eq!(
identity_field(verbose, "host").unwrap(),
"x86_64-unknown-linux-gnu"
);
}
#[test]
fn mappings_do_not_duplicate_home_placeholders() {
let directory = tempfile::tempdir().unwrap();
let mappings = path_mappings(directory.path());
let placeholders = mappings
.iter()
.map(|mapping| &mapping.placeholder)
.collect::<BTreeSet<_>>();
assert_eq!(placeholders.len(), mappings.len());
}
#[test]
fn validates_exact_rustc_output_set() {
let root = tempfile::tempdir().unwrap();
let outputs = test_outputs(root.path());
let files =
validated_outputs(test_output_directory(test_file("libdemo.rlib")), &outputs).unwrap();
assert_eq!(files.len(), 2);
assert!(files.iter().any(|(_, path)| path == &outputs.files[0]));
assert!(files.iter().any(|(_, path)| path == &outputs.dep_info));
}
#[test]
fn rejects_cached_output_path_traversal() {
let root = tempfile::tempdir().unwrap();
let outputs = test_outputs(root.path());
assert!(
validated_outputs(
test_output_directory(test_file("../libdemo.rlib")),
&outputs,
)
.is_err()
);
}
#[test]
fn rejects_executable_rustc_outputs() {
let root = tempfile::tempdir().unwrap();
let outputs = test_outputs(root.path());
let mut file = test_file("libdemo.rlib");
file.executable = true;
assert!(validated_outputs(test_output_directory(file), &outputs).is_err());
}
#[test]
fn rejects_group_or_world_writable_rustc_outputs() {
let root = tempfile::tempdir().unwrap();
let outputs = test_outputs(root.path());
let mut file = test_file("libdemo.rlib");
file.mode = 0o666;
assert!(validated_outputs(test_output_directory(file), &outputs).is_err());
}
#[cfg(unix)]
#[test]
fn publication_masks_unsafe_rustc_output_permissions() {
use std::os::unix::fs::PermissionsExt as _;
let file = tempfile::NamedTempFile::new().unwrap();
file.as_file()
.set_permissions(std::fs::Permissions::from_mode(0o666))
.unwrap();
assert_eq!(file_mode(&file.as_file().metadata().unwrap()), 0o644);
}
#[test]
fn rolls_back_outputs_after_a_partial_persist() {
let root = tempfile::tempdir().unwrap();
let first_destination = root.path().join("first.rlib");
let blocked_destination = root.path().join("blocked.rmeta");
std::fs::create_dir(&blocked_destination).unwrap();
let staged = staged_outputs(
root.path(),
vec![
(b"first", first_destination.clone()),
(b"second", blocked_destination.clone()),
],
);
assert!(persist_outputs(staged).is_err());
assert!(!first_destination.exists());
assert!(blocked_destination.is_dir());
}
#[test]
fn qualification_does_not_publish_cached_outputs() {
let root = tempfile::tempdir().unwrap();
let destination = root.path().join("cached.rlib");
let staged = staged_outputs(root.path(), vec![(b"cached", destination.clone())]);
finalize_restored_outputs(staged, false).unwrap();
assert!(!destination.exists());
}
#[test]
fn materialized_outputs_are_independent_from_the_cas() {
let root = tempfile::tempdir().unwrap();
let source = root.path().join("cas-blob");
std::fs::write(&source, b"artifact").unwrap();
let staging = tempfile::tempdir_in(root.path()).unwrap();
let node = test_file("artifact.rlib");
let output = stage_cached_output(staging.path(), 0, &source, &node).unwrap();
std::fs::write(&output, b"modified").unwrap();
assert_eq!(std::fs::read(source).unwrap(), b"artifact");
assert_eq!(std::fs::read(output).unwrap(), b"modified");
}
#[test]
fn rejects_same_size_corrupt_cached_outputs() {
let root = tempfile::tempdir().unwrap();
let source = root.path().join("cas-blob");
std::fs::write(&source, b"corrupt!").unwrap();
let staging = tempfile::tempdir_in(root.path()).unwrap();
let node = test_file("artifact.rlib");
assert!(stage_cached_output(staging.path(), 0, &source, &node).is_err());
}
#[test]
fn materializes_read_only_cached_outputs() {
let root = tempfile::tempdir().unwrap();
let source = root.path().join("cas-blob");
std::fs::write(&source, b"artifact").unwrap();
let mut permissions = std::fs::metadata(&source).unwrap().permissions();
permissions.set_readonly(true);
std::fs::set_permissions(&source, permissions).unwrap();
let staging = tempfile::tempdir_in(root.path()).unwrap();
let node = test_file("artifact.rlib");
let output = stage_cached_output(staging.path(), 0, &source, &node).unwrap();
assert_eq!(std::fs::read(output).unwrap(), b"artifact");
assert!(std::fs::metadata(&source).unwrap().permissions().readonly());
make_owner_writable(&source).unwrap();
}
#[test]
fn rejects_same_size_corrupt_cached_metadata() {
let root = tempfile::tempdir().unwrap();
let source = root.path().join("cas-blob");
std::fs::write(&source, b"corrupt!").unwrap();
let digest = CacheDigest::blake3(b"artifact");
assert!(read_verified_blob(&source, &digest, "test blob").is_err());
}
#[test]
#[ignore = "local materialization benchmark"]
fn benchmark_cached_output_materialization() {
let size_mib = std::env::var("MISE_CACHE_BENCH_MIB")
.ok()
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(128);
let iterations = std::env::var("MISE_CACHE_BENCH_ITERATIONS")
.ok()
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(4);
let root = tempfile::tempdir().unwrap();
let source = root.path().join("cas-blob");
let mut source_file = std::fs::File::create(&source).unwrap();
let chunk = vec![0x5a; 1024 * 1024];
for _ in 0..size_mib {
source_file.write_all(&chunk).unwrap();
}
source_file.sync_all().unwrap();
drop(source_file);
let digest = CacheDigest::blake3_file(&source).unwrap();
let node = CacheFileNode {
digest: digest.clone(),
executable: false,
mode: if cfg!(unix) { 0o644 } else { 0 },
name: "artifact.rlib".into(),
};
let started = std::time::Instant::now();
for _ in 0..iterations {
let mut temporary = tempfile::NamedTempFile::new_in(root.path()).unwrap();
let mut input = std::fs::File::open(&source).unwrap();
std::io::copy(&mut input, temporary.as_file_mut()).unwrap();
temporary.flush().unwrap();
temporary.as_file().sync_all().unwrap();
assert!(digest.matches_file(temporary.path()).unwrap());
apply_file_mode(temporary.path(), node.mode).unwrap();
}
let copied = started.elapsed();
let staging = tempfile::tempdir_in(root.path()).unwrap();
let started = std::time::Instant::now();
for _ in 0..iterations {
stage_cached_output(staging.path(), 0, &source, &node).unwrap();
}
let materialized = started.elapsed();
println!(
"materialized {iterations} x {size_mib} MiB: copied={copied:.2?}, reflink_or_copy={materialized:.2?}, speedup={:.2}x",
copied.as_secs_f64() / materialized.as_secs_f64()
);
}
}