clean_dev_dirs/
executables.rs1use std::fs;
8use std::path::{Path, PathBuf};
9
10use anyhow::{Context, Result};
11
12use crate::project::{Project, ProjectType};
13
14const RUST_EXCLUDED_EXTENSIONS: &[&str] = &["d", "rmeta", "rlib", "a", "so", "dylib", "dll", "pdb"];
16
17#[cfg(unix)]
22fn is_executable(path: &Path, metadata: &fs::Metadata) -> bool {
23 use std::os::unix::fs::PermissionsExt;
24
25 let _ = path; metadata.permissions().mode() & 0o111 != 0
27}
28
29#[cfg(windows)]
30fn is_executable(path: &Path, _metadata: &fs::Metadata) -> bool {
31 path.extension()
32 .and_then(|e| e.to_str())
33 .is_some_and(|ext| ext.eq_ignore_ascii_case("exe"))
34}
35
36#[derive(Debug)]
38pub struct PreservedExecutable {
39 pub source: PathBuf,
41 pub destination: PathBuf,
43}
44
45pub fn preserve_executables(project: &Project) -> Result<Vec<PreservedExecutable>> {
58 match project.kind {
59 ProjectType::Rust => preserve_rust_executables(project),
60 ProjectType::Python => preserve_python_executables(project),
61 ProjectType::Node | ProjectType::Go => Ok(Vec::new()),
62 }
63}
64
65fn preserve_rust_executables(project: &Project) -> Result<Vec<PreservedExecutable>> {
67 let target_dir = &project.build_arts.path;
68 let bin_dir = project.root_path.join("bin");
69 let mut preserved = Vec::new();
70
71 for profile in &["release", "debug"] {
72 let profile_dir = target_dir.join(profile);
73 if !profile_dir.is_dir() {
74 continue;
75 }
76
77 let dest_dir = bin_dir.join(profile);
78 let executables = find_rust_executables(&profile_dir)?;
79
80 if executables.is_empty() {
81 continue;
82 }
83
84 fs::create_dir_all(&dest_dir)
85 .with_context(|| format!("Failed to create {}", dest_dir.display()))?;
86
87 for exe_path in executables {
88 let file_name = exe_path
89 .file_name()
90 .expect("executable path should have a file name");
91 let dest_path = dest_dir.join(file_name);
92
93 fs::copy(&exe_path, &dest_path).with_context(|| {
94 format!(
95 "Failed to copy {} to {}",
96 exe_path.display(),
97 dest_path.display()
98 )
99 })?;
100
101 preserved.push(PreservedExecutable {
102 source: exe_path,
103 destination: dest_path,
104 });
105 }
106 }
107
108 Ok(preserved)
109}
110
111fn find_rust_executables(profile_dir: &Path) -> Result<Vec<PathBuf>> {
117 let mut executables = Vec::new();
118
119 let entries = fs::read_dir(profile_dir)
120 .with_context(|| format!("Failed to read {}", profile_dir.display()))?;
121
122 for entry in entries {
123 let entry = entry?;
124 let path = entry.path();
125
126 if !path.is_file() {
127 continue;
128 }
129
130 if let Some(ext) = path.extension().and_then(|e| e.to_str())
132 && RUST_EXCLUDED_EXTENSIONS.contains(&ext)
133 {
134 continue;
135 }
136
137 let metadata = path.metadata()?;
139 if is_executable(&path, &metadata) {
140 executables.push(path);
141 }
142 }
143
144 Ok(executables)
145}
146
147fn preserve_python_executables(project: &Project) -> Result<Vec<PreservedExecutable>> {
149 let root = &project.root_path;
150 let bin_dir = root.join("bin");
151 let mut preserved = Vec::new();
152
153 let dist_dir = root.join("dist");
155 if dist_dir.is_dir()
156 && let Ok(entries) = fs::read_dir(&dist_dir)
157 {
158 for entry in entries.flatten() {
159 let path = entry.path();
160 if path.extension().and_then(|e| e.to_str()) == Some("whl") {
161 fs::create_dir_all(&bin_dir)
162 .with_context(|| format!("Failed to create {}", bin_dir.display()))?;
163
164 let file_name = path.file_name().expect("path should have a file name");
165 let dest_path = bin_dir.join(file_name);
166
167 fs::copy(&path, &dest_path).with_context(|| {
168 format!(
169 "Failed to copy {} to {}",
170 path.display(),
171 dest_path.display()
172 )
173 })?;
174
175 preserved.push(PreservedExecutable {
176 source: path,
177 destination: dest_path,
178 });
179 }
180 }
181 }
182
183 let build_dir = root.join("build");
185 if build_dir.is_dir() {
186 for entry in walkdir::WalkDir::new(&build_dir)
187 .into_iter()
188 .filter_map(std::result::Result::ok)
189 {
190 let path = entry.path();
191 if !path.is_file() {
192 continue;
193 }
194
195 let is_extension = path
196 .extension()
197 .and_then(|e| e.to_str())
198 .is_some_and(|ext| ext == "so" || ext == "pyd");
199
200 if is_extension {
201 fs::create_dir_all(&bin_dir)
202 .with_context(|| format!("Failed to create {}", bin_dir.display()))?;
203
204 let file_name = path.file_name().expect("path should have a file name");
205 let dest_path = bin_dir.join(file_name);
206
207 fs::copy(path, &dest_path).with_context(|| {
208 format!(
209 "Failed to copy {} to {}",
210 path.display(),
211 dest_path.display()
212 )
213 })?;
214
215 preserved.push(PreservedExecutable {
216 source: path.to_path_buf(),
217 destination: dest_path,
218 });
219 }
220 }
221 }
222
223 Ok(preserved)
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use crate::project::BuildArtifacts;
230 use tempfile::TempDir;
231
232 fn create_test_project(tmp: &TempDir, kind: ProjectType) -> Project {
233 let root = tmp.path().to_path_buf();
234 let build_dir = match kind {
235 ProjectType::Rust => root.join("target"),
236 ProjectType::Python => root.join("__pycache__"),
237 ProjectType::Node => root.join("node_modules"),
238 ProjectType::Go => root.join("vendor"),
239 };
240
241 fs::create_dir_all(&build_dir).unwrap();
242
243 Project::new(
244 kind,
245 root,
246 BuildArtifacts {
247 path: build_dir,
248 size: 0,
249 },
250 Some("test-project".to_string()),
251 )
252 }
253
254 #[test]
255 #[cfg(unix)]
256 fn test_preserve_rust_executables_unix() {
257 use std::os::unix::fs::PermissionsExt;
258
259 let tmp = TempDir::new().unwrap();
260 let project = create_test_project(&tmp, ProjectType::Rust);
261
262 let release_dir = tmp.path().join("target/release");
264 fs::create_dir_all(&release_dir).unwrap();
265
266 let exe_path = release_dir.join("my-binary");
267 fs::write(&exe_path, b"fake binary").unwrap();
268 fs::set_permissions(&exe_path, fs::Permissions::from_mode(0o755)).unwrap();
269
270 let dep_file = release_dir.join("my-binary.d");
271 fs::write(&dep_file, b"dep info").unwrap();
272
273 let result = preserve_executables(&project).unwrap();
274
275 assert_eq!(result.len(), 1);
276 assert_eq!(
277 result[0].destination,
278 tmp.path().join("bin/release/my-binary")
279 );
280 assert!(result[0].destination.exists());
281 }
282
283 #[test]
284 #[cfg(windows)]
285 fn test_preserve_rust_executables_windows() {
286 let tmp = TempDir::new().unwrap();
287 let project = create_test_project(&tmp, ProjectType::Rust);
288
289 let release_dir = tmp.path().join("target/release");
290 fs::create_dir_all(&release_dir).unwrap();
291
292 let exe_path = release_dir.join("my-binary.exe");
294 fs::write(&exe_path, b"fake binary").unwrap();
295
296 let dep_file = release_dir.join("my-binary.d");
297 fs::write(&dep_file, b"dep info").unwrap();
298
299 let result = preserve_executables(&project).unwrap();
300
301 assert_eq!(result.len(), 1);
302 assert_eq!(
303 result[0].destination,
304 tmp.path().join("bin/release/my-binary.exe")
305 );
306 assert!(result[0].destination.exists());
307 }
308
309 #[test]
310 #[cfg(unix)]
311 fn test_preserve_rust_skips_non_executable_unix() {
312 use std::os::unix::fs::PermissionsExt;
313
314 let tmp = TempDir::new().unwrap();
315 let project = create_test_project(&tmp, ProjectType::Rust);
316
317 let release_dir = tmp.path().join("target/release");
318 fs::create_dir_all(&release_dir).unwrap();
319
320 let non_exe = release_dir.join("some-file");
322 fs::write(&non_exe, b"not executable").unwrap();
323 fs::set_permissions(&non_exe, fs::Permissions::from_mode(0o644)).unwrap();
324
325 let result = preserve_executables(&project).unwrap();
326 assert!(result.is_empty());
327 }
328
329 #[test]
330 #[cfg(windows)]
331 fn test_preserve_rust_skips_non_executable_windows() {
332 let tmp = TempDir::new().unwrap();
333 let project = create_test_project(&tmp, ProjectType::Rust);
334
335 let release_dir = tmp.path().join("target/release");
336 fs::create_dir_all(&release_dir).unwrap();
337
338 let non_exe = release_dir.join("some-file.txt");
340 fs::write(&non_exe, b"not executable").unwrap();
341
342 let result = preserve_executables(&project).unwrap();
343 assert!(result.is_empty());
344 }
345
346 #[test]
347 fn test_node_is_noop() {
348 let tmp = TempDir::new().unwrap();
349 let project = create_test_project(&tmp, ProjectType::Node);
350
351 let result = preserve_executables(&project).unwrap();
352 assert!(result.is_empty());
353 }
354
355 #[test]
356 fn test_go_is_noop() {
357 let tmp = TempDir::new().unwrap();
358 let project = create_test_project(&tmp, ProjectType::Go);
359
360 let result = preserve_executables(&project).unwrap();
361 assert!(result.is_empty());
362 }
363
364 #[test]
365 fn test_preserve_rust_no_profile_dirs() {
366 let tmp = TempDir::new().unwrap();
367 let project = create_test_project(&tmp, ProjectType::Rust);
368
369 let result = preserve_executables(&project).unwrap();
371 assert!(result.is_empty());
372 assert!(!tmp.path().join("bin").exists());
373 }
374}