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/)
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
//! 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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user