Skip to main content

putzen_cli/caches/
defaults.rs

1//! Catalogue of default cache directories scanned by `putzen caches`.
2//!
3//! To add a cache directory:
4//!   1. Add one `///` line above a HOME-relative (or absolute) path literal.
5//!   2. Place it in the right block — see contribution rules in the spec.
6//!   3. Open a PR.
7//!
8//! The `///` is the runtime label *and* the contributor-facing doc.
9
10#[macro_export]
11macro_rules! roots {
12    //—— base case
13    (@build [$($acc:tt)*]) => { &[ $($acc)* ] };
14
15    //—— ERROR: two or more `///` lines on the same entry
16    (@build [$($acc:tt)*]
17        #[doc = $_a:literal] #[doc = $_b:literal] $path:literal $(, $($rest:tt)*)?
18    ) => {
19        compile_error!(concat!(
20            "putzen: cache root \"", $path, "\" has multiple `///` lines. ",
21            "Use exactly one — extend prose with plain `//` instead."
22        ));
23    };
24
25    //—— happy path
26    (@build [$($acc:tt)*]
27        #[doc = $label:literal] $path:literal $(, $($rest:tt)*)?
28    ) => {
29        roots!(@build [
30            $($acc)*
31            $crate::caches::defaults::DefaultRoot {
32                label: $crate::caches::defaults::strip_leading_spaces($label),
33                path:  $path,
34            },
35        ] $($($rest)*)?)
36    };
37
38    //—— ERROR: path literal without preceding `///`
39    (@build [$($acc:tt)*] $path:literal $(, $($rest:tt)*)?) => {
40        compile_error!(concat!(
41            "putzen: cache root \"", $path, "\" is missing its `///` label. ",
42            "Add a one-line `///` doc comment above this entry."
43        ));
44    };
45
46    //—— entrypoint (must be last so `@build` doesn't match it)
47    ( $($t:tt)* ) => { roots!(@build [] $($t)*) };
48}
49
50pub struct DefaultRoot {
51    pub label: &'static str,
52    pub path: &'static str, // HOME-relative unless it begins with '/'
53}
54
55/// Strip leading ASCII spaces from a `///` doc string at compile time.
56///
57/// Rust desugars `/// foo` to `#[doc = " foo"]` (one leading space).
58/// This const helper lets the `roots!` macro normalise the label in a
59/// `const` context, since `str::trim_start_matches` is not `const fn`.
60///
61/// Implementation detail of the [`roots!`] macro — not intended for direct use.
62pub const fn strip_leading_spaces(s: &'static str) -> &'static str {
63    let bytes = s.as_bytes();
64    let mut i = 0;
65    while i < bytes.len() && bytes[i] == b' ' {
66        i += 1;
67    }
68    // Safe split at a known-ASCII boundary.
69    s.split_at(i).1
70}
71
72/// `roots!` requires a `///` doc comment above every path literal.
73///
74/// ```compile_fail
75/// const _: &[putzen_cli::caches::defaults::DefaultRoot] = putzen_cli::roots![
76///     ".a",
77/// ];
78/// ```
79pub const fn _missing_doc_check() {}
80
81/// `roots!` allows at most one `///` line per entry.
82///
83/// ```compile_fail
84/// const _: &[putzen_cli::caches::defaults::DefaultRoot] = putzen_cli::roots![
85///     /// a
86///     /// b
87///     ".x",
88/// ];
89/// ```
90pub const fn _multi_doc_check() {}
91
92// Cross-platform cache paths (resolve via ~ on every OS).
93pub const SEEDS: &[DefaultRoot] = roots![
94    // ── Rust ────────────────────────────────────────────────────────
95    /// cargo packaged crates
96    ".cargo/registry/cache",
97    /// cargo extracted sources
98    ".cargo/registry/src",
99    /// cargo registry index
100    ".cargo/registry/index",
101    /// cargo git checkouts
102    ".cargo/git/checkouts",
103    /// cargo git bare repos
104    ".cargo/git/db",
105    // ── Go / JVM / .NET package caches ──────────────────────────────
106    /// go modules
107    "go/pkg/mod",
108    /// maven local repo
109    ".m2/repository",
110    /// gradle caches
111    ".gradle/caches",
112    /// gradle wrapper distributions
113    ".gradle/wrapper/dists",
114    /// ivy cache
115    ".ivy2/cache",
116    /// sbt boot
117    ".sbt/boot",
118    /// NuGet global packages
119    ".nuget/packages",
120    // ── Other language pkg caches ───────────────────────────────────
121    /// Hex (Elixir) packages
122    ".hex/packages",
123    /// opam download cache
124    ".opam/download-cache",
125    /// Clojure gitlibs
126    ".gitlibs",
127    /// Cabal packages (Haskell)
128    ".cabal/packages",
129    // ── ML / LLM model caches ───────────────────────────────────────
130    /// Ollama models
131    ".ollama/models",
132    /// triton compile cache
133    ".triton/cache",
134    /// CUDA NVRTC compute cache
135    ".nv/ComputeCache",
136];
137
138#[cfg(target_family = "unix")]
139pub const SEEDS_OS: &[DefaultRoot] = roots![
140    /// XDG cache home
141    ".cache",
142    /// macOS per-app caches
143    "Library/Caches",
144    /// Xcode DerivedData
145    "Library/Developer/Xcode/DerivedData",
146    /// Xcode Archives
147    "Library/Developer/Xcode/Archives",
148    /// iOS DeviceSupport
149    "Library/Developer/Xcode/iOS DeviceSupport",
150    /// CoreSimulator caches
151    "Library/Developer/CoreSimulator/Caches",
152    /// npm
153    ".npm",
154    /// yarn cache
155    ".yarn/cache",
156    /// bun install cache
157    ".bun/install/cache",
158    /// pnpm store (legacy dotfile path)
159    ".pnpm-store",
160    /// sccache (Linux XDG)
161    ".cache/sccache",
162    /// sccache (macOS, via Mozilla `directories` crate)
163    "Library/Caches/Mozilla.sccache",
164];
165
166#[cfg(target_family = "windows")]
167pub const SEEDS_OS: &[DefaultRoot] = roots![
168    /// WinINet shared cache
169    "AppData/Local/Microsoft/Windows/INetCache",
170    /// Edge browser cache
171    "AppData/Local/Microsoft/Edge/User Data/Default/Cache",
172    /// Chrome browser cache
173    "AppData/Local/Google/Chrome/User Data/Default/Cache",
174    /// npm
175    "AppData/Roaming/npm-cache",
176    /// yarn
177    "AppData/Local/Yarn/Cache",
178    /// pnpm store
179    "AppData/Local/pnpm",
180    /// pip wheel cache
181    "AppData/Local/pip/Cache",
182    /// uv cache
183    "AppData/Local/uv/cache",
184    /// HuggingFace hub
185    "AppData/Local/huggingface",
186    /// go build cache
187    "AppData/Local/go-build",
188    /// JetBrains caches
189    "AppData/Local/JetBrains",
190    /// VSCode CachedData
191    "AppData/Roaming/Code/CachedData",
192    /// sccache
193    "AppData/Local/Mozilla/sccache",
194];
195
196pub fn defaults() -> impl Iterator<Item = &'static DefaultRoot> {
197    SEEDS.iter().chain(SEEDS_OS.iter())
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn macro_emits_label_and_path() {
206        const SAMPLE: &[DefaultRoot] = roots![
207            /// cargo registry
208            ".cargo/registry/cache",
209        ];
210        assert_eq!(SAMPLE.len(), 1);
211        assert_eq!(SAMPLE[0].label, "cargo registry");
212        assert_eq!(SAMPLE[0].path, ".cargo/registry/cache");
213    }
214
215    #[test]
216    fn macro_emits_multiple_entries() {
217        const SAMPLE: &[DefaultRoot] = roots![
218            /// alpha
219            ".a",
220            /// beta
221            ".b",
222            /// gamma
223            ".c",
224        ];
225        let labels: Vec<_> = SAMPLE.iter().map(|r| r.label).collect();
226        let paths: Vec<_> = SAMPLE.iter().map(|r| r.path).collect();
227        assert_eq!(labels, ["alpha", "beta", "gamma"]);
228        assert_eq!(paths, [".a", ".b", ".c"]);
229    }
230
231    #[test]
232    fn label_has_no_leading_space() {
233        const SAMPLE: &[DefaultRoot] = roots![
234            /// no leading space
235            ".x",
236        ];
237        assert_eq!(SAMPLE[0].label, "no leading space");
238        assert!(!SAMPLE[0].label.starts_with(' '));
239    }
240
241    #[test]
242    fn defaults_contains_cargo_registry() {
243        assert!(defaults().any(|r| r.path == ".cargo/registry/cache"));
244    }
245
246    #[test]
247    fn defaults_has_no_empty_labels() {
248        for r in defaults() {
249            assert!(!r.label.is_empty(), "empty label for {}", r.path);
250        }
251    }
252
253    #[test]
254    fn defaults_paths_are_unique() {
255        let mut seen = std::collections::HashSet::new();
256        for r in defaults() {
257            assert!(seen.insert(r.path), "duplicate path {}", r.path);
258        }
259    }
260}