clean_dev_dirs/
executables.rs1use std::fs;
8use std::os::unix::fs::PermissionsExt;
9use std::path::{Path, PathBuf};
10
11use anyhow::{Context, Result};
12
13use crate::project::{Project, ProjectType};
14
15const RUST_EXCLUDED_EXTENSIONS: &[&str] = &["d", "rmeta", "rlib", "a", "so", "dylib", "dll", "pdb"];
17
18#[derive(Debug)]
20pub struct PreservedExecutable {
21 pub source: PathBuf,
23 pub destination: PathBuf,
25}
26
27pub fn preserve_executables(project: &Project) -> Result<Vec<PreservedExecutable>> {
40 match project.kind {
41 ProjectType::Rust => preserve_rust_executables(project),
42 ProjectType::Python => preserve_python_executables(project),
43 ProjectType::Node | ProjectType::Go => Ok(Vec::new()),
44 }
45}
46
47fn preserve_rust_executables(project: &Project) -> Result<Vec<PreservedExecutable>> {
49 let target_dir = &project.build_arts.path;
50 let bin_dir = project.root_path.join("bin");
51 let mut preserved = Vec::new();
52
53 for profile in &["release", "debug"] {
54 let profile_dir = target_dir.join(profile);
55 if !profile_dir.is_dir() {
56 continue;
57 }
58
59 let dest_dir = bin_dir.join(profile);
60 let executables = find_rust_executables(&profile_dir)?;
61
62 if executables.is_empty() {
63 continue;
64 }
65
66 fs::create_dir_all(&dest_dir)
67 .with_context(|| format!("Failed to create {}", dest_dir.display()))?;
68
69 for exe_path in executables {
70 let file_name = exe_path
71 .file_name()
72 .expect("executable path should have a file name");
73 let dest_path = dest_dir.join(file_name);
74
75 fs::copy(&exe_path, &dest_path).with_context(|| {
76 format!(
77 "Failed to copy {} to {}",
78 exe_path.display(),
79 dest_path.display()
80 )
81 })?;
82
83 preserved.push(PreservedExecutable {
84 source: exe_path,
85 destination: dest_path,
86 });
87 }
88 }
89
90 Ok(preserved)
91}
92
93fn find_rust_executables(profile_dir: &Path) -> Result<Vec<PathBuf>> {
99 let mut executables = Vec::new();
100
101 let entries = fs::read_dir(profile_dir)
102 .with_context(|| format!("Failed to read {}", profile_dir.display()))?;
103
104 for entry in entries {
105 let entry = entry?;
106 let path = entry.path();
107
108 if !path.is_file() {
109 continue;
110 }
111
112 if let Some(ext) = path.extension().and_then(|e| e.to_str())
114 && RUST_EXCLUDED_EXTENSIONS.contains(&ext)
115 {
116 continue;
117 }
118
119 let metadata = path.metadata()?;
121 if metadata.permissions().mode() & 0o111 != 0 {
122 executables.push(path);
123 }
124 }
125
126 Ok(executables)
127}
128
129fn preserve_python_executables(project: &Project) -> Result<Vec<PreservedExecutable>> {
131 let root = &project.root_path;
132 let bin_dir = root.join("bin");
133 let mut preserved = Vec::new();
134
135 let dist_dir = root.join("dist");
137 if dist_dir.is_dir()
138 && let Ok(entries) = fs::read_dir(&dist_dir)
139 {
140 for entry in entries.flatten() {
141 let path = entry.path();
142 if path.extension().and_then(|e| e.to_str()) == Some("whl") {
143 fs::create_dir_all(&bin_dir)
144 .with_context(|| format!("Failed to create {}", bin_dir.display()))?;
145
146 let file_name = path.file_name().expect("path should have a file name");
147 let dest_path = bin_dir.join(file_name);
148
149 fs::copy(&path, &dest_path).with_context(|| {
150 format!(
151 "Failed to copy {} to {}",
152 path.display(),
153 dest_path.display()
154 )
155 })?;
156
157 preserved.push(PreservedExecutable {
158 source: path,
159 destination: dest_path,
160 });
161 }
162 }
163 }
164
165 let build_dir = root.join("build");
167 if build_dir.is_dir() {
168 for entry in walkdir::WalkDir::new(&build_dir)
169 .into_iter()
170 .filter_map(std::result::Result::ok)
171 {
172 let path = entry.path();
173 if !path.is_file() {
174 continue;
175 }
176
177 let is_extension = path
178 .extension()
179 .and_then(|e| e.to_str())
180 .is_some_and(|ext| ext == "so" || ext == "pyd");
181
182 if is_extension {
183 fs::create_dir_all(&bin_dir)
184 .with_context(|| format!("Failed to create {}", bin_dir.display()))?;
185
186 let file_name = path.file_name().expect("path should have a file name");
187 let dest_path = bin_dir.join(file_name);
188
189 fs::copy(path, &dest_path).with_context(|| {
190 format!(
191 "Failed to copy {} to {}",
192 path.display(),
193 dest_path.display()
194 )
195 })?;
196
197 preserved.push(PreservedExecutable {
198 source: path.to_path_buf(),
199 destination: dest_path,
200 });
201 }
202 }
203 }
204
205 Ok(preserved)
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use crate::project::BuildArtifacts;
212 use std::os::unix::fs::PermissionsExt;
213 use tempfile::TempDir;
214
215 fn create_test_project(tmp: &TempDir, kind: ProjectType) -> Project {
216 let root = tmp.path().to_path_buf();
217 let build_dir = match kind {
218 ProjectType::Rust => root.join("target"),
219 ProjectType::Python => root.join("__pycache__"),
220 ProjectType::Node => root.join("node_modules"),
221 ProjectType::Go => root.join("vendor"),
222 };
223
224 fs::create_dir_all(&build_dir).unwrap();
225
226 Project::new(
227 kind,
228 root,
229 BuildArtifacts {
230 path: build_dir,
231 size: 0,
232 },
233 Some("test-project".to_string()),
234 )
235 }
236
237 #[test]
238 fn test_preserve_rust_executables() {
239 let tmp = TempDir::new().unwrap();
240 let project = create_test_project(&tmp, ProjectType::Rust);
241
242 let release_dir = tmp.path().join("target/release");
244 fs::create_dir_all(&release_dir).unwrap();
245
246 let exe_path = release_dir.join("my-binary");
247 fs::write(&exe_path, b"fake binary").unwrap();
248 fs::set_permissions(&exe_path, fs::Permissions::from_mode(0o755)).unwrap();
249
250 let dep_file = release_dir.join("my-binary.d");
251 fs::write(&dep_file, b"dep info").unwrap();
252
253 let result = preserve_executables(&project).unwrap();
254
255 assert_eq!(result.len(), 1);
256 assert_eq!(
257 result[0].destination,
258 tmp.path().join("bin/release/my-binary")
259 );
260 assert!(result[0].destination.exists());
261 }
262
263 #[test]
264 fn test_preserve_rust_skips_non_executable() {
265 let tmp = TempDir::new().unwrap();
266 let project = create_test_project(&tmp, ProjectType::Rust);
267
268 let release_dir = tmp.path().join("target/release");
269 fs::create_dir_all(&release_dir).unwrap();
270
271 let non_exe = release_dir.join("some-file");
273 fs::write(&non_exe, b"not executable").unwrap();
274 fs::set_permissions(&non_exe, fs::Permissions::from_mode(0o644)).unwrap();
275
276 let result = preserve_executables(&project).unwrap();
277 assert!(result.is_empty());
278 }
279
280 #[test]
281 fn test_node_is_noop() {
282 let tmp = TempDir::new().unwrap();
283 let project = create_test_project(&tmp, ProjectType::Node);
284
285 let result = preserve_executables(&project).unwrap();
286 assert!(result.is_empty());
287 }
288
289 #[test]
290 fn test_go_is_noop() {
291 let tmp = TempDir::new().unwrap();
292 let project = create_test_project(&tmp, ProjectType::Go);
293
294 let result = preserve_executables(&project).unwrap();
295 assert!(result.is_empty());
296 }
297
298 #[test]
299 fn test_preserve_rust_no_profile_dirs() {
300 let tmp = TempDir::new().unwrap();
301 let project = create_test_project(&tmp, ProjectType::Rust);
302
303 let result = preserve_executables(&project).unwrap();
305 assert!(result.is_empty());
306 assert!(!tmp.path().join("bin").exists());
307 }
308}