Skip to main content

dev_prune/adapters/
cocoapods.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// CocoaPods adapter for Apple-platform projects.
5//
6// `Pods/` is a download restore: `pod install` reads `Podfile.lock` and checks out the
7// exact pod versions recorded in it, so this adapter is not opt-in.
8//
9// The proof is offline. CocoaPods has no read-only "is the lockfile in sync" command —
10// `pod install` and `pod update` both *fix* drift by rewriting `Podfile.lock` and
11// re-downloading, which is a write and a network round trip in the middle of a delete
12// pass. So the check is the lockfile's own structure plus the manifest timestamps; see
13// [`super::refuse_if_manifest_stale`].
14
15use super::{
16    BloatDir, EnforcePolicy, PackageManager, dir_size, refuse_if_manifest_stale,
17    run_command_with_timeout,
18};
19use anyhow::{Result, anyhow};
20use std::fs;
21use std::path::Path;
22
23/// CocoaPods adapter.
24pub struct CocoaPods;
25
26/// The section every `Podfile.lock` CocoaPods wrote ends with. Its absence means the
27/// file is a fragment — a half-written lock or a merge conflict left in the tree — and
28/// `pod install` would resolve afresh rather than restore what was deleted.
29const LOCK_SENTINEL: &str = "SPEC CHECKSUMS";
30
31impl PackageManager for CocoaPods {
32    fn name(&self) -> &'static str {
33        "cocoapods"
34    }
35
36    fn detect(&self, path: &Path) -> bool {
37        path.join("Podfile").exists()
38    }
39
40    fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
41        let pods = path.join("Pods");
42        if !pods.is_dir() {
43            return Vec::new();
44        }
45        vec![BloatDir {
46            name: "Pods".to_string(),
47            path: pods.clone(),
48            size_bytes: dir_size(&pods),
49            shared_bytes: 0,
50        }]
51    }
52
53    fn enforce_lockfile(&self, path: &Path, _policy: EnforcePolicy) -> Result<()> {
54        let lock = path.join("Podfile.lock");
55        let content = fs::read_to_string(&lock).map_err(|e| {
56            anyhow!(
57                "`Podfile.lock` could not be read ({e}) — without it `pod install` \
58                 resolves afresh instead of restoring the versions being deleted."
59            )
60        })?;
61        if !content.contains(LOCK_SENTINEL) {
62            return Err(anyhow!(
63                "`Podfile.lock` has no `{LOCK_SENTINEL}` section — it is not a complete \
64                 CocoaPods lockfile, so `Pods/` cannot be proven rebuildable from it."
65            ));
66        }
67        refuse_if_manifest_stale(&path.join("Podfile"), &lock, "pod install")
68    }
69
70    fn restore(&self, path: &Path, timeout: std::time::Duration) -> Result<()> {
71        run_command_with_timeout("pod", &["install"], path, timeout)
72    }
73
74    fn lockfiles(&self) -> &'static [&'static str] {
75        &["Podfile.lock"]
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use tempfile::tempdir;
83
84    fn complete_lock() -> &'static str {
85        "PODS:\n  - Alamofire (5.9.1)\n\nSPEC CHECKSUMS:\n  Alamofire: abc\n\nCOCOAPODS: 1.15.2\n"
86    }
87
88    #[test]
89    fn detects_on_the_podfile() {
90        let dir = tempdir().unwrap();
91        assert!(!CocoaPods.detect(dir.path()));
92        fs::write(dir.path().join("Podfile"), "platform :ios").unwrap();
93        assert!(CocoaPods.detect(dir.path()));
94    }
95
96    #[test]
97    fn claims_the_pods_directory() {
98        let dir = tempdir().unwrap();
99        assert!(CocoaPods.bloat_dirs(dir.path()).is_empty());
100        fs::create_dir(dir.path().join("Pods")).unwrap();
101        let dirs = CocoaPods.bloat_dirs(dir.path());
102        assert_eq!(dirs.len(), 1);
103        assert_eq!(dirs[0].name, "Pods");
104    }
105
106    #[test]
107    fn a_missing_or_truncated_lockfile_is_refused() {
108        let dir = tempdir().unwrap();
109        assert!(
110            CocoaPods
111                .enforce_lockfile(dir.path(), EnforcePolicy::default())
112                .is_err()
113        );
114        fs::write(dir.path().join("Podfile.lock"), "PODS:\n").unwrap();
115        assert!(
116            CocoaPods
117                .enforce_lockfile(dir.path(), EnforcePolicy::default())
118                .is_err()
119        );
120        fs::write(dir.path().join("Podfile.lock"), complete_lock()).unwrap();
121        assert!(
122            CocoaPods
123                .enforce_lockfile(dir.path(), EnforcePolicy::default())
124                .is_ok()
125        );
126    }
127}