use std::collections::HashMap; use std::path::{Path, PathBuf}; use sha2::{Digest, Sha256}; use crate::env::EnvLoader; use crate::glob::{expand_files, file_id}; use crate::graph::{Graph, Task}; use crate::{EnvMap, Error, Result}; pub(crate) struct Fingerprinter { graph: Graph, root: PathBuf, env: EnvLoader, memo: HashMap, visiting: Vec, } impl Fingerprinter { pub(crate) fn new(graph: Graph, root: PathBuf) -> Self { Fingerprinter { env: EnvLoader::new(root.clone()), graph, root, memo: HashMap::new(), visiting: Vec::new(), } } pub(crate) fn fingerprint( &mut self, name: &str, args: &[String], env: &EnvMap, ) -> Result { if let Some(fingerprint) = self.memo.get(name) { return Ok(fingerprint.clone()); } if self.visiting.iter().any(|seen| seen == name) { let mut cycle = self.visiting.clone(); cycle.push(name.to_owned()); return Err(Error::Cycle(cycle.join(" -> "))); } let task = self.graph.get(name)?.clone(); self.visiting.push(name.to_owned()); let result = self.fingerprint_task(name, &task, args, env); self.visiting.pop(); let fingerprint = result?; self.memo.insert(name.to_owned(), fingerprint.clone()); Ok(fingerprint) } fn fingerprint_task( &mut self, name: &str, task: &Task, args: &[String], env: &EnvMap, ) -> Result { fn field(hasher: &mut Sha256, value: &[u8]) { hasher.update((value.len() as u64).to_be_bytes()); hasher.update(value); } let mut hasher = Sha256::new(); field(&mut hasher, b"avril-task-cache-v3"); field(&mut hasher, self.root.to_string_lossy().as_bytes()); field(&mut hasher, name.as_bytes()); field(&mut hasher, task.action.fingerprint_text().as_bytes()); field( &mut hasher, if task.parallel { b"parallel".as_slice() } else { b"sequential".as_slice() }, ); for arg in args { field(&mut hasher, arg.as_bytes()); } for var in &task.env { field(&mut hasher, var.as_bytes()); match std::env::var(var) { Ok(value) => { field(&mut hasher, b"set"); field(&mut hasher, value.as_bytes()); } Err(_) => { let normalized = avril_caps::env_name(var).unwrap_or_else(|_| var.as_str().into()); match env.get(normalized.as_ref()) { Some(value) => { field(&mut hasher, b"file"); field(&mut hasher, value.as_bytes()); } None => field(&mut hasher, b"unset"), } } } } for dependency in &task.dependencies { let dependency_task = self.graph.get(dependency)?.clone(); let dependency_env = self.env.effective(&dependency_task, env)?; let fingerprint = self.fingerprint(dependency, &[], &dependency_env)?; field(&mut hasher, dependency.as_bytes()); field(&mut hasher, fingerprint.as_bytes()); } if let Some(patterns) = &task.files { field(&mut hasher, b"cached"); for (relative, path) in expand_files(&self.root, name, patterns)? { field(&mut hasher, relative.as_bytes()); let id = file_id(&path).map_err(|source| Error::Read { task: name.to_owned(), path: relative.clone(), source, })?; field(&mut hasher, id.as_bytes()); } } Ok(format!("{:x}", hasher.finalize())) } } /// The cache fingerprint of `target` in `graph`, without running anything. pub fn graph_fingerprint( graph: &Graph, root: &Path, target: &str, args: &[String], ) -> Result { let mut fingerprinter = Fingerprinter::new(graph.clone(), root.to_path_buf()); fingerprinter.fingerprint(target, args, &EnvMap::new()) }