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

198
Cargo.lock generated
View file

@ -275,6 +275,52 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "axoasset"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dce2f189800bafe8322ef3a4d361ee7373bfc2f8fe052afda404230166dc45f"
dependencies = [
"camino",
"image",
"miette 7.2.0",
"mime",
"serde",
"serde_json",
"thiserror",
"url",
"walkdir",
]
[[package]]
name = "axoprocess"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4de46920588aef95658797996130bacd542436aee090084646521260a74bda7d"
dependencies = [
"miette 7.2.0",
"thiserror",
"tracing",
]
[[package]]
name = "axoupdater"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b3130c1f3911eecdb1caf0412160c62758e314b644377796eb64917539ba8c"
dependencies = [
"axoasset",
"axoprocess",
"camino",
"homedir",
"miette 7.2.0",
"reqwest",
"serde",
"temp-dir",
"thiserror",
"tokio",
]
[[package]]
name = "backoff"
version = "0.4.0"
@ -469,6 +515,15 @@ dependencies = [
"tempfile",
]
[[package]]
name = "camino"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c"
dependencies = [
"serde",
]
[[package]]
name = "cargo-util"
version = "0.2.9"
@ -838,7 +893,7 @@ version = "3.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345"
dependencies = [
"nix",
"nix 0.28.0",
"windows-sys 0.52.0",
]
@ -1469,6 +1524,20 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "homedir"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22074da8bba2ef26fc1737ae6c777b5baab5524c2dc403b5c6a76166766ccda5"
dependencies = [
"cfg-if",
"nix 0.26.4",
"serde",
"widestring",
"windows-sys 0.48.0",
"wmi",
]
[[package]]
name = "html-escape"
version = "0.2.13"
@ -1676,6 +1745,18 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "image"
version = "0.24.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"num-traits",
]
[[package]]
name = "imagesize"
version = "0.11.0"
@ -2046,6 +2127,15 @@ dependencies = [
"libc",
]
[[package]]
name = "memoffset"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
dependencies = [
"autocfg",
]
[[package]]
name = "memoffset"
version = "0.9.0"
@ -2063,7 +2153,7 @@ checksum = "337e1043bbc086dac9d9674983bef52ac991ce150e09b5b8e35c5a73dd83f66c"
dependencies = [
"backtrace",
"backtrace-ext",
"miette-derive",
"miette-derive 6.0.1",
"owo-colors 3.5.0",
"supports-color",
"supports-hyperlinks",
@ -2074,6 +2164,18 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "miette"
version = "7.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1"
dependencies = [
"cfg-if",
"miette-derive 7.2.0",
"thiserror",
"unicode-width",
]
[[package]]
name = "miette-derive"
version = "6.0.1"
@ -2085,6 +2187,17 @@ dependencies = [
"syn 2.0.52",
]
[[package]]
name = "miette-derive"
version = "7.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
]
[[package]]
name = "mimalloc"
version = "0.1.39"
@ -2149,6 +2262,19 @@ dependencies = [
"rand",
]
[[package]]
name = "nix"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
dependencies = [
"bitflags 1.3.2",
"cfg-if",
"libc",
"memoffset 0.7.1",
"pin-utils",
]
[[package]]
name = "nix"
version = "0.28.0"
@ -2637,7 +2763,7 @@ dependencies = [
"cfg-if",
"indoc",
"libc",
"memoffset",
"memoffset 0.9.0",
"parking_lot 0.12.1",
"portable-atomic",
"pyo3-build-config",
@ -2865,7 +2991,7 @@ checksum = "52b1349400e2ffd64a9fb5ed9008e33c0b8ef86bd5bae8f73080839c7082f1d5"
dependencies = [
"cfg-if",
"rustix",
"windows",
"windows 0.54.0",
]
[[package]]
@ -3665,6 +3791,12 @@ dependencies = [
"pin-utils",
]
[[package]]
name = "temp-dir"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd16aa9ffe15fe021c6ee3766772132c6e98dfa395a167e16864f61a9cfb71d6"
[[package]]
name = "tempfile"
version = "3.10.1"
@ -3894,6 +4026,7 @@ dependencies = [
"libc",
"mio",
"num_cpus",
"parking_lot 0.12.1",
"pin-project-lite",
"signal-hook-registry",
"socket2",
@ -4306,6 +4439,7 @@ dependencies = [
"anyhow",
"assert_cmd",
"assert_fs",
"axoupdater",
"base64 0.21.7",
"byteorder",
"chrono",
@ -4323,7 +4457,7 @@ dependencies = [
"insta",
"install-wheel-rs",
"itertools 0.12.1",
"miette",
"miette 6.0.1",
"mimalloc",
"owo-colors 4.0.0",
"pep508_rs",
@ -5026,6 +5160,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "widestring"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8"
[[package]]
name = "winapi"
version = "0.3.9"
@ -5057,6 +5197,18 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
dependencies = [
"windows-core 0.52.0",
"windows-implement",
"windows-interface",
"windows-targets 0.52.4",
]
[[package]]
name = "windows"
version = "0.54.0"
@ -5086,6 +5238,28 @@ dependencies = [
"windows-targets 0.52.4",
]
[[package]]
name = "windows-implement"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
]
[[package]]
name = "windows-interface"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
]
[[package]]
name = "windows-result"
version = "0.1.0"
@ -5270,6 +5444,20 @@ dependencies = [
"url",
]
[[package]]
name = "wmi"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f0a4062ca522aad4705a2948fd4061b3857537990202a8ddd5af21607f79a"
dependencies = [
"chrono",
"futures",
"log",
"serde",
"thiserror",
"windows 0.52.0",
]
[[package]]
name = "wyz"
version = "0.5.1"

View file

@ -19,12 +19,13 @@ license = "MIT OR Apache-2.0"
[workspace.dependencies]
anstream = { version = "0.6.13" }
anyhow = { version = "1.0.80" }
async-compression = { version = "0.4.6" }
async-channel = { version = "2.2.0" }
async-trait = { version = "0.1.78" }
async-compression = { version = "0.4.6" }
async-recursion = { version = "1.0.5" }
async-trait = { version = "0.1.78" }
async_http_range_reader = { version = "0.7.0" }
async_zip = { git = "https://github.com/charliermarsh/rs-async-zip", rev = "d76801da0943de985254fc6255c0e476b57c5836", features = ["deflate"] }
axoupdater = { version = "0.3.1", default-features = false }
backoff = { version = "0.4.0" }
base64 = { version = "0.21.7" }
cachedir = { version = "0.3.1" }

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)