@@ -53,14 +53,31 @@ pub enum Error {
5353 #[ source]
5454 err : io:: Error ,
5555 } ,
56+ #[ error( "Failed to create Python executable link at {} from {}" , to. user_display( ) , from. user_display( ) ) ]
57+ LinkExecutable {
58+ from : PathBuf ,
59+ to : PathBuf ,
60+ #[ source]
61+ err : io:: Error ,
62+ } ,
63+ #[ error( "Failed to create directory for Python executable link at {}" , to. user_display( ) ) ]
64+ ExecutableDirectory {
65+ to : PathBuf ,
66+ #[ source]
67+ err : io:: Error ,
68+ } ,
5669 #[ error( "Failed to read Python installation directory: {0}" , dir. user_display( ) ) ]
5770 ReadError {
5871 dir : PathBuf ,
5972 #[ source]
6073 err : io:: Error ,
6174 } ,
75+ #[ error( "Failed to find a directory to install executables into" ) ]
76+ NoExecutableDirectory ,
6277 #[ error( "Failed to read managed Python directory name: {0}" ) ]
6378 NameError ( String ) ,
79+ #[ error( "Failed to construct absolute path to managed Python directory: {}" , _0. user_display( ) ) ]
80+ AbsolutePath ( PathBuf , #[ source] std:: io:: Error ) ,
6481 #[ error( transparent) ]
6582 NameParseError ( #[ from] installation:: PythonInstallationKeyError ) ,
6683 #[ error( transparent) ]
@@ -267,18 +284,78 @@ impl ManagedPythonInstallation {
267284 . ok_or ( Error :: NameError ( "not a valid string" . to_string ( ) ) ) ?,
268285 ) ?;
269286
287+ let path = std:: path:: absolute ( & path) . map_err ( |err| Error :: AbsolutePath ( path, err) ) ?;
288+
270289 Ok ( Self { path, key } )
271290 }
272291
273- /// The path to this toolchain's Python executable.
292+ /// The path to this managed installation's Python executable.
293+ ///
294+ /// If the installation has multiple execututables i.e., `python`, `python3`, etc., this will
295+ /// return the _canonical_ executable name which the other names link to. On Unix, this is
296+ /// `python{major}.{minor}{variant}` and on Windows, this is `python{exe}`.
274297 pub fn executable ( & self ) -> PathBuf {
275- if cfg ! ( windows) {
276- self . python_dir ( ) . join ( "python.exe" )
298+ let implementation = match self . implementation ( ) {
299+ ImplementationName :: CPython => "python" ,
300+ ImplementationName :: PyPy => "pypy" ,
301+ ImplementationName :: GraalPy => {
302+ unreachable ! ( "Managed installations of GraalPy are not supported" )
303+ }
304+ } ;
305+
306+ let version = match self . implementation ( ) {
307+ ImplementationName :: CPython => {
308+ if cfg ! ( unix) {
309+ format ! ( "{}.{}" , self . key. major, self . key. minor)
310+ } else {
311+ String :: new ( )
312+ }
313+ }
314+ // PyPy uses a full version number, even on Windows.
315+ ImplementationName :: PyPy => format ! ( "{}.{}" , self . key. major, self . key. minor) ,
316+ ImplementationName :: GraalPy => {
317+ unreachable ! ( "Managed installations of GraalPy are not supported" )
318+ }
319+ } ;
320+
321+ // On Windows, the executable is just `python.exe` even for alternative variants
322+ let variant = if cfg ! ( unix) {
323+ self . key . variant . suffix ( )
324+ } else {
325+ ""
326+ } ;
327+
328+ let name = format ! (
329+ "{implementation}{version}{variant}{exe}" ,
330+ exe = std:: env:: consts:: EXE_SUFFIX
331+ ) ;
332+
333+ let executable = if cfg ! ( windows) {
334+ self . python_dir ( ) . join ( name)
277335 } else if cfg ! ( unix) {
278- self . python_dir ( ) . join ( "bin" ) . join ( "python3" )
336+ self . python_dir ( ) . join ( "bin" ) . join ( name )
279337 } else {
280338 unimplemented ! ( "Only Windows and Unix systems are supported." )
339+ } ;
340+
341+ // Workaround for python-build-standalone v20241016 which is missing the standard
342+ // `python.exe` executable in free-threaded distributions on Windows.
343+ //
344+ // See https://github.com/astral-sh/uv/issues/8298
345+ if cfg ! ( windows)
346+ && matches ! ( self . key. variant, PythonVariant :: Freethreaded )
347+ && !executable. exists ( )
348+ {
349+ // This is the alternative executable name for the freethreaded variant
350+ return self . python_dir ( ) . join ( format ! (
351+ "python{}.{}t{}" ,
352+ self . key. major,
353+ self . key. minor,
354+ std:: env:: consts:: EXE_SUFFIX
355+ ) ) ;
281356 }
357+
358+ executable
282359 }
283360
284361 fn python_dir ( & self ) -> PathBuf {
@@ -336,39 +413,38 @@ impl ManagedPythonInstallation {
336413 pub fn ensure_canonical_executables ( & self ) -> Result < ( ) , Error > {
337414 let python = self . executable ( ) ;
338415
339- // Workaround for python-build-standalone v20241016 which is missing the standard
340- // `python.exe` executable in free-threaded distributions on Windows.
341- //
342- // See https://github.com/astral-sh/uv/issues/8298
343- if !python. try_exists ( ) ? {
344- match self . key . variant {
345- PythonVariant :: Default => return Err ( Error :: MissingExecutable ( python. clone ( ) ) ) ,
346- PythonVariant :: Freethreaded => {
347- // This is the alternative executable name for the freethreaded variant
348- let python_in_dist = self . python_dir ( ) . join ( format ! (
349- "python{}.{}t{}" ,
350- self . key. major,
351- self . key. minor,
352- std:: env:: consts:: EXE_SUFFIX
353- ) ) ;
416+ let canonical_names = & [ "python" ] ;
417+
418+ for name in canonical_names {
419+ let executable =
420+ python. with_file_name ( format ! ( "{name}{exe}" , exe = std:: env:: consts:: EXE_SUFFIX ) ) ;
421+
422+ // Do not attempt to perform same-file copies — this is fine on Unix but fails on
423+ // Windows with a permission error instead of 'already exists'
424+ if executable == python {
425+ continue ;
426+ }
427+
428+ match uv_fs:: symlink_copy_fallback_file ( & python, & executable) {
429+ Ok ( ( ) ) => {
354430 debug ! (
355- "Creating link {} -> {}" ,
431+ "Created link {} -> {}" ,
432+ executable. user_display( ) ,
356433 python. user_display( ) ,
357- python_in_dist. user_display( )
358434 ) ;
359- uv_fs:: symlink_copy_fallback_file ( & python_in_dist, & python) . map_err ( |err| {
360- if err. kind ( ) == io:: ErrorKind :: NotFound {
361- Error :: MissingExecutable ( python_in_dist. clone ( ) )
362- } else {
363- Error :: CanonicalizeExecutable {
364- from : python_in_dist,
365- to : python,
366- err,
367- }
368- }
369- } ) ?;
370435 }
371- }
436+ Err ( err) if err. kind ( ) == io:: ErrorKind :: NotFound => {
437+ return Err ( Error :: MissingExecutable ( python. clone ( ) ) )
438+ }
439+ Err ( err) if err. kind ( ) == io:: ErrorKind :: AlreadyExists => { }
440+ Err ( err) => {
441+ return Err ( Error :: CanonicalizeExecutable {
442+ from : executable,
443+ to : python,
444+ err,
445+ } )
446+ }
447+ } ;
372448 }
373449
374450 Ok ( ( ) )
@@ -381,10 +457,7 @@ impl ManagedPythonInstallation {
381457 let stdlib = if matches ! ( self . key. os, Os ( target_lexicon:: OperatingSystem :: Windows ) ) {
382458 self . python_dir ( ) . join ( "Lib" )
383459 } else {
384- let lib_suffix = match self . key . variant {
385- PythonVariant :: Default => "" ,
386- PythonVariant :: Freethreaded => "t" ,
387- } ;
460+ let lib_suffix = self . key . variant . suffix ( ) ;
388461 let python = if matches ! (
389462 self . key. implementation,
390463 LenientImplementationName :: Known ( ImplementationName :: PyPy )
@@ -401,6 +474,31 @@ impl ManagedPythonInstallation {
401474
402475 Ok ( ( ) )
403476 }
477+
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 > {
480+ let python = self . executable ( ) ;
481+
482+ fs_err:: create_dir_all ( bin) . map_err ( |err| Error :: ExecutableDirectory {
483+ to : bin. to_path_buf ( ) ,
484+ err,
485+ } ) ?;
486+
487+ // TODO(zanieb): Add support for a "default" which
488+ let python_in_bin = bin. join ( self . key . versioned_executable_name ( ) ) ;
489+
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 ( ) ) )
494+ }
495+ Err ( err) => Err ( Error :: LinkExecutable {
496+ from : python,
497+ to : python_in_bin,
498+ err,
499+ } ) ,
500+ }
501+ }
404502}
405503
406504/// Generate a platform portion of a key from the environment.
@@ -423,3 +521,9 @@ impl fmt::Display for ManagedPythonInstallation {
423521 )
424522 }
425523}
524+
525+ /// Find the directory to install Python executables into.
526+ pub fn python_executable_dir ( ) -> Result < PathBuf , Error > {
527+ uv_dirs:: user_executable_directory ( Some ( EnvVars :: UV_PYTHON_BIN_DIR ) )
528+ . ok_or ( Error :: NoExecutableDirectory )
529+ }
0 commit comments