1use 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
62 | ProjectType::Go
63 | ProjectType::Java
64 | ProjectType::Cpp
65 | ProjectType::Swift
66 | ProjectType::DotNet
67 | ProjectType::Ruby
68 | ProjectType::Elixir
69 | ProjectType::Deno
70 | ProjectType::Php
71 | ProjectType::Haskell
72 | ProjectType::Dart
73 | ProjectType::Zig
74 | ProjectType::Scala => Ok(Vec::new()),
75 }
76}
77
78fn preserve_rust_executables(project: &Project) -> Result<Vec<PreservedExecutable>> {
80 let Some(primary) = project.build_arts.first() else {
81 return Ok(Vec::new());
82 };
83 let target_dir = &primary.path;
84 let bin_dir = project.root_path.join("bin");
85 let mut preserved = Vec::new();
86
87 for profile in &["release", "debug"] {
88 let profile_dir = target_dir.join(profile);
89 if !profile_dir.is_dir() {
90 continue;
91 }
92
93 let dest_dir = bin_dir.join(profile);
94 let executables = find_rust_executables(&profile_dir)?;
95
96 if executables.is_empty() {
97 continue;
98 }
99
100 fs::create_dir_all(&dest_dir)
101 .with_context(|| format!("Failed to create {}", dest_dir.display()))?;
102
103 for exe_path in executables {
104 let Some(file_name) = exe_path.file_name() else {
105 continue;
106 };
107 let dest_path = dest_dir.join(file_name);
108
109 fs::copy(&exe_path, &dest_path).with_context(|| {
110 format!(
111 "Failed to copy {} to {}",
112 exe_path.display(),
113 dest_path.display()
114 )
115 })?;
116
117 preserved.push(PreservedExecutable {
118 source: exe_path,
119 destination: dest_path,
120 });
121 }
122 }
123
124 Ok(preserved)
125}
126
127fn find_rust_executables(profile_dir: &Path) -> Result<Vec<PathBuf>> {
133 let mut executables = Vec::new();
134
135 let entries = fs::read_dir(profile_dir)
136 .with_context(|| format!("Failed to read {}", profile_dir.display()))?;
137
138 for entry in entries {
139 let entry = entry?;
140 let path = entry.path();
141
142 if !path.is_file() {
143 continue;
144 }
145
146 if let Some(ext) = path.extension().and_then(|e| e.to_str())
148 && RUST_EXCLUDED_EXTENSIONS.contains(&ext)
149 {
150 continue;
151 }
152
153 let metadata = path.metadata()?;
155 if is_executable(&path, &metadata) {
156 executables.push(path);
157 }
158 }
159
160 Ok(executables)
161}
162
163fn preserve_python_executables(project: &Project) -> Result<Vec<PreservedExecutable>> {
165 let root = &project.root_path;
166 let bin_dir = root.join("bin");
167 let mut preserved = Vec::new();
168
169 collect_wheel_files(&root.join("dist"), &bin_dir, &mut preserved)?;
170 collect_native_extensions(&root.join("build"), &bin_dir, &mut preserved)?;
171
172 Ok(preserved)
173}
174
175fn collect_wheel_files(
177 dist_dir: &Path,
178 bin_dir: &Path,
179 preserved: &mut Vec<PreservedExecutable>,
180) -> Result<()> {
181 if !dist_dir.is_dir() {
182 return Ok(());
183 }
184
185 let Ok(entries) = fs::read_dir(dist_dir) else {
186 return Ok(());
187 };
188
189 for entry in entries.flatten() {
190 let path = entry.path();
191 if path.extension().and_then(|e| e.to_str()) == Some("whl") {
192 copy_to_bin(&path, bin_dir, preserved)?;
193 }
194 }
195
196 Ok(())
197}
198
199fn collect_native_extensions(
201 build_dir: &Path,
202 bin_dir: &Path,
203 preserved: &mut Vec<PreservedExecutable>,
204) -> Result<()> {
205 if !build_dir.is_dir() {
206 return Ok(());
207 }
208
209 for entry in walkdir::WalkDir::new(build_dir)
210 .into_iter()
211 .filter_map(std::result::Result::ok)
212 {
213 let path = entry.path();
214 if !path.is_file() {
215 continue;
216 }
217
218 let is_native_ext = path
219 .extension()
220 .and_then(|e| e.to_str())
221 .is_some_and(|ext| ext == "so" || ext == "pyd");
222
223 if is_native_ext {
224 copy_to_bin(path, bin_dir, preserved)?;
225 }
226 }
227
228 Ok(())
229}
230
231fn copy_to_bin(
234 source: &Path,
235 bin_dir: &Path,
236 preserved: &mut Vec<PreservedExecutable>,
237) -> Result<()> {
238 fs::create_dir_all(bin_dir)
239 .with_context(|| format!("Failed to create {}", bin_dir.display()))?;
240
241 let Some(file_name) = source.file_name() else {
242 return Ok(());
243 };
244 let dest_path = bin_dir.join(file_name);
245
246 fs::copy(source, &dest_path).with_context(|| {
247 format!(
248 "Failed to copy {} to {}",
249 source.display(),
250 dest_path.display()
251 )
252 })?;
253
254 preserved.push(PreservedExecutable {
255 source: source.to_path_buf(),
256 destination: dest_path,
257 });
258
259 Ok(())
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use crate::project::BuildArtifacts;
266 use tempfile::TempDir;
267
268 fn create_test_project(tmp: &TempDir, kind: ProjectType) -> anyhow::Result<Project> {
269 let root = tmp.path().to_path_buf();
270 let build_dir = match kind {
271 ProjectType::Rust | ProjectType::Java | ProjectType::Scala => root.join("target"),
272 ProjectType::Python => root.join("__pycache__"),
273 ProjectType::Node | ProjectType::Deno => root.join("node_modules"),
274 ProjectType::Go | ProjectType::Ruby | ProjectType::Php => root.join("vendor"),
275 ProjectType::Cpp | ProjectType::Dart => root.join("build"),
276 ProjectType::Swift => root.join(".build"),
277 ProjectType::DotNet => root.join("obj"),
278 ProjectType::Elixir => root.join("_build"),
279 ProjectType::Haskell => root.join(".stack-work"),
280 ProjectType::Zig => root.join("zig-cache"),
281 };
282
283 fs::create_dir_all(&build_dir)?;
284
285 Ok(Project::new(
286 kind,
287 root,
288 vec![BuildArtifacts {
289 path: build_dir,
290 size: 0,
291 }],
292 Some("test-project".to_string()),
293 ))
294 }
295
296 #[test]
297 #[cfg(unix)]
298 fn test_preserve_rust_executables_unix() -> anyhow::Result<()> {
299 use std::os::unix::fs::PermissionsExt;
300
301 let tmp = TempDir::new()?;
302 let project = create_test_project(&tmp, ProjectType::Rust)?;
303
304 let release_dir = tmp.path().join("target/release");
305 fs::create_dir_all(&release_dir)?;
306
307 let exe_path = release_dir.join("my-binary");
308 fs::write(&exe_path, b"fake binary")?;
309 fs::set_permissions(&exe_path, fs::Permissions::from_mode(0o755))?;
310
311 let dep_file = release_dir.join("my-binary.d");
312 fs::write(&dep_file, b"dep info")?;
313
314 let result = preserve_executables(&project)?;
315
316 assert_eq!(result.len(), 1);
317 assert_eq!(
318 result[0].destination,
319 tmp.path().join("bin/release/my-binary")
320 );
321 assert!(result[0].destination.exists());
322
323 Ok(())
324 }
325
326 #[test]
327 #[cfg(windows)]
328 fn test_preserve_rust_executables_windows() -> anyhow::Result<()> {
329 let tmp = TempDir::new()?;
330 let project = create_test_project(&tmp, ProjectType::Rust)?;
331
332 let release_dir = tmp.path().join("target/release");
333 fs::create_dir_all(&release_dir)?;
334
335 let exe_path = release_dir.join("my-binary.exe");
336 fs::write(&exe_path, b"fake binary")?;
337
338 let dep_file = release_dir.join("my-binary.d");
339 fs::write(&dep_file, b"dep info")?;
340
341 let result = preserve_executables(&project)?;
342
343 assert_eq!(result.len(), 1);
344 assert_eq!(
345 result[0].destination,
346 tmp.path().join("bin/release/my-binary.exe")
347 );
348 assert!(result[0].destination.exists());
349
350 Ok(())
351 }
352
353 #[test]
354 #[cfg(unix)]
355 fn test_preserve_rust_skips_non_executable_unix() -> anyhow::Result<()> {
356 use std::os::unix::fs::PermissionsExt;
357
358 let tmp = TempDir::new()?;
359 let project = create_test_project(&tmp, ProjectType::Rust)?;
360
361 let release_dir = tmp.path().join("target/release");
362 fs::create_dir_all(&release_dir)?;
363
364 let non_exe = release_dir.join("some-file");
365 fs::write(&non_exe, b"not executable")?;
366 fs::set_permissions(&non_exe, fs::Permissions::from_mode(0o644))?;
367
368 let result = preserve_executables(&project)?;
369 assert!(result.is_empty());
370
371 Ok(())
372 }
373
374 #[test]
375 #[cfg(windows)]
376 fn test_preserve_rust_skips_non_executable_windows() -> anyhow::Result<()> {
377 let tmp = TempDir::new()?;
378 let project = create_test_project(&tmp, ProjectType::Rust)?;
379
380 let release_dir = tmp.path().join("target/release");
381 fs::create_dir_all(&release_dir)?;
382
383 let non_exe = release_dir.join("some-file.txt");
384 fs::write(&non_exe, b"not executable")?;
385
386 let result = preserve_executables(&project)?;
387 assert!(result.is_empty());
388
389 Ok(())
390 }
391
392 #[test]
393 fn test_node_is_noop() -> anyhow::Result<()> {
394 let tmp = TempDir::new()?;
395 let project = create_test_project(&tmp, ProjectType::Node)?;
396
397 let result = preserve_executables(&project)?;
398 assert!(result.is_empty());
399
400 Ok(())
401 }
402
403 #[test]
404 fn test_go_is_noop() -> anyhow::Result<()> {
405 let tmp = TempDir::new()?;
406 let project = create_test_project(&tmp, ProjectType::Go)?;
407
408 let result = preserve_executables(&project)?;
409 assert!(result.is_empty());
410
411 Ok(())
412 }
413
414 #[test]
415 fn test_preserve_rust_no_profile_dirs() -> anyhow::Result<()> {
416 let tmp = TempDir::new()?;
417 let project = create_test_project(&tmp, ProjectType::Rust)?;
418
419 let result = preserve_executables(&project)?;
420 assert!(result.is_empty());
421 assert!(!tmp.path().join("bin").exists());
422
423 Ok(())
424 }
425
426 #[test]
429 #[cfg(unix)]
430 fn test_find_multiple_rust_executables_unix() -> anyhow::Result<()> {
431 use std::os::unix::fs::PermissionsExt;
432
433 let tmp = TempDir::new()?;
434 let project = create_test_project(&tmp, ProjectType::Rust)?;
435
436 let release_dir = tmp.path().join("target/release");
437 fs::create_dir_all(&release_dir)?;
438
439 for name in &["binary-a", "binary-b", "binary-c"] {
440 let exe_path = release_dir.join(name);
441 fs::write(&exe_path, b"fake binary")?;
442 fs::set_permissions(&exe_path, fs::Permissions::from_mode(0o755))?;
443 }
444
445 let result = preserve_executables(&project)?;
446 assert_eq!(result.len(), 3);
447
448 for preserved in &result {
449 assert!(preserved.destination.exists());
450 assert!(
451 preserved
452 .destination
453 .starts_with(tmp.path().join("bin/release"))
454 );
455 }
456
457 Ok(())
458 }
459
460 #[test]
461 #[cfg(unix)]
462 fn test_find_rust_executables_excludes_metadata_even_if_executable_unix() -> anyhow::Result<()>
463 {
464 use std::os::unix::fs::PermissionsExt;
465
466 let tmp = TempDir::new()?;
467 let project = create_test_project(&tmp, ProjectType::Rust)?;
468
469 let release_dir = tmp.path().join("target/release");
470 fs::create_dir_all(&release_dir)?;
471
472 let excluded_files = [
473 "dep.d",
474 "lib.rmeta",
475 "lib.rlib",
476 "archive.a",
477 "shared.so",
478 "shared.dylib",
479 "shared.dll",
480 "debug.pdb",
481 ];
482
483 for name in &excluded_files {
484 let file_path = release_dir.join(name);
485 fs::write(&file_path, b"fake content")?;
486 fs::set_permissions(&file_path, fs::Permissions::from_mode(0o755))?;
487 }
488
489 let exe_path = release_dir.join("real-binary");
490 fs::write(&exe_path, b"real binary")?;
491 fs::set_permissions(&exe_path, fs::Permissions::from_mode(0o755))?;
492
493 let result = preserve_executables(&project)?;
494 assert_eq!(result.len(), 1);
495 assert!(
496 result[0]
497 .destination
498 .file_name()
499 .ok_or_else(|| anyhow::anyhow!("missing file name"))?
500 .to_str()
501 .ok_or_else(|| anyhow::anyhow!("non-UTF-8 file name"))?
502 .contains("real-binary")
503 );
504
505 Ok(())
506 }
507
508 #[test]
509 #[cfg(unix)]
510 fn test_is_executable_permission_variants_unix() -> anyhow::Result<()> {
511 use std::os::unix::fs::PermissionsExt;
512
513 let tmp = TempDir::new()?;
514
515 let user_exe = tmp.path().join("user_exe");
516 fs::write(&user_exe, b"content")?;
517 fs::set_permissions(&user_exe, fs::Permissions::from_mode(0o700))?;
518 let meta = user_exe.metadata()?;
519 assert!(is_executable(&user_exe, &meta));
520
521 let group_exe = tmp.path().join("group_exe");
522 fs::write(&group_exe, b"content")?;
523 fs::set_permissions(&group_exe, fs::Permissions::from_mode(0o070))?;
524 let meta = group_exe.metadata()?;
525 assert!(is_executable(&group_exe, &meta));
526
527 let other_exe = tmp.path().join("other_exe");
528 fs::write(&other_exe, b"content")?;
529 fs::set_permissions(&other_exe, fs::Permissions::from_mode(0o601))?;
530 let meta = other_exe.metadata()?;
531 assert!(is_executable(&other_exe, &meta));
532
533 let no_exe = tmp.path().join("no_exe");
534 fs::write(&no_exe, b"content")?;
535 fs::set_permissions(&no_exe, fs::Permissions::from_mode(0o644))?;
536 let meta = no_exe.metadata()?;
537 assert!(!is_executable(&no_exe, &meta));
538
539 Ok(())
540 }
541
542 #[test]
543 #[cfg(unix)]
544 fn test_preserve_rust_debug_and_release_unix() -> anyhow::Result<()> {
545 use std::os::unix::fs::PermissionsExt;
546
547 let tmp = TempDir::new()?;
548 let project = create_test_project(&tmp, ProjectType::Rust)?;
549
550 for profile in &["debug", "release"] {
551 let profile_dir = tmp.path().join("target").join(profile);
552 fs::create_dir_all(&profile_dir)?;
553
554 let exe_path = profile_dir.join("my-binary");
555 fs::write(&exe_path, b"fake binary")?;
556 fs::set_permissions(&exe_path, fs::Permissions::from_mode(0o755))?;
557 }
558
559 let result = preserve_executables(&project)?;
560 assert_eq!(result.len(), 2);
561
562 let dest_names: Vec<_> = result
563 .iter()
564 .map(|p| p.destination.to_string_lossy().to_string())
565 .collect();
566
567 assert!(dest_names.iter().any(|d| d.contains("bin/release")));
568 assert!(dest_names.iter().any(|d| d.contains("bin/debug")));
569
570 Ok(())
571 }
572
573 #[test]
574 #[cfg(unix)]
575 fn test_preserve_python_so_extensions_unix() -> anyhow::Result<()> {
576 let tmp = TempDir::new()?;
577 let project = create_test_project(&tmp, ProjectType::Python)?;
578
579 let build_dir = tmp.path().join("build/lib.linux-x86_64-3.9");
580 fs::create_dir_all(&build_dir)?;
581
582 fs::write(
583 build_dir.join("mymodule.cpython-39-x86_64-linux-gnu.so"),
584 b"shared object",
585 )?;
586 fs::write(build_dir.join("another.so"), b"shared object")?;
587
588 let result = preserve_python_executables(&project)?;
589 assert_eq!(result.len(), 2);
590
591 for preserved in &result {
592 assert!(preserved.destination.exists());
593 assert!(preserved.destination.starts_with(tmp.path().join("bin")));
594 }
595
596 Ok(())
597 }
598
599 #[test]
602 #[cfg(windows)]
603 fn test_is_executable_case_insensitive_exe_windows() -> anyhow::Result<()> {
604 let tmp = TempDir::new()?;
605
606 let exe = tmp.path().join("app.exe");
607 fs::write(&exe, b"content")?;
608 let meta = exe.metadata()?;
609 assert!(is_executable(&exe, &meta));
610
611 let exe_upper = tmp.path().join("app.EXE");
612 fs::write(&exe_upper, b"content")?;
613 let meta = exe_upper.metadata()?;
614 assert!(is_executable(&exe_upper, &meta));
615
616 let exe_mixed = tmp.path().join("app.Exe");
617 fs::write(&exe_mixed, b"content")?;
618 let meta = exe_mixed.metadata()?;
619 assert!(is_executable(&exe_mixed, &meta));
620
621 let not_exe = tmp.path().join("app.txt");
622 fs::write(¬_exe, b"content")?;
623 let meta = not_exe.metadata()?;
624 assert!(!is_executable(¬_exe, &meta));
625
626 let no_ext = tmp.path().join("app");
627 fs::write(&no_ext, b"content")?;
628 let meta = no_ext.metadata()?;
629 assert!(!is_executable(&no_ext, &meta));
630
631 Ok(())
632 }
633
634 #[test]
635 #[cfg(windows)]
636 fn test_preserve_rust_debug_and_release_windows() -> anyhow::Result<()> {
637 let tmp = TempDir::new()?;
638 let project = create_test_project(&tmp, ProjectType::Rust)?;
639
640 for profile in &["debug", "release"] {
641 let profile_dir = tmp.path().join("target").join(profile);
642 fs::create_dir_all(&profile_dir)?;
643
644 let exe_path = profile_dir.join("my-binary.exe");
645 fs::write(&exe_path, b"fake binary")?;
646 }
647
648 let result = preserve_executables(&project)?;
649 assert_eq!(result.len(), 2);
650
651 let dest_names: Vec<_> = result
652 .iter()
653 .map(|p| p.destination.to_string_lossy().to_string())
654 .collect();
655
656 assert!(dest_names.iter().any(|d| d.contains("release")));
657 assert!(dest_names.iter().any(|d| d.contains("debug")));
658
659 Ok(())
660 }
661
662 #[test]
663 #[cfg(windows)]
664 fn test_find_rust_executables_excludes_metadata_windows() -> anyhow::Result<()> {
665 let tmp = TempDir::new()?;
666 let project = create_test_project(&tmp, ProjectType::Rust)?;
667
668 let release_dir = tmp.path().join("target/release");
669 fs::create_dir_all(&release_dir)?;
670
671 fs::write(release_dir.join("dep.d"), b"dep info")?;
672 fs::write(release_dir.join("lib.dll"), b"library")?;
673 fs::write(release_dir.join("debug.pdb"), b"symbols")?;
674 fs::write(release_dir.join("lib.rlib"), b"rust lib")?;
675
676 fs::write(release_dir.join("my-binary.exe"), b"real binary")?;
677
678 let result = preserve_executables(&project)?;
679 assert_eq!(result.len(), 1);
680 assert!(
681 result[0]
682 .destination
683 .file_name()
684 .ok_or_else(|| anyhow::anyhow!("missing file name"))?
685 .to_str()
686 .ok_or_else(|| anyhow::anyhow!("non-UTF-8 file name"))?
687 .contains("my-binary.exe")
688 );
689
690 Ok(())
691 }
692
693 #[test]
694 #[cfg(windows)]
695 fn test_find_multiple_rust_executables_windows() -> anyhow::Result<()> {
696 let tmp = TempDir::new()?;
697 let project = create_test_project(&tmp, ProjectType::Rust)?;
698
699 let release_dir = tmp.path().join("target/release");
700 fs::create_dir_all(&release_dir)?;
701
702 for name in &["binary-a.exe", "binary-b.exe", "binary-c.exe"] {
703 fs::write(release_dir.join(name), b"fake binary")?;
704 }
705
706 let result = preserve_executables(&project)?;
707 assert_eq!(result.len(), 3);
708
709 Ok(())
710 }
711
712 #[test]
713 #[cfg(windows)]
714 fn test_preserve_python_pyd_extensions_windows() -> anyhow::Result<()> {
715 let tmp = TempDir::new()?;
716 let project = create_test_project(&tmp, ProjectType::Python)?;
717
718 let build_dir = tmp.path().join("build/lib.win-amd64-3.9");
719 fs::create_dir_all(&build_dir)?;
720
721 fs::write(
722 build_dir.join("mymodule.cp39-win_amd64.pyd"),
723 b"python extension",
724 )?;
725 fs::write(build_dir.join("another.pyd"), b"python extension")?;
726
727 let result = preserve_python_executables(&project)?;
728 assert_eq!(result.len(), 2);
729
730 for preserved in &result {
731 assert!(preserved.destination.exists());
732 }
733
734 Ok(())
735 }
736
737 #[test]
740 fn test_preserve_python_whl_files() -> anyhow::Result<()> {
741 let tmp = TempDir::new()?;
742 let project = create_test_project(&tmp, ProjectType::Python)?;
743
744 let dist_dir = tmp.path().join("dist");
745 fs::create_dir_all(&dist_dir)?;
746
747 fs::write(
748 dist_dir.join("mypackage-1.0.0-py3-none-any.whl"),
749 b"wheel content",
750 )?;
751 fs::write(dist_dir.join("mypackage-1.0.0.tar.gz"), b"tarball content")?;
752
753 let result = preserve_python_executables(&project)?;
754 assert_eq!(result.len(), 1);
755 assert!(
756 result[0]
757 .destination
758 .extension()
759 .is_some_and(|ext| ext.eq_ignore_ascii_case("whl"))
760 );
761
762 Ok(())
763 }
764
765 #[test]
766 fn test_preserve_python_no_dist_no_build() -> anyhow::Result<()> {
767 let tmp = TempDir::new()?;
768 let project = create_test_project(&tmp, ProjectType::Python)?;
769
770 let result = preserve_python_executables(&project)?;
771 assert!(result.is_empty());
772
773 Ok(())
774 }
775
776 #[test]
777 fn test_preserve_python_empty_dist_and_build() -> anyhow::Result<()> {
778 let tmp = TempDir::new()?;
779 let project = create_test_project(&tmp, ProjectType::Python)?;
780
781 fs::create_dir_all(tmp.path().join("dist"))?;
782 fs::create_dir_all(tmp.path().join("build"))?;
783
784 let result = preserve_python_executables(&project)?;
785 assert!(result.is_empty());
786
787 Ok(())
788 }
789
790 #[test]
791 fn test_preserve_python_whl_and_extensions_combined() -> anyhow::Result<()> {
792 let tmp = TempDir::new()?;
793 let project = create_test_project(&tmp, ProjectType::Python)?;
794
795 let dist_dir = tmp.path().join("dist");
796 fs::create_dir_all(&dist_dir)?;
797 fs::write(dist_dir.join("mypackage-1.0.0-py3-none-any.whl"), b"wheel")?;
798
799 let build_dir = tmp.path().join("build/lib");
800 fs::create_dir_all(&build_dir)?;
801
802 #[cfg(unix)]
803 fs::write(build_dir.join("native.so"), b"shared object")?;
804
805 #[cfg(windows)]
806 fs::write(build_dir.join("native.pyd"), b"python extension")?;
807
808 let result = preserve_python_executables(&project)?;
809 assert_eq!(result.len(), 2);
810
811 Ok(())
812 }
813
814 #[test]
815 fn test_preserve_executables_returns_correct_source_paths() -> anyhow::Result<()> {
816 let tmp = TempDir::new()?;
817 let project = create_test_project(&tmp, ProjectType::Python)?;
818
819 let dist_dir = tmp.path().join("dist");
820 fs::create_dir_all(&dist_dir)?;
821 let whl_path = dist_dir.join("pkg-1.0-py3-none-any.whl");
822 fs::write(&whl_path, b"wheel content")?;
823
824 let result = preserve_python_executables(&project)?;
825 assert_eq!(result.len(), 1);
826 assert_eq!(result[0].source, whl_path);
827 assert_eq!(
828 result[0].destination,
829 tmp.path().join("bin/pkg-1.0-py3-none-any.whl")
830 );
831
832 Ok(())
833 }
834}