use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use serde::{Deserialize, Serialize};
use anyhow::Result;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildCache {
pub project_root: PathBuf,
#[serde(skip)]
pub entries: HashMap<PathBuf, CacheEntry>,
pub classpath_dependencies: HashMap<PathBuf, Vec<PathBuf>>,
pub generated_files: HashMap<PathBuf, GeneratedFile>,
pub last_updated: SystemTime,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheEntry {
pub source_path: PathBuf,
pub source_modified: SystemTime,
pub output_path: PathBuf,
pub dependencies: Vec<PathBuf>,
pub checksum: String,
pub success: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneratedFile {
pub generator: String,
pub sources: Vec<PathBuf>,
pub output_path: PathBuf,
pub checksum: String,
}
impl BuildCache {
pub fn new(project_root: PathBuf) -> Self {
Self {
project_root,
entries: HashMap::new(),
classpath_dependencies: HashMap::new(),
generated_files: HashMap::new(),
last_updated: SystemTime::now(),
}
}
pub fn load(project_root: &Path) -> Result<Self> {
let cache_path = project_root.join(".jbuild").join("cache.json");
if cache_path.exists() {
let content = std::fs::read_to_string(&cache_path)?;
let mut cache: BuildCache = serde_json::from_str(&content)?;
cache.entries = HashMap::new(); Ok(cache)
} else {
Ok(Self::new(project_root.to_path_buf()))
}
}
pub fn save(&self) -> Result<()> {
let cache_dir = self.project_root.join(".jbuild");
std::fs::create_dir_all(&cache_dir)?;
let cache_path = cache_dir.join("cache.json");
let content = serde_json::to_string_pretty(self)?;
std::fs::write(&cache_path, content)?;
Ok(())
}
pub fn needs_recompilation(&self, source_path: &Path) -> bool {
if let Some(entry) = self.entries.get(source_path) {
if let Ok(metadata) = std::fs::metadata(source_path) {
if let Ok(modified) = metadata.modified() {
if modified > entry.source_modified {
return true;
}
}
}
for dep in &entry.dependencies {
if self.needs_recompilation(dep) {
return true;
}
}
false
} else {
true }
}
pub fn update_entry(&mut self, source_path: PathBuf, entry: CacheEntry) {
self.entries.insert(source_path, entry);
self.last_updated = SystemTime::now();
}
pub fn invalidate(&mut self, source_path: &Path) {
let source_pathbuf = source_path.to_path_buf();
let dependents: Vec<_> = self
.entries
.iter()
.filter(|(_, entry)| entry.dependencies.contains(&source_pathbuf))
.map(|(path, _)| path.clone())
.collect();
self.entries.remove(source_path);
for dependent in dependents {
self.invalidate(&dependent);
}
}
pub fn get_stale_sources(&self, sources: &[PathBuf]) -> Vec<PathBuf> {
sources
.iter()
.filter(|p| self.needs_recompilation(p))
.cloned()
.collect()
}
pub fn clean(&mut self) {
let mut to_remove = Vec::new();
for path in self.entries.keys() {
if !path.exists() {
to_remove.push(path.clone());
}
}
for path in to_remove {
self.entries.remove(&path);
}
}
pub fn calculate_checksum(path: &Path) -> Result<String> {
use sha2::{Sha256, Digest};
let content = std::fs::read(path)?;
let mut hasher = Sha256::new();
hasher.update(&content);
Ok(format!("{:x}", hasher.finalize()))
}
pub fn add_classpath_dependency(&mut self, source: PathBuf, dependency: PathBuf) {
self.classpath_dependencies
.entry(source)
.or_default()
.push(dependency);
}
pub fn classpath_changed(&self, source_path: &Path) -> bool {
if let Some(deps) = self.classpath_dependencies.get(source_path) {
deps.iter().any(|dep| self.needs_recompilation(dep))
} else {
false
}
}
pub fn stats(&self) -> CacheStats {
let total_entries = self.entries.len();
let valid_entries = self.entries.values().filter(|e| e.success).count();
let generated_count = self.generated_files.len();
CacheStats {
total_entries,
valid_entries,
hit_rate: if total_entries > 0 {
(valid_entries as f64 / total_entries as f64 * 100.0) as u32
} else {
0
},
generated_count,
}
}
}
#[derive(Debug, Clone)]
pub struct CacheStats {
pub total_entries: usize,
pub valid_entries: usize,
pub hit_rate: u32,
pub generated_count: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_cache_creation() {
let cache = BuildCache::new(PathBuf::from("/project"));
assert_eq!(cache.project_root, PathBuf::from("/project"));
assert_eq!(cache.entries.len(), 0);
}
#[test]
fn test_needs_recompilation_no_cache() {
let cache = BuildCache::new(PathBuf::from("/project"));
assert!(cache.needs_recompilation(Path::new("test.java")));
}
}