Skip to main content

kiln_build/
cache.rs

1//! Content-hashed build cache rooted at `target/kiln/<hash>/`.
2
3use std::path::{Path, PathBuf};
4
5use blake3::Hasher;
6
7use crate::plan::BuildPlan;
8
9/// 32-character lowercase hex hash that uniquely keys a build.
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub struct BuildCacheKey(String);
12
13impl BuildCacheKey {
14    pub fn as_str(&self) -> &str {
15        &self.0
16    }
17
18    /// Compute the cache key for a plan. Includes file *content* hashes
19    /// (not just paths or mtimes), the top module, defines, include
20    /// directories, profile, and a fixed schema version. Changing any of
21    /// these invalidates the cache deterministically; bumping
22    /// [`SCHEMA_VERSION`] invalidates *every* cached build (use sparingly).
23    pub fn for_plan(plan: &BuildPlan) -> std::io::Result<Self> {
24        let mut hasher = Hasher::new();
25        hasher.update(SCHEMA_VERSION.as_bytes());
26        hasher.update(plan.top.as_bytes());
27        hasher.update(plan.profile.as_str().as_bytes());
28        hasher.update(if plan.trace { b"trace=1" } else { b"trace=0" });
29        if let Some(ts) = &plan.timescale {
30            hasher.update(b"timescale=");
31            hasher.update(ts.as_bytes());
32            hasher.update(b"\0");
33        }
34        if let Some(lang) = &plan.language {
35            hasher.update(b"language=");
36            hasher.update(lang.as_bytes());
37            hasher.update(b"\0");
38        }
39        for lib in &plan.libraries {
40            hasher.update(b"lib=");
41            hasher.update(lib.to_string_lossy().as_bytes());
42            hasher.update(b"\0");
43        }
44        for flag in &plan.verilator_lint_flags {
45            hasher.update(flag.as_bytes());
46            hasher.update(b"\0");
47        }
48
49        let mut sorted_defines: Vec<_> = plan.defines.iter().collect();
50        sorted_defines.sort();
51        for (k, v) in sorted_defines {
52            hasher.update(k.as_bytes());
53            hasher.update(b"=");
54            hasher.update(v.as_bytes());
55            hasher.update(b"\0");
56        }
57        for inc in &plan.include_dirs {
58            hasher.update(inc.to_string_lossy().as_bytes());
59            hasher.update(b"\0");
60        }
61        let mut sorted_sources: Vec<_> = plan.sources.iter().collect();
62        sorted_sources.sort();
63        for src in sorted_sources {
64            // Hash the path *and* the content. Two distinct files with the
65            // same content should still produce different keys if their
66            // paths differ (because Verilator records source file names in
67            // its output).
68            hasher.update(src.to_string_lossy().as_bytes());
69            hasher.update(b"\0");
70            let bytes = std::fs::read(src)?;
71            hasher.update(&bytes);
72        }
73        let hex = hasher.finalize().to_hex();
74        Ok(BuildCacheKey(hex.as_str()[..32].to_string()))
75    }
76}
77
78/// Bump this when the cache layout or invocation flags change in a way
79/// that should invalidate every existing cached build.
80pub const SCHEMA_VERSION: &str = "kiln-build-cache-v1";
81
82/// Resolve the on-disk directory for a key. The directory may not exist
83/// yet; backends are responsible for creating it before writing.
84pub fn cache_dir(project_root: &Path, key: &BuildCacheKey) -> PathBuf {
85    project_root.join("target").join("kiln").join(key.as_str())
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::plan::Profile;
92    use std::collections::BTreeMap;
93
94    fn write_src(dir: &Path, name: &str, body: &str) -> PathBuf {
95        let p = dir.join(name);
96        std::fs::write(&p, body).unwrap();
97        p
98    }
99
100    fn plan_for(sources: Vec<PathBuf>, profile: Profile) -> BuildPlan {
101        BuildPlan {
102            project_root: PathBuf::from("/proj"),
103            top: "top".to_string(),
104            sources,
105            include_dirs: vec![],
106            defines: BTreeMap::new(),
107            profile,
108            trace: false,
109            timescale: None,
110            language: None,
111            libraries: vec![],
112            verilator_lint_flags: vec![],
113            extra_verilator_args: vec![],
114            verilator_options: Default::default(),
115            blackbox_modules: vec![],
116        }
117    }
118
119    #[test]
120    fn identical_inputs_yield_same_key() {
121        let tmp = tempfile::tempdir().unwrap();
122        let s = write_src(tmp.path(), "a.sv", "module a; endmodule");
123        let plan = plan_for(vec![s.clone()], Profile::Debug);
124        let k1 = BuildCacheKey::for_plan(&plan).unwrap();
125        let k2 = BuildCacheKey::for_plan(&plan).unwrap();
126        assert_eq!(k1, k2);
127        assert_eq!(k1.as_str().len(), 32);
128    }
129
130    #[test]
131    fn editing_source_changes_key() {
132        let tmp = tempfile::tempdir().unwrap();
133        let s = write_src(tmp.path(), "a.sv", "module a; endmodule");
134        let plan = plan_for(vec![s.clone()], Profile::Debug);
135        let before = BuildCacheKey::for_plan(&plan).unwrap();
136        std::fs::write(&s, "module a;\n  // a comment\nendmodule").unwrap();
137        let after = BuildCacheKey::for_plan(&plan).unwrap();
138        assert_ne!(before, after);
139    }
140
141    #[test]
142    fn changing_profile_changes_key() {
143        let tmp = tempfile::tempdir().unwrap();
144        let s = write_src(tmp.path(), "a.sv", "module a; endmodule");
145        let debug = BuildCacheKey::for_plan(&plan_for(vec![s.clone()], Profile::Debug)).unwrap();
146        let release = BuildCacheKey::for_plan(&plan_for(vec![s], Profile::Release)).unwrap();
147        assert_ne!(debug, release);
148    }
149
150    #[test]
151    fn changing_define_changes_key() {
152        let tmp = tempfile::tempdir().unwrap();
153        let s = write_src(tmp.path(), "a.sv", "module a; endmodule");
154        let mut p1 = plan_for(vec![s.clone()], Profile::Debug);
155        let mut p2 = p1.clone();
156        p1.defines.insert("X".into(), "1".into());
157        p2.defines.insert("X".into(), "2".into());
158        assert_ne!(
159            BuildCacheKey::for_plan(&p1).unwrap(),
160            BuildCacheKey::for_plan(&p2).unwrap()
161        );
162    }
163
164    #[test]
165    fn cache_dir_layout() {
166        let key = BuildCacheKey("abc123".to_string());
167        let dir = cache_dir(Path::new("/proj"), &key);
168        assert_eq!(dir, PathBuf::from("/proj/target/kiln/abc123"));
169    }
170}