use serde::{Deserialize, Serialize};
use tatara_lisp::DeriveTataraDomain;
use crate::SpecError;
#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
#[tatara(keyword = "defeval-cache-format")]
pub struct EvalCacheFormat {
pub name: String,
pub version: u32,
pub backend: EvalCacheBackend,
#[serde(rename = "hashAlgo")]
pub hash_algo: EvalCacheHash,
#[serde(rename = "keyInput")]
pub key_input: EvalCacheKeyInput,
#[serde(rename = "defaultPath")]
pub default_path: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum EvalCacheBackend {
SQLite,
Redb,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum EvalCacheHash {
Sha256,
Blake3,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum EvalCacheKeyInput {
ExprPlusInputs,
CanonicalAst,
}
pub const CANONICAL_EVAL_CACHE_LISP: &str =
include_str!("../specs/eval_cache.lisp");
pub fn load_canonical() -> Result<Vec<EvalCacheFormat>, SpecError> {
crate::loader::load_all::<EvalCacheFormat>(CANONICAL_EVAL_CACHE_LISP)
}
#[derive(Debug, Clone, Default)]
pub struct CacheKeyInputs {
pub expr: String,
pub flake_input_narhashes: Vec<(String, String)>,
pub system: String,
pub impure_env: Vec<(String, String)>,
pub nix_version: String,
}
pub fn derive_cache_key(
format: &EvalCacheFormat,
inputs: &CacheKeyInputs,
) -> Result<String, SpecError> {
let mut buf = String::new();
buf.push_str("nix-version=");
buf.push_str(&inputs.nix_version);
buf.push('\n');
if format.key_input == EvalCacheKeyInput::ExprPlusInputs {
buf.push_str("system=");
buf.push_str(&inputs.system);
buf.push('\n');
let mut narhashes = inputs.flake_input_narhashes.clone();
narhashes.sort();
for (k, v) in &narhashes {
buf.push_str("input.");
buf.push_str(k);
buf.push('=');
buf.push_str(v);
buf.push('\n');
}
let mut impure = inputs.impure_env.clone();
impure.sort();
for (k, v) in &impure {
buf.push_str("env.");
buf.push_str(k);
buf.push('=');
buf.push_str(v);
buf.push('\n');
}
}
buf.push_str("expr=");
buf.push_str(&inputs.expr);
match format.hash_algo {
EvalCacheHash::Sha256 => {
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
h.update(buf.as_bytes());
let bytes = h.finalize();
Ok(bytes.iter().map(|b| format!("{b:02x}")).collect())
}
EvalCacheHash::Blake3 => {
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
h.update(b"blake3-domain-separator:");
h.update(buf.as_bytes());
let bytes = h.finalize();
Ok(bytes.iter().map(|b| format!("{b:02x}")).collect())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_eval_cache_parses() {
let formats = load_canonical().unwrap();
assert!(!formats.is_empty());
}
#[test]
fn cppnix_uses_sqlite_sha256() {
let formats = load_canonical().unwrap();
let cppnix = formats.iter().find(|f| f.name == "cppnix-eval-cache-v5").unwrap();
assert_eq!(cppnix.backend, EvalCacheBackend::SQLite);
assert_eq!(cppnix.hash_algo, EvalCacheHash::Sha256);
}
#[test]
fn sui_uses_redb_blake3() {
let formats = load_canonical().unwrap();
let sui = formats.iter().find(|f| f.name == "sui-eval-cache-v1").unwrap();
assert_eq!(sui.backend, EvalCacheBackend::Redb);
assert_eq!(sui.hash_algo, EvalCacheHash::Blake3);
}
fn cppnix() -> EvalCacheFormat {
load_canonical().unwrap().into_iter()
.find(|f| f.name == "cppnix-eval-cache-v5").unwrap()
}
#[test]
fn derive_cache_key_is_deterministic() {
let f = cppnix();
let inputs = CacheKeyInputs {
expr: "1 + 2".into(),
flake_input_narhashes: vec![("nixpkgs".into(), "sha256:abc".into())],
system: "aarch64-darwin".into(),
impure_env: vec![],
nix_version: "2.18".into(),
};
let k1 = derive_cache_key(&f, &inputs).unwrap();
let k2 = derive_cache_key(&f, &inputs).unwrap();
assert_eq!(k1, k2);
assert_eq!(k1.len(), 64); }
#[test]
fn input_order_doesnt_change_key() {
let f = cppnix();
let a = CacheKeyInputs {
expr: "x".into(),
flake_input_narhashes: vec![
("a".into(), "1".into()),
("b".into(), "2".into()),
],
system: "x86_64-linux".into(),
impure_env: vec![],
nix_version: "2.18".into(),
};
let b = CacheKeyInputs {
flake_input_narhashes: vec![
("b".into(), "2".into()),
("a".into(), "1".into()),
],
..a.clone()
};
assert_eq!(derive_cache_key(&f, &a).unwrap(), derive_cache_key(&f, &b).unwrap());
}
#[test]
fn different_exprs_yield_different_keys() {
let f = cppnix();
let a = CacheKeyInputs { expr: "1".into(), ..Default::default() };
let b = CacheKeyInputs { expr: "2".into(), ..Default::default() };
assert_ne!(derive_cache_key(&f, &a).unwrap(), derive_cache_key(&f, &b).unwrap());
}
}