Skip to main content

dev_prune/adapters/
defold.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// Defold build-output adapter.
5//
6// A Defold project is anchored by `game.project` at its root, an INI-style file the
7// editor and the command-line builder (bob) both read. Beside it they write `build/`:
8// compiled scripts, compressed textures, everything the next build regenerates from
9// the committed sources. Defold's own documentation has it ignored, and either tool
10// rebuilds it in full on the next build of the project.
11//
12// The claim is that one name, directly beside a `game.project` that still parses as
13// one (the format always opens its sections with headers like `[project]`). Nothing
14// else: `.internal/` is the editor's own state and small, and the assets are the
15// sources the build is *from*.
16//
17// Opt-in, and held to `build_idle_days`: the cost of getting it back is a full
18// rebuild, not a download.
19
20use super::{BloatDir, EnforcePolicy, PackageManager, dir_size};
21use anyhow::{Result, anyhow};
22use std::fs;
23use std::path::Path;
24
25/// The manifest at every Defold project's root.
26const MANIFEST: &str = "game.project";
27
28/// The build output the editor and bob regenerate.
29const CACHE_DIRS: &[&str] = &["build"];
30
31/// Defold build-output adapter. Opt-in; see the module comment.
32pub struct Defold;
33
34impl PackageManager for Defold {
35    fn name(&self) -> &'static str {
36        "defold"
37    }
38
39    fn detect(&self, path: &Path) -> bool {
40        path.join(MANIFEST).is_file()
41    }
42
43    fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
44        CACHE_DIRS
45            .iter()
46            .map(|name| path.join(name))
47            .filter(|dir| dir.is_dir())
48            .map(|dir| BloatDir {
49                name: dir
50                    .file_name()
51                    .unwrap_or_default()
52                    .to_string_lossy()
53                    .into_owned(),
54                size_bytes: dir_size(&dir),
55                path: dir,
56                shared_bytes: 0,
57            })
58            .collect()
59    }
60
61    /// What is checked is that `game.project` is still readable and still reads as
62    /// one: the format always carries a `[project]` section, and a file without it is
63    /// whatever it is, not a project the builder can rebuild from. Running bob to find
64    /// out would start the build this pass exists to postpone.
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 from.")
69        })?;
70        if !content.contains("[project]") {
71            return Err(anyhow!(
72                "`{MANIFEST}` has no `[project]` section: refusing to treat the build \
73                 output as regenerable from it."
74            ));
75        }
76        Ok(())
77    }
78
79    fn restore(&self, _path: &Path, _timeout: std::time::Duration) -> Result<()> {
80        println!(
81            "Defold's build output regenerates on the next build, in the editor or \
82             with bob"
83        );
84        Ok(())
85    }
86
87    fn lockfiles(&self) -> &'static [&'static str] {
88        &[MANIFEST]
89    }
90
91    fn opt_in(&self) -> bool {
92        true
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use std::path::PathBuf;
100    use tempfile::tempdir;
101
102    /// A project root holding a plausible `game.project`.
103    fn project(dir: &Path) -> PathBuf {
104        fs::write(
105            dir.join(MANIFEST),
106            "[project]\ntitle = Demo\nversion = 1.0\n\n[display]\nwidth = 960\n",
107        )
108        .unwrap();
109        dir.to_path_buf()
110    }
111
112    fn claimed(project: &Path) -> Vec<String> {
113        Defold
114            .bloat_dirs(project)
115            .into_iter()
116            .map(|b| b.name)
117            .collect()
118    }
119
120    #[test]
121    fn detects_on_the_project_manifest() {
122        let dir = tempdir().unwrap();
123        assert!(!Defold.detect(dir.path()));
124        project(dir.path());
125        assert!(Defold.detect(dir.path()));
126    }
127
128    #[test]
129    fn the_build_output_is_claimed() {
130        let dir = tempdir().unwrap();
131        let root = project(dir.path());
132        fs::create_dir(root.join("build")).unwrap();
133        fs::write(root.join("build").join("game.arcd"), "output").unwrap();
134
135        assert_eq!(claimed(&root), vec!["build"]);
136    }
137
138    #[test]
139    fn nothing_else_beside_the_manifest_is_claimed() {
140        // `.internal/` is the editor's own state and the assets are the sources; the
141        // claim is one name and the test keeps it one name.
142        let dir = tempdir().unwrap();
143        let root = project(dir.path());
144        fs::create_dir(root.join(".internal")).unwrap();
145        fs::create_dir(root.join("assets")).unwrap();
146
147        assert!(claimed(&root).is_empty());
148    }
149
150    #[test]
151    fn a_missing_or_bogus_manifest_is_refused() {
152        let dir = tempdir().unwrap();
153        let policy = EnforcePolicy::default();
154        assert!(Defold.enforce_lockfile(dir.path(), policy).is_err());
155        fs::write(dir.path().join(MANIFEST), "hello there").unwrap();
156        assert!(Defold.enforce_lockfile(dir.path(), policy).is_err());
157        project(dir.path());
158        assert!(Defold.enforce_lockfile(dir.path(), policy).is_ok());
159    }
160
161    #[test]
162    fn defold_is_opt_in() {
163        assert!(Defold.opt_in());
164    }
165}