#[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 upgrading_the_alef_version_turns_a_real_cache_hit_into_a_miss() {
let tmp = tempfile::tempdir().expect("tempdir");
let _cwd = crate::test_support::CwdGuard::enter(tmp.path());
assert_ir_cache_miss_after_version_bump();
assert_lang_cache_miss_after_version_bump(tmp.path());
assert_stage_cache_miss_after_version_bump(tmp.path());
}
fn assert_ir_cache_miss_after_version_bump() {
let api = crate::core::ir::ApiSurface {
crate_name: "sample-crate".to_string(),
..Default::default()
};
let old_key = compute_ir_key_for_version("0.62.12", IR_SCHEMA, SOURCES_HASH, CRATE_VERSION, CONFIG_HASH);
crate::cli::cache::write_ir_cache("sample-crate", &api, &old_key).expect("write ir cache");
assert!(
crate::cli::cache::is_ir_cached("sample-crate", &old_key),
"querying with the version that wrote the cache must still be a hit"
);
let new_key = compute_ir_key_for_version("0.64.0", IR_SCHEMA, SOURCES_HASH, CRATE_VERSION, CONFIG_HASH);
assert!(
!crate::cli::cache::is_ir_cached("sample-crate", &new_key),
"a newer alef querying the same on-disk IR cache for byte-identical extraction \
inputs must be a miss, or it replays a previous release's ApiSurface verbatim"
);
}
fn assert_lang_cache_miss_after_version_bump(root: &std::path::Path) {
let generated = root.join("bindings.py");
std::fs::write(&generated, "# generated\n").expect("write generated output");
let old_key = compute_lang_hash_for_version("0.62.12", IR, "python", CONFIG);
crate::cli::cache::write_lang_hash("sample-crate", "python", &old_key, &[generated])
.expect("write lang hash and manifest");
assert!(
crate::cli::cache::is_lang_cached("sample-crate", "python", &old_key),
"querying with the version that wrote the cache must still be a hit"
);
let new_key = compute_lang_hash_for_version("0.64.0", IR, "python", CONFIG);
assert!(
!crate::cli::cache::is_lang_cached("sample-crate", "python", &new_key),
"a newer alef querying the same on-disk language cache for byte-identical inputs \
must be a miss, or `alef generate` reports the language up to date and skips it"
);
}
fn assert_stage_cache_miss_after_version_bump(root: &std::path::Path) {
let output = root.join("README.md");
std::fs::write(&output, "generated readme\n").expect("write stage output");
let old_key = compute_stage_hash_for_version("0.62.12", IR, "readme", CONFIG, &[]);
crate::cli::cache::write_stage_hash("sample-crate", "readme", old_key.as_str(), &[output])
.expect("write stage hash and manifest");
assert!(
crate::cli::cache::is_stage_cached("sample-crate", "readme", &old_key),
"querying with the version that wrote the cache must still be a hit"
);
let new_key = compute_stage_hash_for_version("0.64.0", IR, "readme", CONFIG, &[]);
assert!(
!crate::cli::cache::is_stage_cached("sample-crate", "readme", &new_key),
"a newer alef querying the same on-disk stage cache must be a miss, or `alef generate` \
reports the stage up to date and skips it"
);
}
#[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",
"5bec01d0c5156f7339c8e1dd14b9245fbf791933156ec252cab9bce3ae8e0e39",
),
(
"sources-abc",
b"",
"6236d147c8bf5653a1aef09d70abdd3ecb1f0cb2e27056b0f03d277c3310bc00",
),
(
"",
b"[workspace]\n",
"8ada6034d43c7dbb07d46e8194b70cc5cc9ad4c3db53342d1bc80d76d7487f2f",
),
];
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"
);
}
}