use super::*;
pub(super) fn artifact_persist_tmp_path(cache_path: &Path) -> PathBuf {
let counter = ARTIFACT_PERSIST_TMP_COUNTER.fetch_add(1, Ordering::Relaxed);
let name = cache_path
.file_name()
.map(|name| name.to_string_lossy())
.unwrap_or_else(|| "artifact".into());
cache_path.with_file_name(format!(".{name}.tmp-{}-{counter}", std::process::id()))
}
pub(super) fn persist_artifact_output(cache_path: &Path, payload: &[u8]) -> std::io::Result<()> {
if let Some(parent) = cache_path.parent() {
std::fs::create_dir_all(parent)?;
}
let tmp_path = artifact_persist_tmp_path(cache_path);
let result = (|| {
std::fs::write(&tmp_path, payload)?;
replace_artifact_cache_file(&tmp_path, cache_path)
})();
if result.is_err() {
let _ = std::fs::remove_file(&tmp_path);
}
result
}
pub(super) const PACK_MAGIC: &[u8; 4] = b"ZCPK";
pub(super) fn pack_mode_enabled() -> bool {
std::env::var("ZCCACHE_PACK_ARTIFACTS")
.ok()
.is_some_and(|v| !v.is_empty() && v != "0")
}
pub(super) fn pack_path_for(artifact_dir: &Path, key_hex: &str) -> PathBuf {
artifact_dir.join(format!("{key_hex}.pack"))
}
pub(super) fn build_pack(payloads: &[Arc<Vec<u8>>]) -> Vec<u8> {
let n = payloads.len();
let header_size = 4 + 4 + n * 16;
let body_size: usize = payloads.iter().map(|p| p.len()).sum();
let mut buf = Vec::with_capacity(header_size + body_size);
buf.extend_from_slice(PACK_MAGIC);
buf.extend_from_slice(&(n as u32).to_le_bytes());
let mut offset = header_size as u64;
for p in payloads {
buf.extend_from_slice(&offset.to_le_bytes());
buf.extend_from_slice(&(p.len() as u64).to_le_bytes());
offset += p.len() as u64;
}
for p in payloads {
buf.extend_from_slice(p);
}
buf
}
pub(super) fn parse_pack_header(data: &[u8]) -> std::io::Result<Vec<(u64, u64)>> {
if data.len() < 8 || &data[..4] != PACK_MAGIC {
return Err(std::io::Error::other("not a zccache pack file"));
}
let n = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize;
let needed = 8 + n * 16;
if data.len() < needed {
return Err(std::io::Error::other("pack header truncated"));
}
let mut entries = Vec::with_capacity(n);
for i in 0..n {
let base = 8 + i * 16;
let offset = u64::from_le_bytes(data[base..base + 8].try_into().unwrap());
let size = u64::from_le_bytes(data[base + 8..base + 16].try_into().unwrap());
entries.push((offset, size));
}
Ok(entries)
}
pub(super) fn try_load_packed_payload(
artifact_dir: &Path,
key_hex: &str,
idx: usize,
) -> Option<Vec<u8>> {
let pack_path = pack_path_for(artifact_dir, key_hex);
let data = std::fs::read(&pack_path).ok()?;
let entries = parse_pack_header(&data).ok()?;
let &(offset, size) = entries.get(idx)?;
let start = offset as usize;
let end = start.checked_add(size as usize)?;
if end > data.len() {
return None;
}
Some(data[start..end].to_vec())
}
pub(super) fn persist_artifact_payloads(
artifact_dir: &Path,
key_hex: &str,
payloads: &[Arc<Vec<u8>>],
) -> std::io::Result<()> {
if pack_mode_enabled() {
let pack = build_pack(payloads);
return persist_artifact_output(&pack_path_for(artifact_dir, key_hex), &pack);
}
if payloads.len() < PAR_WRITE_THRESHOLD {
for (i, payload) in payloads.iter().enumerate() {
let cache_path = artifact_dir.join(format!("{key_hex}_{i}"));
persist_artifact_output(&cache_path, payload)?;
}
return Ok(());
}
use rayon::prelude::*;
payloads
.par_iter()
.enumerate()
.map(|(i, payload)| {
let cache_path = artifact_dir.join(format!("{key_hex}_{i}"));
persist_artifact_output(&cache_path, payload)
})
.reduce(|| Ok(()), |a, b| a.and(b))
}
pub(super) fn persist_artifact_paths(
artifact_dir: &Path,
key_hex: &str,
sources: &[NormalizedPath],
) -> std::io::Result<()> {
if pack_mode_enabled() {
let bytes: Vec<Arc<Vec<u8>>> = sources
.iter()
.map(|p| std::fs::read(p.as_path()).map(Arc::new))
.collect::<std::io::Result<_>>()?;
let pack = build_pack(&bytes);
return persist_artifact_output(&pack_path_for(artifact_dir, key_hex), &pack);
}
if sources.len() < PAR_WRITE_THRESHOLD {
for (i, source) in sources.iter().enumerate() {
let cache_path = artifact_dir.join(format!("{key_hex}_{i}"));
persist_artifact_file(&cache_path, source.as_path())?;
}
return Ok(());
}
use rayon::prelude::*;
sources
.par_iter()
.enumerate()
.map(|(i, source)| {
let cache_path = artifact_dir.join(format!("{key_hex}_{i}"));
persist_artifact_file(&cache_path, source.as_path()).map(|_| ())
})
.reduce(|| Ok(()), |a, b| a.and(b))
}
#[derive(Clone, Copy, Debug, Default)]
pub(super) struct PersistArtifactFileStats {
pub(super) hardlink_count: u64,
pub(super) copy_count: u64,
pub(super) copy_bytes: u64,
}
pub(super) fn persist_artifact_file(
cache_path: &Path,
source_path: &Path,
) -> std::io::Result<PersistArtifactFileStats> {
if let Some(parent) = cache_path.parent() {
std::fs::create_dir_all(parent)?;
}
let tmp_path = artifact_persist_tmp_path(cache_path);
let result = (|| match std::fs::hard_link(source_path, &tmp_path) {
Ok(()) => {
replace_artifact_cache_file(&tmp_path, cache_path)?;
Ok(PersistArtifactFileStats {
hardlink_count: 1,
..PersistArtifactFileStats::default()
})
}
Err(_) => {
let copy_bytes = std::fs::copy(source_path, &tmp_path)?;
replace_artifact_cache_file(&tmp_path, cache_path)?;
Ok(PersistArtifactFileStats {
copy_count: 1,
copy_bytes,
..PersistArtifactFileStats::default()
})
}
})();
if result.is_err() {
let _ = std::fs::remove_file(&tmp_path);
}
result
}
#[cfg(not(windows))]
pub(super) fn replace_artifact_cache_file(
tmp_path: &Path,
cache_path: &Path,
) -> std::io::Result<()> {
std::fs::rename(tmp_path, cache_path)
}
#[cfg(windows)]
pub(super) fn replace_artifact_cache_file(
tmp_path: &Path,
cache_path: &Path,
) -> std::io::Result<()> {
match std::fs::rename(tmp_path, cache_path) {
Ok(()) => Ok(()),
Err(_) if cache_path.exists() => {
std::fs::remove_file(cache_path)?;
std::fs::rename(tmp_path, cache_path)
}
Err(err) => Err(err),
}
}
pub(super) fn write_cached_output(
out_path: &Path,
cache_file: &Path,
data: &[u8],
) -> std::io::Result<()> {
if std::fs::hard_link(cache_file, out_path).is_ok() {
touch_mtime(out_path);
return Ok(());
}
if same_file(out_path, cache_file) {
touch_mtime(out_path);
return Ok(());
}
let _ = std::fs::remove_file(out_path);
if std::fs::hard_link(cache_file, out_path).is_ok() {
touch_mtime(out_path);
return Ok(());
}
std::fs::write(out_path, data)
}
pub(super) fn write_cached_file(out_path: &Path, cache_file: &Path) -> std::io::Result<()> {
if std::fs::hard_link(cache_file, out_path).is_ok() {
touch_mtime(out_path);
return Ok(());
}
if same_file(out_path, cache_file) {
touch_mtime(out_path);
return Ok(());
}
let _ = std::fs::remove_file(out_path);
if std::fs::hard_link(cache_file, out_path).is_ok() {
touch_mtime(out_path);
return Ok(());
}
std::fs::copy(cache_file, out_path)?;
touch_mtime(out_path);
Ok(())
}
pub(super) fn write_cached_payload(
out_path: &Path,
cache_file: &Path,
payload: &CachedPayload,
) -> std::io::Result<()> {
match payload {
CachedPayload::Bytes(data) => write_cached_output(out_path, cache_file, data),
CachedPayload::File(path) => write_cached_file(out_path, path),
}
}
pub(super) const PAR_WRITE_THRESHOLD: usize = 4;
pub(super) fn write_payloads_par<P, Q>(targets: &[(P, Q)], payloads: &[CachedPayload]) -> bool
where
P: AsRef<Path> + Sync,
Q: AsRef<Path> + Sync,
{
debug_assert_eq!(targets.len(), payloads.len());
let write_one = |out: &Path, cache: &Path, payload: &CachedPayload| -> bool {
if let Some(parent) = out.parent() {
let _ = std::fs::create_dir_all(parent);
}
write_cached_payload(out, cache, payload).is_ok()
};
if targets.len() < PAR_WRITE_THRESHOLD {
return targets
.iter()
.zip(payloads.iter())
.all(|((out, cache), payload)| write_one(out.as_ref(), cache.as_ref(), payload));
}
use rayon::prelude::*;
targets
.par_iter()
.zip(payloads.par_iter())
.all(|((out, cache), payload)| write_one(out.as_ref(), cache.as_ref(), payload))
}
pub(super) fn break_output_hardlink_before_compile(path: &Path) -> std::io::Result<()> {
match std::fs::metadata(path) {
Ok(meta) if meta.is_file() => {}
Ok(_) => return Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e),
}
if hard_link_count(path)? <= 1 {
return Ok(());
}
let parent = path.parent().unwrap_or_else(|| Path::new("."));
let file_name = path
.file_name()
.unwrap_or_else(|| std::ffi::OsStr::new("output"))
.to_string_lossy();
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let pid = std::process::id();
let mut last_err = None;
for attempt in 0..32 {
let tmp_path = parent.join(format!(
".zccache-detach-{pid}-{nonce}-{attempt}-{file_name}"
));
let copy_result = (|| {
let mut src = std::fs::File::open(path)?;
let mut dst = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp_path)?;
std::io::copy(&mut src, &mut dst)?;
dst.sync_all()?;
let permissions = src.metadata()?.permissions();
std::fs::set_permissions(&tmp_path, permissions)?;
Ok::<(), std::io::Error>(())
})();
match copy_result {
Ok(()) => {
if let Err(e) = std::fs::remove_file(path) {
let _ = std::fs::remove_file(&tmp_path);
return Err(e);
}
if let Err(e) = std::fs::rename(&tmp_path, path) {
let _ = std::fs::remove_file(&tmp_path);
return Err(e);
}
return Ok(());
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
last_err = Some(e);
}
Err(e) => {
let _ = std::fs::remove_file(&tmp_path);
return Err(e);
}
}
}
Err(last_err.unwrap_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"failed to create hardlink detach temp file",
)
}))
}
#[cfg(unix)]
pub(super) fn hard_link_count(path: &Path) -> std::io::Result<u64> {
use std::os::unix::fs::MetadataExt;
Ok(std::fs::metadata(path)?.nlink())
}
#[cfg(windows)]
pub(super) fn hard_link_count(path: &Path) -> std::io::Result<u64> {
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::Storage::FileSystem::{
CreateFileW, GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION, FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
};
let wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
unsafe {
let handle = CreateFileW(
wide.as_ptr(),
0,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
std::ptr::null(),
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
std::ptr::null_mut(),
);
if handle == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE {
return Err(std::io::Error::last_os_error());
}
let mut info: BY_HANDLE_FILE_INFORMATION = std::mem::zeroed();
let ok = GetFileInformationByHandle(handle, &mut info);
let close_result = CloseHandle(handle);
if ok == 0 {
return Err(std::io::Error::last_os_error());
}
if close_result == 0 {
return Err(std::io::Error::last_os_error());
}
Ok(info.nNumberOfLinks as u64)
}
}
pub(super) fn touch_mtime(_path: &Path) {}
#[cfg(unix)]
pub(super) fn same_file(a: &Path, b: &Path) -> bool {
use std::os::unix::fs::MetadataExt;
match (std::fs::metadata(a), std::fs::metadata(b)) {
(Ok(ma), Ok(mb)) => ma.dev() == mb.dev() && ma.ino() == mb.ino(),
_ => false,
}
}
#[cfg(windows)]
pub(super) fn same_file(a: &Path, b: &Path) -> bool {
get_file_id(a)
.zip(get_file_id(b))
.map(|(ia, ib)| ia == ib)
.unwrap_or(false)
}
#[cfg(windows)]
pub(super) fn get_file_id(path: &Path) -> Option<(u32, u32, u32)> {
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::Storage::FileSystem::{
CreateFileW, GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION, FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
};
let wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
unsafe {
let handle = CreateFileW(
wide.as_ptr(),
0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
std::ptr::null(),
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
std::ptr::null_mut(),
);
if handle == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE {
return None;
}
let mut info: BY_HANDLE_FILE_INFORMATION = std::mem::zeroed();
let ok = GetFileInformationByHandle(handle, &mut info);
CloseHandle(handle);
if ok == 0 {
return None;
}
Some((
info.dwVolumeSerialNumber,
info.nFileIndexHigh,
info.nFileIndexLow,
))
}
}