@@ -53,14 +53,31 @@ pub enum Error {
53
53
#[ source]
54
54
err : io:: Error ,
55
55
} ,
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
+ } ,
56
69
#[ error( "Failed to read Python installation directory: {0}" , dir. user_display( ) ) ]
57
70
ReadError {
58
71
dir : PathBuf ,
59
72
#[ source]
60
73
err : io:: Error ,
61
74
} ,
75
+ #[ error( "Failed to find a directory to install executables into" ) ]
76
+ NoExecutableDirectory ,
62
77
#[ error( "Failed to read managed Python directory name: {0}" ) ]
63
78
NameError ( String ) ,
79
+ #[ error( "Failed to construct absolute path to managed Python directory: {}" , _0. user_display( ) ) ]
80
+ AbsolutePath ( PathBuf , #[ source] std:: io:: Error ) ,
64
81
#[ error( transparent) ]
65
82
NameParseError ( #[ from] installation:: PythonInstallationKeyError ) ,
66
83
#[ error( transparent) ]
@@ -267,18 +284,78 @@ impl ManagedPythonInstallation {
267
284
. ok_or ( Error :: NameError ( "not a valid string" . to_string ( ) ) ) ?,
268
285
) ?;
269
286
287
+ let path = std:: path:: absolute ( & path) . map_err ( |err| Error :: AbsolutePath ( path, err) ) ?;
288
+
270
289
Ok ( Self { path, key } )
271
290
}
272
291
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}`.
274
297
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)
277
335
} else if cfg ! ( unix) {
278
- self . python_dir ( ) . join ( "bin" ) . join ( "python3" )
336
+ self . python_dir ( ) . join ( "bin" ) . join ( name )
279
337
} else {
280
338
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
+ ) ) ;
281
356
}
357
+
358
+ executable
282
359
}
283
360
284
361
fn python_dir ( & self ) -> PathBuf {
@@ -336,39 +413,38 @@ impl ManagedPythonInstallation {
336
413
pub fn ensure_canonical_executables ( & self ) -> Result < ( ) , Error > {
337
414
let python = self . executable ( ) ;
338
415
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 ( ( ) ) => {
354
430
debug ! (
355
- "Creating link {} -> {}" ,
431
+ "Created link {} -> {}" ,
432
+ executable. user_display( ) ,
356
433
python. user_display( ) ,
357
- python_in_dist. user_display( )
358
434
) ;
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
- } ) ?;
370
435
}
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
+ } ;
372
448
}
373
449
374
450
Ok ( ( ) )
@@ -381,10 +457,7 @@ impl ManagedPythonInstallation {
381
457
let stdlib = if matches ! ( self . key. os, Os ( target_lexicon:: OperatingSystem :: Windows ) ) {
382
458
self . python_dir ( ) . join ( "Lib" )
383
459
} 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 ( ) ;
388
461
let python = if matches ! (
389
462
self . key. implementation,
390
463
LenientImplementationName :: Known ( ImplementationName :: PyPy )
@@ -401,6 +474,31 @@ impl ManagedPythonInstallation {
401
474
402
475
Ok ( ( ) )
403
476
}
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
+ }
404
502
}
405
503
406
504
/// Generate a platform portion of a key from the environment.
@@ -423,3 +521,9 @@ impl fmt::Display for ManagedPythonInstallation {
423
521
)
424
522
}
425
523
}
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