Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 142 additions & 100 deletions crates/uv/src/commands/tool/upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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()?;
Expand All @@ -55,116 +55,158 @@ pub(crate) async fn upgrade(
// Determine whether we applied any upgrades.
let mut did_upgrade = false;

for name in names {
debug!("Upgrading tool: `{name}`");
// Determine whether any tool upgrade failed.
let mut failed_upgrade = false;

// 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())),
for name in &names {
debug!("Upgrading tool: `{name}`");
let changelog = upgrade_tool(
name,
printer,
&installed_tools,
&args,
cache,
&filesystem,
connectivity,
concurrency,
native_tls,
cache,
printer,
)
.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,
)?;
.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.
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)
}
79 changes: 79 additions & 0 deletions crates/uv/tests/tool_upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading