Skip to main content

dev_prune/adapters/
zig.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// Zig build cache adapter.
5//
6// `zig build` keeps its incremental compilation state beside the project: `.zig-cache/`
7// since Zig 0.12, `zig-cache/` before that, and `zig-out/` for the installed build
8// artifacts. All three are written only by the build system, all three sit beside
9// `build.zig`, and all three regenerate from `zig build` — which is why the standard
10// Zig `.gitignore` excludes them. `zig-out/` is included deliberately: it is the build
11// system's own install prefix, populated entirely by build steps `build.zig` declares,
12// not a directory a person authors into.
13//
14// Opt-in, and held to `build_idle_days`: getting the caches back means recompiling the
15// project, not downloading dependencies.
16
17use super::{BloatDir, EnforcePolicy, PackageManager, dir_size};
18use anyhow::{Result, anyhow};
19use std::fs;
20use std::path::Path;
21
22/// The build script at every Zig project's root.
23const MANIFEST: &str = "build.zig";
24
25/// The build system's directories: `.zig-cache/` is 0.12+, `zig-cache/` is older, and
26/// `zig-out/` is the default install prefix build steps write into.
27const CACHE_DIRS: &[&str] = &[".zig-cache", "zig-cache", "zig-out"];
28
29/// Zig build cache adapter. Opt-in; see the module comment.
30pub struct Zig;
31
32impl PackageManager for Zig {
33    fn name(&self) -> &'static str {
34        "zig"
35    }
36
37    fn detect(&self, path: &Path) -> bool {
38        path.join(MANIFEST).is_file()
39    }
40
41    fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
42        CACHE_DIRS
43            .iter()
44            .map(|name| path.join(name))
45            .filter(|dir| dir.is_dir())
46            .map(|dir| BloatDir {
47                name: dir
48                    .file_name()
49                    .unwrap_or_default()
50                    .to_string_lossy()
51                    .into_owned(),
52                size_bytes: dir_size(&dir),
53                path: dir,
54                shared_bytes: 0,
55            })
56            .collect()
57    }
58
59    /// What is checked is that `build.zig` is still readable and still reads as a build
60    /// script, because it is what `zig build` is about to execute. Every build script
61    /// exports a `pub fn build` entry point — that is the contract the compiler calls
62    /// into — and a file without one is whatever it is, not something that can rebuild
63    /// these directories. Running `zig build` here to find out would start a compile in
64    /// the middle of a delete pass.
65    fn enforce_lockfile(&self, path: &Path, _policy: EnforcePolicy) -> Result<()> {
66        let manifest = path.join(MANIFEST);
67        let content = fs::read_to_string(&manifest).map_err(|e| {
68            anyhow!("`{MANIFEST}` could not be read ({e}): nothing to rebuild the caches from.")
69        })?;
70        if !content.contains("pub fn build") {
71            return Err(anyhow!(
72                "`{MANIFEST}` has no `pub fn build` entry point: refusing to treat the \
73                 build caches as regenerable from it."
74            ));
75        }
76        Ok(())
77    }
78
79    fn restore(&self, _path: &Path, _timeout: std::time::Duration) -> Result<()> {
80        println!("Zig's caches and outputs regenerate on the next `zig build`");
81        Ok(())
82    }
83
84    fn lockfiles(&self) -> &'static [&'static str] {
85        &[MANIFEST]
86    }
87
88    fn opt_in(&self) -> bool {
89        true
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use std::path::PathBuf;
97    use tempfile::tempdir;
98
99    /// A project root holding a plausible `build.zig`.
100    fn project(dir: &Path) -> PathBuf {
101        fs::write(
102            dir.join(MANIFEST),
103            "const std = @import(\"std\");\n\npub fn build(b: *std.Build) void {\n    _ = b;\n}\n",
104        )
105        .unwrap();
106        dir.to_path_buf()
107    }
108
109    fn claimed(project: &Path) -> Vec<String> {
110        Zig.bloat_dirs(project)
111            .into_iter()
112            .map(|b| b.name)
113            .collect()
114    }
115
116    #[test]
117    fn detects_on_the_build_script() {
118        let dir = tempdir().unwrap();
119        assert!(!Zig.detect(dir.path()));
120        project(dir.path());
121        assert!(Zig.detect(dir.path()));
122    }
123
124    #[test]
125    fn caches_and_install_output_are_claimed() {
126        // A project that lived through the 0.12 rename can carry both cache spellings,
127        // and `zig-out` is the build system's install prefix, not the user's.
128        let dir = tempdir().unwrap();
129        let root = project(dir.path());
130        fs::create_dir(root.join(".zig-cache")).unwrap();
131        fs::create_dir(root.join("zig-cache")).unwrap();
132        fs::create_dir(root.join("zig-out")).unwrap();
133
134        assert_eq!(claimed(&root), vec![".zig-cache", "zig-cache", "zig-out"]);
135    }
136
137    #[test]
138    fn nothing_else_beside_the_manifest_is_claimed() {
139        let dir = tempdir().unwrap();
140        let root = project(dir.path());
141        fs::create_dir(root.join("src")).unwrap();
142
143        assert!(claimed(&root).is_empty());
144    }
145
146    #[test]
147    fn a_missing_or_bogus_build_script_is_refused() {
148        let dir = tempdir().unwrap();
149        let policy = EnforcePolicy::default();
150        assert!(Zig.enforce_lockfile(dir.path(), policy).is_err());
151        fs::write(dir.path().join(MANIFEST), "hello there").unwrap();
152        assert!(Zig.enforce_lockfile(dir.path(), policy).is_err());
153        project(dir.path());
154        assert!(Zig.enforce_lockfile(dir.path(), policy).is_ok());
155    }
156
157    #[test]
158    fn zig_is_opt_in() {
159        assert!(Zig.opt_in());
160    }
161}