1use std::path::{Path, PathBuf};
4
5use blake3::Hasher;
6
7use crate::plan::BuildPlan;
8
9#[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 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 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
78pub const SCHEMA_VERSION: &str = "kiln-build-cache-v1";
81
82pub 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}