Skip to main content

dev_prune/adapters/
godot.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// Godot imported-resource cache adapter.
5//
6// Godot's editor keeps a cache of every imported asset (compressed textures, decoded
7// audio, shader caches, editor metadata) beside the project it belongs to: `.godot/`
8// in Godot 4, `.import/` in Godot 3. Both are written only by the editor, both sit
9// beside `project.godot`, and both regenerate from the committed source assets the
10// next time the editor opens the project, which is why the official starter
11// `.gitignore` for Godot excludes them. On an asset-heavy project the cache is
12// routinely larger than the sources it was imported from.
13//
14// The claim is deliberately narrow: only those two directory names, only directly
15// beside a `project.godot`, and only when that manifest still parses as one (it always
16// carries a `config_version=` line). There is nothing like CMake's cache file to read
17// a source path out of, and nothing is needed: a hidden directory with the editor's
18// own name beside the editor's own manifest is not something anybody writes by hand.
19//
20// Opt-in, and held to `build_idle_days`: getting the cache back means the editor
21// re-importing every asset, which on a large project is a long sit, not a download.
22
23use super::{BloatDir, EnforcePolicy, PackageManager, dir_size};
24use anyhow::{Result, anyhow};
25use std::fs;
26use std::path::Path;
27
28/// The manifest at every Godot project's root, whatever the engine version.
29const MANIFEST: &str = "project.godot";
30
31/// The editor's cache directories: `.godot/` is Godot 4, `.import/` is Godot 3.
32const CACHE_DIRS: &[&str] = &[".godot", ".import"];
33
34/// Godot imported-resource cache adapter. Opt-in; see the module comment.
35pub struct Godot;
36
37impl PackageManager for Godot {
38    fn name(&self) -> &'static str {
39        "godot"
40    }
41
42    fn detect(&self, path: &Path) -> bool {
43        path.join(MANIFEST).is_file()
44    }
45
46    fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
47        CACHE_DIRS
48            .iter()
49            .map(|name| path.join(name))
50            .filter(|dir| dir.is_dir())
51            .map(|dir| BloatDir {
52                name: dir
53                    .file_name()
54                    .unwrap_or_default()
55                    .to_string_lossy()
56                    .into_owned(),
57                size_bytes: dir_size(&dir),
58                path: dir,
59                shared_bytes: 0,
60            })
61            .collect()
62    }
63
64    /// What is checked is that `project.godot` is still readable and still reads as a
65    /// Godot project file, because it is what the editor is about to be pointed at.
66    /// Every version of the format carries a `config_version=` line, and a file
67    /// without one is whatever it is, not a project the editor can re-import from.
68    /// Running the editor here to find out would start an import in the middle of a
69    /// delete pass, which is the opposite of what was asked for.
70    fn enforce_lockfile(&self, path: &Path, _policy: EnforcePolicy) -> Result<()> {
71        let manifest = path.join(MANIFEST);
72        let content = fs::read_to_string(&manifest).map_err(|e| {
73            anyhow!("`{MANIFEST}` could not be read ({e}): nothing to re-import the assets from.")
74        })?;
75        if !content.contains("config_version=") {
76            return Err(anyhow!(
77                "`{MANIFEST}` has no `config_version=` line: refusing to treat the imported-\
78                 resource cache as regenerable from it."
79            ));
80        }
81        Ok(())
82    }
83
84    fn restore(&self, _path: &Path, _timeout: std::time::Duration) -> Result<()> {
85        println!(
86            "Godot's imported-resource cache regenerates when the editor next opens the \
87             project; `godot --headless --import` does it without the window on 4.x"
88        );
89        Ok(())
90    }
91
92    fn lockfiles(&self) -> &'static [&'static str] {
93        &[MANIFEST]
94    }
95
96    fn opt_in(&self) -> bool {
97        true
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use std::path::PathBuf;
105    use tempfile::tempdir;
106
107    /// A project root holding a plausible `project.godot`.
108    fn project(dir: &Path) -> PathBuf {
109        fs::write(
110            dir.join(MANIFEST),
111            "; Engine configuration file.\nconfig_version=5\n\n[application]\n\nconfig/name=\"Demo\"\n",
112        )
113        .unwrap();
114        dir.to_path_buf()
115    }
116
117    fn claimed(project: &Path) -> Vec<String> {
118        Godot
119            .bloat_dirs(project)
120            .into_iter()
121            .map(|b| b.name)
122            .collect()
123    }
124
125    #[test]
126    fn detects_on_the_project_manifest() {
127        let dir = tempdir().unwrap();
128        assert!(!Godot.detect(dir.path()));
129        project(dir.path());
130        assert!(Godot.detect(dir.path()));
131    }
132
133    #[test]
134    fn both_engine_versions_caches_are_claimed() {
135        // A project migrated from Godot 3 to 4 can be carrying both directories, and
136        // both are the editor's to regenerate.
137        let dir = tempdir().unwrap();
138        let root = project(dir.path());
139        fs::create_dir(root.join(".godot")).unwrap();
140        fs::write(root.join(".godot").join("uid_cache.bin"), "cache").unwrap();
141        fs::create_dir(root.join(".import")).unwrap();
142
143        assert_eq!(claimed(&root), vec![".godot", ".import"]);
144    }
145
146    #[test]
147    fn nothing_else_beside_the_manifest_is_claimed() {
148        // The claim is by these two names and nothing else: a directory of the user's
149        // own is not this adapter's to reason about.
150        let dir = tempdir().unwrap();
151        let root = project(dir.path());
152        fs::create_dir(root.join("assets")).unwrap();
153
154        assert!(claimed(&root).is_empty());
155    }
156
157    #[test]
158    fn a_missing_or_bogus_manifest_is_refused() {
159        let dir = tempdir().unwrap();
160        let policy = EnforcePolicy::default();
161        assert!(Godot.enforce_lockfile(dir.path(), policy).is_err());
162        fs::write(dir.path().join(MANIFEST), "hello there").unwrap();
163        assert!(Godot.enforce_lockfile(dir.path(), policy).is_err());
164        project(dir.path());
165        assert!(Godot.enforce_lockfile(dir.path(), policy).is_ok());
166    }
167
168    #[test]
169    fn godot_is_opt_in() {
170        assert!(Godot.opt_in());
171    }
172}