mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-24 09:06:05 +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 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,116 +55,158 @@ 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.
|
||||||
|
let mut failed_upgrade = false;
|
||||||
|
|
||||||
|
for name in &names {
|
||||||
debug!("Upgrading tool: `{name}`");
|
debug!("Upgrading tool: `{name}`");
|
||||||
|
let changelog = upgrade_tool(
|
||||||
// Ensure the tool is installed.
|
name,
|
||||||
let existing_tool_receipt = match installed_tools.get_tool_receipt(&name) {
|
printer,
|
||||||
Ok(Some(receipt)) => receipt,
|
&installed_tools,
|
||||||
Ok(None) => {
|
&args,
|
||||||
let install_command = format!("uv tool install {name}");
|
cache,
|
||||||
writeln!(
|
&filesystem,
|
||||||
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())),
|
|
||||||
connectivity,
|
connectivity,
|
||||||
concurrency,
|
concurrency,
|
||||||
native_tls,
|
native_tls,
|
||||||
cache,
|
|
||||||
printer,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await;
|
||||||
|
|
||||||
did_upgrade |= !changelog.is_empty();
|
match changelog {
|
||||||
|
Ok(changelog) => {
|
||||||
// If we modified the target tool, reinstall the entrypoints.
|
did_upgrade |= !changelog.is_empty();
|
||||||
if changelog.includes(&name) {
|
}
|
||||||
// At this point, we updated the existing environment, so we should remove any of its
|
Err(err) => {
|
||||||
// existing executables.
|
// If we have a single tool, return the error directly.
|
||||||
remove_entrypoints(&existing_tool_receipt);
|
if names.len() > 1 {
|
||||||
|
writeln!(
|
||||||
install_executables(
|
printer.stderr(),
|
||||||
&environment,
|
"Failed to upgrade `{}`: {err}",
|
||||||
&name,
|
name.cyan(),
|
||||||
&installed_tools,
|
)?;
|
||||||
ToolOptions::from(options),
|
} else {
|
||||||
true,
|
writeln!(printer.stderr(), "{err}")?;
|
||||||
existing_tool_receipt.python().to_owned(),
|
}
|
||||||
requirements.to_vec(),
|
failed_upgrade = true;
|
||||||
printer,
|
}
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if failed_upgrade {
|
||||||
|
return Ok(ExitStatus::Failure);
|
||||||
|
}
|
||||||
|
|
||||||
if !did_upgrade {
|
if !did_upgrade {
|
||||||
writeln!(printer.stderr(), "Nothing to upgrade")?;
|
writeln!(printer.stderr(), "Nothing to upgrade")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ExitStatus::Success)
|
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]
|
#[test]
|
||||||
fn test_tool_upgrade_settings() {
|
fn test_tool_upgrade_settings() {
|
||||||
let context = TestContext::new("3.12")
|
let context = TestContext::new("3.12")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue