dev_prune/adapters/
stack.rs1use super::{BloatDir, EnforcePolicy, PackageManager, dir_size};
18use anyhow::{Result, anyhow};
19use std::fs;
20use std::path::Path;
21
22const MANIFEST: &str = "stack.yaml";
24
25const CACHE_DIRS: &[&str] = &[".stack-work"];
27
28pub struct Stack;
30
31impl PackageManager for Stack {
32 fn name(&self) -> &'static str {
33 "stack"
34 }
35
36 fn detect(&self, path: &Path) -> bool {
37 path.join(MANIFEST).is_file()
38 }
39
40 fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
41 CACHE_DIRS
42 .iter()
43 .map(|name| path.join(name))
44 .filter(|dir| dir.is_dir())
45 .map(|dir| BloatDir {
46 name: dir
47 .file_name()
48 .unwrap_or_default()
49 .to_string_lossy()
50 .into_owned(),
51 size_bytes: dir_size(&dir),
52 path: dir,
53 shared_bytes: 0,
54 })
55 .collect()
56 }
57
58 fn enforce_lockfile(&self, path: &Path, _policy: EnforcePolicy) -> Result<()> {
65 let manifest = path.join(MANIFEST);
66 let content = fs::read_to_string(&manifest).map_err(|e| {
67 anyhow!("`{MANIFEST}` could not be read ({e}): nothing to rebuild `.stack-work` from.")
68 })?;
69 let names_snapshot = content.lines().any(|line| {
70 let line = line.trim_start();
71 line.starts_with("resolver:") || line.starts_with("snapshot:")
72 });
73 if !names_snapshot {
74 return Err(anyhow!(
75 "`{MANIFEST}` names no `resolver:` or `snapshot:`: refusing to treat the \
76 build tree as reproducibly regenerable from it."
77 ));
78 }
79 Ok(())
80 }
81
82 fn restore(&self, _path: &Path, _timeout: std::time::Duration) -> Result<()> {
83 println!("Stack's build tree regenerates on the next `stack build`");
84 Ok(())
85 }
86
87 fn lockfiles(&self) -> &'static [&'static str] {
88 &[MANIFEST]
89 }
90
91 fn opt_in(&self) -> bool {
92 true
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use std::path::PathBuf;
100 use tempfile::tempdir;
101
102 fn project(dir: &Path) -> PathBuf {
104 fs::write(
105 dir.join(MANIFEST),
106 "resolver: lts-22.33\n\npackages:\n- .\n",
107 )
108 .unwrap();
109 dir.to_path_buf()
110 }
111
112 fn claimed(project: &Path) -> Vec<String> {
113 Stack
114 .bloat_dirs(project)
115 .into_iter()
116 .map(|b| b.name)
117 .collect()
118 }
119
120 #[test]
121 fn detects_on_the_project_file() {
122 let dir = tempdir().unwrap();
123 assert!(!Stack.detect(dir.path()));
124 project(dir.path());
125 assert!(Stack.detect(dir.path()));
126 }
127
128 #[test]
129 fn the_build_tree_is_claimed_and_nothing_else() {
130 let dir = tempdir().unwrap();
131 let root = project(dir.path());
132 fs::create_dir(root.join(".stack-work")).unwrap();
133 fs::create_dir(root.join("src")).unwrap();
134
135 assert_eq!(claimed(&root), vec![".stack-work"]);
136 }
137
138 #[test]
139 fn the_newer_snapshot_key_also_passes() {
140 let dir = tempdir().unwrap();
141 fs::write(
142 dir.path().join(MANIFEST),
143 "snapshot: nightly-2026-01-01\npackages:\n- .\n",
144 )
145 .unwrap();
146 assert!(
147 Stack
148 .enforce_lockfile(dir.path(), EnforcePolicy::default())
149 .is_ok()
150 );
151 }
152
153 #[test]
154 fn a_missing_or_snapshotless_project_file_is_refused() {
155 let dir = tempdir().unwrap();
156 let policy = EnforcePolicy::default();
157 assert!(Stack.enforce_lockfile(dir.path(), policy).is_err());
158 fs::write(dir.path().join(MANIFEST), "packages:\n- .\n").unwrap();
159 assert!(Stack.enforce_lockfile(dir.path(), policy).is_err());
160 project(dir.path());
161 assert!(Stack.enforce_lockfile(dir.path(), policy).is_ok());
162 }
163
164 #[test]
165 fn stack_is_opt_in() {
166 assert!(Stack.opt_in());
167 }
168}