Skip to main content

dev_prune/adapters/
cargo_adapter.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// Cargo/Rust package manager adapter.
5
6use super::{BloatDir, EnforcePolicy, PackageManager, dir_size, enforce_two_tier};
7use anyhow::Result;
8use std::path::Path;
9
10/// Adapter for Cargo-based Rust projects.
11pub struct Cargo;
12
13impl PackageManager for Cargo {
14    fn name(&self) -> &'static str {
15        "cargo"
16    }
17
18    fn detect(&self, path: &Path) -> bool {
19        path.join("Cargo.toml").exists()
20    }
21
22    fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
23        let mut dirs = Vec::new();
24        let target_path = path.join("target");
25        if target_path.exists() {
26            dirs.push(BloatDir {
27                name: "target".to_string(),
28                path: target_path.clone(),
29                size_bytes: dir_size(&target_path),
30            });
31        }
32        dirs
33    }
34
35    /// `cargo metadata --locked` resolves the graph and fails if `Cargo.lock` would need
36    /// updating — without ever writing to it. `generate-lockfile` re-resolves and
37    /// rewrites it, which is what a user with a stale lockfile wants and what `metadata
38    /// --locked` refuses to do for them; it is reached when there is no lockfile to
39    /// preserve, or when they opted in. `--offline` is deliberately never passed —
40    /// re-resolving against a stale local index is how you get a lockfile that does not
41    /// build.
42    fn enforce_lockfile(&self, path: &Path, policy: EnforcePolicy) -> Result<()> {
43        enforce_two_tier(
44            &path.join("Cargo.lock"),
45            "cargo",
46            &["metadata", "--locked", "--format-version", "1"],
47            &["generate-lockfile"],
48            path,
49            policy,
50        )
51    }
52
53    fn restore(&self, _path: &Path) -> Result<()> {
54        println!("Rust target/ will regenerate on next cargo build");
55        Ok(())
56    }
57
58    fn lockfiles(&self) -> &'static [&'static str] {
59        &["Cargo.lock"]
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use std::fs;
67    use std::fs::File;
68    use tempfile::tempdir;
69
70    #[test]
71    fn test_name() {
72        let adapter = Cargo;
73        assert_eq!(adapter.name(), "cargo");
74    }
75
76    /// The invariant the whole two-tier design exists for: a default pass may fail, but
77    /// it may not leave the lockfile different from how it found it.
78    ///
79    /// Uses a lockfile that does not list the manifest's dependency, which is the exact
80    /// state `--locked` is there to refuse. Skipped rather than failed when `cargo` is
81    /// absent, so the suite still runs somewhere without a Rust toolchain on `PATH`.
82    #[test]
83    fn a_default_pass_never_rewrites_a_stale_lockfile() {
84        if !super::super::binary_available("cargo") {
85            return;
86        }
87        let dir = tempdir().unwrap();
88        fs::write(
89            dir.path().join("Cargo.toml"),
90            "[package]\nname = \"stale\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n\
91             [dependencies]\nserde = \"1\"\n",
92        )
93        .unwrap();
94        fs::create_dir(dir.path().join("src")).unwrap();
95        fs::write(dir.path().join("src").join("lib.rs"), "").unwrap();
96
97        // A lockfile that knows only about the root package — `serde` is missing, so the
98        // graph cannot be resolved from it.
99        let stale = "version = 3\n\n[[package]]\nname = \"stale\"\nversion = \"0.1.0\"\n";
100        fs::write(dir.path().join("Cargo.lock"), stale).unwrap();
101
102        let result = Cargo.enforce_lockfile(dir.path(), EnforcePolicy::default());
103
104        assert!(
105            result.is_err(),
106            "a lockfile that cannot resolve the manifest must not pass verification"
107        );
108        assert_eq!(
109            fs::read_to_string(dir.path().join("Cargo.lock")).unwrap(),
110            stale,
111            "the read-only verification rewrote Cargo.lock"
112        );
113    }
114
115    #[test]
116    fn test_detect_positive() {
117        let dir = tempdir().unwrap();
118        File::create(dir.path().join("Cargo.toml")).unwrap();
119
120        let adapter = Cargo;
121        assert!(adapter.detect(dir.path()));
122    }
123
124    #[test]
125    fn test_detect_negative() {
126        let dir = tempdir().unwrap();
127
128        let adapter = Cargo;
129        assert!(!adapter.detect(dir.path()));
130    }
131
132    #[test]
133    fn test_bloat_dirs_present() {
134        let dir = tempdir().unwrap();
135        fs::create_dir(dir.path().join("target")).unwrap();
136
137        let adapter = Cargo;
138        let dirs = adapter.bloat_dirs(dir.path());
139        assert_eq!(dirs.len(), 1);
140        assert_eq!(dirs[0].name, "target");
141    }
142
143    #[test]
144    fn test_bloat_dirs_absent() {
145        let dir = tempdir().unwrap();
146
147        let adapter = Cargo;
148        let dirs = adapter.bloat_dirs(dir.path());
149        assert!(dirs.is_empty());
150    }
151}