use super::*;
use serde::{Deserialize, Serialize};
use std::io::Write as _;
pub(super) const FORMAT_VERSION: u32 = 1;
#[derive(Clone, Serialize, Deserialize)]
pub(super) struct CompilerHashEntry {
pub(super) mtime: std::time::SystemTime,
pub(super) size: u64,
pub(super) hash: ContentHash,
}
#[derive(Serialize, Deserialize)]
struct PersistedCompilerHashes {
version: u32,
entries: Vec<(NormalizedPath, CompilerHashEntry)>,
}
#[derive(Default)]
pub(super) struct CompilerHashCache {
pub(super) entries: DashMap<NormalizedPath, CompilerHashEntry>,
}
impl CompilerHashCache {
pub(super) fn new() -> Self {
Self {
entries: DashMap::new(),
}
}
#[cfg(test)]
pub(super) fn len(&self) -> usize {
self.entries.len()
}
pub(super) fn get_or_hash_with<F>(&self, path: &Path, hasher: F) -> Option<ContentHash>
where
F: FnOnce(&Path) -> Option<ContentHash>,
{
let metadata = std::fs::metadata(path).ok()?;
let mtime = metadata.modified().ok()?;
let size = metadata.len();
let key = NormalizedPath::new(path);
if let Some(entry) = self.entries.get(&key) {
if entry.mtime == mtime && entry.size == size {
return Some(entry.hash);
}
}
let hash = hasher(path)?;
let post_metadata = std::fs::metadata(path).ok()?;
let post_mtime = post_metadata.modified().ok()?;
let post_size = post_metadata.len();
if post_mtime != mtime || post_size != size {
return Some(hash);
}
self.entries
.insert(key, CompilerHashEntry { mtime, size, hash });
Some(hash)
}
pub(super) fn save_to_disk(&self, path: &Path) -> std::io::Result<()> {
let entries: Vec<(NormalizedPath, CompilerHashEntry)> = self
.entries
.iter()
.map(|e| (e.key().clone(), e.value().clone()))
.collect();
if entries.is_empty() {
tracing::debug!(
path = %path.display(),
"compiler hash cache flush: 0 entries, skipping write"
);
return Ok(());
}
let entry_count = entries.len();
let snapshot = PersistedCompilerHashes {
version: FORMAT_VERSION,
entries,
};
let bytes = bincode::serialize(&snapshot)
.map_err(|e| std::io::Error::other(format!("bincode serialize: {e}")))?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "compiler_hash.bin".into());
let tmp = path.with_file_name(format!(".{name}.tmp-{}", std::process::id()));
let result = write_atomic_durable(&tmp, path, &bytes);
if result.is_err() {
let _ = std::fs::remove_file(&tmp);
}
if result.is_ok() {
tracing::info!(
path = %path.display(),
entries = entry_count,
bytes = bytes.len(),
"compiler hash cache flushed to disk"
);
}
result
}
pub(super) fn load_from_disk(path: &Path) -> std::io::Result<Self> {
let bytes = match std::fs::read(path) {
Ok(bytes) => bytes,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
tracing::debug!(
path = %path.display(),
"compiler hash cache file not found, starting empty"
);
return Ok(Self::new());
}
Err(e) => return Err(e),
};
let snapshot: PersistedCompilerHashes = bincode::deserialize(&bytes)
.map_err(|e| std::io::Error::other(format!("bincode deserialize: {e}")))?;
if snapshot.version != FORMAT_VERSION {
return Err(std::io::Error::other(format!(
"compiler hash cache version mismatch: file={}, expected={}",
snapshot.version, FORMAT_VERSION
)));
}
let entries = DashMap::with_capacity(snapshot.entries.len());
let entry_count = snapshot.entries.len();
for (key, value) in snapshot.entries {
entries.insert(key, value);
}
tracing::info!(
path = %path.display(),
entries = entry_count,
"compiler hash cache restored from disk"
);
Ok(Self { entries })
}
}
fn write_atomic_durable(tmp: &Path, target: &Path, bytes: &[u8]) -> std::io::Result<()> {
{
let mut f = std::fs::File::create(tmp)?;
f.write_all(bytes)?;
f.sync_all()?;
}
std::fs::rename(tmp, target)?;
if let Some(parent) = target.parent() {
if let Ok(dir) = std::fs::File::open(parent) {
let _ = dir.sync_all();
}
}
Ok(())
}
pub(super) fn hash_rustc_identity(path: &Path) -> Option<ContentHash> {
match std::process::Command::new(path).arg("-vV").output() {
Ok(output) if output.status.success() && !output.stdout.is_empty() => {
Some(crate::hash::hash_bytes(&output.stdout))
}
_ => crate::hash::hash_file(path).ok(),
}
}