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 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<ExitStatus> {
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<Changelog> {
// 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)
}

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