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