use std::collections::HashMap;
use std::path::Path;
use std::time::SystemTime;
use serde::{Deserialize, Serialize};
use anyhow::Result;
use sha2::{Sha256, Digest};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersistentBuildCache {
pub version: String,
pub project_id: String,
pub compilation_cache: HashMap<String, CompilationEntry>,
pub dependency_cache: HashMap<String, DependencyEntry>,
pub test_cache: HashMap<String, TestEntry>,
pub last_updated: SystemTime,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompilationEntry {
pub source_hash: String,
pub output_hash: String,
pub dependencies: Vec<String>,
pub compiled_at: SystemTime,
pub compiler_version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyEntry {
pub artifact_id: String,
pub version: String,
pub resolved_version: String,
pub transitive_deps: Vec<String>,
pub resolved_at: SystemTime,
pub checksum: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestEntry {
pub test_class: String,
pub source_hash: String,
pub last_result: TestResult,
pub executed_at: SystemTime,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TestResult {
Passed,
Failed { message: String },
Skipped,
}
impl PersistentBuildCache {
const CACHE_VERSION: &'static str = "1.0.0";
pub fn new(project_id: String) -> Self {
Self {
version: Self::CACHE_VERSION.to_string(),
project_id,
compilation_cache: HashMap::new(),
dependency_cache: HashMap::new(),
test_cache: HashMap::new(),
last_updated: SystemTime::now(),
}
}
pub fn load(cache_dir: &Path, project_id: &str) -> Result<Self> {
let cache_file = cache_dir.join(format!("{}.cache", project_id));
if cache_file.exists() {
let content = std::fs::read_to_string(&cache_file)?;
let cache: PersistentBuildCache = serde_json::from_str(&content)?;
if cache.version != Self::CACHE_VERSION {
tracing::warn!(
"Cache version mismatch: {} vs {}, creating new cache",
cache.version,
Self::CACHE_VERSION
);
return Ok(Self::new(project_id.to_string()));
}
Ok(cache)
} else {
Ok(Self::new(project_id.to_string()))
}
}
pub fn save(&self, cache_dir: &Path) -> Result<()> {
std::fs::create_dir_all(cache_dir)?;
let cache_file = cache_dir.join(format!("{}.cache", self.project_id));
let content = serde_json::to_string_pretty(self)?;
std::fs::write(&cache_file, content)?;
Ok(())
}
pub fn needs_compilation(&self, source_path: &Path) -> bool {
let source_hash = match Self::hash_file(source_path) {
Ok(hash) => hash,
Err(_) => return true,
};
if let Some(entry) = self.compilation_cache.get(&source_hash) {
for dep in &entry.dependencies {
if self.compilation_cache.get(dep).is_none() {
return true;
}
}
false
} else {
true
}
}
pub fn add_compilation(
&mut self,
source_path: &Path,
output_path: &Path,
dependencies: Vec<String>,
compiler_version: String,
) -> Result<()> {
let source_hash = Self::hash_file(source_path)?;
let output_hash = Self::hash_file(output_path)?;
self.compilation_cache.insert(
source_hash.clone(),
CompilationEntry {
source_hash,
output_hash,
dependencies,
compiled_at: SystemTime::now(),
compiler_version,
},
);
self.last_updated = SystemTime::now();
Ok(())
}
pub fn get_cached_dependency(&self, artifact_id: &str, version: &str) -> Option<&DependencyEntry> {
let key = format!("{}:{}", artifact_id, version);
self.dependency_cache.get(&key)
}
pub fn cache_dependency(
&mut self,
artifact_id: String,
version: String,
resolved_version: String,
transitive_deps: Vec<String>,
checksum: String,
) {
let key = format!("{}:{}", artifact_id, version);
self.dependency_cache.insert(
key,
DependencyEntry {
artifact_id,
version,
resolved_version,
transitive_deps,
resolved_at: SystemTime::now(),
checksum,
},
);
self.last_updated = SystemTime::now();
}
pub fn can_skip_test(&self, test_class: &str, source_hash: &str) -> bool {
if let Some(entry) = self.test_cache.get(test_class) {
entry.source_hash == source_hash && matches!(entry.last_result, TestResult::Passed)
} else {
false
}
}
pub fn cache_test_result(&mut self, test_class: String, source_hash: String, result: TestResult) {
self.test_cache.insert(
test_class.clone(),
TestEntry {
test_class,
source_hash,
last_result: result,
executed_at: SystemTime::now(),
},
);
self.last_updated = SystemTime::now();
}
pub fn clean_stale(&mut self, max_age_days: u64) {
let now = SystemTime::now();
let max_age = std::time::Duration::from_secs(max_age_days * 24 * 60 * 60);
self.compilation_cache.retain(|_, entry| {
now.duration_since(entry.compiled_at)
.map(|d| d < max_age)
.unwrap_or(false)
});
self.dependency_cache.retain(|_, entry| {
now.duration_since(entry.resolved_at)
.map(|d| d < max_age)
.unwrap_or(false)
});
self.test_cache.retain(|_, entry| {
now.duration_since(entry.executed_at)
.map(|d| d < max_age)
.unwrap_or(false)
});
}
pub fn hash_file(path: &Path) -> Result<String> {
let content = std::fs::read(path)?;
let mut hasher = Sha256::new();
hasher.update(&content);
Ok(format!("{:x}", hasher.finalize()))
}
pub fn stats(&self) -> CacheStatistics {
CacheStatistics {
compilation_entries: self.compilation_cache.len(),
dependency_entries: self.dependency_cache.len(),
test_entries: self.test_cache.len(),
total_size_estimate: self.estimate_size(),
}
}
fn estimate_size(&self) -> usize {
self.compilation_cache.len() * 256
+ self.dependency_cache.len() * 512
+ self.test_cache.len() * 128
}
}
#[derive(Debug, Clone)]
pub struct CacheStatistics {
pub compilation_entries: usize,
pub dependency_entries: usize,
pub test_entries: usize,
pub total_size_estimate: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_creation() {
let cache = PersistentBuildCache::new("test-project".to_string());
assert_eq!(cache.project_id, "test-project");
assert_eq!(cache.version, PersistentBuildCache::CACHE_VERSION);
}
#[test]
fn test_cache_dependency() {
let mut cache = PersistentBuildCache::new("test".to_string());
cache.cache_dependency(
"org.example:lib".to_string(),
"1.0.0".to_string(),
"1.0.0".to_string(),
vec![],
"abc123".to_string(),
);
let entry = cache.get_cached_dependency("org.example:lib", "1.0.0");
assert!(entry.is_some());
}
}