use crate::core::hash::Sha256;
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
static TMP_COUNTER: AtomicU64 = AtomicU64::new(1);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CachedOutput {
pub relative_path: PathBuf,
pub cas_hash: String,
pub file_size: u64,
pub executable: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ActionCacheEntry {
pub action_key: String,
pub action_id: String,
pub created_at_secs: u64,
pub outputs: Vec<CachedOutput>,
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
}
#[derive(Debug, Clone)]
pub struct ActionCache {
root: PathBuf,
tmp_dir: PathBuf,
locks_dir: PathBuf,
}
impl ActionCache {
pub fn new(root: PathBuf) -> Result<Self> {
let root = if root.is_relative() {
std::env::current_dir()
.map(|cwd| cwd.join(&root))
.unwrap_or(root)
} else {
root
};
let tmp_dir = root.join("tmp");
let locks_dir = root.parent().unwrap_or(&root).join("locks");
let manifests_dir = root.join("manifests");
fs::create_dir_all(&root)
.with_context(|| format!("Failed to create Action Cache directory: {:?}", root))?;
fs::create_dir_all(&tmp_dir).with_context(|| {
format!("Failed to create Action Cache tmp directory: {:?}", tmp_dir)
})?;
fs::create_dir_all(&locks_dir)
.with_context(|| format!("Failed to create locks directory: {:?}", locks_dir))?;
fs::create_dir_all(&manifests_dir).with_context(|| {
format!("Failed to create manifests directory: {:?}", manifests_dir)
})?;
Ok(Self {
root,
tmp_dir,
locks_dir,
})
}
pub fn default_path() -> PathBuf {
if let Ok(dir) = std::env::var("MITTS_CACHE_DIR") {
return PathBuf::from(dir).join("ac");
}
if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
return PathBuf::from(xdg).join("mitts").join("ac");
}
if let Ok(home) = std::env::var("HOME") {
return PathBuf::from(home).join(".cache").join("mitts").join("ac");
}
PathBuf::from(".mitts_cache").join("ac")
}
pub fn default_cache() -> Result<Self> {
Self::new(Self::default_path())
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn locks_dir(&self) -> &Path {
&self.locks_dir
}
pub fn lock_path(&self, action_key: &str) -> PathBuf {
let name = safe_key_file_name(action_key);
self.locks_dir.join(format!("{}.lock", name))
}
pub fn entry_path(&self, action_key: &str) -> PathBuf {
let name = safe_key_file_name(action_key);
self.root.join(format!("{}.json", name))
}
pub fn get(&self, action_key: &str) -> Result<Option<ActionCacheEntry>> {
let path = self.entry_path(action_key);
if !path.is_file() {
return Ok(None);
}
let content = fs::read_to_string(&path)
.with_context(|| format!("Failed to read AC entry from {:?}", path))?;
let entry: ActionCacheEntry = serde_json::from_str(&content)
.with_context(|| format!("Failed to parse AC entry from {:?}", path))?;
Ok(Some(entry))
}
pub fn manifest_dir(&self) -> PathBuf {
self.root.join("manifests")
}
pub fn manifest_path(&self, base_key: &str) -> PathBuf {
let name = safe_key_file_name(base_key);
self.manifest_dir().join(format!("{}.json", name))
}
pub fn manifest_lock_path(&self, base_key: &str) -> PathBuf {
let name = safe_key_file_name(base_key);
self.locks_dir.join(format!("{}.manifest.lock", name))
}
pub fn get_manifest(
&self,
base_key: &str,
) -> Result<Option<super::action_manifest::ActionManifest>> {
super::action_manifest::ActionManifest::from_file(&self.manifest_path(base_key))
}
pub fn record_manifest_entry(
&self,
base_key: &str,
extra_inputs: Vec<(String, String)>,
result_key: &str,
) -> Result<()> {
let lock_path = self.manifest_lock_path(base_key);
let _lock = crate::core::lock::FileLock::acquire_exclusive(&lock_path)?;
let mut manifest = self.get_manifest(base_key)?.unwrap_or_else(|| {
super::action_manifest::ActionManifest {
base_key: base_key.to_string(),
entries: Vec::new(),
}
});
manifest.record_entry(extra_inputs, result_key.to_string());
manifest.save_atomic(&self.manifest_path(base_key), &self.tmp_dir)
}
pub fn put(&self, entry: &ActionCacheEntry) -> Result<()> {
let json_bytes = serde_json::to_vec_pretty(entry)
.context("Failed to serialize ActionCacheEntry to JSON")?;
let counter = TMP_COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let safe_key = safe_key_file_name(&entry.action_key);
let tmp_path = self
.tmp_dir
.join(format!("{}.{}.{}.tmp", safe_key, pid, counter));
fs::write(&tmp_path, json_bytes)
.with_context(|| format!("Failed to write AC tmp entry to {:?}", tmp_path))?;
let dest_path = self.entry_path(&entry.action_key);
if let Err(err) = fs::rename(&tmp_path, &dest_path) {
let _ = fs::remove_file(&tmp_path);
bail!(
"Failed to atomically publish AC entry to {:?}: {}",
dest_path,
err
);
}
Ok(())
}
pub fn derive_action_key(
toolchain_version: &str,
target_triple: &str,
profile: &str,
flags: &[String],
features: &[String],
source_hashes: &[(&str, &str)],
dep_hashes: &[(&str, &str)],
) -> String {
let mut sorted_features = features.to_vec();
sorted_features.sort();
let mut sorted_sources = source_hashes.to_vec();
sorted_sources.sort_by_key(|(path, _)| *path);
let mut sorted_deps = dep_hashes.to_vec();
sorted_deps.sort_by_key(|(name, _)| *name);
let mut payload = String::new();
payload.push_str("action_schema: v2\n");
payload.push_str("toolchain: ");
payload.push_str(toolchain_version.trim());
payload.push('\n');
payload.push_str("target: ");
payload.push_str(target_triple.trim());
payload.push('\n');
payload.push_str("profile: ");
payload.push_str(profile.trim());
payload.push('\n');
payload.push_str("flags:\n");
for f in flags {
payload.push_str(" ");
payload.push_str(f);
payload.push('\n');
}
payload.push_str("features:\n");
for feat in &sorted_features {
payload.push_str(" ");
payload.push_str(feat);
payload.push('\n');
}
payload.push_str("sources:\n");
for (path, hash) in sorted_sources {
payload.push_str(" ");
payload.push_str(path);
payload.push_str(": ");
payload.push_str(hash);
payload.push('\n');
}
payload.push_str("deps:\n");
for (name, hash) in sorted_deps {
payload.push_str(" ");
payload.push_str(name);
payload.push_str(": ");
payload.push_str(hash);
payload.push('\n');
}
Sha256::digest(payload.as_bytes())
}
}
pub fn safe_key_file_name(key: &str) -> String {
if key.len() > 64
|| !key
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
{
Sha256::digest(key.as_bytes())
} else {
key.to_string()
}
}