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,
Hardlink,
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)> {
stage_verified_cached_output_with(directory, index, source, node, |source, destination| {
reflink_copy::reflink(source, destination)
})
}
pub(crate) fn stage_verified_cached_output_with(
directory: &Path,
index: usize,
source: &Path,
node: &CacheFileNode,
clone: impl FnOnce(&Path, &Path) -> std::io::Result<()>,
) -> Result<(tempfile::TempPath, Materialization)> {
let temporary = directory.join(format!("output-{index}"));
let materialization = materialize_cached_output(source, &temporary, node, clone)
.wrap_err_with(|| format!("failed to materialize cached output {}", node.name))?;
let temporary = tempfile::TempPath::try_from_path(temporary)?;
if materialization != Materialization::Hardlink {
make_owner_writable(&temporary)?;
}
if std::fs::metadata(&temporary)?.len() != node.digest.size {
bail!(
"materialized cached output has the wrong size: {}",
node.name
);
}
set_modified_now(&temporary)?;
if materialization != Materialization::Hardlink {
apply_file_mode(&temporary, node.mode, node.executable)?;
}
Ok((temporary, materialization))
}
fn materialize_cached_output(
source: &Path,
destination: &Path,
node: &CacheFileNode,
clone: impl FnOnce(&Path, &Path) -> std::io::Result<()>,
) -> Result<Materialization> {
if clone(source, destination).is_ok() {
return Ok(Materialization::Reflink);
}
let _ = std::fs::remove_file(destination);
if session::restore_hardlink_requested() && hard_link_cached_output(source, destination, node) {
return Ok(Materialization::Hardlink);
}
let written = std::fs::copy(source, destination)?;
if written != node.digest.size {
bail!(
"materialized cached output has the wrong size: {}",
node.name
);
}
Ok(Materialization::Copy)
}
#[cfg(unix)]
fn hard_link_cached_output(source: &Path, destination: &Path, node: &CacheFileNode) -> bool {
use std::os::unix::fs::MetadataExt as _;
use std::os::unix::fs::PermissionsExt as _;
let wanted = linkable_mode(node);
if !readable_by_owner(wanted) {
return false;
}
let Ok(file) = std::fs::File::open(source) else {
return false;
};
if !take_exclusive_lock(&file) {
return false;
}
let Ok(metadata) = file.metadata() else {
return false;
};
let current = metadata.permissions().mode() & 0o7777;
if current & 0o111 != wanted & 0o111 {
return false;
}
if current != wanted {
if wanted & !current != 0 {
return false;
}
if file
.set_permissions(std::fs::Permissions::from_mode(wanted))
.is_err()
{
return false;
}
}
if std::fs::hard_link(source, destination).is_err() {
return false;
}
let linked = std::fs::metadata(destination);
match linked {
Ok(linked)
if linked.ino() == metadata.ino() && linked.permissions().mode() & 0o7777 == wanted =>
{
true
}
_ => {
let _ = std::fs::remove_file(destination);
false
}
}
}
#[cfg(windows)]
fn hard_link_cached_output(_source: &Path, _destination: &Path, _node: &CacheFileNode) -> bool {
false
}
#[cfg(unix)]
fn linkable_mode(node: &CacheFileNode) -> u32 {
(node.mode & 0o444) | if node.executable { 0o111 } else { 0 }
}
#[cfg(unix)]
fn take_exclusive_lock(file: &std::fs::File) -> bool {
use std::os::unix::io::AsRawFd as _;
unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) == 0 }
}
#[cfg(unix)]
fn readable_by_owner(mode: u32) -> bool {
mode & 0o400 != 0
}
pub(crate) fn set_modified_now(path: &Path) -> Result<()> {
set_modified(path, std::time::SystemTime::now())
}
pub(crate) fn set_modified(path: &Path, modified: std::time::SystemTime) -> Result<()> {
let times = std::fs::FileTimes::new().set_modified(modified);
#[cfg(unix)]
let file = std::fs::OpenOptions::new().read(true).open(path)?;
#[cfg(windows)]
let file = {
use std::os::windows::fs::OpenOptionsExt as _;
std::fs::OpenOptions::new()
.access_mode(windows_sys::Win32::Storage::FileSystem::FILE_WRITE_ATTRIBUTES)
.open(path)?
};
file.set_times(times)?;
Ok(())
}
pub(crate) fn clear_linked_outputs<'a>(paths: impl IntoIterator<Item = &'a Path>) {
for path in paths {
let Ok(metadata) = std::fs::symlink_metadata(path) else {
continue;
};
if !metadata.is_file() || owner_can_write(&metadata) {
continue;
}
let _ = std::fs::remove_file(path);
}
}
#[cfg(unix)]
fn owner_can_write(metadata: &std::fs::Metadata) -> bool {
use std::os::unix::fs::PermissionsExt as _;
metadata.permissions().mode() & 0o200 != 0
}
#[cfg(windows)]
fn owner_can_write(metadata: &std::fs::Metadata) -> bool {
!metadata.permissions().readonly()
}
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.extend(session::unit_outcome_requests("hit", Some(crate_name)));
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 verification_warning(
adapter: &str,
unit: &str,
action: &CacheDigest,
divergence: &str,
) -> String {
format!(
"shadow verification diverged from cached output: {adapter} action {} ({}): {divergence}",
action.hash,
unit.escape_default()
)
}
pub(crate) fn stream_divergence(name: &str, cached: &[u8], compiled: &[u8]) -> Option<String> {
if cached == compiled {
return None;
}
let offset = cached
.iter()
.zip(compiled)
.position(|(a, b)| a != b)
.unwrap_or(cached.len().min(compiled.len()));
let line = cached[..offset].iter().filter(|&&b| b == b'\n').count() + 1;
let excerpt = |bytes: &[u8]| {
let start = offset.saturating_sub(24);
let end = bytes.len().min(offset.saturating_add(48));
format!(
"{}{}{}",
if start > 0 { "..." } else { "" },
bytes[start..end].escape_ascii(),
if end < bytes.len() { "..." } else { "" }
)
};
Some(format!(
"{name} differs at byte {offset} (line {line}): cached=\"{}\", compiled=\"{}\"",
excerpt(cached),
excerpt(compiled)
))
}
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 {
crate::phase_timing::finish();
unsafe { windows_sys::Win32::System::Threading::ExitProcess(status.code().unwrap_or(1) as u32) }
}
#[cfg(test)]
mod verification_tests {
use super::*;
#[test]
fn stream_comparison_handles_eof_and_reports_the_first_differing_line() {
assert_eq!(stream_divergence("stderr", b"same", b"same"), None);
let difference = stream_divergence("stderr", b"same\nold", b"same\nnew").unwrap();
assert!(difference.contains("byte 5 (line 2)"), "{difference}");
assert!(difference.contains(r"same\nold"), "{difference}");
for (cached, compiled) in [
(b"".as_slice(), b"extra".as_slice()),
(b"extra".as_slice(), b"".as_slice()),
] {
let difference = stream_divergence("stdout", cached, compiled).unwrap();
assert!(difference.contains("byte 0 (line 1)"), "{difference}");
}
}
#[test]
fn excerpts_are_bounded_and_escape_binary_and_terminal_bytes() {
let cached = vec![b'a'; 10_000];
let mut compiled = cached.clone();
compiled.extend_from_slice(b"\x1b\0\xff\n");
let difference = stream_divergence("stderr", &cached, &compiled).unwrap();
assert!(difference.contains("byte 10000"), "{difference}");
assert!(difference.contains(r"\x1b\x00\xff\n"), "{difference}");
assert!(!difference.contains(['\n', '\0', '\x1b']));
assert!(difference.len() < 512, "{difference}");
}
#[test]
fn verification_unit_names_escape_terminal_controls() {
let warning = verification_warning(
"cc",
"cc:bad\x1b[2J\n\0.c",
&CacheDigest::blake3(b"action"),
"standard error differs",
);
assert!(!warning.chars().any(char::is_control), "{warning:?}");
assert!(warning.contains(r"cc:bad\u{1b}[2J\n\u{0}.c"), "{warning}");
assert!(warning.ends_with("standard error differs"));
}
#[test]
fn different_actions_of_one_unit_keep_distinct_warning_identities() {
let first = verification_warning(
"rustc",
"example",
&CacheDigest::blake3(b"lib"),
"standard error differs",
);
let second = verification_warning(
"rustc",
"example",
&CacheDigest::blake3(b"test"),
"standard error differs",
);
assert_ne!(
first, second,
"session deduplication must preserve both units"
);
assert!(first.contains("rustc action"));
assert!(first.contains("(example)"));
}
}
#[cfg(all(test, unix))]
mod materialization_tests {
use super::*;
use std::os::unix::fs::MetadataExt as _;
use std::os::unix::fs::PermissionsExt as _;
fn no_clone(_: &Path, _: &Path) -> std::io::Result<()> {
Err(std::io::ErrorKind::Unsupported.into())
}
fn blob(directory: &Path, name: &str, contents: &[u8], mode: u32) -> PathBuf {
let path = directory.join(name);
std::fs::write(&path, contents).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode)).unwrap();
path
}
fn node(contents: &[u8], executable: bool) -> CacheFileNode {
CacheFileNode {
digest: CacheDigest::blake3(contents),
executable,
mode: 0o644,
name: "libdemo.rlib".into(),
}
}
#[test]
fn a_filesystem_without_clones_links_the_object_instead_of_copying_it() {
let root = tempfile::tempdir().unwrap();
let source = blob(root.path(), "blob", b"rlib", 0o644);
let before = std::fs::metadata(&source).unwrap();
let (staged, materialization) = stage_verified_cached_output_with(
root.path(),
0,
&source,
&node(b"rlib", false),
no_clone,
)
.unwrap();
assert_eq!(materialization, Materialization::Hardlink);
let after = std::fs::metadata(&staged).unwrap();
assert_eq!(after.ino(), before.ino(), "the link shares the object");
assert_eq!(after.permissions().mode() & 0o777, 0o444);
assert_eq!(
std::fs::metadata(&source).unwrap().permissions().mode() & 0o777,
0o444,
"the object itself is what was made unwritable"
);
assert!(after.modified().unwrap() > before.modified().unwrap());
}
#[test]
fn an_executable_output_keeps_its_executable_bit_through_a_link() {
let root = tempfile::tempdir().unwrap();
let source = blob(root.path(), "blob", b"binary", 0o755);
let (staged, materialization) = stage_verified_cached_output_with(
root.path(),
0,
&source,
&node(b"binary", true),
no_clone,
)
.unwrap();
assert_eq!(materialization, Materialization::Hardlink);
assert_eq!(
std::fs::metadata(&staged).unwrap().permissions().mode() & 0o777,
0o555
);
}
#[test]
fn an_object_that_would_have_to_gain_permissions_is_copied() {
let root = tempfile::tempdir().unwrap();
let source = blob(root.path(), "blob", b"binary", 0o644);
let before = std::fs::metadata(&source).unwrap();
let (staged, materialization) = stage_verified_cached_output_with(
root.path(),
0,
&source,
&node(b"binary", true),
no_clone,
)
.unwrap();
assert_eq!(materialization, Materialization::Copy);
assert_ne!(std::fs::metadata(&staged).unwrap().ino(), before.ino());
assert_eq!(
std::fs::metadata(&source).unwrap().permissions().mode() & 0o777,
0o644,
"an object nothing may widen keeps the mode it had"
);
}
#[test]
fn a_non_executable_output_never_takes_the_executable_bit_off_a_shared_object() {
let root = tempfile::tempdir().unwrap();
let source = blob(root.path(), "blob", b"program", 0o555);
let theirs = root.path().join("their-binary");
std::fs::hard_link(&source, &theirs).unwrap();
let (staged, materialization) = stage_verified_cached_output_with(
root.path(),
0,
&source,
&node(b"program", false),
no_clone,
)
.unwrap();
assert_eq!(materialization, Materialization::Copy);
assert_eq!(
std::fs::metadata(&staged).unwrap().permissions().mode() & 0o777,
0o644
);
assert_eq!(
std::fs::metadata(&theirs).unwrap().permissions().mode() & 0o111,
0o111,
"their binary still runs"
);
}
const HOLD_RELABEL_LOCK_ENV: &str = "MBX_TEST_HOLD_RELABEL_LOCK";
#[test]
fn an_object_another_restore_is_relabelling_is_copied_rather_than_waited_on() {
use std::io::BufRead as _;
if let Some(source) = std::env::var_os(HOLD_RELABEL_LOCK_ENV) {
let held = std::fs::File::open(source).unwrap();
assert!(take_exclusive_lock(&held));
eprintln!("locked");
let _ = std::io::stdin().read_line(&mut String::new());
return;
}
let root = tempfile::tempdir().unwrap();
let source = blob(root.path(), "blob", b"contended", 0o644);
let mut held = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"an_object_another_restore_is_relabelling_is_copied_rather_than_waited_on",
"--nocapture",
])
.env(HOLD_RELABEL_LOCK_ENV, &source)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let stderr = std::io::BufReader::new(held.stderr.take().unwrap());
assert!(
stderr
.lines()
.any(|line| line.is_ok_and(|line| line == "locked")),
"the other restore never took the lock"
);
let (staged, materialization) = stage_verified_cached_output_with(
root.path(),
0,
&source,
&node(b"contended", false),
no_clone,
)
.unwrap();
assert_eq!(materialization, Materialization::Copy);
assert_eq!(
std::fs::metadata(&source).unwrap().permissions().mode() & 0o777,
0o644,
"the object is left to whoever holds the lock"
);
assert_eq!(std::fs::read(&staged).unwrap(), b"contended");
held.kill().unwrap();
held.wait().unwrap();
let (_, materialization) = stage_verified_cached_output_with(
root.path(),
1,
&source,
&node(b"contended", false),
no_clone,
)
.unwrap();
assert_eq!(materialization, Materialization::Hardlink);
}
#[test]
fn an_output_whose_recorded_mode_cannot_be_read_is_copied() {
let root = tempfile::tempdir().unwrap();
let source = blob(root.path(), "blob", b"write-only", 0o644);
let mut unreadable = node(b"write-only", false);
unreadable.mode = 0o200;
let (_staged, materialization) =
stage_verified_cached_output_with(root.path(), 0, &source, &unreadable, no_clone)
.unwrap();
assert_eq!(materialization, Materialization::Copy);
assert_eq!(
std::fs::metadata(&source).unwrap().permissions().mode() & 0o777,
0o644,
"the store's object stays readable for every later lookup"
);
}
#[test]
fn a_clone_is_preferred_over_a_link_and_stays_a_private_writable_copy() {
let root = tempfile::tempdir().unwrap();
let source = blob(root.path(), "blob", b"rlib", 0o644);
let (staged, materialization) = stage_verified_cached_output_with(
root.path(),
0,
&source,
&node(b"rlib", false),
|source, destination| std::fs::copy(source, destination).map(|_| ()),
)
.unwrap();
assert_eq!(materialization, Materialization::Reflink);
assert_eq!(
std::fs::metadata(&staged).unwrap().permissions().mode() & 0o777,
0o644
);
}
#[test]
fn an_owner_private_output_keeps_its_readership_through_a_link() {
let root = tempfile::tempdir().unwrap();
let source = blob(root.path(), "blob", b"private", 0o600);
let mut private = node(b"private", false);
private.mode = 0o600;
let (staged, materialization) =
stage_verified_cached_output_with(root.path(), 0, &source, &private, no_clone).unwrap();
assert_eq!(materialization, Materialization::Hardlink);
assert_eq!(
std::fs::metadata(&staged).unwrap().permissions().mode() & 0o777,
0o400,
"a link must not publish bytes the compiler kept to its owner"
);
}
#[test]
fn relabelling_a_shared_object_may_only_take_permissions_away() {
let root = tempfile::tempdir().unwrap();
let source = blob(root.path(), "blob", b"shared", 0o444);
let theirs = root.path().join("their-output");
std::fs::hard_link(&source, &theirs).unwrap();
let mut private = node(b"shared", false);
private.mode = 0o600;
let (staged, materialization) =
stage_verified_cached_output_with(root.path(), 0, &source, &private, no_clone).unwrap();
assert_eq!(materialization, Materialization::Hardlink);
assert_eq!(
std::fs::metadata(&staged).unwrap().permissions().mode() & 0o777,
0o400
);
assert_eq!(
std::fs::metadata(&theirs).unwrap().permissions().mode() & 0o777,
0o400
);
}
#[test]
fn an_output_already_in_place_can_be_marked_restored_while_read_only() {
let root = tempfile::tempdir().unwrap();
let source = blob(root.path(), "blob", b"rlib", 0o644);
let (staged, materialization) = stage_verified_cached_output_with(
root.path(),
0,
&source,
&node(b"rlib", false),
no_clone,
)
.unwrap();
assert_eq!(materialization, Materialization::Hardlink);
let before = std::fs::metadata(&staged).unwrap().modified().unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
set_modified_now(&staged).expect("a linked output can be marked restored");
assert!(std::fs::metadata(&staged).unwrap().modified().unwrap() > before);
}
#[test]
fn linked_outputs_are_unlinked_before_a_compiler_writes_where_they_were() {
let root = tempfile::tempdir().unwrap();
let linked = blob(root.path(), "restored.rlib", b"restored", 0o444);
let written = blob(root.path(), "compiled.rlib", b"compiled", 0o644);
let missing = root.path().join("never-restored.rlib");
clear_linked_outputs([linked.as_path(), written.as_path(), missing.as_path()]);
assert!(!linked.exists(), "a read-only restore is removed");
assert!(written.exists(), "an ordinary output is left alone");
}
}