Allow uv tool upgrade --all to continue on individual upgrade failure (#7333)

## Summary

Resolves #7294 

## Test Plan

`cargo test`
This commit is contained in:
Ahmed Ilyas 2024-09-12 21:41:11 +02:00 committed by GitHub
parent e1e85ab4c8
commit b896657275
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 220 additions and 99 deletions

View file

@ -13,6 +13,7 @@ use uv_settings::{Combine, ResolverInstallerOptions, ToolOptions};
use uv_tool::InstalledTools; use uv_tool::InstalledTools;
use crate::commands::pip::loggers::{SummaryResolveLogger, UpgradeInstallLogger}; use crate::commands::pip::loggers::{SummaryResolveLogger, UpgradeInstallLogger};
use crate::commands::pip::operations::Changelog;
use crate::commands::project::{update_environment, EnvironmentUpdate}; use crate::commands::project::{update_environment, EnvironmentUpdate};
use crate::commands::tool::common::remove_entrypoints; use crate::commands::tool::common::remove_entrypoints;
use crate::commands::{tool::common::install_executables, ExitStatus, SharedState}; use crate::commands::{tool::common::install_executables, ExitStatus, SharedState};
@ -28,7 +29,6 @@ pub(crate) async fn upgrade(
concurrency: Concurrency, concurrency: Concurrency,
native_tls: bool, native_tls: bool,
cache: &Cache, cache: &Cache,
printer: Printer, printer: Printer,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
let installed_tools = InstalledTools::from_settings()?.init()?; let installed_tools = InstalledTools::from_settings()?.init()?;
@ -55,55 +55,104 @@ pub(crate) async fn upgrade(
// Determine whether we applied any upgrades. // Determine whether we applied any upgrades.
let mut did_upgrade = false; let mut did_upgrade = false;
for name in names { // Determine whether any tool upgrade failed.
debug!("Upgrading tool: `{name}`"); let mut failed_upgrade = false;
for name in &names {
debug!("Upgrading tool: `{name}`");
let changelog = upgrade_tool(
name,
printer,
&installed_tools,
&args,
cache,
&filesystem,
connectivity,
concurrency,
native_tls,
)
.await;
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<Changelog> {
// Ensure the tool is installed. // Ensure the tool is installed.
let existing_tool_receipt = match installed_tools.get_tool_receipt(&name) { let existing_tool_receipt = match installed_tools.get_tool_receipt(name) {
Ok(Some(receipt)) => receipt, Ok(Some(receipt)) => receipt,
Ok(None) => { Ok(None) => {
let install_command = format!("uv tool install {name}"); let install_command = format!("uv tool install {name}");
writeln!( return Err(anyhow::anyhow!(
printer.stderr(),
"`{}` is not installed; run `{}` to install", "`{}` is not installed; run `{}` to install",
name.cyan(), name.cyan(),
install_command.green() install_command.green()
)?; ));
return Ok(ExitStatus::Failure);
} }
Err(_) => { Err(_) => {
let install_command = format!("uv tool install --force {name}"); let install_command = format!("uv tool install --force {name}");
writeln!( return Err(anyhow::anyhow!(
printer.stderr(),
"`{}` is missing a valid receipt; run `{}` to reinstall", "`{}` is missing a valid receipt; run `{}` to reinstall",
name.cyan(), name.cyan(),
install_command.green() install_command.green()
)?; ));
return Ok(ExitStatus::Failure);
} }
}; };
let existing_environment = match installed_tools.get_environment(&name, cache) { let existing_environment = match installed_tools.get_environment(name, cache) {
Ok(Some(environment)) => environment, Ok(Some(environment)) => environment,
Ok(None) => { Ok(None) => {
let install_command = format!("uv tool install {name}"); let install_command = format!("uv tool install {name}");
writeln!( return Err(anyhow::anyhow!(
printer.stderr(),
"`{}` is not installed; run `{}` to install", "`{}` is not installed; run `{}` to install",
name.cyan(), name.cyan(),
install_command.green() install_command.green()
)?; ));
return Ok(ExitStatus::Failure);
} }
Err(_) => { Err(_) => {
let install_command = format!("uv tool install --force {name}"); let install_command = format!("uv tool install --force {name}");
writeln!( return Err(anyhow::anyhow!(
printer.stderr(),
"`{}` is missing a valid environment; run `{}` to reinstall", "`{}` is missing a valid environment; run `{}` to reinstall",
name.cyan(), name.cyan(),
install_command.green() install_command.green()
)?; ));
return Ok(ExitStatus::Failure);
} }
}; };
@ -141,18 +190,16 @@ pub(crate) async fn upgrade(
) )
.await?; .await?;
did_upgrade |= !changelog.is_empty();
// If we modified the target tool, reinstall the entrypoints. // If we modified the target tool, reinstall the entrypoints.
if changelog.includes(&name) { if changelog.includes(name) {
// At this point, we updated the existing environment, so we should remove any of its // At this point, we updated the existing environment, so we should remove any of its
// existing executables. // existing executables.
remove_entrypoints(&existing_tool_receipt); remove_entrypoints(&existing_tool_receipt);
install_executables( install_executables(
&environment, &environment,
&name, name,
&installed_tools, installed_tools,
ToolOptions::from(options), ToolOptions::from(options),
true, true,
existing_tool_receipt.python().to_owned(), existing_tool_receipt.python().to_owned(),
@ -160,11 +207,6 @@ pub(crate) async fn upgrade(
printer, printer,
)?; )?;
} }
}
if !did_upgrade { Ok(changelog)
writeln!(printer.stderr(), "Nothing to upgrade")?;
}
Ok(ExitStatus::Success)
} }

View file

@ -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] #[test]
fn test_tool_upgrade_settings() { fn test_tool_upgrade_settings() {
let context = TestContext::new("3.12") let context = TestContext::new("3.12")