Skip to content

Commit 0caa5f5

Browse files
committed
Update preview installation of Python executables to be non-fatal
With a `--bin` opt-in and a `--no-bin` opt-out
1 parent 4d82e88 commit 0caa5f5

7 files changed

Lines changed: 206 additions & 40 deletions

File tree

crates/uv-cli/src/lib.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4941,6 +4941,19 @@ pub struct PythonInstallArgs {
49414941
#[arg(long, short, env = EnvVars::UV_PYTHON_INSTALL_DIR)]
49424942
pub install_dir: Option<PathBuf>,
49434943

4944+
/// Install a Python executable into the `bin` directory.
4945+
///
4946+
/// This is the default behavior. If this flag is provided explicitly, uv will error if the
4947+
/// executable cannot be installed.
4948+
///
4949+
/// See `UV_PYTHON_BIN_DIR` to customize the target directory.
4950+
#[arg(long, overrides_with("no_bin"), hide = true)]
4951+
pub bin: bool,
4952+
4953+
/// Do not install a Python executable into the `bin` directory.
4954+
#[arg(long, overrides_with("bin"), conflicts_with("default"))]
4955+
pub no_bin: bool,
4956+
49444957
/// The Python version(s) to install.
49454958
///
49464959
/// If not provided, the requested Python version(s) will be read from the `UV_PYTHON`
@@ -5003,7 +5016,7 @@ pub struct PythonInstallArgs {
50035016
/// and `python`.
50045017
///
50055018
/// If multiple Python versions are requested, uv will exit with an error.
5006-
#[arg(long)]
5019+
#[arg(long, conflicts_with("no_bin"))]
50075020
pub default: bool,
50085021
}
50095022

crates/uv-python/src/windows_registry.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,13 @@ fn read_registry_entry(company: &str, tag: &str, tag_key: &Key) -> Option<Window
129129
pub enum ManagedPep514Error {
130130
#[error("Windows has an unknown pointer width for arch: `{_0}`")]
131131
InvalidPointerSize(Arch),
132+
#[error("Failed to write registry entry: {0}")]
133+
WriteError(#[from] windows_registry::Error),
132134
}
133135

134136
/// Register a managed Python installation in the Windows registry following PEP 514.
135137
pub fn create_registry_entry(
136138
installation: &ManagedPythonInstallation,
137-
errors: &mut Vec<(PythonInstallationKey, anyhow::Error)>,
138139
) -> Result<(), ManagedPep514Error> {
139140
let pointer_width = match installation.key().arch().family().pointer_width() {
140141
Ok(PointerWidth::U32) => 32,
@@ -146,9 +147,7 @@ pub fn create_registry_entry(
146147
}
147148
};
148149

149-
if let Err(err) = write_registry_entry(installation, pointer_width) {
150-
errors.push((installation.key().clone(), err.into()));
151-
}
150+
write_registry_entry(installation, pointer_width)?;
152151

153152
Ok(())
154153
}

crates/uv/src/commands/python/install.rs

Lines changed: 111 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
140147
pub(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)]
676729
fn 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

884962
pub(crate) fn format_executables(

crates/uv/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1402,6 +1402,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
14021402
args.targets,
14031403
args.reinstall,
14041404
upgrade,
1405+
args.bin,
14051406
args.force,
14061407
args.python_install_mirror,
14071408
args.pypy_install_mirror,
@@ -1430,6 +1431,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
14301431
args.targets,
14311432
reinstall,
14321433
upgrade,
1434+
args.bin,
14331435
args.force,
14341436
args.python_install_mirror,
14351437
args.pypy_install_mirror,

crates/uv/src/settings.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,7 @@ pub(crate) struct PythonInstallSettings {
933933
pub(crate) targets: Vec<String>,
934934
pub(crate) reinstall: bool,
935935
pub(crate) force: bool,
936+
pub(crate) bin: Option<bool>,
936937
pub(crate) python_install_mirror: Option<String>,
937938
pub(crate) pypy_install_mirror: Option<String>,
938939
pub(crate) python_downloads_json_url: Option<String>,
@@ -961,6 +962,8 @@ impl PythonInstallSettings {
961962
install_dir,
962963
targets,
963964
reinstall,
965+
bin,
966+
no_bin,
964967
force,
965968
mirror: _,
966969
pypy_mirror: _,
@@ -973,6 +976,7 @@ impl PythonInstallSettings {
973976
targets,
974977
reinstall,
975978
force,
979+
bin: flag(bin, no_bin, "bin"),
976980
python_install_mirror: python_mirror,
977981
pypy_install_mirror: pypy_mirror,
978982
python_downloads_json_url,
@@ -992,6 +996,7 @@ pub(crate) struct PythonUpgradeSettings {
992996
pub(crate) pypy_install_mirror: Option<String>,
993997
pub(crate) python_downloads_json_url: Option<String>,
994998
pub(crate) default: bool,
999+
pub(crate) bin: Option<bool>,
9951000
}
9961001

9971002
impl PythonUpgradeSettings {
@@ -1013,6 +1018,7 @@ impl PythonUpgradeSettings {
10131018
args.python_downloads_json_url.or(python_downloads_json_url);
10141019
let force = false;
10151020
let default = false;
1021+
let bin = Some(false);
10161022

10171023
let PythonUpgradeArgs {
10181024
install_dir,
@@ -1030,6 +1036,7 @@ impl PythonUpgradeSettings {
10301036
pypy_install_mirror: pypy_mirror,
10311037
python_downloads_json_url,
10321038
default,
1039+
bin,
10331040
}
10341041
}
10351042
}

0 commit comments

Comments
 (0)