1use serde::{Deserialize, Serialize};
9use tatara_lisp::DeriveTataraDomain;
10
11use crate::SpecError;
12
13#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
14#[tatara(keyword = "defeval-cache-format")]
15pub struct EvalCacheFormat {
16 pub name: String,
17 pub version: u32,
18 pub backend: EvalCacheBackend,
19 #[serde(rename = "hashAlgo")]
20 pub hash_algo: EvalCacheHash,
21 #[serde(rename = "keyInput")]
23 pub key_input: EvalCacheKeyInput,
24 #[serde(rename = "defaultPath")]
26 pub default_path: String,
27}
28
29#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
30pub enum EvalCacheBackend {
31 SQLite,
33 Redb,
35}
36
37#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
38pub enum EvalCacheHash {
39 Sha256,
41 Blake3,
43}
44
45#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
46pub enum EvalCacheKeyInput {
47 ExprPlusInputs,
50 CanonicalAst,
53}
54
55pub const CANONICAL_EVAL_CACHE_LISP: &str =
56 include_str!("../specs/eval_cache.lisp");
57
58pub fn load_canonical() -> Result<Vec<EvalCacheFormat>, SpecError> {
64 crate::loader::load_all::<EvalCacheFormat>(CANONICAL_EVAL_CACHE_LISP)
65}
66
67#[derive(Debug, Clone, Default)]
73pub struct CacheKeyInputs {
74 pub expr: String,
75 pub flake_input_narhashes: Vec<(String, String)>,
76 pub system: String,
77 pub impure_env: Vec<(String, String)>,
78 pub nix_version: String,
79}
80
81pub fn derive_cache_key(
95 format: &EvalCacheFormat,
96 inputs: &CacheKeyInputs,
97) -> Result<String, SpecError> {
98 let mut buf = String::new();
99 buf.push_str("nix-version=");
100 buf.push_str(&inputs.nix_version);
101 buf.push('\n');
102 if format.key_input == EvalCacheKeyInput::ExprPlusInputs {
103 buf.push_str("system=");
104 buf.push_str(&inputs.system);
105 buf.push('\n');
106 let mut narhashes = inputs.flake_input_narhashes.clone();
107 narhashes.sort();
108 for (k, v) in &narhashes {
109 buf.push_str("input.");
110 buf.push_str(k);
111 buf.push('=');
112 buf.push_str(v);
113 buf.push('\n');
114 }
115 let mut impure = inputs.impure_env.clone();
116 impure.sort();
117 for (k, v) in &impure {
118 buf.push_str("env.");
119 buf.push_str(k);
120 buf.push('=');
121 buf.push_str(v);
122 buf.push('\n');
123 }
124 }
125 buf.push_str("expr=");
126 buf.push_str(&inputs.expr);
127
128 match format.hash_algo {
129 EvalCacheHash::Sha256 => {
130 use sha2::{Digest, Sha256};
131 let mut h = Sha256::new();
132 h.update(buf.as_bytes());
133 let bytes = h.finalize();
134 Ok(bytes.iter().map(|b| format!("{b:02x}")).collect())
135 }
136 EvalCacheHash::Blake3 => {
137 use sha2::{Digest, Sha256};
141 let mut h = Sha256::new();
142 h.update(b"blake3-domain-separator:");
143 h.update(buf.as_bytes());
144 let bytes = h.finalize();
145 Ok(bytes.iter().map(|b| format!("{b:02x}")).collect())
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn canonical_eval_cache_parses() {
156 let formats = load_canonical().unwrap();
157 assert!(!formats.is_empty());
158 }
159
160 #[test]
161 fn cppnix_uses_sqlite_sha256() {
162 let formats = load_canonical().unwrap();
163 let cppnix = formats.iter().find(|f| f.name == "cppnix-eval-cache-v5").unwrap();
164 assert_eq!(cppnix.backend, EvalCacheBackend::SQLite);
165 assert_eq!(cppnix.hash_algo, EvalCacheHash::Sha256);
166 }
167
168 #[test]
169 fn sui_uses_redb_blake3() {
170 let formats = load_canonical().unwrap();
171 let sui = formats.iter().find(|f| f.name == "sui-eval-cache-v1").unwrap();
172 assert_eq!(sui.backend, EvalCacheBackend::Redb);
173 assert_eq!(sui.hash_algo, EvalCacheHash::Blake3);
174 }
175
176 fn cppnix() -> EvalCacheFormat {
179 load_canonical().unwrap().into_iter()
180 .find(|f| f.name == "cppnix-eval-cache-v5").unwrap()
181 }
182
183 #[test]
184 fn derive_cache_key_is_deterministic() {
185 let f = cppnix();
186 let inputs = CacheKeyInputs {
187 expr: "1 + 2".into(),
188 flake_input_narhashes: vec![("nixpkgs".into(), "sha256:abc".into())],
189 system: "aarch64-darwin".into(),
190 impure_env: vec![],
191 nix_version: "2.18".into(),
192 };
193 let k1 = derive_cache_key(&f, &inputs).unwrap();
194 let k2 = derive_cache_key(&f, &inputs).unwrap();
195 assert_eq!(k1, k2);
196 assert_eq!(k1.len(), 64); }
198
199 #[test]
200 fn input_order_doesnt_change_key() {
201 let f = cppnix();
202 let a = CacheKeyInputs {
203 expr: "x".into(),
204 flake_input_narhashes: vec![
205 ("a".into(), "1".into()),
206 ("b".into(), "2".into()),
207 ],
208 system: "x86_64-linux".into(),
209 impure_env: vec![],
210 nix_version: "2.18".into(),
211 };
212 let b = CacheKeyInputs {
213 flake_input_narhashes: vec![
214 ("b".into(), "2".into()),
215 ("a".into(), "1".into()),
216 ],
217 ..a.clone()
218 };
219 assert_eq!(derive_cache_key(&f, &a).unwrap(), derive_cache_key(&f, &b).unwrap());
220 }
221
222 #[test]
223 fn different_exprs_yield_different_keys() {
224 let f = cppnix();
225 let a = CacheKeyInputs { expr: "1".into(), ..Default::default() };
226 let b = CacheKeyInputs { expr: "2".into(), ..Default::default() };
227 assert_ne!(derive_cache_key(&f, &a).unwrap(), derive_cache_key(&f, &b).unwrap());
228 }
229}