From 1bf48c91f2f55b81eb44a45e4e886ad9ce144943 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 19 Mar 2024 16:02:49 -0400 Subject: [PATCH] Add a `uv self update` command (#2228) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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](https://github.com/astral-sh/uv/assets/1309177/04bb7a11-6557-4317-8e86-18288fbc13c6) Updated: ![Screenshot 2024-03-06 at 12 13 54 AM](https://github.com/astral-sh/uv/assets/1309177/c08ad739-5a2b-47cf-bf13-018a8d708330) No receipt: ![Screenshot 2024-03-06 at 12 14 13 AM](https://github.com/astral-sh/uv/assets/1309177/317bbfaf-a787-4cbf-9f93-a4ce8ca7a988) --- Cargo.lock | 198 +++++++++++++++++++++++++- Cargo.toml | 5 +- crates/uv/Cargo.toml | 1 + crates/uv/src/commands/mod.rs | 2 + crates/uv/src/commands/self_update.rs | 114 +++++++++++++++ crates/uv/src/main.rs | 25 +++- 6 files changed, 331 insertions(+), 14 deletions(-) create mode 100644 crates/uv/src/commands/self_update.rs diff --git a/Cargo.lock b/Cargo.lock index 95f62a2db..e69064024 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 8fc7293c6..55d19ee62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 08c99a53b..166ceb57d 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -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"] } diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs index 2ff170a8f..cbf0f0e3c 100644 --- a/crates/uv/src/commands/mod.rs +++ b/crates/uv/src/commands/mod.rs @@ -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; diff --git a/crates/uv/src/commands/self_update.rs b/crates/uv/src/commands/self_update.rs new file mode 100644 index 000000000..8cea63eec --- /dev/null +++ b/crates/uv/src/commands/self_update.rs @@ -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 { + 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) +} diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 306eb48b3..ee99de892 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -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, } -#[derive(Args)] -#[allow(clippy::struct_excessive_bools)] -struct DirArgs { - /// The packages to remove from the cache. - package: Vec, -} - #[derive(Args)] struct PipNamespace { #[clap(subcommand)] @@ -1794,6 +1802,9 @@ async fn run() -> Result { ) .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)