@@ -135,6 +135,13 @@ impl Changelog {
135135 }
136136}
137137
138+ enum InstallErrorKind {
139+ DownloadUnpack ,
140+ Bin ,
141+ #[ cfg( windows) ]
142+ Registry ,
143+ }
144+
138145/// Download and install Python versions.
139146#[ allow( clippy:: fn_params_excessive_bools) ]
140147pub ( crate ) async fn install (
@@ -143,6 +150,7 @@ pub(crate) async fn install(
143150 targets : Vec < String > ,
144151 reinstall : bool ,
145152 upgrade : bool ,
153+ bin : Option < bool > ,
146154 force : bool ,
147155 python_install_mirror : Option < String > ,
148156 pypy_install_mirror : Option < String > ,
@@ -432,12 +440,16 @@ pub(crate) async fn install(
432440 downloaded. push ( installation. clone ( ) ) ;
433441 }
434442 Err ( err) => {
435- errors. push ( ( download. key ( ) . clone ( ) , anyhow:: Error :: new ( err) ) ) ;
443+ errors. push ( (
444+ InstallErrorKind :: DownloadUnpack ,
445+ download. key ( ) . clone ( ) ,
446+ anyhow:: Error :: new ( err) ,
447+ ) ) ;
436448 }
437449 }
438450 }
439451
440- let bin = if preview. is_enabled ( ) {
452+ let bin_dir = if matches ! ( bin , Some ( true ) ) || preview. is_enabled ( ) {
441453 Some ( python_executable_dir ( ) ?)
442454 } else {
443455 None
@@ -460,35 +472,46 @@ pub(crate) async fn install(
460472 continue ;
461473 }
462474
463- let bin = bin
475+ let bin_dir = bin_dir
464476 . as_ref ( )
465477 . expect ( "We should have a bin directory with preview enabled" )
466478 . as_path ( ) ;
467479
468480 let upgradeable = ( default || is_default_install)
469481 || requested_minor_versions. contains ( & installation. key ( ) . version ( ) . python_version ( ) ) ;
470482
471- create_bin_links (
472- installation,
473- bin,
474- reinstall,
475- force,
476- default,
477- upgradeable,
478- upgrade,
479- is_default_install,
480- first_request,
481- & existing_installations,
482- & installations,
483- & mut changelog,
484- & mut errors,
485- preview,
486- ) ?;
483+ if !matches ! ( bin, Some ( false ) ) {
484+ create_bin_links (
485+ installation,
486+ bin_dir,
487+ reinstall,
488+ force,
489+ default,
490+ upgradeable,
491+ upgrade,
492+ is_default_install,
493+ first_request,
494+ & existing_installations,
495+ & installations,
496+ & mut changelog,
497+ & mut errors,
498+ preview,
499+ ) ;
500+ }
487501
488502 if preview. is_enabled ( ) {
489503 #[ cfg( windows) ]
490504 {
491- uv_python:: windows_registry:: create_registry_entry ( installation, & mut errors) ?;
505+ match uv_python:: windows_registry:: create_registry_entry ( installation) {
506+ Ok ( ( ) ) => { }
507+ Err ( err) => {
508+ errors. push ( (
509+ InstallErrorKind :: Registry ,
510+ installation. key ( ) . clone ( ) ,
511+ err. into ( ) ,
512+ ) ) ;
513+ }
514+ }
492515 }
493516 }
494517 }
@@ -636,24 +659,47 @@ pub(crate) async fn install(
636659 }
637660 }
638661
639- if preview. is_enabled ( ) {
640- let bin = bin
662+ if preview. is_enabled ( ) && ! matches ! ( bin , Some ( false ) ) {
663+ let bin_dir = bin_dir
641664 . as_ref ( )
642665 . expect ( "We should have a bin directory with preview enabled" )
643666 . as_path ( ) ;
644- warn_if_not_on_path ( bin ) ;
667+ warn_if_not_on_path ( bin_dir ) ;
645668 }
646669 }
647670
648671 if !errors. is_empty ( ) {
649- for ( key, err) in errors
672+ // If there are only bin install errors and the user didn't opt-in, we're only going to warn
673+ let fatal = errors
674+ . iter ( )
675+ . all ( |( kind, _, _) | matches ! ( kind, InstallErrorKind :: Bin ) )
676+ && bin. is_none ( ) ;
677+
678+ for ( kind, key, err) in errors
650679 . into_iter ( )
651- . sorted_unstable_by ( |( key_a, _) , ( key_b, _) | key_a. cmp ( key_b) )
680+ . sorted_unstable_by ( |( _ , key_a, _) , ( _ , key_b, _) | key_a. cmp ( key_b) )
652681 {
682+ let ( level, verb) = match kind {
683+ InstallErrorKind :: DownloadUnpack => ( "error" . red ( ) . bold ( ) . to_string ( ) , "install" ) ,
684+ InstallErrorKind :: Bin => {
685+ let level = match bin {
686+ None => "warning" . yellow ( ) . bold ( ) . to_string ( ) ,
687+ Some ( false ) => continue ,
688+ Some ( true ) => "error" . red ( ) . bold ( ) . to_string ( ) ,
689+ } ;
690+ ( level, "install executable for" )
691+ }
692+ #[ cfg( windows) ]
693+ InstallErrorKind :: Registry => (
694+ "error" . red ( ) . bold ( ) . to_string ( ) ,
695+ "install registry entry for" ,
696+ ) ,
697+ } ;
698+
653699 writeln ! (
654700 printer. stderr( ) ,
655- "{}: Failed to install {}" ,
656- "error" . red ( ) . bold( ) ,
701+ "{level}{} Failed to {verb} {}" ,
702+ ":" . bold( ) ,
657703 key. green( )
658704 ) ?;
659705 for err in err. chain ( ) {
@@ -665,13 +711,20 @@ pub(crate) async fn install(
665711 ) ?;
666712 }
667713 }
714+
715+ if fatal {
716+ return Ok ( ExitStatus :: Success ) ;
717+ }
718+
668719 return Ok ( ExitStatus :: Failure ) ;
669720 }
670721
671722 Ok ( ExitStatus :: Success )
672723}
673724
674725/// Link the binaries of a managed Python installation to the bin directory.
726+ ///
727+ /// This function is fallible, but errors are pushed to `errors` instead of being thrown.
675728#[ allow( clippy:: fn_params_excessive_bools) ]
676729fn create_bin_links (
677730 installation : & ManagedPythonInstallation ,
@@ -686,9 +739,9 @@ fn create_bin_links(
686739 existing_installations : & [ ManagedPythonInstallation ] ,
687740 installations : & [ & ManagedPythonInstallation ] ,
688741 changelog : & mut Changelog ,
689- errors : & mut Vec < ( PythonInstallationKey , Error ) > ,
742+ errors : & mut Vec < ( InstallErrorKind , PythonInstallationKey , Error ) > ,
690743 preview : PreviewMode ,
691- ) -> Result < ( ) , Error > {
744+ ) {
692745 let targets =
693746 if ( default || is_default_install) && first_request. matches_installation ( installation) {
694747 vec ! [
@@ -773,6 +826,7 @@ fn create_bin_links(
773826 ) ;
774827 } else {
775828 errors. push ( (
829+ InstallErrorKind :: Bin ,
776830 installation. key ( ) . clone ( ) ,
777831 anyhow:: anyhow!(
778832 "Executable already exists at `{}` but is not managed by uv; use `--force` to replace it" ,
@@ -848,7 +902,17 @@ fn create_bin_links(
848902 }
849903
850904 // Replace the existing link
851- fs_err:: remove_file ( & to) ?;
905+ if let Err ( err) = fs_err:: remove_file ( & to) {
906+ errors. push ( (
907+ InstallErrorKind :: Bin ,
908+ installation. key ( ) . clone ( ) ,
909+ anyhow:: anyhow!(
910+ "Executable already exists at `{}` but could not be removed: {err}" ,
911+ to. simplified_display( )
912+ ) ,
913+ ) ) ;
914+ continue ;
915+ }
852916
853917 if let Some ( existing) = existing {
854918 // Ensure we do not report installation of this executable for an existing
@@ -860,7 +924,18 @@ fn create_bin_links(
860924 . remove ( & target) ;
861925 }
862926
863- create_link_to_executable ( & target, executable) ?;
927+ if let Err ( err) = create_link_to_executable ( & target, executable) {
928+ errors. push ( (
929+ InstallErrorKind :: Bin ,
930+ installation. key ( ) . clone ( ) ,
931+ anyhow:: anyhow!(
932+ "Failed to create link at `{}`: {err}" ,
933+ target. simplified_display( )
934+ ) ,
935+ ) ) ;
936+ continue ;
937+ }
938+
864939 debug ! (
865940 "Updated executable at `{}` to {}" ,
866941 target. simplified_display( ) ,
@@ -874,11 +949,14 @@ fn create_bin_links(
874949 . insert ( target. clone ( ) ) ;
875950 }
876951 Err ( err) => {
877- errors. push ( ( installation. key ( ) . clone ( ) , anyhow:: Error :: new ( err) ) ) ;
952+ errors. push ( (
953+ InstallErrorKind :: Bin ,
954+ installation. key ( ) . clone ( ) ,
955+ anyhow:: Error :: new ( err) ,
956+ ) ) ;
878957 }
879958 }
880959 }
881- Ok ( ( ) )
882960}
883961
884962pub ( crate ) fn format_executables (
0 commit comments