Skip to main content

dmc/engine/
cache.rs

1//! Persistent per-file compile cache at `<output_dir>/.cache/dmc/`,
2//! one `{16-hex blake3}.json` per record. Cache hits are O(read + parse).
3//! Key encodes dmc version + source bytes + path + config fingerprint;
4//! nothing overwrites in place.
5
6use blake3::Hasher;
7use serde_json::Value;
8use std::path::{Path, PathBuf};
9
10const VERSION: &str = env!("CARGO_PKG_VERSION");
11
12#[derive(Debug, Clone)]
13pub struct FileCache {
14  dir: PathBuf,
15}
16
17impl FileCache {
18  /// Returns `None` (cache disabled) if the dir can't be created.
19  pub fn open(dir: PathBuf) -> Option<Self> {
20    std::fs::create_dir_all(&dir).ok()?;
21    Some(Self { dir })
22  }
23
24  pub fn key(source: &[u8], path: &Path, cfg_fingerprint: &[u8]) -> String {
25    let mut h = Hasher::new();
26    h.update(b"dmc/v1");
27    h.update(VERSION.as_bytes());
28    h.update(b"\0src\0");
29    h.update(source);
30    h.update(b"\0path\0");
31    h.update(path.to_string_lossy().as_bytes());
32    h.update(b"\0cfg\0");
33    h.update(cfg_fingerprint);
34    let hex = h.finalize().to_hex();
35    hex.as_str()[..16].to_string()
36  }
37
38  pub fn get(&self, key: &str) -> Option<Value> {
39    let p = self.path_for(key);
40    let s = std::fs::read_to_string(p).ok()?;
41    serde_json::from_str(&s).ok()
42  }
43
44  /// Write errors are swallowed: a cache failure must not break the build.
45  pub fn put(&self, key: &str, value: &Value) {
46    let p = self.path_for(key);
47    if let Ok(json) = serde_json::to_string(value) {
48      let _ = std::fs::write(p, json);
49    }
50  }
51
52  fn path_for(&self, key: &str) -> PathBuf {
53    self.dir.join(format!("{key}.json"))
54  }
55}
56
57/// Empty vec on serialisation failure (cache still works, collides
58/// across non-serialisable configs).
59pub fn fingerprint<T: serde::Serialize>(cfg: &T) -> Vec<u8> {
60  let Ok(json) = serde_json::to_vec(cfg) else { return Vec::new() };
61  blake3::hash(&json).as_bytes().to_vec()
62}