1use std::collections::hash_map::DefaultHasher;
2use std::fs;
3use std::hash::{
4 Hash,
5 Hasher,
6};
7use std::path::{
8 Path,
9 PathBuf,
10};
11
12use anyhow::Context as _;
13use glob::glob;
14use hashbrown::HashMap;
15use serde::{
16 Deserialize,
17 Serialize,
18};
19
20use crate::file::ToUtf8 as _;
21use crate::utils::resolve_path;
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct CacheEntry {
25 pub fingerprint: String,
26 pub outputs: Vec<String>,
27 pub updated_at: String,
28}
29
30#[derive(Debug, Default, Serialize, Deserialize)]
31pub struct CacheStore {
32 pub tasks: HashMap<String, CacheEntry>,
33}
34
35impl CacheStore {
36 pub fn load() -> anyhow::Result<Self> {
37 Self::load_in_dir(Path::new("."))
38 }
39
40 pub fn load_in_dir(base_dir: &Path) -> anyhow::Result<Self> {
41 let path = cache_path_in_dir(base_dir);
42 if !path.exists() {
43 return Ok(Self::default());
44 }
45
46 let contents = fs::read_to_string(&path).with_context(|| {
47 format!(
48 "Failed to read cache file - {}",
49 path.to_utf8().unwrap_or("<non-utf8-path>")
50 )
51 })?;
52 Ok(serde_json::from_str(&contents)?)
53 }
54
55 pub fn save(&self) -> anyhow::Result<()> {
56 self.save_in_dir(Path::new("."))
57 }
58
59 pub fn save_in_dir(&self, base_dir: &Path) -> anyhow::Result<()> {
60 let path = cache_path_in_dir(base_dir);
61 if let Some(parent) = path.parent() {
62 fs::create_dir_all(parent)?;
63 }
64
65 fs::write(&path, serde_json::to_string_pretty(self)?).with_context(|| {
66 format!(
67 "Failed to write cache file - {}",
68 path.to_utf8().unwrap_or("<non-utf8-path>")
69 )
70 })?;
71 Ok(())
72 }
73
74 pub fn remove() -> anyhow::Result<()> {
75 Self::remove_in_dir(Path::new("."))
76 }
77
78 pub fn remove_in_dir(base_dir: &Path) -> anyhow::Result<()> {
79 let path = cache_path_in_dir(base_dir);
80 if path.exists() {
81 fs::remove_file(&path).with_context(|| {
82 format!(
83 "Failed to remove cache file - {}",
84 path.to_utf8().unwrap_or("<non-utf8-path>")
85 )
86 })?;
87 }
88 Ok(())
89 }
90}
91
92pub fn cache_path() -> PathBuf {
93 cache_path_in_dir(Path::new("."))
94}
95
96pub fn cache_path_in_dir(base_dir: &Path) -> PathBuf {
97 base_dir.join(".mk").join("cache.json")
98}
99
100pub fn expand_patterns(patterns: &[String]) -> anyhow::Result<Vec<PathBuf>> {
101 expand_patterns_in_dir(Path::new("."), patterns)
102}
103
104pub fn expand_patterns_in_dir(base_dir: &Path, patterns: &[String]) -> anyhow::Result<Vec<PathBuf>> {
105 let mut paths = Vec::new();
106
107 for pattern in patterns {
108 let mut matched = false;
109 let resolved_pattern = resolve_path(base_dir, pattern);
110 let resolved_pattern = resolved_pattern.to_string_lossy().into_owned();
111 for entry in glob(&resolved_pattern)? {
112 matched = true;
113 let path = entry?;
114 paths.push(path);
115 }
116
117 if !matched {
118 paths.push(resolve_path(base_dir, pattern));
119 }
120 }
121
122 paths.sort();
123 paths.dedup();
124 Ok(paths)
125}
126
127pub fn compute_fingerprint(
128 task_name: &str,
129 task_debug: &str,
130 env_vars: &[(String, String)],
131 inputs: &[PathBuf],
132 env_files: &[PathBuf],
133 outputs: &[PathBuf],
134) -> anyhow::Result<String> {
135 let mut hasher = DefaultHasher::new();
136
137 task_name.hash(&mut hasher);
138 task_debug.hash(&mut hasher);
139 outputs.hash(&mut hasher);
140
141 for (key, value) in env_vars {
142 key.hash(&mut hasher);
143 value.hash(&mut hasher);
144 }
145
146 for path in inputs {
147 path.to_string_lossy().hash(&mut hasher);
148 hash_path(path, &mut hasher)?;
149 }
150
151 for path in env_files {
152 path.to_string_lossy().hash(&mut hasher);
153 hash_path(path, &mut hasher)?;
154 }
155
156 Ok(format!("{:016x}", hasher.finish()))
157}
158
159fn hash_path(path: &Path, hasher: &mut DefaultHasher) -> anyhow::Result<()> {
160 if !path.exists() {
161 "missing".hash(hasher);
162 return Ok(());
163 }
164
165 let metadata = fs::metadata(path)?;
166 metadata.len().hash(hasher);
167
168 if metadata.is_file() {
169 let bytes = fs::read(path)?;
170 bytes.hash(hasher);
171 } else {
172 let modified = metadata.modified().ok();
173 format!("{modified:?}").hash(hasher);
174 }
175
176 Ok(())
177}