use std::collections::BTreeMap;
use std::path::Path;
use std::path::PathBuf;
use std::time::UNIX_EPOCH;
use deno_config::glob::FileCollector;
use deno_config::glob::FilePatterns;
use deno_config::glob::PathOrPatternSet;
use deno_core::serde_json;
use serde::Deserialize;
use serde::Serialize;
use sha2::Digest;
use sha2::Sha256;
use crate::sys::CliSys;
pub enum CacheLookup {
Hit(String),
Miss(Fingerprint),
NotCacheable,
}
pub struct Fingerprint {
pub fingerprint: String,
static_hash: String,
content_hash: String,
files: Vec<FileStat>,
prior_outputs: Vec<String>,
}
pub struct TaskCache {
dir: PathBuf,
}
impl TaskCache {
pub fn new(deno_dir_root: &Path) -> Self {
Self {
dir: deno_dir_root.join("task_cache_v1"),
}
}
pub fn lookup(&self, key: &TaskCacheKey<'_>) -> CacheLookup {
if key.files.is_empty() {
return CacheLookup::NotCacheable;
}
let Some(dep_fingerprints) = key.dep_fingerprints else {
return CacheLookup::NotCacheable;
};
let Some(inputs) = collect_inputs(key) else {
return CacheLookup::NotCacheable;
};
let static_hash = compute_static_hash(key, dep_fingerprints);
let stats: Vec<FileStat> = inputs.iter().map(|i| i.stat.clone()).collect();
let stored = std::fs::read_to_string(self.manifest_path(key))
.ok()
.and_then(|s| serde_json::from_str::<CacheManifest>(&s).ok());
if let Some(stored) = &stored
&& stored.static_hash == static_hash
&& stored.files == stats
{
self.restore_outputs(key, stored);
return CacheLookup::Hit(stored.fingerprint.clone());
}
let content_hash = compute_content_hash(key, &inputs);
let fingerprint = combine_fingerprint(&static_hash, &content_hash);
if let Some(stored) = &stored
&& stored.static_hash == static_hash
&& stored.content_hash == content_hash
{
let manifest = CacheManifest {
fingerprint: fingerprint.clone(),
static_hash,
content_hash,
files: stats,
outputs: stored.outputs.clone(),
};
self.write_manifest(key, &manifest);
self.restore_outputs(key, stored);
return CacheLookup::Hit(fingerprint);
}
CacheLookup::Miss(Fingerprint {
fingerprint,
static_hash,
content_hash,
files: stats,
prior_outputs: stored.map(|s| s.outputs).unwrap_or_default(),
})
}
pub fn clean_stale_outputs(
&self,
key: &TaskCacheKey<'_>,
fingerprint: &Fingerprint,
) {
for rel in &fingerprint.prior_outputs {
let path = key.cwd.join(rel);
if let Err(err) = remove_if_exists(&path) {
log::debug!("failed to remove stale output {}: {err}", path.display());
}
}
}
pub fn store(&self, key: &TaskCacheKey<'_>, fingerprint: &Fingerprint) {
let outputs = self.capture_outputs(key);
let manifest = CacheManifest {
fingerprint: fingerprint.fingerprint.clone(),
static_hash: fingerprint.static_hash.clone(),
content_hash: fingerprint.content_hash.clone(),
files: fingerprint.files.clone(),
outputs,
};
self.write_manifest(key, &manifest);
}
fn capture_outputs(&self, key: &TaskCacheKey<'_>) -> Vec<String> {
if key.output.is_empty() {
return Vec::new();
}
let Some(files) = collect_files(key.cwd, key.output) else {
return Vec::new();
};
let outputs_dir = self.outputs_dir(key);
let _ = remove_if_exists(&outputs_dir);
let mut captured = Vec::with_capacity(files.len());
for abs in files {
let Ok(rel) = abs.strip_prefix(key.cwd) else {
continue;
};
let dest = outputs_dir.join(rel);
if let Some(parent) = dest.parent()
&& let Err(err) = std::fs::create_dir_all(parent)
{
log::debug!("failed to create output cache dir: {err}");
continue;
}
if let Err(err) = std::fs::copy(&abs, &dest) {
log::debug!("failed to cache output {}: {err}", abs.display());
continue;
}
captured.push(rel.to_string_lossy().into_owned());
}
captured
}
fn restore_outputs(&self, key: &TaskCacheKey<'_>, manifest: &CacheManifest) {
let outputs_dir = self.outputs_dir(key);
for rel in &manifest.outputs {
let cached = outputs_dir.join(rel);
let dest = key.cwd.join(rel);
if !cached.exists() {
continue;
}
if files_have_equal_contents(&cached, &dest) {
continue;
}
if let Some(parent) = dest.parent()
&& let Err(err) = std::fs::create_dir_all(parent)
{
log::debug!("failed to create output dir {}: {err}", parent.display());
continue;
}
if let Err(err) = std::fs::copy(&cached, &dest) {
log::debug!("failed to restore output {}: {err}", dest.display());
}
}
}
fn write_manifest(&self, key: &TaskCacheKey<'_>, manifest: &CacheManifest) {
let entry_dir = self.entry_dir(key);
if let Err(err) = std::fs::create_dir_all(&entry_dir) {
log::debug!("failed to create task cache dir: {err}");
return;
}
let json = match serde_json::to_string(manifest) {
Ok(json) => json,
Err(err) => {
log::debug!("failed to serialize task cache entry: {err}");
return;
}
};
let path = self.manifest_path(key);
if let Err(err) = std::fs::write(&path, json) {
log::debug!("failed to write task cache entry {}: {err}", path.display());
}
}
fn entry_dir(&self, key: &TaskCacheKey<'_>) -> PathBuf {
let mut hasher = Sha256::new();
hasher.update(b"deno-task-cache-identity-v1");
hasher.update(key.package_name.unwrap_or("").as_bytes());
hasher.update([0]);
hasher.update(key.task_name.as_bytes());
hasher.update([0]);
hasher.update(key.cwd.as_os_str().as_encoded_bytes());
self.dir.join(faster_hex::hex_string(&hasher.finalize()))
}
fn manifest_path(&self, key: &TaskCacheKey<'_>) -> PathBuf {
self.entry_dir(key).join("manifest.json")
}
fn outputs_dir(&self, key: &TaskCacheKey<'_>) -> PathBuf {
self.entry_dir(key).join("outputs")
}
}
pub struct TaskCacheKey<'a> {
pub package_name: Option<&'a str>,
pub task_name: &'a str,
pub cwd: &'a Path,
pub command: &'a str,
pub argv: &'a [String],
pub files: &'a [String],
pub output: &'a [String],
pub env_names: &'a [String],
pub env: &'a BTreeMap<String, String>,
pub dep_fingerprints: Option<&'a [String]>,
}
#[derive(Serialize, Deserialize)]
struct CacheManifest {
fingerprint: String,
static_hash: String,
content_hash: String,
files: Vec<FileStat>,
#[serde(default)]
outputs: Vec<String>,
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
struct FileStat {
path: String,
size: u64,
mtime: u64,
}
struct InputFile {
abs_path: PathBuf,
stat: FileStat,
}
fn collect_inputs(key: &TaskCacheKey<'_>) -> Option<Vec<InputFile>> {
let files = collect_files(key.cwd, key.files)?;
Some(
files
.into_iter()
.map(|abs_path| {
let rel = abs_path
.strip_prefix(key.cwd)
.unwrap_or(&abs_path)
.to_string_lossy()
.into_owned();
let (size, mtime) = match std::fs::metadata(&abs_path) {
Ok(meta) => {
let mtime = meta
.modified()
.ok()
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
(meta.len(), mtime)
}
Err(_) => (0, 0),
};
InputFile {
abs_path,
stat: FileStat {
path: rel,
size,
mtime,
},
}
})
.collect(),
)
}
fn collect_files(cwd: &Path, globs: &[String]) -> Option<Vec<PathBuf>> {
let patterns = build_file_patterns(cwd, globs)?;
let mut files = FileCollector::new(|_| true)
.ignore_git_folder()
.ignore_node_modules()
.collect_file_patterns(&CliSys::default(), &patterns);
files.sort();
if files.is_empty() {
return None;
}
Some(files)
}
fn compute_static_hash(
key: &TaskCacheKey<'_>,
dep_fingerprints: &[String],
) -> String {
let mut hasher = FingerprintHasher::new("deno-task-cache-static-v1");
hasher.write_str(env!("CARGO_PKG_VERSION"));
hasher.write_str(std::env::consts::OS);
hasher.write_str(std::env::consts::ARCH);
hasher.write_str(key.command);
hasher.write_u64(key.argv.len() as u64);
for arg in key.argv {
hasher.write_str(arg);
}
let mut deps = dep_fingerprints.to_vec();
deps.sort();
hasher.write_u64(deps.len() as u64);
for dep in &deps {
hasher.write_str(dep);
}
let mut sorted_env = key.env_names.to_vec();
sorted_env.sort();
sorted_env.dedup();
for name in &sorted_env {
hasher.write_str(name);
match key.env.get(name) {
Some(value) => {
hasher.write_u8(1);
hasher.write_str(value);
}
None => {
hasher.write_u8(0);
}
}
}
hasher.finish_hex()
}
fn compute_content_hash(
key: &TaskCacheKey<'_>,
inputs: &[InputFile],
) -> String {
let mut hasher = FingerprintHasher::new("deno-task-cache-content-v1");
for input in inputs {
let rel = input
.abs_path
.strip_prefix(key.cwd)
.unwrap_or(&input.abs_path);
hasher.write_bytes(rel.as_os_str().as_encoded_bytes());
match std::fs::read(&input.abs_path) {
Ok(bytes) => {
hasher.write_u8(1);
hasher.write_bytes(&bytes);
}
Err(_) => {
hasher.write_u8(0);
}
}
}
hasher.finish_hex()
}
fn combine_fingerprint(static_hash: &str, content_hash: &str) -> String {
let mut hasher = FingerprintHasher::new("deno-task-cache-combined-v1");
hasher.write_str(static_hash);
hasher.write_str(content_hash);
hasher.finish_hex()
}
fn build_file_patterns(cwd: &Path, entries: &[String]) -> Option<FilePatterns> {
let include =
PathOrPatternSet::from_include_relative_path_or_patterns(cwd, entries)
.ok()?;
Some(FilePatterns {
base: cwd.to_path_buf(),
include: Some(include),
exclude: PathOrPatternSet::new(Vec::new()),
})
}
fn remove_if_exists(path: &Path) -> std::io::Result<()> {
match std::fs::metadata(path) {
Ok(meta) if meta.is_dir() => std::fs::remove_dir_all(path),
Ok(_) => std::fs::remove_file(path),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(err) => Err(err),
}
}
fn files_have_equal_contents(a: &Path, b: &Path) -> bool {
match (std::fs::read(a), std::fs::read(b)) {
(Ok(a), Ok(b)) => a == b,
_ => false,
}
}
struct FingerprintHasher(Sha256);
impl FingerprintHasher {
fn new(domain: &str) -> Self {
let mut hasher = Sha256::new();
hasher.update(domain.as_bytes());
hasher.update([0]);
Self(hasher)
}
fn write_bytes(&mut self, bytes: &[u8]) {
self.0.update((bytes.len() as u64).to_le_bytes());
self.0.update(bytes);
}
fn write_str(&mut self, s: &str) {
self.write_bytes(s.as_bytes());
}
fn write_u8(&mut self, value: u8) {
self.0.update([value]);
}
fn write_u64(&mut self, value: u64) {
self.0.update(value.to_le_bytes());
}
fn finish_hex(self) -> String {
faster_hex::hex_string(&self.0.finalize())
}
}