From b8966572751639c443ad5c89bac69bd64f9f559c Mon Sep 17 00:00:00 2001 From: Ahmed Ilyas Date: Thu, 12 Sep 2024 21:41:11 +0200 Subject: [PATCH] Allow `uv tool upgrade --all` to continue on individual upgrade failure (#7333) ## Summary Resolves #7294 ## Test Plan `cargo test` --- crates/uv/src/commands/tool/upgrade.rs | 240 +++++++++++++++---------- crates/uv/tests/tool_upgrade.rs | 79 ++++++++ 2 files changed, 220 insertions(+), 99 deletions(-) diff --git a/crates/uv/src/commands/tool/upgrade.rs b/crates/uv/src/commands/tool/upgrade.rs index d3f3e3978..7f4ba7172 100644 --- a/crates/uv/src/commands/tool/upgrade.rs +++ b/crates/uv/src/commands/tool/upgrade.rs @@ -13,6 +13,7 @@ use uv_settings::{Combine, ResolverInstallerOptions, ToolOptions}; use uv_tool::InstalledTools; use crate::commands::pip::loggers::{SummaryResolveLogger, UpgradeInstallLogger}; +use crate::commands::pip::operations::Changelog; use crate::commands::project::{update_environment, EnvironmentUpdate}; use crate::commands::tool::common::remove_entrypoints; use crate::commands::{tool::common::install_executables, ExitStatus, SharedState}; @@ -28,7 +29,6 @@ pub(crate) async fn upgrade( concurrency: Concurrency, native_tls: bool, cache: &Cache, - printer: Printer, ) -> Result { let installed_tools = InstalledTools::from_settings()?.init()?; @@ -55,116 +55,158 @@ pub(crate) async fn upgrade( // Determine whether we applied any upgrades. let mut did_upgrade = false; - for name in names { + // Determine whether any tool upgrade failed. + let mut failed_upgrade = false; + + for name in &names { debug!("Upgrading tool: `{name}`"); - - // Ensure the tool is installed. - let existing_tool_receipt = match installed_tools.get_tool_receipt(&name) { - Ok(Some(receipt)) => receipt, - Ok(None) => { - let install_command = format!("uv tool install {name}"); - writeln!( - printer.stderr(), - "`{}` is not installed; run `{}` to install", - name.cyan(), - install_command.green() - )?; - return Ok(ExitStatus::Failure); - } - Err(_) => { - let install_command = format!("uv tool install --force {name}"); - writeln!( - printer.stderr(), - "`{}` is missing a valid receipt; run `{}` to reinstall", - name.cyan(), - install_command.green() - )?; - return Ok(ExitStatus::Failure); - } - }; - - let existing_environment = match installed_tools.get_environment(&name, cache) { - Ok(Some(environment)) => environment, - Ok(None) => { - let install_command = format!("uv tool install {name}"); - writeln!( - printer.stderr(), - "`{}` is not installed; run `{}` to install", - name.cyan(), - install_command.green() - )?; - return Ok(ExitStatus::Failure); - } - Err(_) => { - let install_command = format!("uv tool install --force {name}"); - writeln!( - printer.stderr(), - "`{}` is missing a valid environment; run `{}` to reinstall", - name.cyan(), - install_command.green() - )?; - return Ok(ExitStatus::Failure); - } - }; - - // Resolve the appropriate settings, preferring: CLI > receipt > user. - let options = args.clone().combine( - ResolverInstallerOptions::from(existing_tool_receipt.options().clone()) - .combine(filesystem.clone()), - ); - let settings = ResolverInstallerSettings::from(options.clone()); - - // Resolve the requirements. - let requirements = existing_tool_receipt.requirements(); - let spec = RequirementsSpecification::from_requirements(requirements.to_vec()); - - // Initialize any shared state. - let state = SharedState::default(); - - // TODO(zanieb): Build the environment in the cache directory then copy into the tool - // directory. - let EnvironmentUpdate { - environment, - changelog, - } = update_environment( - existing_environment, - spec, - &settings, - &state, - Box::new(SummaryResolveLogger), - Box::new(UpgradeInstallLogger::new(name.clone())), + let changelog = upgrade_tool( + name, + printer, + &installed_tools, + &args, + cache, + &filesystem, connectivity, concurrency, native_tls, - cache, - printer, ) - .await?; + .await; - did_upgrade |= !changelog.is_empty(); - - // If we modified the target tool, reinstall the entrypoints. - if changelog.includes(&name) { - // At this point, we updated the existing environment, so we should remove any of its - // existing executables. - remove_entrypoints(&existing_tool_receipt); - - install_executables( - &environment, - &name, - &installed_tools, - ToolOptions::from(options), - true, - existing_tool_receipt.python().to_owned(), - requirements.to_vec(), - printer, - )?; + match changelog { + Ok(changelog) => { + did_upgrade |= !changelog.is_empty(); + } + Err(err) => { + // If we have a single tool, return the error directly. + if names.len() > 1 { + writeln!( + printer.stderr(), + "Failed to upgrade `{}`: {err}", + name.cyan(), + )?; + } else { + writeln!(printer.stderr(), "{err}")?; + } + failed_upgrade = true; + } } } + if failed_upgrade { + return Ok(ExitStatus::Failure); + } + if !did_upgrade { writeln!(printer.stderr(), "Nothing to upgrade")?; } Ok(ExitStatus::Success) } + +async fn upgrade_tool( + name: &PackageName, + printer: Printer, + installed_tools: &InstalledTools, + args: &ResolverInstallerOptions, + cache: &Cache, + filesystem: &ResolverInstallerOptions, + connectivity: Connectivity, + concurrency: Concurrency, + native_tls: bool, +) -> Result { + // Ensure the tool is installed. + let existing_tool_receipt = match installed_tools.get_tool_receipt(name) { + Ok(Some(receipt)) => receipt, + Ok(None) => { + let install_command = format!("uv tool install {name}"); + return Err(anyhow::anyhow!( + "`{}` is not installed; run `{}` to install", + name.cyan(), + install_command.green() + )); + } + Err(_) => { + let install_command = format!("uv tool install --force {name}"); + return Err(anyhow::anyhow!( + "`{}` is missing a valid receipt; run `{}` to reinstall", + name.cyan(), + install_command.green() + )); + } + }; + + let existing_environment = match installed_tools.get_environment(name, cache) { + Ok(Some(environment)) => environment, + Ok(None) => { + let install_command = format!("uv tool install {name}"); + return Err(anyhow::anyhow!( + "`{}` is not installed; run `{}` to install", + name.cyan(), + install_command.green() + )); + } + Err(_) => { + let install_command = format!("uv tool install --force {name}"); + return Err(anyhow::anyhow!( + "`{}` is missing a valid environment; run `{}` to reinstall", + name.cyan(), + install_command.green() + )); + } + }; + + // Resolve the appropriate settings, preferring: CLI > receipt > user. + let options = args.clone().combine( + ResolverInstallerOptions::from(existing_tool_receipt.options().clone()) + .combine(filesystem.clone()), + ); + let settings = ResolverInstallerSettings::from(options.clone()); + + // Resolve the requirements. + let requirements = existing_tool_receipt.requirements(); + let spec = RequirementsSpecification::from_requirements(requirements.to_vec()); + + // Initialize any shared state. + let state = SharedState::default(); + + // TODO(zanieb): Build the environment in the cache directory then copy into the tool + // directory. + let EnvironmentUpdate { + environment, + changelog, + } = update_environment( + existing_environment, + spec, + &settings, + &state, + Box::new(SummaryResolveLogger), + Box::new(UpgradeInstallLogger::new(name.clone())), + connectivity, + concurrency, + native_tls, + cache, + printer, + ) + .await?; + + // If we modified the target tool, reinstall the entrypoints. + if changelog.includes(name) { + // At this point, we updated the existing environment, so we should remove any of its + // existing executables. + remove_entrypoints(&existing_tool_receipt); + + install_executables( + &environment, + name, + installed_tools, + ToolOptions::from(options), + true, + existing_tool_receipt.python().to_owned(), + requirements.to_vec(), + printer, + )?; + } + + Ok(changelog) +} diff --git a/crates/uv/tests/tool_upgrade.rs b/crates/uv/tests/tool_upgrade.rs index 8cb76ddd8..fbb065cbf 100644 --- a/crates/uv/tests/tool_upgrade.rs +++ b/crates/uv/tests/tool_upgrade.rs @@ -242,6 +242,85 @@ fn test_tool_upgrade_non_existing_package() { "###); } +#[test] +fn test_tool_upgrade_not_stop_if_upgrade_fails() -> anyhow::Result<()> { + let context = TestContext::new("3.12") + .with_filtered_counts() + .with_filtered_exe_suffix(); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + // Install `python-dotenv` from Test PyPI, to get an outdated version. + uv_snapshot!(context.filters(), context.tool_install() + .arg("python-dotenv") + .arg("--index-url") + .arg("https://test.pypi.org/simple/") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()) + .env("PATH", bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + python-dotenv==0.10.2.post2 + Installed 1 executable: dotenv + "###); + + // Install `babel` from Test PyPI, to get an outdated version. + uv_snapshot!(context.filters(), context.tool_install() + .arg("babel") + .arg("--index-url") + .arg("https://test.pypi.org/simple/") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()) + .env("PATH", bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + babel==2.6.0 + + pytz==2018.5 + Installed 1 executable: pybabel + "###); + + // Break the receipt for python-dotenv + tool_dir + .child("python-dotenv") + .child("uv-receipt.toml") + .write_str("Invalid receipt")?; + + // Upgrade all from PyPI. + uv_snapshot!(context.filters(), context.tool_upgrade() + .arg("--all") + .arg("--index-url") + .arg("https://pypi.org/simple/") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()) + .env("PATH", bin_dir.as_os_str()), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Updated babel v2.6.0 -> v2.14.0 + - babel==2.6.0 + + babel==2.14.0 + - pytz==2018.5 + Installed 1 executable: pybabel + Failed to upgrade `python-dotenv`: `python-dotenv` is missing a valid receipt; run `uv tool install --force python-dotenv` to reinstall + "###); + + Ok(()) +} + #[test] fn test_tool_upgrade_settings() { let context = TestContext::new("3.12")