Skip to main content

mk_lib/
cache.rs

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}