use crate::ContentHash;
use std::collections::BTreeMap;
#[derive(Debug, Default)]
pub struct CacheKeyBuilder {
compiler_id: Option<ContentHash>,
arguments: Vec<String>,
env_vars: BTreeMap<String, String>,
source_hash: Option<ContentHash>,
dependency_hashes: BTreeMap<String, ContentHash>,
}
impl CacheKeyBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn compiler(mut self, hash: ContentHash) -> Self {
self.compiler_id = Some(hash);
self
}
#[must_use]
pub fn arg(mut self, arg: impl Into<String>) -> Self {
self.arguments.push(arg.into());
self
}
#[must_use]
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env_vars.insert(key.into(), value.into());
self
}
#[must_use]
pub fn source(mut self, hash: ContentHash) -> Self {
self.source_hash = Some(hash);
self
}
#[must_use]
pub fn dependency(mut self, name: impl Into<String>, hash: ContentHash) -> Self {
self.dependency_hashes.insert(name.into(), hash);
self
}
#[must_use]
pub fn build(self) -> ContentHash {
let mut hasher = blake3::Hasher::new();
hasher.update(b"zccache-cache-key-v1");
let compiler = self.compiler_id.expect("compiler hash is required");
hasher.update(compiler.as_bytes());
for arg in &self.arguments {
hasher.update(arg.as_bytes());
hasher.update(b"\0");
}
for (key, value) in &self.env_vars {
hasher.update(key.as_bytes());
hasher.update(b"=");
hasher.update(value.as_bytes());
hasher.update(b"\0");
}
let source = self.source_hash.expect("source hash is required");
hasher.update(source.as_bytes());
for (name, hash) in &self.dependency_hashes {
hasher.update(name.as_bytes());
hasher.update(hash.as_bytes());
}
ContentHash::from_bytes(*hasher.finalize().as_bytes())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash_bytes;
#[test]
fn cache_key_deterministic() {
let compiler = hash_bytes(b"gcc-12");
let source = hash_bytes(b"int main() {}");
let k1 = CacheKeyBuilder::new()
.compiler(compiler)
.source(source)
.arg("-O2")
.build();
let k2 = CacheKeyBuilder::new()
.compiler(compiler)
.source(source)
.arg("-O2")
.build();
assert_eq!(k1, k2);
}
#[test]
fn different_args_different_key() {
let compiler = hash_bytes(b"gcc-12");
let source = hash_bytes(b"int main() {}");
let k1 = CacheKeyBuilder::new()
.compiler(compiler)
.source(source)
.arg("-O2")
.build();
let k2 = CacheKeyBuilder::new()
.compiler(compiler)
.source(source)
.arg("-O0")
.build();
assert_ne!(k1, k2);
}
#[test]
fn argument_order_is_preserved_in_key() {
let compiler = hash_bytes(b"gcc-12");
let source = hash_bytes(b"int main() {}");
let k1 = CacheKeyBuilder::new()
.compiler(compiler)
.source(source)
.arg("-include")
.arg("a.h")
.build();
let k2 = CacheKeyBuilder::new()
.compiler(compiler)
.source(source)
.arg("-include")
.arg("a.h")
.build();
assert_eq!(k1, k2);
}
#[test]
fn different_argument_order_produces_different_key() {
let compiler = hash_bytes(b"gcc-12");
let source = hash_bytes(b"int main() {}");
let k1 = CacheKeyBuilder::new()
.compiler(compiler)
.source(source)
.arg("-DNAME=first")
.arg("-DNAME=second")
.build();
let k2 = CacheKeyBuilder::new()
.compiler(compiler)
.source(source)
.arg("-DNAME=second")
.arg("-DNAME=first")
.build();
assert_ne!(k1, k2, "swapping compile arg order must change the key");
}
}