use crate::NormalizedPath;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct Config {
pub cache_dir: NormalizedPath,
pub max_cache_size: u64,
pub idle_timeout_secs: u64,
pub enable_watcher: bool,
pub watcher_poll_fallback: bool,
pub log_level: String,
pub max_memory_bytes: u64,
pub eviction_interval_secs: u64,
pub disk_gc_interval_secs: u64,
}
impl Default for Config {
fn default() -> Self {
Self {
cache_dir: default_cache_dir(),
max_cache_size: 10 * 1024 * 1024 * 1024, idle_timeout_secs: 3600,
enable_watcher: true,
watcher_poll_fallback: false,
log_level: String::from("info"),
max_memory_bytes: 1_073_741_824, eviction_interval_secs: 30,
disk_gc_interval_secs: 300,
}
}
}
#[must_use]
pub fn default_cache_dir() -> NormalizedPath {
dirs_fallback().join(".zccache")
}
#[must_use]
pub fn artifacts_dir() -> NormalizedPath {
default_cache_dir().join("artifacts")
}
#[must_use]
pub fn tmp_dir() -> NormalizedPath {
default_cache_dir().join("tmp")
}
#[must_use]
pub fn depfile_dir() -> NormalizedPath {
tmp_dir().join("depfiles")
}
pub fn cleanup_stale_depfile_dirs<F>(is_alive: F) -> usize
where
F: Fn(u32) -> bool,
{
let base = depfile_dir();
let entries = match std::fs::read_dir(&base) {
Ok(entries) => entries,
Err(_) => return 0,
};
let mut cleaned = 0;
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n,
None => continue,
};
let pid: u32 = match name.split('-').next().and_then(|s| s.parse().ok()) {
Some(p) => p,
None => continue,
};
if !is_alive(pid) {
match std::fs::remove_dir_all(&path) {
Ok(()) => {
cleaned += 1;
tracing::info!(path = %path.display(), "removed stale depfile dir");
}
Err(e) => {
tracing::warn!(
path = %path.display(),
"failed to remove stale depfile dir: {e}"
);
}
}
}
}
cleaned
}
#[must_use]
pub fn depgraph_dir() -> NormalizedPath {
default_cache_dir().join("depgraph")
}
#[must_use]
pub fn index_path() -> NormalizedPath {
default_cache_dir().join("index.redb")
}
#[must_use]
pub fn crash_dump_dir() -> NormalizedPath {
default_cache_dir().join("crashes")
}
#[must_use]
pub fn log_dir() -> NormalizedPath {
default_cache_dir().join("logs")
}
fn dirs_fallback() -> NormalizedPath {
std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.map(NormalizedPath::from)
.unwrap_or_else(|_| ".".into())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_cache_dir_ends_with_zccache() {
let dir = default_cache_dir();
assert!(dir.ends_with(".zccache"));
}
#[test]
fn crash_dump_dir_ends_with_crashes() {
let dir = crash_dump_dir();
assert!(dir.ends_with("crashes"));
}
#[test]
fn crash_dump_dir_is_under_cache_dir() {
let cache = default_cache_dir();
let crashes = crash_dump_dir();
assert!(crashes.starts_with(&cache));
}
#[test]
fn log_dir_ends_with_logs() {
let dir = log_dir();
assert!(dir.ends_with("logs"));
}
#[test]
fn log_dir_is_under_cache_dir() {
let cache = default_cache_dir();
let logs = log_dir();
assert!(logs.starts_with(&cache));
}
#[test]
fn artifacts_dir_ends_with_artifacts() {
let dir = artifacts_dir();
assert!(dir.ends_with("artifacts"));
assert!(dir.starts_with(default_cache_dir()));
}
#[test]
fn tmp_dir_ends_with_tmp() {
let dir = tmp_dir();
assert!(dir.ends_with("tmp"));
assert!(dir.starts_with(default_cache_dir()));
}
#[test]
fn depgraph_dir_ends_with_depgraph() {
let dir = depgraph_dir();
assert!(dir.ends_with("depgraph"));
assert!(dir.starts_with(default_cache_dir()));
}
#[test]
fn depfile_dir_under_tmp() {
let dir = depfile_dir();
assert!(dir.ends_with("depfiles"));
assert!(dir.starts_with(tmp_dir()));
}
#[test]
fn cleanup_stale_depfile_dirs_removes_dead() {
let base = tempfile::tempdir().unwrap();
let depfiles = base.path().join("depfiles");
std::fs::create_dir_all(&depfiles).unwrap();
std::fs::create_dir(depfiles.join("99999999-0")).unwrap();
std::fs::create_dir(depfiles.join("not-a-pid")).unwrap();
let entries = std::fs::read_dir(&depfiles).unwrap();
let dirs: Vec<_> = entries.flatten().collect();
assert_eq!(dirs.len(), 2);
let cleaned = cleanup_stale_with_base(&depfiles, |_| false);
assert_eq!(cleaned, 1);
assert!(depfiles.join("not-a-pid").is_dir());
assert!(!depfiles.join("99999999-0").exists());
}
#[test]
fn cleanup_stale_depfile_dirs_skips_alive() {
let base = tempfile::tempdir().unwrap();
let depfiles = base.path().join("depfiles");
std::fs::create_dir_all(&depfiles).unwrap();
std::fs::create_dir(depfiles.join("12345-0")).unwrap();
let cleaned = cleanup_stale_with_base(&depfiles, |_| true);
assert_eq!(cleaned, 0);
assert!(depfiles.join("12345-0").is_dir());
}
#[test]
fn cleanup_stale_depfile_dirs_empty() {
let cleaned = cleanup_stale_with_base(std::path::Path::new("/nonexistent/path"), |_| false);
assert_eq!(cleaned, 0);
}
fn cleanup_stale_with_base<F>(base: &std::path::Path, is_alive: F) -> usize
where
F: Fn(u32) -> bool,
{
let entries = match std::fs::read_dir(base) {
Ok(entries) => entries,
Err(_) => return 0,
};
let mut cleaned = 0;
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n,
None => continue,
};
let pid: u32 = match name.split('-').next().and_then(|s| s.parse().ok()) {
Some(p) => p,
None => continue,
};
if !is_alive(pid) && std::fs::remove_dir_all(&path).is_ok() {
cleaned += 1;
}
}
cleaned
}
#[test]
fn disk_gc_interval_default() {
let config = Config::default();
assert_eq!(config.disk_gc_interval_secs, 300);
}
#[test]
fn index_path_ends_with_redb() {
let p = index_path();
assert!(p.ends_with("index.redb"));
assert!(p.starts_with(default_cache_dir()));
}
}