Files
el/engrams/el-build/src/cache.rs
T
Will Anderson a42429012e rename crates/ to engrams/; add el-compiler el package with bootstrap artifact
- crates/ → engrams/ (Rust engrams live here)
- el-compiler/ added: el self-hosting compiler as an el package
  - src/{compiler,lexer,parser,codegen}.el
  - bootstrap/el-compiler.elc (114KB, Rust-compiled seed)
- el.toml Cargo.toml workspace paths updated
- neuron-rs cross-repo path deps fixed (were pointing to products/ instead of foundation/)
2026-04-29 03:27:32 -05:00

165 lines
5.5 KiB
Rust

//! Incremental build cache.
//!
//! Stores BLAKE3 hashes of source files in `.el/build-cache.json` so the
//! build system can skip recompiling files that haven't changed.
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
/// The on-disk structure of the build cache.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct BuildCache {
/// Map of file path (relative to workspace root) → BLAKE3 hex hash.
pub file_hashes: HashMap<String, String>,
}
impl BuildCache {
/// Load the build cache from `.el/build-cache.json`.
///
/// Returns an empty cache if the file doesn't exist yet.
pub fn load(workspace_root: &Path) -> Self {
let path = cache_path(workspace_root);
if !path.exists() {
return Self::default();
}
let text = std::fs::read_to_string(&path).unwrap_or_default();
serde_json::from_str(&text).unwrap_or_default()
}
/// Persist the cache back to disk.
pub fn save(&self, workspace_root: &Path) -> Result<(), std::io::Error> {
let path = cache_path(workspace_root);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(self)?;
std::fs::write(&path, json)
}
/// Hash a single file and return the BLAKE3 hex string.
pub fn hash_file(path: &Path) -> Result<String, std::io::Error> {
let bytes = std::fs::read(path)?;
Ok(hex_encode(blake3::hash(&bytes).as_bytes()))
}
/// Returns `true` if the file's current hash matches the cached hash.
pub fn is_up_to_date(&self, rel_path: &str, current_hash: &str) -> bool {
self.file_hashes
.get(rel_path)
.map(|cached| cached == current_hash)
.unwrap_or(false)
}
/// Update the cached hash for a file.
pub fn record(&mut self, rel_path: impl Into<String>, hash: impl Into<String>) {
self.file_hashes.insert(rel_path.into(), hash.into());
}
/// Collect all `.el` source files under a directory recursively.
pub fn collect_sources(root: &Path) -> Vec<PathBuf> {
let mut sources = Vec::new();
collect_el_files(root, &mut sources);
sources.sort();
sources
}
}
fn cache_path(workspace_root: &Path) -> PathBuf {
workspace_root.join(".el").join("build-cache.json")
}
fn collect_el_files(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
// Skip hidden dirs and build outputs
let name = path.file_name().unwrap_or_default().to_string_lossy();
if name.starts_with('.') || name == "dist" || name == "target" {
continue;
}
collect_el_files(&path, out);
} else if path.extension().map(|e| e == "el").unwrap_or(false) {
out.push(path);
}
}
}
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::TempDir;
fn temp_dir() -> TempDir {
tempfile::TempDir::new().unwrap()
}
#[test]
fn test_cache_empty_by_default() {
let dir = temp_dir();
let cache = BuildCache::load(dir.path());
assert!(cache.file_hashes.is_empty());
}
#[test]
fn test_cache_save_and_load() {
let dir = temp_dir();
let mut cache = BuildCache::default();
cache.record("src/main.el", "abc123");
cache.save(dir.path()).unwrap();
let loaded = BuildCache::load(dir.path());
assert_eq!(loaded.file_hashes.get("src/main.el").map(|s| s.as_str()), Some("abc123"));
}
#[test]
fn test_is_up_to_date() {
let mut cache = BuildCache::default();
cache.record("src/main.el", "deadbeef");
assert!(cache.is_up_to_date("src/main.el", "deadbeef"));
assert!(!cache.is_up_to_date("src/main.el", "different"));
assert!(!cache.is_up_to_date("src/other.el", "deadbeef"));
}
#[test]
fn test_hash_file() {
let dir = temp_dir();
let path = dir.path().join("test.el");
std::fs::write(&path, b"let x = 1").unwrap();
let hash1 = BuildCache::hash_file(&path).unwrap();
let hash2 = BuildCache::hash_file(&path).unwrap();
assert_eq!(hash1, hash2); // deterministic
std::fs::write(&path, b"let x = 2").unwrap();
let hash3 = BuildCache::hash_file(&path).unwrap();
assert_ne!(hash1, hash3); // different content → different hash
}
#[test]
fn test_collect_sources() {
let dir = temp_dir();
let src = dir.path().join("src");
std::fs::create_dir(&src).unwrap();
std::fs::write(src.join("main.el"), b"fn main() {}").unwrap();
std::fs::write(src.join("lib.el"), b"fn helper() {}").unwrap();
std::fs::write(src.join("README.md"), b"# README").unwrap();
let sources = BuildCache::collect_sources(dir.path());
assert_eq!(sources.len(), 2);
assert!(sources.iter().any(|p| p.file_name().unwrap() == "main.el"));
assert!(sources.iter().any(|p| p.file_name().unwrap() == "lib.el"));
}
}