char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somdeslop(task): unify watchset + glob machinerya29d09f

main
5.2 KiB157 linesraw
1use std::collections::{BTreeMap, BTreeSet};
2use std::io::Write;
3use std::path::{Path, PathBuf};
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use glob::Pattern;
7
8use crate::Reporter;
9use crate::glob::{MATCH_OPTIONS, collect_paths, file_id, normalize_pattern};
10use crate::graph::Task;
11
12#[derive(Debug, Clone)]
13pub struct Cache {
14    root: PathBuf,
15}
16
17impl Cache {
18    pub fn new(root: PathBuf) -> Self {
19        Cache { root }
20    }
21
22    pub fn entry(&self, fingerprint: &str) -> PathBuf {
23        self.root.join(format!("{fingerprint}.json"))
24    }
25
26    fn manifest_for(
27        root: &Path,
28        task: &Task,
29    ) -> std::io::Result<(BTreeMap<String, String>, Vec<String>)> {
30        let (matches, unmatched) = expand_outputs(root, &task.output)?;
31        let mut manifest = BTreeMap::new();
32        for relative in matches {
33            manifest_path(&root.join(&relative), &relative, &mut manifest)?;
34        }
35        Ok((manifest, unmatched))
36    }
37    pub(crate) fn manifest_matches(
38        &self,
39        root: &Path,
40        task: &Task,
41        fingerprint: &str,
42    ) -> std::io::Result<bool> {
43        let entry = self.entry(fingerprint);
44        let Ok(bytes) = std::fs::read(entry) else {
45            return Ok(false);
46        };
47        let stored: BTreeMap<String, String> = serde_json::from_slice(&bytes)
48            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
49        Ok(Self::manifest_for(root, task)?.0 == stored)
50    }
51
52    pub(crate) fn record_at(
53        &self,
54        root: &Path,
55        task: &Task,
56        fingerprint: &str,
57        reporter: &dyn Reporter,
58        task_name: &str,
59    ) -> std::io::Result<()> {
60        let (manifest, unmatched) = Self::manifest_for(root, task)?;
61        for pattern in unmatched {
62            reporter.unmatched_output(task_name, &pattern);
63        }
64        std::fs::create_dir_all(&self.root)?;
65        let entry = self.entry(fingerprint);
66        let bytes = serde_json::to_vec(&manifest)
67            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
68        static SEQUENCE: AtomicU64 = AtomicU64::new(0);
69        for _ in 0..16 {
70            let staging = self.root.join(format!(
71                ".tmp-{}-{}-{}.json",
72                entry.file_name().unwrap().to_string_lossy(),
73                std::process::id(),
74                SEQUENCE.fetch_add(1, Ordering::SeqCst)
75            ));
76            let mut file = match std::fs::OpenOptions::new()
77                .write(true)
78                .create_new(true)
79                .open(&staging)
80            {
81                Ok(file) => file,
82                Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
83                Err(error) => return Err(error),
84            };
85            if let Err(error) = file.write_all(&bytes) {
86                drop(file);
87                let _ = std::fs::remove_file(&staging);
88                return Err(error);
89            }
90            drop(file);
91            if entry.exists() {
92                std::fs::remove_file(&entry)?;
93            }
94            return std::fs::rename(&staging, &entry);
95        }
96        Err(std::io::Error::new(
97            std::io::ErrorKind::AlreadyExists,
98            "Task cache staging file names are exhausted",
99        ))
100    }
101}
102
103/// The minimal matched paths (a matched directory covers its contents),
104/// and the patterns that matched nothing.
105fn expand_outputs(
106    base: &Path,
107    patterns: &[String],
108) -> std::io::Result<(BTreeSet<String>, Vec<String>)> {
109    let entries = collect_paths(base, true);
110    let mut matched = BTreeSet::new();
111    let mut unmatched = Vec::new();
112    for pattern in patterns {
113        let compiled = Pattern::new(&normalize_pattern(pattern))
114            .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
115        let mut any = false;
116        for relative in &entries {
117            if compiled.matches_with(relative, MATCH_OPTIONS) {
118                any = true;
119                matched.insert(relative.clone());
120            }
121        }
122        if !any {
123            unmatched.push(pattern.clone());
124        }
125    }
126    let minimal = matched
127        .iter()
128        .filter(|relative| {
129            let parts: Vec<&str> = relative.split('/').collect();
130            !(1..parts.len()).any(|end| matched.contains(&parts[..end].join("/")))
131        })
132        .cloned()
133        .collect();
134    Ok((minimal, unmatched))
135}
136
137fn manifest_path(
138    path: &Path,
139    relative: &str,
140    manifest: &mut BTreeMap<String, String>,
141) -> std::io::Result<()> {
142    let metadata = path.symlink_metadata()?;
143    if metadata.is_dir() && !metadata.file_type().is_symlink()
144        || std::fs::metadata(path).is_ok_and(|target| target.is_dir())
145    {
146        manifest.insert(relative.to_owned(), "dir".into());
147        let mut entries: Vec<_> = std::fs::read_dir(path)?.collect::<std::io::Result<_>>()?;
148        entries.sort_by_key(|entry| entry.file_name());
149        for entry in entries {
150            let name = entry.file_name().to_string_lossy().into_owned();
151            manifest_path(&entry.path(), &format!("{relative}/{name}"), manifest)?;
152        }
153        return Ok(());
154    }
155    manifest.insert(relative.to_owned(), file_id(path)?);
156    Ok(())
157}