Skip to main content

dev_prune/adapters/
gradle.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// Gradle build-tool adapter for Java/Kotlin/Android projects.
5//
6// Opt-in (`devp config set enable_gradle true`), for the same reason as Maven: `build/`
7// comes back by recompiling, not by re-downloading, and on an Android project that can
8// mean a very long first build. The engine holds these directories to the longer
9// `build_idle_days` window on top of the opt-in.
10//
11// What it claims: `build/` (compiled outputs, all derived from the sources in the
12// tree) and the project-local `.gradle/` (per-project caches: configuration cache,
13// file-hash indexes — bookkeeping Gradle rebuilds on the next invocation). Gradle's
14// *user-home* caches (`~/.gradle/caches`) live outside the repository and are never
15// this tool's business.
16
17use super::{BloatDir, EnforcePolicy, PackageManager, dir_size};
18use anyhow::{Result, anyhow};
19use std::path::Path;
20
21/// The manifests any Gradle project has at least one of.
22const GRADLE_MANIFESTS: [&str; 4] = [
23    "build.gradle",
24    "build.gradle.kts",
25    "settings.gradle",
26    "settings.gradle.kts",
27];
28
29/// Adapter for Gradle-based projects. Opt-in; see the module comment.
30pub struct Gradle;
31
32fn has_manifest(path: &Path) -> bool {
33    GRADLE_MANIFESTS.iter().any(|m| path.join(m).exists())
34}
35
36impl PackageManager for Gradle {
37    fn name(&self) -> &'static str {
38        "gradle"
39    }
40
41    fn detect(&self, path: &Path) -> bool {
42        has_manifest(path)
43    }
44
45    fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
46        let mut dirs = Vec::new();
47        for name in ["build", ".gradle"] {
48            let dir = path.join(name);
49            if dir.is_dir() {
50                dirs.push(BloatDir {
51                    name: name.to_string(),
52                    path: dir.clone(),
53                    size_bytes: dir_size(&dir),
54                    shared_bytes: 0,
55                });
56            }
57        }
58        dirs
59    }
60
61    /// Like Maven: the rebuild starts from the manifests in the tree, so their
62    /// presence is the recoverability proof. Invoking Gradle itself here would run a
63    /// configuration phase that can resolve plugins over the network mid-prune.
64    fn enforce_lockfile(&self, path: &Path, _policy: EnforcePolicy) -> Result<()> {
65        if !has_manifest(path) {
66            return Err(anyhow!(
67                "no Gradle manifest (build.gradle[.kts] / settings.gradle[.kts]) — \
68                 nothing to rebuild `build/` from."
69            ));
70        }
71        Ok(())
72    }
73
74    fn restore(&self, path: &Path, _timeout: std::time::Duration) -> Result<()> {
75        let wrapper = if cfg!(windows) {
76            "gradlew.bat"
77        } else {
78            "gradlew"
79        };
80        let cmd = if path.join(wrapper).exists() {
81            "./gradlew build"
82        } else {
83            "gradle build"
84        };
85        println!("Gradle build/ will regenerate on the next `{cmd}`");
86        Ok(())
87    }
88
89    fn opt_in(&self) -> bool {
90        true
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use std::fs;
98    use tempfile::tempdir;
99
100    #[test]
101    fn detects_on_any_gradle_manifest() {
102        let dir = tempdir().unwrap();
103        assert!(!Gradle.detect(dir.path()));
104        fs::write(dir.path().join("build.gradle.kts"), "").unwrap();
105        assert!(Gradle.detect(dir.path()));
106    }
107
108    #[test]
109    fn claims_build_and_project_local_gradle_dir() {
110        let dir = tempdir().unwrap();
111        fs::create_dir(dir.path().join("build")).unwrap();
112        fs::create_dir(dir.path().join(".gradle")).unwrap();
113        let names: Vec<String> = Gradle
114            .bloat_dirs(dir.path())
115            .into_iter()
116            .map(|b| b.name)
117            .collect();
118        assert_eq!(names, vec!["build", ".gradle"]);
119    }
120
121    #[test]
122    fn a_vanished_manifest_is_refused() {
123        let dir = tempdir().unwrap();
124        assert!(
125            Gradle
126                .enforce_lockfile(dir.path(), EnforcePolicy::default())
127                .is_err()
128        );
129        fs::write(dir.path().join("settings.gradle"), "").unwrap();
130        assert!(
131            Gradle
132                .enforce_lockfile(dir.path(), EnforcePolicy::default())
133                .is_ok()
134        );
135    }
136
137    #[test]
138    fn gradle_is_opt_in() {
139        assert!(Gradle.opt_in());
140    }
141}