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