1
1
use core:: fmt;
2
- use fs_err as fs;
3
- use itertools:: Itertools ;
4
2
use std:: cmp:: Reverse ;
5
3
use std:: ffi:: OsStr ;
6
4
use std:: io:: { self , Write } ;
7
5
use std:: path:: { Path , PathBuf } ;
8
6
use std:: str:: FromStr ;
7
+
8
+ use fs_err as fs;
9
+ use itertools:: Itertools ;
10
+ use same_file:: is_same_file;
9
11
use thiserror:: Error ;
10
12
use tracing:: { debug, warn} ;
11
13
14
+ use uv_fs:: { symlink_or_copy_file, LockedFile , Simplified } ;
12
15
use uv_state:: { StateBucket , StateStore } ;
16
+ use uv_static:: EnvVars ;
17
+ use uv_trampoline_builder:: { windows_python_launcher, Launcher } ;
13
18
14
19
use crate :: downloads:: Error as DownloadError ;
15
20
use crate :: implementation:: {
@@ -21,9 +26,6 @@ use crate::platform::Error as PlatformError;
21
26
use crate :: platform:: { Arch , Libc , Os } ;
22
27
use crate :: python_version:: PythonVersion ;
23
28
use crate :: { PythonRequest , PythonVariant } ;
24
- use uv_fs:: { LockedFile , Simplified } ;
25
- use uv_static:: EnvVars ;
26
-
27
29
#[ derive( Error , Debug ) ]
28
30
pub enum Error {
29
31
#[ error( transparent) ]
@@ -74,6 +76,8 @@ pub enum Error {
74
76
} ,
75
77
#[ error( "Failed to find a directory to install executables into" ) ]
76
78
NoExecutableDirectory ,
79
+ #[ error( transparent) ]
80
+ LauncherError ( #[ from] uv_trampoline_builder:: Error ) ,
77
81
#[ error( "Failed to read managed Python directory name: {0}" ) ]
78
82
NameError ( String ) ,
79
83
#[ error( "Failed to construct absolute path to managed Python directory: {}" , _0. user_display( ) ) ]
@@ -425,7 +429,7 @@ impl ManagedPythonInstallation {
425
429
continue ;
426
430
}
427
431
428
- match uv_fs:: symlink_copy_fallback_file ( & python, & executable) {
432
+ match uv_fs:: symlink_or_copy_file ( & python, & executable) {
429
433
Ok ( ( ) ) => {
430
434
debug ! (
431
435
"Created link {} -> {}" ,
@@ -475,28 +479,67 @@ impl ManagedPythonInstallation {
475
479
Ok ( ( ) )
476
480
}
477
481
478
- /// Create a link to the Python executable in the given `bin` directory.
479
- pub fn create_bin_link ( & self , bin : & Path ) -> Result < PathBuf , Error > {
482
+ /// Create a link to the managed Python executable.
483
+ ///
484
+ /// If the file already exists at the target path, an error will be returned.
485
+ pub fn create_bin_link ( & self , target : & Path ) -> Result < ( ) , Error > {
480
486
let python = self . executable ( ) ;
481
487
488
+ let bin = target. parent ( ) . ok_or ( Error :: NoExecutableDirectory ) ?;
482
489
fs_err:: create_dir_all ( bin) . map_err ( |err| Error :: ExecutableDirectory {
483
490
to : bin. to_path_buf ( ) ,
484
491
err,
485
492
} ) ?;
486
493
487
- // TODO(zanieb): Add support for a "default" which
488
- let python_in_bin = bin. join ( self . key . versioned_executable_name ( ) ) ;
494
+ if cfg ! ( unix) {
495
+ // Note this will never copy on Unix — we use it here to allow compilation on Windows
496
+ match symlink_or_copy_file ( & python, target) {
497
+ Ok ( ( ) ) => Ok ( ( ) ) ,
498
+ Err ( err) if err. kind ( ) == io:: ErrorKind :: NotFound => {
499
+ Err ( Error :: MissingExecutable ( python. clone ( ) ) )
500
+ }
501
+ Err ( err) => Err ( Error :: LinkExecutable {
502
+ from : python,
503
+ to : target. to_path_buf ( ) ,
504
+ err,
505
+ } ) ,
506
+ }
507
+ } else if cfg ! ( windows) {
508
+ // TODO(zanieb): Install GUI launchers as well
509
+ let launcher = windows_python_launcher ( & python, false ) ?;
510
+
511
+ // OK to use `std::fs` here, `fs_err` does not support `File::create_new` and we attach
512
+ // error context anyway
513
+ #[ allow( clippy:: disallowed_types) ]
514
+ {
515
+ std:: fs:: File :: create_new ( target)
516
+ . and_then ( |mut file| file. write_all ( launcher. as_ref ( ) ) )
517
+ . map_err ( |err| Error :: LinkExecutable {
518
+ from : python,
519
+ to : target. to_path_buf ( ) ,
520
+ err,
521
+ } )
522
+ }
523
+ } else {
524
+ unimplemented ! ( "Only Windows and Unix systems are supported." )
525
+ }
526
+ }
489
527
490
- match uv_fs:: symlink_copy_fallback_file ( & python, & python_in_bin) {
491
- Ok ( ( ) ) => Ok ( python_in_bin) ,
492
- Err ( err) if err. kind ( ) == io:: ErrorKind :: NotFound => {
493
- Err ( Error :: MissingExecutable ( python. clone ( ) ) )
528
+ /// Returns `true` if the path is a link to this installation's binary, e.g., as created by
529
+ /// [`ManagedPythonInstallation::create_bin_link`].
530
+ pub fn is_bin_link ( & self , path : & Path ) -> bool {
531
+ if cfg ! ( unix) {
532
+ is_same_file ( path, self . executable ( ) ) . unwrap_or_default ( )
533
+ } else if cfg ! ( windows) {
534
+ let Some ( launcher) = Launcher :: try_from_path ( path) . unwrap_or_default ( ) else {
535
+ return false ;
536
+ } ;
537
+ if !matches ! ( launcher. kind, uv_trampoline_builder:: LauncherKind :: Python ) {
538
+ return false ;
494
539
}
495
- Err ( err) => Err ( Error :: LinkExecutable {
496
- from : python,
497
- to : python_in_bin,
498
- err,
499
- } ) ,
540
+ launcher. python_path == self . executable ( )
541
+ } else {
542
+ unreachable ! ( "Only Windows and Unix are supported" )
500
543
}
501
544
}
502
545
}
0 commit comments