Add a uv self update command (#2228)

## Summary

Powered by Axo: https://github.com/axodotdev/axoupdater.

Closes https://github.com/astral-sh/uv/issues/1591.

## Test Plan

To test locally:

- `rm -f ~/.config/uv/uv-receipt.json /Users/crmarsh/.cargo/bin/uv`
- `curl --proto '=https' --tlsv1.2 -LsSf
https://github.com/astral-sh/uv/releases/download/0.1.14/uv-installer.sh
| sh`
- `cargo run self update`

Up-to-date:

![Screenshot 2024-03-06 at 12 13
36 AM](04bb7a11-6557-4317-8e86-18288fbc13c6)

Updated:

![Screenshot 2024-03-06 at 12 13
54 AM](c08ad739-5a2b-47cf-bf13-018a8d708330)

No receipt:

![Screenshot 2024-03-06 at 12 14
13 AM](317bbfaf-a787-4cbf-9f93-a4ce8ca7a988)
This commit is contained in:
Charlie Marsh 2024-03-19 16:02:49 -04:00 committed by GitHub
parent c4107f9c40
commit 1bf48c91f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 331 additions and 14 deletions

View file

@ -35,6 +35,7 @@ uv-warnings = { path = "../uv-warnings" }
anstream = { workspace = true }
anyhow = { workspace = true }
axoupdater = { workspace = true, features = ["github_releases", "tokio"] }
base64 = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true, features = ["derive", "string"] }

View file

@ -15,6 +15,7 @@ pub(crate) use pip_list::pip_list;
pub(crate) use pip_show::pip_show;
pub(crate) use pip_sync::pip_sync;
pub(crate) use pip_uninstall::pip_uninstall;
pub(crate) use self_update::self_update;
use uv_cache::Cache;
use uv_fs::Simplified;
use uv_installer::compile_tree;
@ -36,6 +37,7 @@ mod pip_show;
mod pip_sync;
mod pip_uninstall;
mod reporters;
mod self_update;
mod venv;
mod version;

View file

@ -0,0 +1,114 @@
use std::fmt::Write;
use anyhow::Result;
use axoupdater::{AxoUpdater, AxoupdateError};
use owo_colors::OwoColorize;
use tracing::debug;
use uv_client::BetterReqwestError;
use crate::commands::ExitStatus;
use crate::printer::Printer;
/// Attempt to update the `uv` binary.
pub(crate) async fn self_update(printer: Printer) -> Result<ExitStatus> {
let mut updater = AxoUpdater::new_for("uv");
updater.disable_installer_output();
// Load the "install receipt" for the current binary. If the receipt is not found, then
// `uv` was likely installed via a package manager.
let Ok(updater) = updater.load_receipt() else {
debug!("no receipt found; assuming `uv` was installed via a package manager");
writeln!(
printer.stderr(),
"{}",
format_args!(
concat!(
"{}{} Self-update is only available for `uv` binaries installed via the standalone installation scripts.",
"\n",
"\n",
"If you installed `uv` with `pip`, `brew`, or another package manager, update `uv` with `pip install --upgrade`, `brew upgrade`, or similar."
),
"warning".yellow().bold(),
":".bold()
)
)?;
return Ok(ExitStatus::Error);
};
// Ensure the receipt is for the current binary. If it's not, then the user likely has multiple
// `uv` binaries installed, and the current binary was _not_ installed via the standalone
// installation scripts.
if !updater.check_receipt_is_for_this_executable()? {
debug!(
"receipt is not for this executable; assuming `uv` was installed via a package manager"
);
writeln!(
printer.stderr(),
"{}",
format_args!(
concat!(
"{}{} Self-update is only available for `uv` binaries installed via the standalone installation scripts.",
"\n",
"\n",
"If you installed `uv` with `pip`, `brew`, or another package manager, update `uv` with `pip install --upgrade`, `brew upgrade`, or similar."
),
"warning".yellow().bold(),
":".bold()
)
)?;
return Ok(ExitStatus::Error);
}
writeln!(
printer.stderr(),
"{}",
format_args!(
"{}{} Checking for updates...",
"info".cyan().bold(),
":".bold()
)
)?;
// Run the updater. This involves a network request, since we need to determine the latest
// available version of `uv`.
match updater.run().await {
Ok(Some(result)) => {
writeln!(
printer.stderr(),
"{}",
format_args!(
"{}{} Upgraded `uv` to {}! {}",
"success".green().bold(),
":".bold(),
format!("v{}", result.new_version).bold().white(),
format!(
"https://github.com/astral-sh/uv/releases/tag/{}",
result.new_version_tag
)
.cyan()
)
)?;
}
Ok(None) => {
writeln!(
printer.stderr(),
"{}",
format_args!(
"{}{} You're on the latest version of `uv` ({}).",
"success".green().bold(),
":".bold(),
format!("v{}", env!("CARGO_PKG_VERSION")).bold().white()
)
)?;
}
Err(err) => {
return Err(if let AxoupdateError::Reqwest(err) = err {
BetterReqwestError::from(err).into()
} else {
err.into()
});
}
}
Ok(ExitStatus::Success)
}

View file

@ -137,6 +137,9 @@ enum Commands {
Venv(VenvArgs),
/// Manage the cache.
Cache(CacheNamespace),
/// Manage the `uv` executable.
#[clap(name = "self")]
Self_(SelfNamespace),
/// Remove all items from the cache.
#[clap(hide = true)]
Clean(CleanArgs),
@ -150,6 +153,18 @@ enum Commands {
GenerateShellCompletion { shell: clap_complete_command::Shell },
}
#[derive(Args)]
struct SelfNamespace {
#[clap(subcommand)]
command: SelfCommand,
}
#[derive(Subcommand)]
enum SelfCommand {
/// Update `uv` to the latest version.
Update,
}
#[derive(Args)]
struct CacheNamespace {
#[clap(subcommand)]
@ -171,13 +186,6 @@ struct CleanArgs {
package: Vec<PackageName>,
}
#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
struct DirArgs {
/// The packages to remove from the cache.
package: Vec<PackageName>,
}
#[derive(Args)]
struct PipNamespace {
#[clap(subcommand)]
@ -1794,6 +1802,9 @@ async fn run() -> Result<ExitStatus> {
)
.await
}
Commands::Self_(SelfNamespace {
command: SelfCommand::Update,
}) => commands::self_update(printer).await,
Commands::Version { output_format } => {
commands::version(output_format, &mut stdout())?;
Ok(ExitStatus::Success)