1use crate::NormalizedPath;
4
5#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
7#[serde(default)]
8pub struct Config {
9 pub cache_dir: NormalizedPath,
11 pub max_cache_size: u64,
13 pub idle_timeout_secs: u64,
15 pub enable_watcher: bool,
17 pub watcher_poll_fallback: bool,
19 pub log_level: String,
21 pub max_memory_bytes: u64,
23 pub eviction_interval_secs: u64,
25 pub disk_gc_interval_secs: u64,
27}
28
29impl Default for Config {
30 fn default() -> Self {
31 Self {
32 cache_dir: default_cache_dir(),
33 max_cache_size: 10 * 1024 * 1024 * 1024, idle_timeout_secs: 3600,
35 enable_watcher: true,
36 watcher_poll_fallback: false,
37 log_level: String::from("info"),
38 max_memory_bytes: 1_073_741_824, eviction_interval_secs: 30,
40 disk_gc_interval_secs: 300,
41 }
42 }
43}
44
45#[must_use]
47pub fn default_cache_dir() -> NormalizedPath {
48 dirs_fallback().join(".zccache")
49}
50
51#[must_use]
53pub fn artifacts_dir() -> NormalizedPath {
54 default_cache_dir().join("artifacts")
55}
56
57#[must_use]
59pub fn tmp_dir() -> NormalizedPath {
60 default_cache_dir().join("tmp")
61}
62
63#[must_use]
68pub fn depfile_dir() -> NormalizedPath {
69 tmp_dir().join("depfiles")
70}
71
72pub fn cleanup_stale_depfile_dirs<F>(is_alive: F) -> usize
78where
79 F: Fn(u32) -> bool,
80{
81 let base = depfile_dir();
82 let entries = match std::fs::read_dir(&base) {
83 Ok(entries) => entries,
84 Err(_) => return 0,
85 };
86
87 let mut cleaned = 0;
88 for entry in entries.flatten() {
89 let path = entry.path();
90 if !path.is_dir() {
91 continue;
92 }
93 let name = match path.file_name().and_then(|n| n.to_str()) {
94 Some(n) => n,
95 None => continue,
96 };
97 let pid: u32 = match name.split('-').next().and_then(|s| s.parse().ok()) {
98 Some(p) => p,
99 None => continue,
100 };
101 if !is_alive(pid) {
102 match std::fs::remove_dir_all(&path) {
103 Ok(()) => {
104 cleaned += 1;
105 tracing::info!(path = %path.display(), "removed stale depfile dir");
106 }
107 Err(e) => {
108 tracing::warn!(
109 path = %path.display(),
110 "failed to remove stale depfile dir: {e}"
111 );
112 }
113 }
114 }
115 }
116 cleaned
117}
118
119#[must_use]
121pub fn depgraph_dir() -> NormalizedPath {
122 default_cache_dir().join("depgraph")
123}
124
125#[must_use]
127pub fn index_path() -> NormalizedPath {
128 default_cache_dir().join("index.redb")
129}
130
131#[must_use]
133pub fn crash_dump_dir() -> NormalizedPath {
134 default_cache_dir().join("crashes")
135}
136
137#[must_use]
139pub fn log_dir() -> NormalizedPath {
140 default_cache_dir().join("logs")
141}
142
143fn dirs_fallback() -> NormalizedPath {
144 std::env::var("HOME")
145 .or_else(|_| std::env::var("USERPROFILE"))
146 .map(NormalizedPath::from)
147 .unwrap_or_else(|_| ".".into())
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn default_cache_dir_ends_with_zccache() {
156 let dir = default_cache_dir();
157 assert!(dir.ends_with(".zccache"));
158 }
159
160 #[test]
161 fn crash_dump_dir_ends_with_crashes() {
162 let dir = crash_dump_dir();
163 assert!(dir.ends_with("crashes"));
164 }
165
166 #[test]
167 fn crash_dump_dir_is_under_cache_dir() {
168 let cache = default_cache_dir();
169 let crashes = crash_dump_dir();
170 assert!(crashes.starts_with(&cache));
171 }
172
173 #[test]
174 fn log_dir_ends_with_logs() {
175 let dir = log_dir();
176 assert!(dir.ends_with("logs"));
177 }
178
179 #[test]
180 fn log_dir_is_under_cache_dir() {
181 let cache = default_cache_dir();
182 let logs = log_dir();
183 assert!(logs.starts_with(&cache));
184 }
185
186 #[test]
187 fn artifacts_dir_ends_with_artifacts() {
188 let dir = artifacts_dir();
189 assert!(dir.ends_with("artifacts"));
190 assert!(dir.starts_with(default_cache_dir()));
191 }
192
193 #[test]
194 fn tmp_dir_ends_with_tmp() {
195 let dir = tmp_dir();
196 assert!(dir.ends_with("tmp"));
197 assert!(dir.starts_with(default_cache_dir()));
198 }
199
200 #[test]
201 fn depgraph_dir_ends_with_depgraph() {
202 let dir = depgraph_dir();
203 assert!(dir.ends_with("depgraph"));
204 assert!(dir.starts_with(default_cache_dir()));
205 }
206
207 #[test]
208 fn depfile_dir_under_tmp() {
209 let dir = depfile_dir();
210 assert!(dir.ends_with("depfiles"));
211 assert!(dir.starts_with(tmp_dir()));
212 }
213
214 #[test]
215 fn cleanup_stale_depfile_dirs_removes_dead() {
216 let base = tempfile::tempdir().unwrap();
217 let depfiles = base.path().join("depfiles");
218 std::fs::create_dir_all(&depfiles).unwrap();
219
220 std::fs::create_dir(depfiles.join("99999999-0")).unwrap();
222 std::fs::create_dir(depfiles.join("not-a-pid")).unwrap();
224
225 let entries = std::fs::read_dir(&depfiles).unwrap();
226 let dirs: Vec<_> = entries.flatten().collect();
227 assert_eq!(dirs.len(), 2);
228
229 let cleaned = cleanup_stale_with_base(&depfiles, |_| false);
231 assert_eq!(cleaned, 1); assert!(depfiles.join("not-a-pid").is_dir());
235 assert!(!depfiles.join("99999999-0").exists());
236 }
237
238 #[test]
239 fn cleanup_stale_depfile_dirs_skips_alive() {
240 let base = tempfile::tempdir().unwrap();
241 let depfiles = base.path().join("depfiles");
242 std::fs::create_dir_all(&depfiles).unwrap();
243 std::fs::create_dir(depfiles.join("12345-0")).unwrap();
244
245 let cleaned = cleanup_stale_with_base(&depfiles, |_| true);
246 assert_eq!(cleaned, 0);
247 assert!(depfiles.join("12345-0").is_dir());
248 }
249
250 #[test]
251 fn cleanup_stale_depfile_dirs_empty() {
252 let cleaned = cleanup_stale_with_base(std::path::Path::new("/nonexistent/path"), |_| false);
254 assert_eq!(cleaned, 0);
255 }
256
257 fn cleanup_stale_with_base<F>(base: &std::path::Path, is_alive: F) -> usize
259 where
260 F: Fn(u32) -> bool,
261 {
262 let entries = match std::fs::read_dir(base) {
263 Ok(entries) => entries,
264 Err(_) => return 0,
265 };
266 let mut cleaned = 0;
267 for entry in entries.flatten() {
268 let path = entry.path();
269 if !path.is_dir() {
270 continue;
271 }
272 let name = match path.file_name().and_then(|n| n.to_str()) {
273 Some(n) => n,
274 None => continue,
275 };
276 let pid: u32 = match name.split('-').next().and_then(|s| s.parse().ok()) {
277 Some(p) => p,
278 None => continue,
279 };
280 if !is_alive(pid) && std::fs::remove_dir_all(&path).is_ok() {
281 cleaned += 1;
282 }
283 }
284 cleaned
285 }
286
287 #[test]
288 fn disk_gc_interval_default() {
289 let config = Config::default();
290 assert_eq!(config.disk_gc_interval_secs, 300);
291 }
292
293 #[test]
294 fn index_path_ends_with_redb() {
295 let p = index_path();
296 assert!(p.ends_with("index.redb"));
297 assert!(p.starts_with(default_cache_dir()));
298 }
299}