Skip to main content

dev_prune/adapters/
dart.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// Dart and Flutter adapter.
5//
6// Opt-in (`devp config set enable_dart true`), and held to `build_idle_days`, for a
7// reason that is worth stating precisely because it is not the obvious one.
8//
9// `.dart_tool/` is not where the dependencies live. Pub downloads packages once into a
10// machine-wide cache — `~/.pub-cache` — and `.dart_tool/package_config.json` is a list of
11// pointers into it. Restoring that part is `dart pub get`, offline, in under a second.
12// What actually takes up the space is everything else in there: `build/` from
13// `build_runner`'s generated code, and `flutter_build/` from Flutter's incremental
14// compiler, which on an app of any size is hundreds of megabytes and comes back only by
15// recompiling. That is compiler output, so it follows the same rule cargo, gradle, maven
16// and swift do — nobody finds it gone without having switched it on.
17//
18// `build/` at the project root is Flutter's *output* directory (the APK, the web bundle)
19// and is not claimed at all. Neither are `ios/Pods` or `android/.gradle`: those belong to
20// the CocoaPods and Gradle adapters, which have their own lockfile proofs.
21
22use super::{
23    BloatDir, EnforcePolicy, PackageManager, dir_size, refuse_if_manifest_stale,
24    run_command_with_timeout,
25};
26use anyhow::{Result, anyhow};
27use std::fs;
28use std::path::Path;
29
30/// Dart and Flutter adapter.
31pub struct Dart;
32
33impl Dart {
34    /// Whether this is a Flutter project rather than a plain Dart one.
35    ///
36    /// It decides which binary restores the tree: `flutter pub get` does the Flutter
37    /// SDK's own bookkeeping as well as pub's, and running plain `dart pub get` in a
38    /// Flutter app leaves the tooling reconfiguring itself on the next build.
39    fn is_flutter(path: &Path) -> bool {
40        fs::read_to_string(path.join("pubspec.yaml"))
41            .map(|manifest| manifest.contains("flutter:") || manifest.contains("sdk: flutter"))
42            .unwrap_or(false)
43    }
44}
45
46impl PackageManager for Dart {
47    fn name(&self) -> &'static str {
48        "dart"
49    }
50
51    fn detect(&self, path: &Path) -> bool {
52        path.join("pubspec.yaml").exists()
53    }
54
55    fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
56        let tool = path.join(".dart_tool");
57        if !tool.is_dir() {
58            return Vec::new();
59        }
60        vec![BloatDir {
61            name: ".dart_tool".to_string(),
62            path: tool.clone(),
63            size_bytes: dir_size(&tool),
64            shared_bytes: 0,
65        }]
66    }
67
68    fn enforce_lockfile(&self, path: &Path, _policy: EnforcePolicy) -> Result<()> {
69        let lock = path.join("pubspec.lock");
70        let content = fs::read_to_string(&lock).map_err(|e| {
71            anyhow!(
72                "`pubspec.lock` could not be read ({e}) — without it `pub get` resolves \
73                 afresh instead of restoring the versions being deleted."
74            )
75        })?;
76        // Every pub lockfile is a YAML document with a `packages:` mapping, even when a
77        // project depends on nothing. A file without it is a fragment or a merge
78        // conflict, not something `pub get` can be held to.
79        if !content.contains("packages:") {
80            return Err(anyhow!(
81                "`pubspec.lock` has no `packages:` section — it is not a complete pub \
82                 lockfile, so `.dart_tool` cannot be proven rebuildable from it."
83            ));
84        }
85        // Same offline evidence as Mix and CocoaPods, and for the same reason: `pub get`
86        // resolves and writes rather than reporting, so running it to check would be a
87        // write in the middle of a delete pass.
88        refuse_if_manifest_stale(&path.join("pubspec.yaml"), &lock, "dart pub get")
89    }
90
91    fn restore(&self, path: &Path, timeout: std::time::Duration) -> Result<()> {
92        let program = if Self::is_flutter(path) {
93            "flutter"
94        } else {
95            "dart"
96        };
97        run_command_with_timeout(program, &["pub", "get"], path, timeout)
98    }
99
100    fn lockfiles(&self) -> &'static [&'static str] {
101        &["pubspec.lock"]
102    }
103
104    fn opt_in(&self) -> bool {
105        true
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use tempfile::tempdir;
113
114    #[test]
115    fn detects_on_the_pub_manifest() {
116        let dir = tempdir().unwrap();
117        assert!(!Dart.detect(dir.path()));
118        fs::write(dir.path().join("pubspec.yaml"), "name: example\n").unwrap();
119        assert!(Dart.detect(dir.path()));
120    }
121
122    #[test]
123    fn claims_the_tool_directory_and_never_the_output_one() {
124        let dir = tempdir().unwrap();
125        fs::create_dir(dir.path().join(".dart_tool")).unwrap();
126        fs::create_dir(dir.path().join("build")).unwrap();
127        let names: Vec<String> = Dart
128            .bloat_dirs(dir.path())
129            .into_iter()
130            .map(|b| b.name)
131            .collect();
132        assert_eq!(names, vec![".dart_tool"]);
133    }
134
135    #[test]
136    fn a_missing_or_malformed_lockfile_is_refused() {
137        let dir = tempdir().unwrap();
138        assert!(
139            Dart.enforce_lockfile(dir.path(), EnforcePolicy::default())
140                .is_err()
141        );
142        fs::write(dir.path().join("pubspec.lock"), "<<<<<<< HEAD\n").unwrap();
143        assert!(
144            Dart.enforce_lockfile(dir.path(), EnforcePolicy::default())
145                .is_err()
146        );
147        fs::write(
148            dir.path().join("pubspec.lock"),
149            "packages:\n  http:\n    version: \"1.2.0\"\n",
150        )
151        .unwrap();
152        assert!(
153            Dart.enforce_lockfile(dir.path(), EnforcePolicy::default())
154                .is_ok()
155        );
156    }
157
158    #[test]
159    fn a_flutter_app_is_told_apart_from_a_plain_dart_package() {
160        let dir = tempdir().unwrap();
161        fs::write(
162            dir.path().join("pubspec.yaml"),
163            "name: cli\ndependencies:\n",
164        )
165        .unwrap();
166        assert!(!Dart::is_flutter(dir.path()));
167        fs::write(
168            dir.path().join("pubspec.yaml"),
169            "name: app\ndependencies:\n  flutter:\n    sdk: flutter\n",
170        )
171        .unwrap();
172        assert!(Dart::is_flutter(dir.path()));
173    }
174
175    #[test]
176    fn is_opt_in_because_the_bulk_of_the_directory_is_compiler_output() {
177        assert!(Dart.opt_in());
178    }
179}