@@ -9,11 +9,11 @@ use fs_err::{DirEntry, File};
9
9
use mailparse:: parse_headers;
10
10
use rustc_hash:: FxHashMap ;
11
11
use sha2:: { Digest , Sha256 } ;
12
- use tracing:: { instrument, warn} ;
12
+ use tracing:: { debug , instrument, trace , warn} ;
13
13
use walkdir:: WalkDir ;
14
14
15
15
use uv_cache_info:: CacheInfo ;
16
- use uv_fs:: { persist_with_retry_sync, relative_to, rename_with_retry_sync , Simplified } ;
16
+ use uv_fs:: { persist_with_retry_sync, relative_to, Simplified } ;
17
17
use uv_normalize:: PackageName ;
18
18
use uv_pypi_types:: DirectUrl ;
19
19
use uv_shell:: escape_posix_for_single_quotes;
@@ -312,6 +312,7 @@ pub(crate) fn move_folder_recorded(
312
312
site_packages : & Path ,
313
313
record : & mut [ RecordEntry ] ,
314
314
) -> Result < ( ) , Error > {
315
+ let mut rename_or_copy = RenameOrCopy :: default ( ) ;
315
316
fs:: create_dir_all ( dest_dir) ?;
316
317
for entry in WalkDir :: new ( src_dir) {
317
318
let entry = entry?;
@@ -330,7 +331,7 @@ pub(crate) fn move_folder_recorded(
330
331
if entry. file_type ( ) . is_dir ( ) {
331
332
fs:: create_dir_all ( & target) ?;
332
333
} else {
333
- fs :: rename ( src, & target) ?;
334
+ rename_or_copy . rename_or_copy ( src, & target) ?;
334
335
let entry = record
335
336
. iter_mut ( )
336
337
. find ( |entry| Path :: new ( & entry. path ) == relative_to_site_packages)
@@ -349,13 +350,14 @@ pub(crate) fn move_folder_recorded(
349
350
350
351
/// Installs a single script (not an entrypoint).
351
352
///
352
- /// Has to deal with both binaries files (just move) and scripts (rewrite the shebang if applicable) .
353
+ /// Binary files are moved with a copy fallback, while we rewrite scripts' shebangs if applicable.
353
354
fn install_script (
354
355
layout : & Layout ,
355
356
relocatable : bool ,
356
357
site_packages : & Path ,
357
358
record : & mut [ RecordEntry ] ,
358
359
file : & DirEntry ,
360
+ #[ allow( unused) ] rename_or_copy : & mut RenameOrCopy ,
359
361
) -> Result < ( ) , Error > {
360
362
let file_type = file. file_type ( ) ?;
361
363
@@ -460,12 +462,13 @@ fn install_script(
460
462
use std:: os:: unix:: fs:: PermissionsExt ;
461
463
462
464
let permissions = fs:: metadata ( & path) ?. permissions ( ) ;
463
-
464
465
if permissions. mode ( ) & 0o111 == 0o111 {
465
466
// If the permissions are already executable, we don't need to change them.
466
- rename_with_retry_sync ( & path, & script_absolute) ?;
467
+ // We fall back to copy when the file is on another drive.
468
+ rename_or_copy. rename_or_copy ( & path, & script_absolute) ?;
467
469
} else {
468
- // If we have to modify the permissions, copy the file, since we might not own it.
470
+ // If we have to modify the permissions, copy the file, since we might not own it,
471
+ // and we may not be allowed to change permissions on an unowned moved file.
469
472
warn ! (
470
473
"Copying script from {} to {} (permissions: {:o})" ,
471
474
path. simplified_display( ) ,
@@ -484,7 +487,21 @@ fn install_script(
484
487
485
488
#[ cfg( not( unix) ) ]
486
489
{
487
- rename_with_retry_sync ( & path, & script_absolute) ?;
490
+ // Here, two wrappers over rename are clashing: We want to retry for security software
491
+ // blocking the file, but we also need the copy fallback is the problem was trying to
492
+ // move a file cross-drive.
493
+ match uv_fs:: with_retry_sync ( & path, & script_absolute, "renaming" , || {
494
+ fs_err:: rename ( & path, & script_absolute)
495
+ } ) {
496
+ Ok ( ( ) ) => ( ) ,
497
+ Err ( err) => {
498
+ debug ! ( "Failed to rename, falling back to copy: {err}" ) ;
499
+ uv_fs:: with_retry_sync ( & path, & script_absolute, "copying" , || {
500
+ fs_err:: copy ( & path, & script_absolute) ?;
501
+ Ok ( ( ) )
502
+ } ) ?;
503
+ }
504
+ }
488
505
}
489
506
490
507
None
@@ -533,10 +550,13 @@ pub(crate) fn install_data(
533
550
534
551
match path. file_name ( ) . and_then ( |name| name. to_str ( ) ) {
535
552
Some ( "data" ) => {
553
+ trace ! ( ?dist_name, "Installing data/data" ) ;
536
554
// Move the content of the folder to the root of the venv
537
555
move_folder_recorded ( & path, & layout. scheme . data , site_packages, record) ?;
538
556
}
539
557
Some ( "scripts" ) => {
558
+ trace ! ( ?dist_name, "Installing data/scripts" ) ;
559
+ let mut rename_or_copy = RenameOrCopy :: default ( ) ;
540
560
let mut initialized = false ;
541
561
for file in fs:: read_dir ( path) ? {
542
562
let file = file?;
@@ -563,17 +583,27 @@ pub(crate) fn install_data(
563
583
initialized = true ;
564
584
}
565
585
566
- install_script ( layout, relocatable, site_packages, record, & file) ?;
586
+ install_script (
587
+ layout,
588
+ relocatable,
589
+ site_packages,
590
+ record,
591
+ & file,
592
+ & mut rename_or_copy,
593
+ ) ?;
567
594
}
568
595
}
569
596
Some ( "headers" ) => {
597
+ trace ! ( ?dist_name, "Installing data/headers" ) ;
570
598
let target_path = layout. scheme . include . join ( dist_name. as_str ( ) ) ;
571
599
move_folder_recorded ( & path, & target_path, site_packages, record) ?;
572
600
}
573
601
Some ( "purelib" ) => {
602
+ trace ! ( ?dist_name, "Installing data/purelib" ) ;
574
603
move_folder_recorded ( & path, & layout. scheme . purelib , site_packages, record) ?;
575
604
}
576
605
Some ( "platlib" ) => {
606
+ trace ! ( ?dist_name, "Installing data/platlib" ) ;
577
607
move_folder_recorded ( & path, & layout. scheme . platlib , site_packages, record) ?;
578
608
}
579
609
_ => {
@@ -801,6 +831,37 @@ pub(crate) fn parse_scripts(
801
831
scripts_from_ini ( extras, python_minor, ini)
802
832
}
803
833
834
+ /// Rename a file with a fallback to copy that switches over on the first failure.
835
+ #[ derive( Default , Copy , Clone ) ]
836
+ enum RenameOrCopy {
837
+ #[ default]
838
+ Rename ,
839
+ Copy ,
840
+ }
841
+
842
+ impl RenameOrCopy {
843
+ /// Try to rename, and on failure, copy.
844
+ ///
845
+ /// Usually, source and target are on the same device, so we can rename, but if that fails, we
846
+ /// have to copy. If renaming failed once, we switch to copy permanently.
847
+ fn rename_or_copy ( & mut self , from : impl AsRef < Path > , to : impl AsRef < Path > ) -> io:: Result < ( ) > {
848
+ match self {
849
+ Self :: Rename => match fs_err:: rename ( from. as_ref ( ) , to. as_ref ( ) ) {
850
+ Ok ( ( ) ) => { }
851
+ Err ( err) => {
852
+ * self = RenameOrCopy :: Copy ;
853
+ debug ! ( "Failed to rename, falling back to copy: {err}" ) ;
854
+ fs_err:: copy ( from. as_ref ( ) , to. as_ref ( ) ) ?;
855
+ }
856
+ } ,
857
+ Self :: Copy => {
858
+ fs_err:: copy ( from. as_ref ( ) , to. as_ref ( ) ) ?;
859
+ }
860
+ }
861
+ Ok ( ( ) )
862
+ }
863
+ }
864
+
804
865
#[ cfg( test) ]
805
866
mod test {
806
867
use std:: io:: Cursor ;
0 commit comments