11use core:: fmt;
2- use fs_err as fs;
3- use itertools:: Itertools ;
42use std:: cmp:: Reverse ;
53use std:: ffi:: OsStr ;
64use std:: io:: { self , Write } ;
75use std:: path:: { Path , PathBuf } ;
86use std:: str:: FromStr ;
7+
8+ use fs_err as fs;
9+ use itertools:: Itertools ;
10+ use same_file:: is_same_file;
911use thiserror:: Error ;
1012use tracing:: { debug, warn} ;
1113
14+ use uv_fs:: { symlink_or_copy_file, LockedFile , Simplified } ;
1215use uv_state:: { StateBucket , StateStore } ;
16+ use uv_static:: EnvVars ;
17+ use uv_trampoline_builder:: { windows_python_launcher, Launcher } ;
1318
1419use crate :: downloads:: Error as DownloadError ;
1520use crate :: implementation:: {
@@ -21,9 +26,6 @@ use crate::platform::Error as PlatformError;
2126use crate :: platform:: { Arch , Libc , Os } ;
2227use crate :: python_version:: PythonVersion ;
2328use crate :: { PythonRequest , PythonVariant } ;
24- use uv_fs:: { LockedFile , Simplified } ;
25- use uv_static:: EnvVars ;
26-
2729#[ derive( Error , Debug ) ]
2830pub enum Error {
2931 #[ error( transparent) ]
@@ -74,6 +76,8 @@ pub enum Error {
7476 } ,
7577 #[ error( "Failed to find a directory to install executables into" ) ]
7678 NoExecutableDirectory ,
79+ #[ error( transparent) ]
80+ LauncherError ( #[ from] uv_trampoline_builder:: Error ) ,
7781 #[ error( "Failed to read managed Python directory name: {0}" ) ]
7882 NameError ( String ) ,
7983 #[ error( "Failed to construct absolute path to managed Python directory: {}" , _0. user_display( ) ) ]
@@ -425,7 +429,7 @@ impl ManagedPythonInstallation {
425429 continue ;
426430 }
427431
428- match uv_fs:: symlink_copy_fallback_file ( & python, & executable) {
432+ match uv_fs:: symlink_or_copy_file ( & python, & executable) {
429433 Ok ( ( ) ) => {
430434 debug ! (
431435 "Created link {} -> {}" ,
@@ -475,28 +479,67 @@ impl ManagedPythonInstallation {
475479 Ok ( ( ) )
476480 }
477481
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 > {
480486 let python = self . executable ( ) ;
481487
488+ let bin = target. parent ( ) . ok_or ( Error :: NoExecutableDirectory ) ?;
482489 fs_err:: create_dir_all ( bin) . map_err ( |err| Error :: ExecutableDirectory {
483490 to : bin. to_path_buf ( ) ,
484491 err,
485492 } ) ?;
486493
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+ }
489527
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 ;
494539 }
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" )
500543 }
501544 }
502545}
0 commit comments