use crate::session::{self, STAGING_ENV};
use eyre::{Result, WrapErr as _, bail};
use mbx_cache_core::{
ActionDiagnostic, AgentRequest, AgentResponse, CacheDigest, CacheFileNode, RestoreStats,
canonical_json,
};
use mbx_cache_rustc::PathMapping;
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::ffi::OsStr;
use std::io::{Read as _, Write as _};
use std::path::{Path, PathBuf};
use std::process::{ExitCode, ExitStatus};
pub(crate) struct CachedCompilation {
pub(crate) action: CacheDigest,
pub(crate) stdout: Vec<u8>,
pub(crate) stderr: Vec<u8>,
pub(crate) outputs: Vec<CachedOutput>,
pub(crate) restore: RestoreStats,
}
pub(crate) struct CachedOutput {
pub(crate) path: PathBuf,
pub(crate) digest: CacheDigest,
pub(crate) executable: bool,
pub(crate) mode: u32,
}
pub(crate) struct StagedOutputs {
pub(crate) directory: tempfile::TempDir,
pub(crate) files: Vec<(tempfile::TempPath, PathBuf)>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Materialization {
Reflink,
Copy,
}
pub(crate) fn find_blobs(digests: &[CacheDigest]) -> Result<Vec<PathBuf>> {
let responses = session::request_agent(&[AgentRequest::FindBlobs {
digests: digests.to_vec(),
}])?;
let Some(response) = responses.into_iter().next() else {
bail!("cache agent did not return a blob lookup response");
};
match response {
AgentResponse::Blobs { paths } if paths.len() == digests.len() => paths
.into_iter()
.zip(digests)
.map(|(path, digest)| match path {
Some(path) => Ok(path),
None => bail!("cached action is missing blob {}", digest.hash),
})
.collect(),
AgentResponse::Blobs { .. } => {
bail!("cache agent returned an incomplete blob lookup response")
}
AgentResponse::Blob { path: Some(path) } if digests.len() == 1 => Ok(vec![path]),
AgentResponse::Blob { path: None } if digests.len() == 1 => {
let digest = &digests[0];
bail!("cached action is missing blob {}", digest.hash)
}
AgentResponse::Error { message } => bail!(message),
_ => bail!("cache agent returned an unexpected blob lookup response"),
}
}
pub(crate) 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)
}
pub(crate) 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)
}
pub(crate) fn stage_verified_cached_output(
directory: &Path,
index: usize,
source: &Path,
node: &CacheFileNode,
) -> Result<(tempfile::TempPath, Materialization)> {
let temporary = directory.join(format!("output-{index}"));
let copied_bytes = reflink_copy::reflink_or_copy(source, &temporary)
.wrap_err_with(|| format!("failed to materialize cached output {}", node.name))?;
let materialization = match copied_bytes {
None => Materialization::Reflink,
Some(written) if written == node.digest.size => Materialization::Copy,
Some(_) => bail!(
"materialized cached output has the wrong size: {}",
node.name
),
};
let temporary = tempfile::TempPath::try_from_path(temporary)?;
make_owner_writable(&temporary)?;
if std::fs::metadata(&temporary)?.len() != node.digest.size {
bail!(
"materialized cached output has the wrong size: {}",
node.name
);
}
apply_file_mode(&temporary, node.mode, node.executable)?;
Ok((temporary, materialization))
}
pub(crate) 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) => session::report_shim_warning(&format!(
"failed to roll back {}: {remove_error}",
destination.display()
)),
}
}
return Err(error);
}
}
Ok(())
}
pub(crate) 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(())
}
pub(crate) fn staging_directory() -> Result<tempfile::TempDir> {
let root = match std::env::var_os(STAGING_ENV).filter(|root| !root.is_empty()) {
Some(root) => PathBuf::from(root),
None => crate::config::Config::load()?
.store_dir()
.join("standalone-staging"),
};
std::fs::create_dir_all(&root)?;
Ok(tempfile::tempdir_in(root)?)
}
pub(crate) 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()
)
})
}
pub(crate) fn record_action_hit(action: &CacheDigest, restore: RestoreStats, crate_name: &str) {
record_action_hit_with_diagnostic(action, restore, crate_name, None);
}
pub(crate) fn record_action_hit_with_diagnostic(
action: &CacheDigest,
restore: RestoreStats,
crate_name: &str,
diagnostic: Option<ActionDiagnostic>,
) {
let mut requests = Vec::new();
if let Some(diagnostic) = diagnostic
&& let Some(request) =
session::action_diagnostic_request("hit", Some(crate_name), diagnostic)
{
requests.push(request);
}
requests.push(AgentRequest::RecordActionHit {
action: action.clone(),
restore,
crate_name: Some(crate_name.to_string()),
});
let responses = session::request_agent(&requests);
match responses.map(|mut responses| responses.pop()) {
Ok(Some(AgentResponse::ActionHitRecorded)) => {}
Ok(Some(AgentResponse::Error { message })) => {
session::report_shim_warning(&format!("hit was not recorded: {message}"));
}
Ok(_) => session::report_shim_warning("hit was not recorded"),
Err(error) => {
session::report_shim_warning(&format!("hit was not recorded: {error:#}"));
}
}
}
pub(crate) 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 })) => {
session::report_shim_warning(&format!("verification was not recorded: {message}"));
}
Ok(_) => session::report_shim_warning("verification was not recorded"),
Err(error) => {
session::report_shim_warning(&format!("verification was not recorded: {error:#}"));
}
}
}
#[cfg(unix)]
pub(crate) fn file_mode(metadata: &std::fs::Metadata) -> u32 {
use std::os::unix::fs::PermissionsExt as _;
metadata.permissions().mode() & 0o644
}
#[cfg(windows)]
pub(crate) fn file_mode(_metadata: &std::fs::Metadata) -> u32 {
0
}
#[cfg(unix)]
pub(crate) fn validate_file_mode(node: &CacheFileNode, executable: bool) -> Result<()> {
if node.executable != executable
|| node.mode & !0o777 != 0
|| node.mode & 0o111 != 0
|| node.mode & 0o022 != 0
{
bail!("cached output has an unsafe file mode: {}", node.name);
}
Ok(())
}
#[cfg(windows)]
pub(crate) fn validate_file_mode(node: &CacheFileNode, executable: bool) -> Result<()> {
if node.executable != executable || node.mode != 0 {
bail!("cached output has an unsafe file mode: {}", node.name);
}
Ok(())
}
#[cfg(unix)]
pub(crate) fn apply_file_mode(temporary: &Path, mode: u32, executable: bool) -> Result<()> {
use std::os::unix::fs::PermissionsExt as _;
let executable_mode = if executable { 0o111 } else { 0 };
std::fs::set_permissions(
temporary,
std::fs::Permissions::from_mode(mode | executable_mode),
)?;
Ok(())
}
#[cfg(windows)]
pub(crate) fn apply_file_mode(_temporary: &Path, _mode: u32, _executable: bool) -> Result<()> {
Ok(())
}
#[cfg(unix)]
pub(crate) fn executable_mode_matches(metadata: &std::fs::Metadata, executable: bool) -> bool {
use std::os::unix::fs::PermissionsExt as _;
(metadata.permissions().mode() & 0o111 != 0) == executable
}
#[cfg(windows)]
pub(crate) fn executable_mode_matches(_metadata: &std::fs::Metadata, _executable: bool) -> bool {
true
}
#[cfg(unix)]
pub(crate) 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)]
pub(crate) 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(())
}
pub(crate) fn normalize_output_text(bytes: &[u8], mappings: &[PathMapping]) -> Vec<u8> {
let mut normalized = bytes.to_vec();
for (root, placeholder) in root_spellings(mappings) {
normalized = replace_bytes(&normalized, root.as_bytes(), placeholder.as_bytes());
}
normalized
}
pub(crate) fn denormalize_output_text(bytes: &[u8], mappings: &[PathMapping]) -> Vec<u8> {
let mut text = bytes.to_vec();
for (root, placeholder) in root_spellings(mappings) {
text = replace_bytes(&text, placeholder.as_bytes(), root.as_bytes());
}
text
}
fn root_spellings(mappings: &[PathMapping]) -> Vec<(String, String)> {
let mut spellings = Vec::new();
for mapping in PathMapping::ordered(mappings) {
let Some(root) = mapping.root.to_str() else {
continue;
};
let root = root.trim_end_matches(['/', '\\']);
if root.is_empty() {
continue;
}
for (spelling, suffix) in [
(root.to_string(), ""),
(root.replace('\\', "\\\\"), ":escaped"),
(root.replace('\\', "/"), ":slash"),
] {
if spellings.iter().any(|(existing, _)| existing == &spelling) {
continue;
}
spellings.push((spelling, format!("${{{}{suffix}}}", mapping.placeholder)));
}
}
spellings
}
fn ends_at_boundary(haystack: &[u8], end: usize) -> bool {
match haystack.get(end) {
None => true,
Some(byte) => !(byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')),
}
}
fn replace_bytes(haystack: &[u8], needle: &[u8], replacement: &[u8]) -> Vec<u8> {
if needle.is_empty() || haystack.len() < needle.len() {
return haystack.to_vec();
}
let mut out = Vec::with_capacity(haystack.len());
let mut index = 0;
while index <= haystack.len() - needle.len() {
if &haystack[index..index + needle.len()] == needle
&& ends_at_boundary(haystack, index + needle.len())
{
out.extend_from_slice(replacement);
index += needle.len();
} else {
out.push(haystack[index]);
index += 1;
}
}
out.extend_from_slice(&haystack[index..]);
out
}
#[cfg(unix)]
pub(crate) 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)]
pub(crate) fn exit_code(status: ExitStatus) -> ExitCode {
unsafe { windows_sys::Win32::System::Threading::ExitProcess(status.code().unwrap_or(1) as u32) }
}