dev_prune/adapters/
cargo_adapter.rs1use super::{BloatDir, EnforcePolicy, PackageManager, dir_size, enforce_two_tier};
7use anyhow::Result;
8use std::path::{Path, PathBuf};
9
10pub struct Cargo;
12
13fn workspace_lockfile(path: &Path) -> Option<PathBuf> {
21 let mut dir = path;
22 loop {
23 let candidate = dir.join("Cargo.lock");
24 if candidate.exists() {
25 return Some(candidate);
26 }
27 if dir.join(".git").exists() {
29 return None;
30 }
31 dir = dir.parent()?;
32 }
33}
34
35impl PackageManager for Cargo {
36 fn name(&self) -> &'static str {
37 "cargo"
38 }
39
40 fn detect(&self, path: &Path) -> bool {
41 path.join("Cargo.toml").exists()
42 }
43
44 fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
45 let mut dirs = Vec::new();
46 let target_path = path.join("target");
47 if target_path.exists() {
48 dirs.push(BloatDir {
49 name: "target".to_string(),
50 path: target_path.clone(),
51 size_bytes: dir_size(&target_path),
52 shared_bytes: 0,
53 });
54 }
55 dirs
56 }
57
58 fn enforce_lockfile(&self, path: &Path, policy: EnforcePolicy) -> Result<()> {
66 if path.join("target").join("criterion").is_dir() {
71 crate::output::print_warning(&format!(
72 "{}: `target/criterion` holds benchmark history that a rebuild does not \
73 bring back — copy it first if the baselines matter.",
74 crate::output::clean_path(path)
75 ));
76 }
77 let lockfile = workspace_lockfile(path).unwrap_or_else(|| path.join("Cargo.lock"));
78 enforce_two_tier(
79 &lockfile,
80 "cargo",
81 &["metadata", "--locked", "--format-version", "1"],
82 &["generate-lockfile"],
83 path,
84 policy,
85 )
86 }
87
88 fn restore(&self, _path: &Path, _timeout: std::time::Duration) -> Result<()> {
89 println!("Rust target/ will regenerate on next cargo build");
90 Ok(())
91 }
92
93 fn lockfiles(&self) -> &'static [&'static str] {
94 &["Cargo.lock"]
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use std::fs;
102 use std::fs::File;
103 use tempfile::tempdir;
104
105 #[test]
106 fn test_name() {
107 let adapter = Cargo;
108 assert_eq!(adapter.name(), "cargo");
109 }
110
111 #[test]
118 fn a_default_pass_never_rewrites_a_stale_lockfile() {
119 if !super::super::binary_available("cargo") {
120 return;
121 }
122 let dir = tempdir().unwrap();
123 fs::write(
124 dir.path().join("Cargo.toml"),
125 "[package]\nname = \"stale\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n\
126 [dependencies]\nserde = \"1\"\n",
127 )
128 .unwrap();
129 fs::create_dir(dir.path().join("src")).unwrap();
130 fs::write(dir.path().join("src").join("lib.rs"), "").unwrap();
131
132 let stale = "version = 3\n\n[[package]]\nname = \"stale\"\nversion = \"0.1.0\"\n";
135 fs::write(dir.path().join("Cargo.lock"), stale).unwrap();
136
137 let result = Cargo.enforce_lockfile(dir.path(), EnforcePolicy::default());
138
139 assert!(
140 result.is_err(),
141 "a lockfile that cannot resolve the manifest must not pass verification"
142 );
143 assert_eq!(
144 fs::read_to_string(dir.path().join("Cargo.lock")).unwrap(),
145 stale,
146 "the read-only verification rewrote Cargo.lock"
147 );
148 }
149
150 #[test]
151 fn test_detect_positive() {
152 let dir = tempdir().unwrap();
153 File::create(dir.path().join("Cargo.toml")).unwrap();
154
155 let adapter = Cargo;
156 assert!(adapter.detect(dir.path()));
157 }
158
159 #[test]
160 fn test_detect_negative() {
161 let dir = tempdir().unwrap();
162
163 let adapter = Cargo;
164 assert!(!adapter.detect(dir.path()));
165 }
166
167 #[test]
168 fn test_bloat_dirs_present() {
169 let dir = tempdir().unwrap();
170 fs::create_dir(dir.path().join("target")).unwrap();
171
172 let adapter = Cargo;
173 let dirs = adapter.bloat_dirs(dir.path());
174 assert_eq!(dirs.len(), 1);
175 assert_eq!(dirs[0].name, "target");
176 }
177
178 #[test]
179 fn test_bloat_dirs_absent() {
180 let dir = tempdir().unwrap();
181
182 let adapter = Cargo;
183 let dirs = adapter.bloat_dirs(dir.path());
184 assert!(dirs.is_empty());
185 }
186}