1
use std::collections::hash_map::DefaultHasher;
2
use std::fs;
3
use std::hash::{
4
  Hash,
5
  Hasher,
6
};
7
use std::path::{
8
  Path,
9
  PathBuf,
10
};
11

            
12
use anyhow::Context as _;
13
use glob::glob;
14
use hashbrown::HashMap;
15
use serde::{
16
  Deserialize,
17
  Serialize,
18
};
19

            
20
use crate::file::ToUtf8 as _;
21
use crate::utils::resolve_path;
22

            
23
#[derive(Debug, Clone, Serialize, Deserialize)]
24
pub struct CacheEntry {
25
  pub fingerprint: String,
26
  pub outputs: Vec<String>,
27
  pub updated_at: String,
28
}
29

            
30
#[derive(Debug, Default, Serialize, Deserialize)]
31
pub struct CacheStore {
32
  pub tasks: HashMap<String, CacheEntry>,
33
}
34

            
35
impl CacheStore {
36
  pub fn load() -> anyhow::Result<Self> {
37
    Self::load_in_dir(Path::new("."))
38
  }
39

            
40
80
  pub fn load_in_dir(base_dir: &Path) -> anyhow::Result<Self> {
41
80
    let path = cache_path_in_dir(base_dir);
42
80
    if !path.exists() {
43
80
      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
80
  }
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

            
92
pub fn cache_path() -> PathBuf {
93
  cache_path_in_dir(Path::new("."))
94
}
95

            
96
80
pub fn cache_path_in_dir(base_dir: &Path) -> PathBuf {
97
80
  base_dir.join(".mk").join("cache.json")
98
80
}
99

            
100
pub fn expand_patterns(patterns: &[String]) -> anyhow::Result<Vec<PathBuf>> {
101
  expand_patterns_in_dir(Path::new("."), patterns)
102
}
103

            
104
pub 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

            
127
4
pub fn compute_fingerprint(
128
4
  task_name: &str,
129
4
  task_fingerprint: &str,
130
4
  env_vars: &[(String, String)],
131
4
  inputs: &[PathBuf],
132
4
  env_files: &[PathBuf],
133
4
  outputs: &[PathBuf],
134
4
) -> anyhow::Result<String> {
135
4
  let mut hasher = DefaultHasher::new();
136

            
137
4
  task_name.hash(&mut hasher);
138
4
  task_fingerprint.hash(&mut hasher);
139

            
140
4
  for (key, value) in env_vars {
141
    key.hash(&mut hasher);
142
    value.hash(&mut hasher);
143
  }
144

            
145
4
  for path in inputs {
146
2
    path.to_string_lossy().hash(&mut hasher);
147
2
    hash_path(path, &mut hasher)?;
148
  }
149

            
150
4
  for path in env_files {
151
    path.to_string_lossy().hash(&mut hasher);
152
    hash_path(path, &mut hasher)?;
153
  }
154

            
155
4
  for path in outputs {
156
2
    path.to_string_lossy().hash(&mut hasher);
157
2
    hash_path(path, &mut hasher)?;
158
  }
159

            
160
4
  Ok(format!("{:016x}", hasher.finish()))
161
4
}
162

            
163
4
fn hash_path(path: &Path, hasher: &mut DefaultHasher) -> anyhow::Result<()> {
164
4
  if !path.exists() {
165
    "missing".hash(hasher);
166
    return Ok(());
167
4
  }
168

            
169
4
  hash_path_contents(path, path, hasher)?;
170

            
171
4
  Ok(())
172
4
}
173

            
174
8
fn hash_path_contents(root: &Path, path: &Path, hasher: &mut DefaultHasher) -> anyhow::Result<()> {
175
8
  let metadata = fs::symlink_metadata(path)?;
176

            
177
8
  if metadata.file_type().is_symlink() {
178
    "symlink".hash(hasher);
179
    fs::read_link(path)?.to_string_lossy().hash(hasher);
180
    return Ok(());
181
8
  }
182

            
183
8
  if metadata.is_file() {
184
4
    "file".hash(hasher);
185
4
    metadata.len().hash(hasher);
186
4
    let bytes = fs::read(path)?;
187
4
    bytes.hash(hasher);
188
4
    return Ok(());
189
4
  }
190

            
191
4
  if metadata.is_dir() {
192
4
    "dir".hash(hasher);
193

            
194
4
    let mut entries = fs::read_dir(path)?
195
4
      .collect::<Result<Vec<_>, _>>()?
196
4
      .into_iter()
197
4
      .map(|entry| entry.path())
198
4
      .collect::<Vec<_>>();
199
4
    entries.sort();
200

            
201
4
    for entry in entries {
202
4
      let relative = entry.strip_prefix(root).unwrap_or(&entry);
203
4
      relative.to_string_lossy().hash(hasher);
204
4
      hash_path_contents(root, &entry, hasher)?;
205
    }
206

            
207
4
    return Ok(());
208
  }
209

            
210
  "other".hash(hasher);
211
  metadata.len().hash(hasher);
212
  Ok(())
213
8
}
214

            
215
#[cfg(test)]
216
mod tests {
217
  use super::*;
218
  use assert_fs::TempDir;
219

            
220
  #[test]
221
1
  fn test_compute_fingerprint_changes_when_output_content_changes() -> anyhow::Result<()> {
222
1
    let temp_dir = TempDir::new()?;
223
1
    let output = temp_dir.path().join("output.txt");
224
1
    fs::write(&output, "one")?;
225

            
226
1
    let first = compute_fingerprint("build", "task", &[], &[], &[], std::slice::from_ref(&output))?;
227

            
228
1
    fs::write(&output, "two")?;
229

            
230
1
    let second = compute_fingerprint("build", "task", &[], &[], &[], &[output])?;
231
1
    assert_ne!(first, second);
232
1
    Ok(())
233
1
  }
234

            
235
  #[test]
236
1
  fn test_compute_fingerprint_changes_when_directory_child_changes() -> anyhow::Result<()> {
237
1
    let temp_dir = TempDir::new()?;
238
1
    let input_dir = temp_dir.path().join("input");
239
1
    fs::create_dir_all(input_dir.join("nested"))?;
240
1
    fs::write(input_dir.join("nested/file.txt"), "one")?;
241

            
242
1
    let first = compute_fingerprint("build", "task", &[], std::slice::from_ref(&input_dir), &[], &[])?;
243

            
244
1
    fs::write(input_dir.join("nested/file.txt"), "two")?;
245

            
246
1
    let second = compute_fingerprint("build", "task", &[], &[input_dir], &[], &[])?;
247
1
    assert_ne!(first, second);
248
1
    Ok(())
249
1
  }
250
}