@@ -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
@@ -459,32 +461,37 @@ fn install_script(
459
461
use std:: fs:: Permissions ;
460
462
use std:: os:: unix:: fs:: PermissionsExt ;
461
463
462
- let permissions = fs:: metadata ( & path) ?. permissions ( ) ;
464
+ // We fall back to copy when the file is on another drive and if we don't own the file.
465
+ rename_or_copy. rename_or_copy ( & path, & script_absolute) ?;
463
466
464
- if permissions. mode ( ) & 0o111 == 0o111 {
465
- // If the permissions are already executable, we don't need to change them.
466
- rename_with_retry_sync ( & path, & script_absolute) ?;
467
- } else {
468
- // If we have to modify the permissions, copy the file, since we might not own it.
469
- warn ! (
470
- "Copying script from {} to {} (permissions: {:o})" ,
471
- path. simplified_display( ) ,
472
- script_absolute. simplified_display( ) ,
473
- permissions. mode( )
474
- ) ;
475
-
476
- uv_fs:: copy_atomic_sync ( & path, & script_absolute) ?;
467
+ let permissions = fs:: metadata ( & script_absolute) ?. permissions ( ) ;
477
468
469
+ if permissions. mode ( ) & 0o111 != 0o111 {
470
+ trace ! (
471
+ "Adjusting permissions for {} from {} to {}" ,
472
+ script_absolute. display( ) ,
473
+ permissions. mode( ) ,
474
+ permissions. mode( ) | 0o111
475
+ ) ;
478
476
fs:: set_permissions (
479
- script_absolute,
477
+ & script_absolute,
480
478
Permissions :: from_mode ( permissions. mode ( ) | 0o111 ) ,
481
479
) ?;
482
480
}
483
481
}
484
482
485
483
#[ cfg( not( unix) ) ]
486
484
{
487
- rename_with_retry_sync ( & path, & script_absolute) ?;
485
+ // Here, two wrappers over rename are clashing: We want to retry for security software
486
+ // blocking the file, but we also need the copy fallback is the problem was trying to
487
+ // move a file cross-drive.
488
+ match uv_fs:: rename_with_retry_sync ( & path, & script_absolute) {
489
+ Ok ( ( ) ) => ( ) ,
490
+ Err ( err) => {
491
+ debug ! ( "Failed to rename, falling back to copy: {err}" ) ;
492
+ fs_err:: copy ( & path, & script_absolute) ?;
493
+ }
494
+ }
488
495
}
489
496
490
497
None
@@ -533,10 +540,13 @@ pub(crate) fn install_data(
533
540
534
541
match path. file_name ( ) . and_then ( |name| name. to_str ( ) ) {
535
542
Some ( "data" ) => {
543
+ trace ! ( ?dist_name, "Installing data/data" ) ;
536
544
// Move the content of the folder to the root of the venv
537
545
move_folder_recorded ( & path, & layout. scheme . data , site_packages, record) ?;
538
546
}
539
547
Some ( "scripts" ) => {
548
+ trace ! ( ?dist_name, "Installing data/scripts" ) ;
549
+ let mut rename_or_copy = RenameOrCopy :: default ( ) ;
540
550
let mut initialized = false ;
541
551
for file in fs:: read_dir ( path) ? {
542
552
let file = file?;
@@ -563,17 +573,27 @@ pub(crate) fn install_data(
563
573
initialized = true ;
564
574
}
565
575
566
- install_script ( layout, relocatable, site_packages, record, & file) ?;
576
+ install_script (
577
+ layout,
578
+ relocatable,
579
+ site_packages,
580
+ record,
581
+ & file,
582
+ & mut rename_or_copy,
583
+ ) ?;
567
584
}
568
585
}
569
586
Some ( "headers" ) => {
587
+ trace ! ( ?dist_name, "Installing data/headers" ) ;
570
588
let target_path = layout. scheme . include . join ( dist_name. as_str ( ) ) ;
571
589
move_folder_recorded ( & path, & target_path, site_packages, record) ?;
572
590
}
573
591
Some ( "purelib" ) => {
592
+ trace ! ( ?dist_name, "Installing data/purelib" ) ;
574
593
move_folder_recorded ( & path, & layout. scheme . purelib , site_packages, record) ?;
575
594
}
576
595
Some ( "platlib" ) => {
596
+ trace ! ( ?dist_name, "Installing data/platlib" ) ;
577
597
move_folder_recorded ( & path, & layout. scheme . platlib , site_packages, record) ?;
578
598
}
579
599
_ => {
@@ -801,6 +821,40 @@ pub(crate) fn parse_scripts(
801
821
scripts_from_ini ( extras, python_minor, ini)
802
822
}
803
823
824
+ /// Rename a file with a fallback to copy that switches over on the first failure.
825
+ #[ derive( Default ) ]
826
+ enum RenameOrCopy {
827
+ #[ default]
828
+ Rename ,
829
+ Copy ,
830
+ }
831
+
832
+ impl RenameOrCopy {
833
+ /// Try to rename, and on failure, copy.
834
+ ///
835
+ /// Usually, source and target are on the same device, so we can rename, but if that fails, we
836
+ /// have to copy. If renaming failed once, we switch to copy permanently.
837
+ fn rename_or_copy ( & mut self , from : impl AsRef < Path > , to : impl AsRef < Path > ) -> io:: Result < ( ) > {
838
+ match self {
839
+ Self :: Rename => {
840
+ match fs_err:: rename ( from. as_ref ( ) , to. as_ref ( ) ) {
841
+ Ok ( ( ) ) => return Ok ( ( ) ) ,
842
+ Err ( err) => {
843
+ * self = RenameOrCopy :: Copy ;
844
+ debug ! ( "Failed to rename, falling back to copy: {err}" ) ;
845
+ }
846
+ }
847
+ fs_err:: copy ( from. as_ref ( ) , to. as_ref ( ) ) ?;
848
+ Ok ( ( ) )
849
+ }
850
+ Self :: Copy => {
851
+ fs_err:: copy ( from. as_ref ( ) , to. as_ref ( ) ) ?;
852
+ Ok ( ( ) )
853
+ }
854
+ }
855
+ }
856
+ }
857
+
804
858
#[ cfg( test) ]
805
859
mod test {
806
860
use std:: io:: Cursor ;
0 commit comments