#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CacheKey {
value: String,
}
impl CacheKey {
#[must_use]
pub fn as_str(&self) -> &str {
&self.value
}
}
#[cfg(test)]
pub(crate) fn key_for_test(label: &str) -> CacheKey {
compute_lang_hash("", label, "")
}
fn finish(mut hasher: blake3::Hasher, alef_version: &str) -> CacheKey {
hasher.update(binary_identity().as_bytes());
hasher.update(alef_version.as_bytes());
CacheKey {
value: hasher.finalize().to_hex().to_string(),
}
}
pub(crate) fn alef_version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
fn binary_identity() -> String {
std::env::current_exe()
.ok()
.and_then(|p| std::fs::metadata(&p).ok())
.map(|m| {
let mtime = m
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
format!("{mtime}:{}", m.len())
})
.unwrap_or_default()
}
pub fn compute_ir_key(schema_version: &str, sources_hash: &str, crate_version: &str, config_hash: &str) -> CacheKey {
compute_ir_key_for_version(alef_version(), schema_version, sources_hash, crate_version, config_hash)
}
pub fn compute_lang_hash(ir_json: &str, lang: &str, config_toml: &str) -> CacheKey {
compute_lang_hash_for_version(alef_version(), ir_json, lang, config_toml)
}
pub fn compute_stage_hash(ir_json: &str, stage: &str, config_toml: &str, extra: &[u8]) -> CacheKey {
compute_stage_hash_for_version(alef_version(), ir_json, stage, config_toml, extra)
}
pub(crate) fn compute_ir_key_for_version(
alef_version: &str,
schema_version: &str,
sources_hash: &str,
crate_version: &str,
config_hash: &str,
) -> CacheKey {
let mut hasher = blake3::Hasher::new();
hasher.update(schema_version.as_bytes());
hasher.update(b"\0");
hasher.update(sources_hash.as_bytes());
hasher.update(b"\0");
hasher.update(crate_version.as_bytes());
hasher.update(b"\0");
hasher.update(config_hash.as_bytes());
finish(hasher, alef_version)
}
fn compute_lang_hash_for_version(alef_version: &str, ir_json: &str, lang: &str, config_toml: &str) -> CacheKey {
let mut hasher = blake3::Hasher::new();
hasher.update(ir_json.as_bytes());
hasher.update(lang.as_bytes());
hasher.update(config_toml.as_bytes());
finish(hasher, alef_version)
}
fn compute_stage_hash_for_version(
alef_version: &str,
ir_json: &str,
stage: &str,
config_toml: &str,
extra: &[u8],
) -> CacheKey {
let mut hasher = blake3::Hasher::new();
hasher.update(ir_json.as_bytes());
hasher.update(stage.as_bytes());
hasher.update(config_toml.as_bytes());
if !extra.is_empty() {
hasher.update(extra);
}
finish(hasher, alef_version)
}
#[cfg(test)]
mod tests {
use super::*;
const IR: &str = r#"{"crate_name":"sample","functions":[]}"#;
const CONFIG: &str = "[workspace]\nlanguages = [\"node\"]\n";
#[test]
fn stage_hash_changes_when_the_alef_version_changes() {
let before = compute_stage_hash_for_version("0.62.12", IR, "stubs", CONFIG, &[]);
let after = compute_stage_hash_for_version("0.64.0", IR, "stubs", CONFIG, &[]);
assert_ne!(
before, after,
"a new alef release must invalidate the stage cache; identical hashes mean \
`alef generate` skips every stage and silently keeps the old release's output"
);
}
const IR_SCHEMA: &str = "ir-cache-v2";
const SOURCES_HASH: &str = "sources-abc";
const CRATE_VERSION: &str = "1.4.0";
const CONFIG_HASH: &str = "config-abc";
#[test]
fn ir_key_changes_when_the_alef_version_changes() {
let before = compute_ir_key_for_version("0.67.2", IR_SCHEMA, SOURCES_HASH, CRATE_VERSION, CONFIG_HASH);
let after = compute_ir_key_for_version("0.67.5", IR_SCHEMA, SOURCES_HASH, CRATE_VERSION, CONFIG_HASH);
assert_ne!(
before, after,
"a new alef release must invalidate the IR cache; identical keys mean the new \
binary extracts nothing and generates from the previous release's ApiSurface"
);
}
#[test]
fn ir_key_still_separates_every_extraction_input_within_one_version() {
let version = "0.67.5";
let base = compute_ir_key_for_version(version, IR_SCHEMA, SOURCES_HASH, CRATE_VERSION, CONFIG_HASH);
assert_ne!(
base,
compute_ir_key_for_version(version, "ir-cache-v3", SOURCES_HASH, CRATE_VERSION, CONFIG_HASH)
);
assert_ne!(
base,
compute_ir_key_for_version(version, IR_SCHEMA, "sources-xyz", CRATE_VERSION, CONFIG_HASH)
);
assert_ne!(
base,
compute_ir_key_for_version(version, IR_SCHEMA, SOURCES_HASH, "1.5.0", CONFIG_HASH)
);
assert_ne!(
base,
compute_ir_key_for_version(version, IR_SCHEMA, SOURCES_HASH, CRATE_VERSION, "config-xyz")
);
assert_eq!(
base,
compute_ir_key_for_version(version, IR_SCHEMA, SOURCES_HASH, CRATE_VERSION, CONFIG_HASH),
"the IR key must be deterministic within one alef build"
);
}
#[test]
fn compute_ir_key_uses_the_compiled_in_alef_version() {
assert_eq!(
compute_ir_key(IR_SCHEMA, SOURCES_HASH, CRATE_VERSION, CONFIG_HASH),
compute_ir_key_for_version(
env!("CARGO_PKG_VERSION"),
IR_SCHEMA,
SOURCES_HASH,
CRATE_VERSION,
CONFIG_HASH
)
);
}
#[test]
fn lang_hash_changes_when_the_alef_version_changes() {
let before = compute_lang_hash_for_version("0.62.12", IR, "node", CONFIG);
let after = compute_lang_hash_for_version("0.64.0", IR, "node", CONFIG);
assert_ne!(
before, after,
"a new alef release must invalidate the per-language cache"
);
}
#[test]
fn stage_hash_still_separates_stages_and_inputs_within_one_version() {
let version = "0.64.0";
let base = compute_stage_hash_for_version(version, IR, "stubs", CONFIG, &[]);
assert_ne!(base, compute_stage_hash_for_version(version, IR, "docs", CONFIG, &[]));
assert_ne!(
base,
compute_stage_hash_for_version(version, r#"{"crate_name":"other"}"#, "stubs", CONFIG, &[])
);
assert_ne!(
base,
compute_stage_hash_for_version(version, IR, "stubs", "[workspace]\n", &[])
);
assert_ne!(
base,
compute_stage_hash_for_version(version, IR, "stubs", CONFIG, b"fixture")
);
}
#[test]
fn hashes_are_deterministic_for_one_version() {
assert_eq!(
compute_stage_hash_for_version("0.64.0", IR, "stubs", CONFIG, &[]),
compute_stage_hash_for_version("0.64.0", IR, "stubs", CONFIG, &[])
);
assert_eq!(
compute_lang_hash_for_version("0.64.0", IR, "node", CONFIG),
compute_lang_hash_for_version("0.64.0", IR, "node", CONFIG)
);
}
#[test]
fn public_entry_points_use_the_compiled_in_alef_version() {
assert_eq!(
compute_stage_hash(IR, "stubs", CONFIG, &[]),
compute_stage_hash_for_version(env!("CARGO_PKG_VERSION"), IR, "stubs", CONFIG, &[])
);
assert_eq!(
compute_lang_hash(IR, "node", CONFIG),
compute_lang_hash_for_version(env!("CARGO_PKG_VERSION"), IR, "node", CONFIG)
);
}
#[test]
fn embedded_inputs_hash_is_unaffected_by_cache_key_identity() {
let cases: [(&str, &[u8], &str); 3] = [
(
"sources-abc",
b"[workspace]\nlanguages = [\"node\"]\n",
"b87e401397681a7347a51d94496237d2ea3cb721f42e5b136a98e86f2c1f8ecb",
),
(
"sources-abc",
b"",
"e2404a2956e25be55cd89779670638ce19288a7ae787a693cc97e984fb1d4de9",
),
(
"",
b"[workspace]\n",
"911e97c6dce240140b3c74c57ed23a41535202f33c586a6f6101d3b7f8b4623d",
),
];
for (sources_hash, toml_bytes, expected) in cases {
assert_eq!(
crate::core::hash::compute_inputs_hash(sources_hash, toml_bytes),
expected,
"compute_inputs_hash drifted for sources_hash={sources_hash:?}; the embedded \
alef:hash: value must not change when cache-key inputs change"
);
}
}
#[test]
fn alef_version_pin_moves_cache_keys_but_not_the_embedded_inputs_hash() {
let pinned_old = b"[workspace]\nlanguages = [\"node\"]\nalef_version = \"0.62.12\"\n";
let pinned_new = b"[workspace]\nlanguages = [\"node\"]\nalef_version = \"0.64.0\"\n";
assert_eq!(
crate::core::hash::compute_inputs_hash("sources-abc", pinned_old),
crate::core::hash::compute_inputs_hash("sources-abc", pinned_new),
"bumping the alef_version pin must not restamp generated files"
);
let version = "0.64.0";
assert_ne!(
compute_stage_hash_for_version(version, IR, "stubs", &String::from_utf8_lossy(pinned_old), &[]),
compute_stage_hash_for_version(version, IR, "stubs", &String::from_utf8_lossy(pinned_new), &[]),
"the cache keys hash raw alef.toml text, so a pin bump must still invalidate them"
);
}
}