mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-22 08:12:44 +00:00
Allow uv tool upgrade --all
to continue on individual upgrade failure (#7333)
## Summary Resolves #7294 ## Test Plan `cargo test`
This commit is contained in:
parent
e1e85ab4c8
commit
b896657275
2 changed files with 220 additions and 99 deletions
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue