Skip to main content

dev_prune/adapters/
swift.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// Swift Package Manager adapter.
5//
6// Opt-in (`devp config set enable_swift true`), for the same reason as Gradle and
7// Maven: `.build/` is not a dependency tree, it is a dependency tree *plus* every
8// compiled module, and it comes back through `swift build` rather than a download. The
9// engine also holds it to the longer `build_idle_days` window.
10//
11// `.swiftpm/` is deliberately not claimed — it holds editor and scheme configuration
12// people commit and nothing regenerates.
13
14use super::{BloatDir, EnforcePolicy, PackageManager, dir_size};
15use anyhow::{Result, anyhow};
16use std::fs;
17use std::path::Path;
18
19/// Swift Package Manager adapter. Opt-in; see the module comment.
20pub struct Swift;
21
22impl PackageManager for Swift {
23    fn name(&self) -> &'static str {
24        "swift"
25    }
26
27    fn detect(&self, path: &Path) -> bool {
28        path.join("Package.swift").exists()
29    }
30
31    fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
32        let build = path.join(".build");
33        if !build.is_dir() {
34            return Vec::new();
35        }
36        vec![BloatDir {
37            name: ".build".to_string(),
38            path: build.clone(),
39            size_bytes: dir_size(&build),
40            shared_bytes: 0,
41        }]
42    }
43
44    /// The manifest is the proof, as it is for Maven: `.build/` is derived entirely from
45    /// `Package.swift`, the sources beside it and — when one exists — `Package.resolved`.
46    /// Running `swift package resolve` here instead would fetch dependencies over the
47    /// network in the middle of a delete pass, for no stronger answer.
48    fn enforce_lockfile(&self, path: &Path, _policy: EnforcePolicy) -> Result<()> {
49        let manifest = path.join("Package.swift");
50        let content = fs::read_to_string(&manifest).map_err(|e| {
51            anyhow!("`Package.swift` could not be read ({e}) — nothing to rebuild `.build/` from.")
52        })?;
53        if !content.contains("Package(") {
54            return Err(anyhow!(
55                "`Package.swift` declares no `Package(` — refusing to treat `.build/` as \
56                 rebuildable from it."
57            ));
58        }
59        Ok(())
60    }
61
62    fn restore(&self, _path: &Path, _timeout: std::time::Duration) -> Result<()> {
63        println!("SwiftPM .build/ will regenerate on the next `swift build`");
64        Ok(())
65    }
66
67    fn lockfiles(&self) -> &'static [&'static str] {
68        &["Package.resolved"]
69    }
70
71    fn opt_in(&self) -> bool {
72        true
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use tempfile::tempdir;
80
81    #[test]
82    fn detects_on_the_package_manifest() {
83        let dir = tempdir().unwrap();
84        assert!(!Swift.detect(dir.path()));
85        fs::write(
86            dir.path().join("Package.swift"),
87            "// swift-tools-version:5.9",
88        )
89        .unwrap();
90        assert!(Swift.detect(dir.path()));
91    }
92
93    #[test]
94    fn claims_the_build_directory_and_not_the_swiftpm_one() {
95        let dir = tempdir().unwrap();
96        fs::create_dir(dir.path().join(".build")).unwrap();
97        fs::create_dir(dir.path().join(".swiftpm")).unwrap();
98        let names: Vec<String> = Swift
99            .bloat_dirs(dir.path())
100            .into_iter()
101            .map(|b| b.name)
102            .collect();
103        assert_eq!(names, vec![".build"]);
104    }
105
106    #[test]
107    fn a_missing_or_bogus_manifest_is_refused() {
108        let dir = tempdir().unwrap();
109        assert!(
110            Swift
111                .enforce_lockfile(dir.path(), EnforcePolicy::default())
112                .is_err()
113        );
114        fs::write(dir.path().join("Package.swift"), "let x = 1").unwrap();
115        assert!(
116            Swift
117                .enforce_lockfile(dir.path(), EnforcePolicy::default())
118                .is_err()
119        );
120        fs::write(
121            dir.path().join("Package.swift"),
122            "let package = Package(name: \"x\")",
123        )
124        .unwrap();
125        assert!(
126            Swift
127                .enforce_lockfile(dir.path(), EnforcePolicy::default())
128                .is_ok()
129        );
130    }
131
132    #[test]
133    fn swift_is_opt_in() {
134        assert!(Swift.opt_in());
135    }
136}