use anyhow::Result;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
const FINGERPRINT_FILE: &str = "fingerprints.json";
mod tag {
pub const SRC: &[u8] = b"src:";
pub const DEP: &[u8] = b"dep:";
pub const MVN: &[u8] = b"mvn:";
pub const CP: &[u8] = b"cp:";
pub const AP: &[u8] = b"ap:";
pub const VER: &[u8] = b"ver:";
pub const ENC: &[u8] = b"enc:";
pub const LINT: &[u8] = b"lint:";
pub const ARG: &[u8] = b"arg:";
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Fingerprints {
entries: HashMap<String, FileEntry>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct FileEntry {
pub source_hash: String,
pub abi_hash: Option<String>,
pub mtime_secs: u64,
}
impl Fingerprints {
pub fn load(cache_dir: &Path) -> Self {
let path = cache_dir.join(FINGERPRINT_FILE);
if let Ok(content) = std::fs::read_to_string(&path) {
serde_json::from_str(&content).unwrap_or_default()
} else {
Self::default()
}
}
pub fn save(&self, cache_dir: &Path) -> Result<()> {
std::fs::create_dir_all(cache_dir)?;
let path = cache_dir.join(FINGERPRINT_FILE);
let content = serde_json::to_string(self)?;
std::fs::write(path, content)?;
Ok(())
}
pub fn get_changed_files(&self, source_dirs: &[PathBuf]) -> Result<(Vec<PathBuf>, Vec<PathBuf>)> {
let mut changed = Vec::new();
let mut all = Vec::new();
for (path, rel_key, mtime) in walk_java_files(source_dirs)? {
all.push(path.clone());
if let Some(existing) = self.entries.get(&rel_key) {
if existing.mtime_secs == mtime {
continue;
}
if hash_file(&path)? == existing.source_hash {
continue;
}
}
changed.push(path);
}
Ok((changed, all))
}
pub fn update_source(&mut self, path: &Path, source_hash: &str, mtime_secs: u64) {
let key = crate::normalize_cache_path(path);
let entry = self.entries.entry(key).or_insert_with(|| FileEntry {
source_hash: String::new(),
abi_hash: None,
mtime_secs: 0,
});
entry.source_hash = source_hash.to_string();
entry.mtime_secs = mtime_secs;
}
pub fn update_abi(&mut self, source_path: &Path, abi_hash: &str) {
let key = crate::normalize_cache_path(source_path);
if let Some(entry) = self.entries.get_mut(&key) {
entry.abi_hash = Some(abi_hash.to_string());
}
}
#[allow(dead_code)]
pub fn abi_changed(&self, source_path: &Path, new_abi_hash: &str) -> bool {
let key = crate::normalize_cache_path(source_path);
match self.entries.get(&key) {
Some(entry) => entry.abi_hash.as_deref() != Some(new_abi_hash),
None => true,
}
}
pub fn prune(&mut self, existing_files: &[PathBuf]) -> Vec<String> {
let existing_keys: std::collections::HashSet<String> = existing_files
.iter()
.map(|p| crate::normalize_cache_path(p))
.collect();
let removed: Vec<String> = self.entries.keys()
.filter(|k| !existing_keys.contains(k.as_str()))
.cloned()
.collect();
self.entries.retain(|k, _| existing_keys.contains(k));
removed
}
}
fn file_mtime_secs(entry: &walkdir::DirEntry) -> u64 {
entry
.metadata()
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0)
}
fn aggregate_abi_from_fingerprints(fingerprints: &Fingerprints) -> String {
let mut entries: Vec<(&str, &str)> = fingerprints
.entries
.iter()
.filter_map(|(k, e)| e.abi_hash.as_deref().map(|h| (k.as_str(), h)))
.collect();
entries.sort_by(|a, b| a.0.cmp(&b.0));
let mut hasher = Sha256::new();
for (key, abi) in &entries {
hasher.update(key.as_bytes());
hasher.update(abi.as_bytes());
}
format!("{:x}", hasher.finalize())
}
fn cache_timestamp() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
fn walk_java_files(source_dirs: &[PathBuf]) -> Result<Vec<(PathBuf, String, u64)>> {
let mut files = Vec::new();
for src_dir in source_dirs {
if !src_dir.exists() {
continue;
}
for entry in walkdir::WalkDir::new(src_dir) {
let entry = entry?;
if entry.path().extension().and_then(|e| e.to_str()) != Some("java") {
continue;
}
let path = entry.path().to_path_buf();
let rel_key = crate::normalize_cache_path(&path);
let mtime = file_mtime_secs(&entry);
files.push((path, rel_key, mtime));
}
}
Ok(files)
}
pub fn hash_file(path: &Path) -> Result<String> {
let content = std::fs::read(path)?;
Ok(hash_bytes(&content))
}
pub fn hash_bytes(data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
format!("{:x}", hasher.finalize())
}
pub fn compute_class_abi_hash(class_file: &Path) -> Result<String> {
let data = std::fs::read(class_file)?;
match extract_abi_bytes(&data) {
Some(abi) => Ok(hash_bytes(&abi)),
None => Ok(hash_bytes(&data)), }
}
fn extract_abi_bytes(data: &[u8]) -> Option<Vec<u8>> {
let len = data.len();
if len < 10 {
return None;
}
if data[0..4] != [0xCA, 0xFE, 0xBA, 0xBE] {
return None;
}
let mut abi = Vec::with_capacity(len);
abi.extend_from_slice(&data[0..8]);
let mut pos = 8;
let cp_count = read_u16(data, pos)? as usize;
abi.extend_from_slice(&data[pos..pos + 2]);
pos += 2;
let cp_start = pos;
let mut i = 1; while i < cp_count {
if pos >= len {
return None;
}
let tag = data[pos];
match tag {
1 => {
if pos + 3 > len {
return None;
}
let str_len = read_u16(data, pos + 1)? as usize;
pos += 3 + str_len;
}
3 | 4 => pos += 5, 5 | 6 => {
pos += 9; i += 1;
}
7 | 8 | 16 | 19 | 20 => pos += 3, 9 | 10 | 11 | 12 | 17 | 18 => pos += 5, 15 => pos += 4, _ => return None, }
i += 1;
}
abi.extend_from_slice(&data[cp_start..pos]);
if pos + 6 > len {
return None;
}
abi.extend_from_slice(&data[pos..pos + 6]);
pos += 6;
if pos + 2 > len {
return None;
}
let iface_count = read_u16(data, pos)? as usize;
let iface_bytes = 2 + iface_count * 2;
if pos + iface_bytes > len {
return None;
}
abi.extend_from_slice(&data[pos..pos + iface_bytes]);
pos += iface_bytes;
pos = extract_members_abi(data, pos, &mut abi, false)?;
pos = extract_members_abi(data, pos, &mut abi, true)?;
if pos + 2 <= len {
abi.extend_from_slice(&data[pos..len.min(pos + (len - pos))]);
}
Some(abi)
}
const ACC_PRIVATE: u16 = 0x0002;
fn extract_members_abi(data: &[u8], mut pos: usize, abi: &mut Vec<u8>, skip_code: bool) -> Option<usize> {
let len = data.len();
if pos + 2 > len {
return None;
}
let count = read_u16(data, pos)? as usize;
let count_pos = abi.len();
abi.extend_from_slice(&[0, 0]); pos += 2;
let mut included_count: u16 = 0;
for _ in 0..count {
if pos + 8 > len {
return None;
}
let access_flags = read_u16(data, pos)?;
let _name_idx = read_u16(data, pos + 2)?;
let _desc_idx = read_u16(data, pos + 4)?;
let attr_count = read_u16(data, pos + 6)? as usize;
let is_private = (access_flags & ACC_PRIVATE) != 0;
if !is_private {
abi.extend_from_slice(&data[pos..pos + 6]);
included_count += 1;
}
pos += 8;
let attr_count_pos = abi.len();
if !is_private {
abi.extend_from_slice(&[0, 0]); }
let mut included_attrs: u16 = 0;
for _ in 0..attr_count {
if pos + 6 > len {
return None;
}
let attr_name_idx = read_u16(data, pos)?;
let attr_len = read_u32(data, pos + 2)? as usize;
let attr_end = pos + 6 + attr_len;
if attr_end > len {
return None;
}
if !is_private {
let is_code_attr = skip_code && is_utf8_constant(data, attr_name_idx, b"Code");
if !is_code_attr {
abi.extend_from_slice(&data[pos..attr_end]);
included_attrs += 1;
}
}
pos = attr_end;
}
if !is_private {
abi[attr_count_pos] = (included_attrs >> 8) as u8;
abi[attr_count_pos + 1] = (included_attrs & 0xFF) as u8;
}
}
abi[count_pos] = (included_count >> 8) as u8;
abi[count_pos + 1] = (included_count & 0xFF) as u8;
Some(pos)
}
fn is_utf8_constant(data: &[u8], target_idx: u16, expected: &[u8]) -> bool {
if data.len() < 10 {
return false;
}
let cp_count = match read_u16(data, 8) {
Some(c) => c as usize,
None => return false,
};
let mut pos = 10;
let mut idx: u16 = 1;
while (idx as usize) < cp_count && pos < data.len() {
let tag = data[pos];
if idx == target_idx {
if tag == 1 {
if let Some(str_len) = read_u16(data, pos + 1) {
let str_start = pos + 3;
let str_end = str_start + str_len as usize;
if str_end <= data.len() {
return &data[str_start..str_end] == expected;
}
}
}
return false;
}
match tag {
1 => {
let str_len = read_u16(data, pos + 1).unwrap_or(0) as usize;
pos += 3 + str_len;
}
3 | 4 => pos += 5,
5 | 6 => {
pos += 9;
idx += 1;
}
7 | 8 | 16 | 19 | 20 => pos += 3,
9 | 10 | 11 | 12 | 17 | 18 => pos += 5,
15 => pos += 4,
_ => return false,
}
idx += 1;
}
false
}
fn read_u16(data: &[u8], pos: usize) -> Option<u16> {
if pos + 2 > data.len() {
return None;
}
Some(((data[pos] as u16) << 8) | data[pos + 1] as u16)
}
fn read_u32(data: &[u8], pos: usize) -> Option<u32> {
if pos + 4 > data.len() {
return None;
}
Some(
((data[pos] as u32) << 24)
| ((data[pos + 1] as u32) << 16)
| ((data[pos + 2] as u32) << 8)
| data[pos + 3] as u32,
)
}
pub fn incremental_compile(
config: &super::CompileConfig,
cache_dir: &Path,
pool: Option<&super::worker::CompilerPool>,
) -> Result<super::CompileResult> {
let fp_dir = fingerprint_dir_for(cache_dir, &config.output_dir);
let mut fingerprints = Fingerprints::load(&fp_dir);
let (changed, all_files) = fingerprints.get_changed_files(&config.source_dirs)?;
let has_classes = config.output_dir.exists()
&& std::fs::read_dir(&config.output_dir)
.map(|mut d| d.next().is_some())
.unwrap_or(false);
let files_to_compile = if !has_classes {
if !fingerprints.entries.is_empty() {
fingerprints.entries.clear();
fingerprints.save(&fp_dir);
}
if !all_files.is_empty() {
if let Some(result) = try_restore_build_cache(config, &all_files, &mut fingerprints, &fp_dir)? {
return Ok(result);
}
}
all_files.clone()
} else if changed.is_empty() {
let missing: Vec<PathBuf> = all_files
.iter()
.filter(|src| {
if find_class_for_source(src, &config.source_dirs, &config.output_dir).is_some() {
return false; }
let key = crate::normalize_cache_path(src);
!fingerprints.entries.contains_key(&key)
})
.cloned()
.collect();
if missing.is_empty() {
return Ok(super::CompileResult {
success: true,
outcome: super::CompileOutcome::UpToDate,
errors: String::new(),
module_abi_hash: Some(aggregate_abi_from_fingerprints(&fingerprints)),
});
}
missing
} else {
changed.clone()
};
let mut classpath = config.classpath.clone();
if has_classes && !classpath.contains(&config.output_dir) {
classpath.push(config.output_dir.clone());
}
let incremental_config = super::CompileConfig {
source_dirs: Vec::new(), output_dir: config.output_dir.clone(),
classpath,
java_version: config.java_version.clone(),
encoding: config.encoding.clone(),
annotation_processors: config.annotation_processors.clone(),
lint: config.lint.clone(),
extra_args: config.extra_args.clone(),
};
let result = compile_files(&incremental_config, &files_to_compile, pool)?;
let is_full_compile = files_to_compile.len() == all_files.len();
if result.success {
for file in &files_to_compile {
let hash = hash_file(file).unwrap_or_default();
let mtime = std::fs::metadata(file)
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0);
fingerprints.update_source(file, &hash, mtime);
if let Some(class_file) = find_class_for_source(file, &config.source_dirs, &config.output_dir) {
if let Ok(abi_hash) = compute_class_abi_hash(&class_file) {
fingerprints.update_abi(file, &abi_hash);
}
}
}
let removed = fingerprints.prune(&all_files);
for removed_src in &removed {
let src_path = Path::new(removed_src);
if let Some(class_file) = find_class_for_source(src_path, &config.source_dirs, &config.output_dir) {
let _ = std::fs::remove_file(&class_file);
}
}
fingerprints.save(&fp_dir)?;
if is_full_compile && !files_to_compile.is_empty() {
if let Err(e) = save_build_cache(config, &all_files) {
eprintln!(" Warning: failed to save build cache: {}", e);
}
}
}
let abi = if result.success {
Some(aggregate_abi_from_fingerprints(&fingerprints))
} else {
None
};
Ok(super::CompileResult {
success: result.success,
outcome: if files_to_compile.is_empty() {
super::CompileOutcome::UpToDate
} else {
super::CompileOutcome::Compiled(files_to_compile.len())
},
errors: result.errors,
module_abi_hash: abi,
})
}
fn find_class_for_source(source: &Path, source_dirs: &[PathBuf], output_dir: &Path) -> Option<PathBuf> {
for src_dir in source_dirs {
if let Ok(rel) = source.strip_prefix(src_dir) {
let class_rel = rel.with_extension("class");
let class_file = output_dir.join(class_rel);
if class_file.exists() {
return Some(class_file);
}
}
}
None
}
fn fingerprint_dir_for(cache_dir: &Path, output_dir: &Path) -> PathBuf {
let hash = hash_bytes(crate::normalize_cache_path(output_dir).as_bytes());
cache_dir.join("fingerprints").join(&hash[..16])
}
fn feed_compiler_config(hasher: &mut Sha256, config: &super::CompileConfig) {
if let Some(ref v) = config.java_version {
hasher.update(tag::VER);
hasher.update(v.as_bytes());
}
if let Some(ref e) = config.encoding {
hasher.update(tag::ENC);
hasher.update(e.as_bytes());
}
for l in &config.lint {
hasher.update(tag::LINT);
hasher.update(l.as_bytes());
}
for arg in &config.extra_args {
hasher.update(tag::ARG);
hasher.update(arg.as_bytes());
}
}
fn compute_build_cache_key(config: &super::CompileConfig, source_files: &[PathBuf]) -> Result<String> {
let mut hasher = Sha256::new();
let mut source_hashes: Vec<(String, String)> = Vec::new();
for f in source_files {
let h = hash_file(f)?;
let rel = crate::normalize_cache_path(f);
source_hashes.push((rel, h));
}
source_hashes.sort_by(|a, b| a.0.cmp(&b.0));
for (path, hash) in &source_hashes {
hasher.update(path.as_bytes());
hasher.update(hash.as_bytes());
}
let mut cp: Vec<String> = config.classpath.iter()
.map(|p| crate::normalize_cache_path(p))
.collect();
cp.sort();
for p in &cp {
hasher.update(tag::CP);
hasher.update(p.as_bytes());
}
feed_compiler_config(&mut hasher, config);
for ap in &config.annotation_processors {
hasher.update(tag::AP);
hasher.update(crate::normalize_cache_path(ap).as_bytes());
}
Ok(format!("{:x}", hasher.finalize()))
}
fn build_cache_dir(key: &str) -> PathBuf {
crate::home_dir()
.join(crate::config::CACHE_DIR)
.join(crate::config::BUILD_CACHE_DIR)
.join(key)
}
fn try_restore_build_cache(
config: &super::CompileConfig,
source_files: &[PathBuf],
fingerprints: &mut Fingerprints,
fp_dir: &Path,
) -> Result<Option<super::CompileResult>> {
let key = compute_build_cache_key(config, source_files)?;
let cache_dir = build_cache_dir(&key);
if !cache_dir.exists() {
return Ok(None);
}
std::fs::create_dir_all(&config.output_dir)?;
copy_dir_recursive(&cache_dir, &config.output_dir)?;
for file in source_files {
let hash = hash_file(file).unwrap_or_default();
let mtime = std::fs::metadata(file)
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0);
fingerprints.update_source(file, &hash, mtime);
if let Some(class_file) = find_class_for_source(file, &config.source_dirs, &config.output_dir) {
if let Ok(abi_hash) = compute_class_abi_hash(&class_file) {
fingerprints.update_abi(file, &abi_hash);
}
}
}
fingerprints.save(fp_dir)?;
Ok(Some(super::CompileResult {
success: true,
outcome: super::CompileOutcome::Cached,
errors: String::new(),
module_abi_hash: Some(aggregate_abi_from_fingerprints(&fingerprints)),
}))
}
fn save_build_cache(config: &super::CompileConfig, source_files: &[PathBuf]) -> Result<()> {
let key = compute_build_cache_key(config, source_files)?;
let cache_dir = build_cache_dir(&key);
if cache_dir.exists() {
return Ok(()); }
std::fs::create_dir_all(&cache_dir)?;
copy_dir_recursive(&config.output_dir, &cache_dir)?;
Ok(())
}
fn hardlink_or_copy_dir(src: &Path, dst: &Path) -> Result<()> {
let mut use_hardlink = true;
for entry in walkdir::WalkDir::new(src) {
let entry = entry?;
let rel = entry.path().strip_prefix(src)?;
let dest = dst.join(rel);
if entry.file_type().is_dir() {
std::fs::create_dir_all(&dest)?;
} else {
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?;
}
if use_hardlink {
match std::fs::hard_link(entry.path(), &dest) {
Ok(()) => continue,
Err(_) => {
use_hardlink = false;
std::fs::copy(entry.path(), &dest)?;
}
}
} else {
std::fs::copy(entry.path(), &dest)?;
}
}
}
Ok(())
}
pub fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
for entry in walkdir::WalkDir::new(src) {
let entry = entry?;
let rel = entry.path().strip_prefix(src)?;
let dest = dst.join(rel);
if entry.file_type().is_dir() {
std::fs::create_dir_all(&dest)?;
} else {
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::copy(entry.path(), &dest)?;
}
}
Ok(())
}
fn compile_files(
config: &super::CompileConfig,
files: &[PathBuf],
pool: Option<&super::worker::CompilerPool>,
) -> Result<super::CompileResult> {
if files.is_empty() {
return Ok(super::CompileResult {
success: true,
outcome: super::CompileOutcome::UpToDate,
errors: String::new(),
module_abi_hash: None,
});
}
std::fs::create_dir_all(&config.output_dir)?;
if let Some(pool) = pool {
pool.compile(config, files)
} else {
compile_with_javac(config, files)
}
}
pub fn compile_files_direct(
config: &super::CompileConfig,
files: &[PathBuf],
) -> Result<super::CompileResult> {
if files.is_empty() {
return Ok(super::CompileResult {
success: true,
outcome: super::CompileOutcome::UpToDate,
errors: String::new(),
module_abi_hash: None,
});
}
std::fs::create_dir_all(&config.output_dir)?;
compile_with_javac(config, files)
}
fn compile_with_javac(
config: &super::CompileConfig,
files: &[PathBuf],
) -> Result<super::CompileResult> {
let mut cmd = std::process::Command::new("javac");
cmd.arg("-d").arg(&config.output_dir);
if let Some(ref ver) = config.java_version {
cmd.arg("--release").arg(ver);
}
if let Some(ref enc) = config.encoding {
cmd.arg("-encoding").arg(enc);
}
let _cp_argfile_guard;
if !config.classpath.is_empty() {
let sep = if cfg!(windows) { ";" } else { ":" };
let cp = config
.classpath
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect::<Vec<_>>()
.join(sep);
if cp.len() > 8000 {
let cp_file = config.output_dir.join(".ym-classpath.txt");
std::fs::write(&cp_file, format!("-cp\n{}", cp))?;
cmd.arg(format!("@{}", cp_file.display()));
_cp_argfile_guard = Some(ArgfileCleanup(cp_file));
} else {
_cp_argfile_guard = None;
cmd.arg("-cp").arg(&cp);
}
} else {
_cp_argfile_guard = None;
}
if !config.annotation_processors.is_empty() {
let sep = if cfg!(windows) { ";" } else { ":" };
let ap = config
.annotation_processors
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect::<Vec<_>>()
.join(sep);
cmd.arg("-processorpath").arg(&ap);
} else {
cmd.arg("-proc:none");
}
for lint_opt in &config.lint {
cmd.arg(format!("-Xlint:{}", lint_opt));
}
for arg in &config.extra_args {
cmd.arg(arg);
}
let _argfile_guard;
if files.len() > 50 {
let argfile = config.output_dir.join(".ym-sources.txt");
let content = files
.iter()
.map(|f| f.to_string_lossy().to_string())
.collect::<Vec<_>>()
.join("\n");
std::fs::write(&argfile, &content)?;
cmd.arg(format!("@{}", argfile.display()));
_argfile_guard = Some(ArgfileCleanup(argfile));
} else {
_argfile_guard = None;
for f in files {
cmd.arg(f);
}
}
let output = cmd.output()?;
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
Ok(super::CompileResult {
success: output.status.success(),
outcome: super::CompileOutcome::Compiled(files.len()),
errors: stderr,
module_abi_hash: None,
})
}
pub(crate) struct ArgfileCleanup(pub(crate) PathBuf);
impl Drop for ArgfileCleanup {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
pub fn compute_source_content_hashes(
source_dirs: &[PathBuf],
cache_dir: &Path,
output_dir: &Path,
) -> Result<Vec<(String, String)>> {
let fp_dir = fingerprint_dir_for(cache_dir, output_dir);
let fingerprints = Fingerprints::load(&fp_dir);
let mut hashes: Vec<(String, String)> = Vec::new();
for (path, rel_key, mtime) in walk_java_files(source_dirs)? {
let content_hash = match fingerprints.entries.get(&rel_key) {
Some(e) if e.mtime_secs == mtime => e.source_hash.clone(),
_ => hash_file(&path)?,
};
hashes.push((rel_key, content_hash));
}
hashes.sort_by(|a, b| a.0.cmp(&b.0));
Ok(hashes)
}
pub fn compute_module_abi_hash(output_dir: &Path) -> Result<String> {
let mut entries: Vec<(String, String)> = Vec::new();
if !output_dir.exists() {
return Ok(hash_bytes(b"empty"));
}
for entry in walkdir::WalkDir::new(output_dir) {
let entry = entry?;
if entry.path().extension().and_then(|e| e.to_str()) != Some("class") {
continue;
}
let rel = entry
.path()
.strip_prefix(output_dir)
.unwrap_or(entry.path())
.to_string_lossy()
.to_string();
let abi_hash = compute_class_abi_hash(entry.path()).unwrap_or_else(|_| {
hash_file(entry.path()).unwrap_or_else(|_| hash_bytes(b"unreadable"))
});
entries.push((rel, abi_hash));
}
entries.sort_by(|a, b| a.0.cmp(&b.0));
let mut hasher = Sha256::new();
for (path, hash) in &entries {
hasher.update(path.as_bytes());
hasher.update(hash.as_bytes());
}
Ok(format!("{:x}", hasher.finalize()))
}
pub struct ModuleCacheInput<'a> {
pub source_hashes: &'a [(String, String)],
pub dep_abi_hashes: &'a [(String, String)],
pub maven_jar_sha256s: &'a [(String, String)],
pub config: &'a super::CompileConfig,
pub ap_jar_sha256s: &'a [(String, String)],
}
pub fn compute_module_cache_key(input: &ModuleCacheInput) -> String {
let mut hasher = Sha256::new();
hasher.update(b"v1:");
for (path, hash) in input.source_hashes {
hasher.update(tag::SRC);
hasher.update(path.as_bytes());
hasher.update(hash.as_bytes());
}
for (name, abi) in input.dep_abi_hashes {
hasher.update(tag::DEP);
hasher.update(name.as_bytes());
hasher.update(abi.as_bytes());
}
for (coord, sha) in input.maven_jar_sha256s {
hasher.update(tag::MVN);
hasher.update(coord.as_bytes());
hasher.update(sha.as_bytes());
}
feed_compiler_config(&mut hasher, input.config);
for (path, sha) in input.ap_jar_sha256s {
hasher.update(tag::AP);
hasher.update(path.as_bytes());
hasher.update(sha.as_bytes());
}
format!("{:x}", hasher.finalize())
}
pub fn try_restore_module_cache(
cache_key: &str,
output_dir: &Path,
) -> Result<Option<String>> {
let cache_dir = build_cache_dir(cache_key);
let classes_dir = cache_dir.join("classes");
if !classes_dir.exists() {
return Ok(None);
}
if output_dir.exists() {
let _ = std::fs::remove_dir_all(output_dir);
}
std::fs::create_dir_all(output_dir)?;
copy_dir_recursive(&classes_dir, output_dir)?;
let abi_path = cache_dir.join("abi_hash");
let abi_hash = std::fs::read_to_string(&abi_path)
.map(|s| s.trim().to_string())
.unwrap_or_default();
let meta_path = cache_dir.join("meta.json");
let _ = std::fs::OpenOptions::new().write(true).open(&meta_path);
Ok(Some(abi_hash))
}
pub fn save_module_cache(
cache_key: &str,
output_dir: &Path,
abi_hash: &str,
module_name: &str,
) -> Result<()> {
let cache_dir = build_cache_dir(cache_key);
let classes_dir = cache_dir.join("classes");
if classes_dir.exists() {
return Ok(()); }
std::fs::create_dir_all(&classes_dir)?;
hardlink_or_copy_dir(output_dir, &classes_dir)?;
std::fs::write(cache_dir.join("abi_hash"), abi_hash)?;
let now = cache_timestamp();
let meta = serde_json::json!({
"created_at": now,
"last_accessed": now,
"module": module_name,
});
std::fs::write(cache_dir.join("meta.json"), meta.to_string())?;
Ok(())
}
const CACHE_MAX_AGE_DAYS: u64 = 30;
pub fn evict_stale_build_cache() {
let cache_root = crate::home_dir()
.join(crate::config::CACHE_DIR)
.join(crate::config::BUILD_CACHE_DIR);
let entries = match std::fs::read_dir(&cache_root) {
Ok(e) => e,
Err(_) => return,
};
let cutoff = cache_timestamp().saturating_sub(CACHE_MAX_AGE_DAYS * 86400);
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
if !path.is_dir() {
continue;
}
let meta = path.join("meta.json");
let mtime = std::fs::metadata(&meta)
.or_else(|_| std::fs::metadata(&path))
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0);
if mtime < cutoff {
let _ = std::fs::remove_dir_all(&path);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build_test_class(method_code: &[u8]) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&[0xCA, 0xFE, 0xBA, 0xBE]);
data.extend_from_slice(&[0x00, 0x00, 0x00, 0x34]);
data.extend_from_slice(&[0x00, 0x0B]);
data.push(1);
data.extend_from_slice(&[0x00, 0x04]);
data.extend_from_slice(b"Test");
data.push(7);
data.extend_from_slice(&[0x00, 0x01]);
data.push(1);
data.extend_from_slice(&[0x00, 0x10]);
data.extend_from_slice(b"java/lang/Object");
data.push(7);
data.extend_from_slice(&[0x00, 0x03]);
data.push(1);
data.extend_from_slice(&[0x00, 0x05]);
data.extend_from_slice(b"hello");
data.push(1);
data.extend_from_slice(&[0x00, 0x03]);
data.extend_from_slice(b"()V");
data.push(1);
data.extend_from_slice(&[0x00, 0x04]);
data.extend_from_slice(b"Code");
data.push(1);
data.extend_from_slice(&[0x00, 0x0A]);
data.extend_from_slice(b"SourceFile");
data.push(1);
data.extend_from_slice(&[0x00, 0x09]);
data.extend_from_slice(b"Test.java");
data.push(1);
data.extend_from_slice(&[0x00, 0x0A]);
data.extend_from_slice(b"Exceptions");
data.extend_from_slice(&[0x00, 0x01]);
data.extend_from_slice(&[0x00, 0x02]);
data.extend_from_slice(&[0x00, 0x04]);
data.extend_from_slice(&[0x00, 0x00]);
data.extend_from_slice(&[0x00, 0x00]);
data.extend_from_slice(&[0x00, 0x01]);
data.extend_from_slice(&[0x00, 0x01]);
data.extend_from_slice(&[0x00, 0x05]);
data.extend_from_slice(&[0x00, 0x06]);
data.extend_from_slice(&[0x00, 0x01]);
data.extend_from_slice(&[0x00, 0x07]);
let code_len = method_code.len() as u32 + 12; data.extend_from_slice(&code_len.to_be_bytes());
data.extend_from_slice(&[0x00, 0x01]);
data.extend_from_slice(&[0x00, 0x01]);
data.extend_from_slice(&(method_code.len() as u32).to_be_bytes());
data.extend_from_slice(method_code);
data.extend_from_slice(&[0x00, 0x00]);
data.extend_from_slice(&[0x00, 0x00]);
data.extend_from_slice(&[0x00, 0x00]);
data
}
#[test]
fn test_extract_abi_bytes_valid_class() {
let class_data = build_test_class(&[0xB1]); let abi = extract_abi_bytes(&class_data);
assert!(abi.is_some());
}
#[test]
fn test_extract_abi_bytes_invalid_magic() {
let data = vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
assert!(extract_abi_bytes(&data).is_none());
}
#[test]
fn test_extract_abi_bytes_too_short() {
let data = vec![0xCA, 0xFE, 0xBA, 0xBE];
assert!(extract_abi_bytes(&data).is_none());
}
#[test]
fn test_abi_unchanged_when_method_body_changes() {
let class1 = build_test_class(&[0xB1]); let class2 = build_test_class(&[0x03, 0x57, 0xB1]);
let abi1 = extract_abi_bytes(&class1).unwrap();
let abi2 = extract_abi_bytes(&class2).unwrap();
assert_eq!(hash_bytes(&abi1), hash_bytes(&abi2));
}
#[test]
fn test_abi_changes_when_signature_changes() {
let class1 = build_test_class(&[0xB1]);
let mut class2 = class1.clone();
for i in 0..class2.len() - 2 {
if &class2[i..i + 3] == b"()V" {
class2[i + 2] = b'I';
break;
}
}
let abi1 = extract_abi_bytes(&class1).unwrap();
let abi2 = extract_abi_bytes(&class2).unwrap();
assert_ne!(hash_bytes(&abi1), hash_bytes(&abi2));
}
#[test]
fn test_abi_excludes_private_members() {
let class1 = build_test_class(&[0xB1]);
let mut class2 = class1.clone();
for i in 0..class2.len() - 6 {
if class2[i] == 0x00 && class2[i+1] == 0x00
&& class2[i+2] == 0x00 && class2[i+3] == 0x01
&& class2[i+4] == 0x00 && class2[i+5] == 0x01
{
class2[i+5] = 0x02; break;
}
}
let abi1 = extract_abi_bytes(&class1).unwrap();
let abi2 = extract_abi_bytes(&class2).unwrap();
assert_ne!(hash_bytes(&abi1), hash_bytes(&abi2));
}
#[test]
fn test_is_utf8_constant_lookup() {
let class_data = build_test_class(&[0xB1]);
assert!(is_utf8_constant(&class_data, 7, b"Code"));
assert!(is_utf8_constant(&class_data, 5, b"hello"));
assert!(is_utf8_constant(&class_data, 1, b"Test"));
assert!(!is_utf8_constant(&class_data, 1, b"Nope"));
assert!(!is_utf8_constant(&class_data, 2, b"Test"));
}
#[test]
fn test_read_u16_u32_helpers() {
let data = [0x01, 0x02, 0x03, 0x04];
assert_eq!(read_u16(&data, 0), Some(0x0102));
assert_eq!(read_u16(&data, 2), Some(0x0304));
assert_eq!(read_u32(&data, 0), Some(0x01020304));
assert_eq!(read_u16(&data, 3), None);
assert_eq!(read_u32(&data, 2), None);
}
#[test]
fn test_fingerprints_abi_tracking() {
let mut fp = Fingerprints::default();
let path = Path::new("src/Foo.java");
fp.update_source(path, "hash1", 1000);
fp.update_abi(path, "abi_v1");
assert!(!fp.abi_changed(path, "abi_v1"));
assert!(fp.abi_changed(path, "abi_v2"));
assert!(fp.abi_changed(Path::new("src/Bar.java"), "abi_v1"));
}
}