a42429012e
- 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/)
165 lines
5.5 KiB
Rust
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"));
|
|
}
|
|
}
|