use crate::core::hash::Sha256;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ActionManifestEntry {
pub extra_inputs: Vec<(String, String)>,
pub result_key: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ActionManifest {
pub base_key: String,
pub entries: Vec<ActionManifestEntry>,
}
impl ActionManifest {
pub fn from_file(path: &Path) -> Result<Option<Self>> {
if !path.is_file() {
return Ok(None);
}
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read action manifest from {:?}", path))?;
let manifest: Self = serde_json::from_str(&content)
.with_context(|| format!("Failed to parse action manifest from {:?}", path))?;
Ok(Some(manifest))
}
pub fn save_atomic(&self, dest_path: &Path, tmp_dir: &Path) -> Result<()> {
let json_bytes = serde_json::to_vec_pretty(self)
.context("Failed to serialize ActionManifest to JSON")?;
let pid = std::process::id();
let counter = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let safe_base = crate::core::action_cache::safe_key_file_name(&self.base_key);
let tmp_path = tmp_dir.join(format!("{}.{}.{}.manifest.tmp", safe_base, pid, counter));
fs::write(&tmp_path, json_bytes)
.with_context(|| format!("Failed to write tmp manifest to {:?}", tmp_path))?;
if let Err(err) = fs::rename(&tmp_path, dest_path) {
let _ = fs::remove_file(&tmp_path);
anyhow::bail!(
"Failed to atomically rename manifest to {:?}: {}",
dest_path,
err
);
}
Ok(())
}
pub fn find_matching_entry(
&self,
ws_root: Option<&Path>,
effective_env: Option<&HashMap<String, String>>,
out_dir: Option<&Path>,
) -> Option<String> {
for entry in &self.entries {
let mut matches = true;
for (path_str, expected_hash) in &entry.extra_inputs {
if let Some(var_name) = path_str.strip_prefix("env:") {
let actual_hash = if let Some(env) = effective_env {
if let Some(val) = env.get(var_name) {
Sha256::digest(val.as_bytes())
} else {
match std::env::var(var_name) {
Ok(v) => Sha256::digest(v.as_bytes()),
Err(_) => Sha256::digest(b"__UNSET__"),
}
}
} else {
match std::env::var(var_name) {
Ok(v) => Sha256::digest(v.as_bytes()),
Err(_) => Sha256::digest(b"__UNSET__"),
}
};
if actual_hash != *expected_hash {
matches = false;
break;
}
continue;
}
let file_path = if let Some(rel) = path_str.strip_prefix("ws:") {
if let Some(root) = ws_root {
root.join(rel)
} else {
PathBuf::from(rel)
}
} else if let Some(rel) = path_str.strip_prefix("out:") {
if let Some(out) = out_dir {
out.join(rel)
} else {
PathBuf::from(rel)
}
} else {
PathBuf::from(path_str)
};
if !file_path.is_file() {
matches = false;
break;
}
match Sha256::digest_file(&file_path) {
Ok(actual_hash) if actual_hash == *expected_hash => {}
_ => {
matches = false;
break;
}
}
}
if matches {
return Some(entry.result_key.clone());
}
}
None
}
pub fn record_entry(&mut self, mut extra_inputs: Vec<(String, String)>, result_key: String) {
extra_inputs.sort_by(|a, b| a.0.cmp(&b.0));
extra_inputs.dedup_by(|a, b| a.0 == b.0);
for entry in &mut self.entries {
if entry.extra_inputs == extra_inputs {
entry.result_key = result_key;
return;
}
}
self.entries.push(ActionManifestEntry {
extra_inputs,
result_key,
});
if self.entries.len() > 50 {
self.entries.drain(0..self.entries.len() - 50);
}
}
}