From a6579b02253fba808fd9211f2173efa3793341c8 Mon Sep 17 00:00:00 2001 From: Luca Palmieri <20745048+LukeMathWalker@users.noreply.github.com> Date: Wed, 17 Jan 2024 11:05:51 +0100 Subject: [PATCH] Version management #2: self-updating and uninstalling (#167) Another piece of the version management story: the `pavex` CLI can now install a more recent version of itself. For completeness, we also provide an uninstall command that removes the binary as well as all the transient data that Pavex builds when compiling projects. --- .../cookbook/project/Cargo.lock | 10 +- .../core_concepts/project/Cargo.lock | 10 +- .../buffered_body/project/Cargo.lock | 10 +- .../request_data/json/project/Cargo.lock | 10 +- .../request_data/path/project/Cargo.lock | 10 +- .../request_data/query/project/Cargo.lock | 10 +- .../query_params/project/Cargo.lock | 10 +- .../request_target/project/Cargo.lock | 10 +- .../route_params/project/Cargo.lock | 10 +- libs/Cargo.lock | 25 ++- libs/pavex/tests/server.rs | 2 + libs/pavex_cli/Cargo.toml | 1 + libs/pavex_cli/src/confirmation.rs | 32 ++++ libs/pavex_cli/src/env.rs | 5 +- libs/pavex_cli/src/lib.rs | 6 +- libs/pavex_cli/src/main.rs | 148 +++++++++++++++--- libs/pavex_cli/src/pavexc/install.rs | 4 +- libs/pavex_cli/src/pavexc/mod.rs | 1 - libs/pavex_cli/src/{pavexc => }/prebuilt.rs | 36 +++-- libs/pavex_cli/src/state.rs | 4 +- libs/pavex_cli/src/utils.rs | 20 +++ libs/pavex_cli/src/version.rs | 24 +++ 22 files changed, 314 insertions(+), 84 deletions(-) create mode 100644 libs/pavex_cli/src/confirmation.rs rename libs/pavex_cli/src/{pavexc => }/prebuilt.rs (81%) create mode 100644 libs/pavex_cli/src/utils.rs create mode 100644 libs/pavex_cli/src/version.rs diff --git a/doc_examples/guide/dependency_injection/cookbook/project/Cargo.lock b/doc_examples/guide/dependency_injection/cookbook/project/Cargo.lock index 2b3085e06..e3c19626b 100644 --- a/doc_examples/guide/dependency_injection/cookbook/project/Cargo.lock +++ b/doc_examples/guide/dependency_injection/cookbook/project/Cargo.lock @@ -387,7 +387,7 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pavex" -version = "0.1.2" +version = "0.1.5" dependencies = [ "anyhow", "bytes", @@ -422,7 +422,7 @@ dependencies = [ [[package]] name = "pavex_bp_schema" -version = "0.1.2" +version = "0.1.5" dependencies = [ "pavex_reflection", "serde", @@ -430,7 +430,7 @@ dependencies = [ [[package]] name = "pavex_cli_client" -version = "0.1.2" +version = "0.1.5" dependencies = [ "anyhow", "pavex", @@ -439,7 +439,7 @@ dependencies = [ [[package]] name = "pavex_macros" -version = "0.1.2" +version = "0.1.5" dependencies = [ "proc-macro2", "quote", @@ -448,7 +448,7 @@ dependencies = [ [[package]] name = "pavex_reflection" -version = "0.1.2" +version = "0.1.5" dependencies = [ "serde", ] diff --git a/doc_examples/guide/middleware/core_concepts/project/Cargo.lock b/doc_examples/guide/middleware/core_concepts/project/Cargo.lock index 697b96374..3b3b9fc08 100644 --- a/doc_examples/guide/middleware/core_concepts/project/Cargo.lock +++ b/doc_examples/guide/middleware/core_concepts/project/Cargo.lock @@ -382,7 +382,7 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pavex" -version = "0.1.2" +version = "0.1.5" dependencies = [ "anyhow", "bytes", @@ -417,7 +417,7 @@ dependencies = [ [[package]] name = "pavex_bp_schema" -version = "0.1.2" +version = "0.1.5" dependencies = [ "pavex_reflection", "serde", @@ -425,7 +425,7 @@ dependencies = [ [[package]] name = "pavex_cli_client" -version = "0.1.2" +version = "0.1.5" dependencies = [ "anyhow", "pavex", @@ -434,7 +434,7 @@ dependencies = [ [[package]] name = "pavex_macros" -version = "0.1.2" +version = "0.1.5" dependencies = [ "proc-macro2", "quote", @@ -443,7 +443,7 @@ dependencies = [ [[package]] name = "pavex_reflection" -version = "0.1.2" +version = "0.1.5" dependencies = [ "serde", ] diff --git a/doc_examples/guide/request_data/buffered_body/project/Cargo.lock b/doc_examples/guide/request_data/buffered_body/project/Cargo.lock index c21ee584b..7f7e7fa75 100644 --- a/doc_examples/guide/request_data/buffered_body/project/Cargo.lock +++ b/doc_examples/guide/request_data/buffered_body/project/Cargo.lock @@ -388,7 +388,7 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pavex" -version = "0.1.2" +version = "0.1.5" dependencies = [ "anyhow", "bytes", @@ -423,7 +423,7 @@ dependencies = [ [[package]] name = "pavex_bp_schema" -version = "0.1.2" +version = "0.1.5" dependencies = [ "pavex_reflection", "serde", @@ -431,7 +431,7 @@ dependencies = [ [[package]] name = "pavex_cli_client" -version = "0.1.2" +version = "0.1.5" dependencies = [ "anyhow", "pavex", @@ -440,7 +440,7 @@ dependencies = [ [[package]] name = "pavex_macros" -version = "0.1.2" +version = "0.1.5" dependencies = [ "proc-macro2", "quote", @@ -449,7 +449,7 @@ dependencies = [ [[package]] name = "pavex_reflection" -version = "0.1.2" +version = "0.1.5" dependencies = [ "serde", ] diff --git a/doc_examples/guide/request_data/json/project/Cargo.lock b/doc_examples/guide/request_data/json/project/Cargo.lock index a40f478e8..740c9b234 100644 --- a/doc_examples/guide/request_data/json/project/Cargo.lock +++ b/doc_examples/guide/request_data/json/project/Cargo.lock @@ -388,7 +388,7 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pavex" -version = "0.1.2" +version = "0.1.5" dependencies = [ "anyhow", "bytes", @@ -423,7 +423,7 @@ dependencies = [ [[package]] name = "pavex_bp_schema" -version = "0.1.2" +version = "0.1.5" dependencies = [ "pavex_reflection", "serde", @@ -431,7 +431,7 @@ dependencies = [ [[package]] name = "pavex_cli_client" -version = "0.1.2" +version = "0.1.5" dependencies = [ "anyhow", "pavex", @@ -440,7 +440,7 @@ dependencies = [ [[package]] name = "pavex_macros" -version = "0.1.2" +version = "0.1.5" dependencies = [ "proc-macro2", "quote", @@ -449,7 +449,7 @@ dependencies = [ [[package]] name = "pavex_reflection" -version = "0.1.2" +version = "0.1.5" dependencies = [ "serde", ] diff --git a/doc_examples/guide/request_data/path/project/Cargo.lock b/doc_examples/guide/request_data/path/project/Cargo.lock index 350e13dff..138ab996a 100644 --- a/doc_examples/guide/request_data/path/project/Cargo.lock +++ b/doc_examples/guide/request_data/path/project/Cargo.lock @@ -388,7 +388,7 @@ dependencies = [ [[package]] name = "pavex" -version = "0.1.2" +version = "0.1.5" dependencies = [ "anyhow", "bytes", @@ -423,7 +423,7 @@ dependencies = [ [[package]] name = "pavex_bp_schema" -version = "0.1.2" +version = "0.1.5" dependencies = [ "pavex_reflection", "serde", @@ -431,7 +431,7 @@ dependencies = [ [[package]] name = "pavex_cli_client" -version = "0.1.2" +version = "0.1.5" dependencies = [ "anyhow", "pavex", @@ -440,7 +440,7 @@ dependencies = [ [[package]] name = "pavex_macros" -version = "0.1.2" +version = "0.1.5" dependencies = [ "proc-macro2", "quote", @@ -449,7 +449,7 @@ dependencies = [ [[package]] name = "pavex_reflection" -version = "0.1.2" +version = "0.1.5" dependencies = [ "serde", ] diff --git a/doc_examples/guide/request_data/query/project/Cargo.lock b/doc_examples/guide/request_data/query/project/Cargo.lock index 8d8d9d4a5..26bff6cf5 100644 --- a/doc_examples/guide/request_data/query/project/Cargo.lock +++ b/doc_examples/guide/request_data/query/project/Cargo.lock @@ -366,7 +366,7 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pavex" -version = "0.1.2" +version = "0.1.5" dependencies = [ "anyhow", "bytes", @@ -401,7 +401,7 @@ dependencies = [ [[package]] name = "pavex_bp_schema" -version = "0.1.2" +version = "0.1.5" dependencies = [ "pavex_reflection", "serde", @@ -409,7 +409,7 @@ dependencies = [ [[package]] name = "pavex_cli_client" -version = "0.1.2" +version = "0.1.5" dependencies = [ "anyhow", "pavex", @@ -418,7 +418,7 @@ dependencies = [ [[package]] name = "pavex_macros" -version = "0.1.2" +version = "0.1.5" dependencies = [ "proc-macro2", "quote", @@ -427,7 +427,7 @@ dependencies = [ [[package]] name = "pavex_reflection" -version = "0.1.2" +version = "0.1.5" dependencies = [ "serde", ] diff --git a/doc_examples/guide/request_data/query_params/project/Cargo.lock b/doc_examples/guide/request_data/query_params/project/Cargo.lock index 35822a37c..4e023a8dc 100644 --- a/doc_examples/guide/request_data/query_params/project/Cargo.lock +++ b/doc_examples/guide/request_data/query_params/project/Cargo.lock @@ -366,7 +366,7 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pavex" -version = "0.1.2" +version = "0.1.5" dependencies = [ "anyhow", "bytes", @@ -401,7 +401,7 @@ dependencies = [ [[package]] name = "pavex_bp_schema" -version = "0.1.2" +version = "0.1.5" dependencies = [ "pavex_reflection", "serde", @@ -409,7 +409,7 @@ dependencies = [ [[package]] name = "pavex_cli_client" -version = "0.1.2" +version = "0.1.5" dependencies = [ "anyhow", "pavex", @@ -418,7 +418,7 @@ dependencies = [ [[package]] name = "pavex_macros" -version = "0.1.2" +version = "0.1.5" dependencies = [ "proc-macro2", "quote", @@ -427,7 +427,7 @@ dependencies = [ [[package]] name = "pavex_reflection" -version = "0.1.2" +version = "0.1.5" dependencies = [ "serde", ] diff --git a/doc_examples/guide/request_data/request_target/project/Cargo.lock b/doc_examples/guide/request_data/request_target/project/Cargo.lock index 3ea97c5d6..d88c5117b 100644 --- a/doc_examples/guide/request_data/request_target/project/Cargo.lock +++ b/doc_examples/guide/request_data/request_target/project/Cargo.lock @@ -366,7 +366,7 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pavex" -version = "0.1.2" +version = "0.1.5" dependencies = [ "anyhow", "bytes", @@ -401,7 +401,7 @@ dependencies = [ [[package]] name = "pavex_bp_schema" -version = "0.1.2" +version = "0.1.5" dependencies = [ "pavex_reflection", "serde", @@ -409,7 +409,7 @@ dependencies = [ [[package]] name = "pavex_cli_client" -version = "0.1.2" +version = "0.1.5" dependencies = [ "anyhow", "pavex", @@ -418,7 +418,7 @@ dependencies = [ [[package]] name = "pavex_macros" -version = "0.1.2" +version = "0.1.5" dependencies = [ "proc-macro2", "quote", @@ -427,7 +427,7 @@ dependencies = [ [[package]] name = "pavex_reflection" -version = "0.1.2" +version = "0.1.5" dependencies = [ "serde", ] diff --git a/doc_examples/guide/request_data/route_params/project/Cargo.lock b/doc_examples/guide/request_data/route_params/project/Cargo.lock index b0f570941..525e8e0b0 100644 --- a/doc_examples/guide/request_data/route_params/project/Cargo.lock +++ b/doc_examples/guide/request_data/route_params/project/Cargo.lock @@ -366,7 +366,7 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pavex" -version = "0.1.2" +version = "0.1.5" dependencies = [ "anyhow", "bytes", @@ -401,7 +401,7 @@ dependencies = [ [[package]] name = "pavex_bp_schema" -version = "0.1.2" +version = "0.1.5" dependencies = [ "pavex_reflection", "serde", @@ -409,7 +409,7 @@ dependencies = [ [[package]] name = "pavex_cli_client" -version = "0.1.2" +version = "0.1.5" dependencies = [ "anyhow", "pavex", @@ -418,7 +418,7 @@ dependencies = [ [[package]] name = "pavex_macros" -version = "0.1.2" +version = "0.1.5" dependencies = [ "proc-macro2", "quote", @@ -427,7 +427,7 @@ dependencies = [ [[package]] name = "pavex_reflection" -version = "0.1.2" +version = "0.1.5" dependencies = [ "serde", ] diff --git a/libs/Cargo.lock b/libs/Cargo.lock index cb5de0e3d..ce02eafb5 100644 --- a/libs/Cargo.lock +++ b/libs/Cargo.lock @@ -813,6 +813,15 @@ dependencies = [ "serde", ] +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "fastrand" version = "2.0.1" @@ -1200,7 +1209,7 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de6225e2de30b6e9bca2d9f1cc4731640fcef0fb3cabddceee366e7e85d3e94f" dependencies = [ - "fastrand", + "fastrand 2.0.1", ] [[package]] @@ -2362,6 +2371,7 @@ dependencies = [ "pavexc", "pavexc_cli_client", "remove_dir_all", + "self-replace", "semver", "serde", "serde_json", @@ -3117,6 +3127,17 @@ dependencies = [ "libc", ] +[[package]] +name = "self-replace" +version = "1.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525db198616b2bcd0f245daf7bfd8130222f7ee6af9ff9984c19a61bf1160c55" +dependencies = [ + "fastrand 1.9.0", + "tempfile", + "windows-sys 0.48.0", +] + [[package]] name = "semver" version = "1.0.21" @@ -3441,7 +3462,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", - "fastrand", + "fastrand 2.0.1", "redox_syscall 0.4.1", "rustix", "windows-sys 0.48.0", diff --git a/libs/pavex/tests/server.rs b/libs/pavex/tests/server.rs index f8f0b06e9..17c9013cc 100644 --- a/libs/pavex/tests/server.rs +++ b/libs/pavex/tests/server.rs @@ -112,6 +112,7 @@ impl SlowHandlerState { } #[tokio::test] +#[cfg_attr(target_os = "windows", ignore = "Flaky on Windows")] async fn graceful() { let (incoming, addr) = test_incoming().await; let delay = Duration::from_millis(100); @@ -182,6 +183,7 @@ async fn forced() { } #[tokio::test] +#[cfg_attr(target_os = "windows", ignore = "Flaky on Windows")] async fn graceful_but_too_fast() { let (incoming, addr) = test_incoming().await; let delay = Duration::from_millis(200); diff --git a/libs/pavex_cli/Cargo.toml b/libs/pavex_cli/Cargo.toml index 362304eef..770ad4167 100644 --- a/libs/pavex_cli/Cargo.toml +++ b/libs/pavex_cli/Cargo.toml @@ -44,6 +44,7 @@ serde = { version = "1", features = ["derive"] } toml = "0.8.8" semver = { version = "1.0.21", features = ["serde"] } serde_json = "1.0.111" +self-replace = "1.3.7" [dev-dependencies] pavex_test_runner = { path = "../pavex_test_runner" } diff --git a/libs/pavex_cli/src/confirmation.rs b/libs/pavex_cli/src/confirmation.rs new file mode 100644 index 000000000..ca4fee0b5 --- /dev/null +++ b/libs/pavex_cli/src/confirmation.rs @@ -0,0 +1,32 @@ +use anyhow::{anyhow, Context}; +use std::io::{BufRead, Write}; + +/// Prompt the user for confirmation. +pub fn confirm(question: &str, default: bool) -> Result { + write!(std::io::stdout().lock(), "{question} ")?; + let _ = std::io::stdout().flush(); + let input = read_line()?; + + let r = match &*input.to_lowercase() { + "y" | "yes" => true, + "n" | "no" => false, + "" => default, + _ => false, + }; + + writeln!(std::io::stdout().lock())?; + + Ok(r) +} + +fn read_line() -> Result { + let stdin = std::io::stdin(); + let stdin = stdin.lock(); + let mut lines = stdin.lines(); + let lines = lines.next().transpose()?; + match lines { + None => Err(anyhow!("No lines found from stdin")), + Some(v) => Ok(v), + } + .context("Unable to read from stdin for confirmation") +} diff --git a/libs/pavex_cli/src/env.rs b/libs/pavex_cli/src/env.rs index 4ecd27e3a..e2948a1c1 100644 --- a/libs/pavex_cli/src/env.rs +++ b/libs/pavex_cli/src/env.rs @@ -28,6 +28,7 @@ pub const fn commit_sha() -> &'static str { } /// The version of `pavex_cli` that is being used. -pub const fn version() -> &'static str { - env!("CARGO_PKG_VERSION") +pub fn version() -> semver::Version { + semver::Version::parse(env!("CARGO_PKG_VERSION")) + .expect("CLI version is not valid according to SemVer") } diff --git a/libs/pavex_cli/src/lib.rs b/libs/pavex_cli/src/lib.rs index 0c7a58a29..6452d8b4a 100644 --- a/libs/pavex_cli/src/lib.rs +++ b/libs/pavex_cli/src/lib.rs @@ -1,7 +1,11 @@ -mod cargo_install; +pub mod cargo_install; +pub mod confirmation; pub mod env; pub mod locator; pub mod package_graph; pub mod pavexc; +pub mod prebuilt; pub mod state; pub mod toolchain; +pub mod utils; +pub mod version; diff --git a/libs/pavex_cli/src/main.rs b/libs/pavex_cli/src/main.rs index b835756b6..5cb23332a 100644 --- a/libs/pavex_cli/src/main.rs +++ b/libs/pavex_cli/src/main.rs @@ -1,18 +1,25 @@ use anyhow::Context; use cargo_like_utils::shell::Shell; use std::fmt::{Display, Formatter}; -use std::path::PathBuf; +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; use std::process::ExitCode; use std::str::FromStr; use clap::{Parser, Subcommand}; +use pavex_cli::cargo_install::{cargo_install, GitSourceRevision, Source}; +use pavex_cli::confirmation::confirm; use pavex_cli::locator::PavexLocator; use pavex_cli::package_graph::compute_package_graph; use pavex_cli::pavexc::{get_or_install_from_graph, get_or_install_from_version}; +use pavex_cli::prebuilt::{download_prebuilt, PrebuiltBinaryKind}; use pavex_cli::state::State; +use pavex_cli::utils; +use pavex_cli::version::latest_released_version; use pavexc_cli_client::commands::generate::{BlueprintArgument, GenerateError}; use pavexc_cli_client::commands::new::NewError; use pavexc_cli_client::Client; +use semver::Version; use tracing_chrome::{ChromeLayerBuilder, FlushGuard}; use tracing_subscriber::fmt::format::FmtSpan; use tracing_subscriber::layer::SubscriberExt; @@ -70,28 +77,48 @@ impl FromStr for Color { #[derive(Subcommand)] enum Commands { - /// Generate application runtime code according to an application blueprint. + /// Generate the server SDK code for an application blueprint. Generate { /// The source path for the serialized application blueprint. #[clap(short, long, value_parser)] blueprint: PathBuf, - /// Optional. If provided, pavex will serialize diagnostic information about + /// Optional. + /// If provided, Pavex will serialize diagnostic information about /// the application to the specified path. #[clap(long, value_parser)] diagnostics: Option, - /// The path to the directory that will contain the manifest and the source code for the generated application crate. - /// If the provided path is relative, it is interpreted as relative to the root of the current workspace. + /// The directory that will contain the newly generated server SDK crate. + /// If the directory path is relative, + /// it is interpreted as relative to the root of the current workspace. #[clap(short, long, value_parser)] output: PathBuf, }, /// Scaffold a new Pavex project at . New { - /// The path of the new directory that will contain the project files. + /// The directory that will contain the project files. /// /// If any of the intermediate directories in the path don't exist, they'll be created. #[arg(index = 1)] path: PathBuf, }, + /// Modify the installation of the Pavex CLI. + #[command(name = "self")] + Self_ { + #[clap(subcommand)] + command: SelfCommands, + }, +} + +#[derive(Subcommand)] +enum SelfCommands { + /// Download and install a newer version of Pavex CLI, if available. + Update, + /// Uninstall Pavex CLI and remove all its dependencies and artifacts. + Uninstall { + /// Don't ask for confirmation before uninstalling Pavex CLI. + #[clap(short, long, value_parser)] + y: bool, + }, } fn init_telemetry() -> FlushGuard { @@ -168,11 +195,15 @@ fn main() -> Result { output, } => generate(&mut shell, client, &locator, blueprint, diagnostics, output), Commands::New { path } => scaffold_project(client, &locator, &mut shell, path), + Commands::Self_ { command } => match command { + SelfCommands::Update => update(&mut shell), + SelfCommands::Uninstall { y } => uninstall(&mut shell, !y, locator), + }, } - .map_err(anyhow2miette) + .map_err(utils::anyhow2miette) } -#[tracing::instrument("Generate server sdk", skip(client, locator))] +#[tracing::instrument("Generate server sdk", skip(client, locator, shell))] fn generate( shell: &mut Shell, mut client: Client, @@ -206,7 +237,7 @@ fn generate( } } -#[tracing::instrument("Scaffold new project", skip(client, locator))] +#[tracing::instrument("Scaffold new project", skip(client, locator, shell))] fn scaffold_project( mut client: Client, locator: &PavexLocator, @@ -232,21 +263,100 @@ fn scaffold_project( } } -fn anyhow2miette(err: anyhow::Error) -> miette::Error { - #[derive(Debug, miette::Diagnostic)] - struct InteropError(anyhow::Error); +#[tracing::instrument("Uninstall Pavex CLI", skip(shell, locator))] +fn uninstall( + shell: &mut Shell, + must_prompt_user: bool, + locator: PavexLocator, +) -> Result { + shell.status("Thanks", "for hacking with Pavex!")?; + if must_prompt_user { + shell.warn( + "This process will uninstall Pavex and all its associated data from your system.", + )?; + let continue_ = confirm("\nDo you wish to continue? (y/N)", false)?; + if !continue_ { + shell.status("Abort", "Uninstalling Pavex CLI")?; + return Ok(ExitCode::SUCCESS); + } + } - impl Display for InteropError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) + shell.status("Uninstalling", "Pavex")?; + if let Err(e) = fs_err::remove_dir_all(locator.root_dir()) { + if ErrorKind::NotFound != e.kind() { + Err(e).context("Failed to remove Pavex data")?; } } + self_replace::self_delete().context("Failed to delete the current Pavex CLI binary")?; + shell.status("Uninstalled", "Pavex")?; - impl std::error::Error for InteropError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - self.0.source() + Ok(ExitCode::SUCCESS) +} + +#[tracing::instrument("Update Pavex CLI", skip(shell))] +fn update(shell: &mut Shell) -> Result { + shell.status("Checking", "for updates to Pavex CLI")?; + let latest_version = latest_released_version()?; + let current_version = pavex_cli::env::version(); + if latest_version <= current_version { + shell.status( + "Up to date", + format!("{current_version} is the most recent version"), + )?; + return Ok(ExitCode::SUCCESS); + } + + shell.status( + "Update available", + format!("You're running {current_version}, but {latest_version} is available"), + )?; + + let new_cli_path = tempfile::NamedTempFile::new() + .context("Failed to create a temporary file to download the new Pavex CLI binary")?; + download_or_compile( + shell, + PrebuiltBinaryKind::Pavex, + &latest_version, + new_cli_path.path(), + )?; + self_replace::self_replace(new_cli_path.path()) + .context("Failed to replace the current Pavex CLI with the newly downloaded version")?; + + Ok(ExitCode::SUCCESS) +} + +fn download_or_compile( + shell: &mut Shell, + kind: PrebuiltBinaryKind, + version: &Version, + destination: &Path, +) -> Result<(), anyhow::Error> { + let _ = shell.status("Downloading", format!("prebuilt `{kind}@{version}` binary")); + match download_prebuilt(destination, kind, version) { + Ok(_) => { + let _ = shell.status("Downloaded", format!("prebuilt `{kind}@{version}` binary")); + return Ok(()); + } + Err(e) => { + let _ = shell.warn("Download failed: {e}.\nI'll try compiling from source instead."); + tracing::warn!( + error.msg = %e, + error.cause = ?e, + "Failed to download prebuilt `{kind}` binary. I'll try to build it from source instead.", + ); } } - miette::Error::from(InteropError(err)) + let _ = shell.status("Compiling", format!("`{kind}@{version}` from source")); + cargo_install( + Source::Git { + url: "https://github.com/LukeMathWalker/pavex".into(), + rev: GitSourceRevision::Tag(version.to_string()), + }, + &kind.to_string(), + &format!("{kind}_cli"), + destination, + )?; + let _ = shell.status("Compiled", format!("`{kind}@{version}` from source")); + Ok(()) } diff --git a/libs/pavex_cli/src/pavexc/install.rs b/libs/pavex_cli/src/pavexc/install.rs index e937d0a07..d0eff6e1f 100644 --- a/libs/pavex_cli/src/pavexc/install.rs +++ b/libs/pavex_cli/src/pavexc/install.rs @@ -1,5 +1,5 @@ use crate::cargo_install::{cargo_install, GitSourceRevision, Source}; -use crate::pavexc::prebuilt::download_prebuilt; +use crate::prebuilt::{download_prebuilt, PrebuiltBinaryKind}; use cargo_like_utils::shell::Shell; use guppy::graph::PackageSource; use guppy::Version; @@ -159,7 +159,7 @@ pub(super) fn install( if try_prebuilt { let _ = shell.status("Downloading", format!("prebuilt `pavexc@{version}` binary")); - match download_prebuilt(pavexc_cli_path, version) { + match download_prebuilt(pavexc_cli_path, PrebuiltBinaryKind::Pavexc, version) { Ok(_) => { let _ = shell.status("Downloaded", format!("prebuilt `pavexc@{version}` binary")); return Ok(()); diff --git a/libs/pavex_cli/src/pavexc/mod.rs b/libs/pavex_cli/src/pavexc/mod.rs index 4d269110b..fa6ded86b 100644 --- a/libs/pavex_cli/src/pavexc/mod.rs +++ b/libs/pavex_cli/src/pavexc/mod.rs @@ -9,7 +9,6 @@ use std::path::{Path, PathBuf}; mod install; mod location; -mod prebuilt; mod version; static PAVEX_GITHUB_URL: &str = "https://github.com/LukeMathWalker/pavex"; diff --git a/libs/pavex_cli/src/pavexc/prebuilt.rs b/libs/pavex_cli/src/prebuilt.rs similarity index 81% rename from libs/pavex_cli/src/pavexc/prebuilt.rs rename to libs/pavex_cli/src/prebuilt.rs index e06d438bb..0337b046f 100644 --- a/libs/pavex_cli/src/pavexc/prebuilt.rs +++ b/libs/pavex_cli/src/prebuilt.rs @@ -3,15 +3,30 @@ use guppy::Version; use std::io::Read; use std::path::Path; -/// Given the version and source for the `pavex` library crate, try to download -/// a prebuilt `pavexc` binary (if it exists). -pub(super) fn download_prebuilt( - expected_pavexc_cli_path: &Path, +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PrebuiltBinaryKind { + Pavex, + Pavexc, +} + +impl std::fmt::Display for PrebuiltBinaryKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PrebuiltBinaryKind::Pavex => write!(f, "pavex"), + PrebuiltBinaryKind::Pavexc => write!(f, "pavexc"), + } + } +} + +/// Try downloading a prebuilt binary for the current host triple to the specified destination path. +pub fn download_prebuilt( + destination: &Path, + binary_kind: PrebuiltBinaryKind, version: &Version, ) -> Result<(), DownloadPrebuiltError> { let host_triple = get_host_triple()?; let url_prefix = - format!("https://github.com/LukeMathWalker/pavex/releases/download/{version}/pavexc_cli-{host_triple}"); + format!("https://github.com/LukeMathWalker/pavex/releases/download/{version}/{binary_kind}_cli-{host_triple}"); let download_url = match host_triple.as_str() { "x86_64-unknown-linux-gnu" | "x86_64-apple-darwin" | "aarch64-apple-darwin" => { format!("{url_prefix}.tar.xz") @@ -42,7 +57,7 @@ pub(super) fn download_prebuilt( .read_to_end(&mut bytes) .context(err_msg)?; - extract_binary(&download_url, bytes, expected_pavexc_cli_path) + extract_binary(&download_url, binary_kind, bytes, destination) .context("Failed to unpack prebuilt binary")?; Ok(()) @@ -52,14 +67,17 @@ pub(super) fn download_prebuilt( /// destination path. fn extract_binary( source_url: &str, + prebuilt_binary_kind: PrebuiltBinaryKind, bytes: Vec, destination: &Path, ) -> Result<(), anyhow::Error> { let expected_filename = destination .file_name() - .expect("pavexc's destination has no filename") + .with_context(|| format!("{prebuilt_binary_kind}'s destination has no filename"))? .to_str() - .expect("pavexc's destination filename is not valid UTF-8"); + .with_context(|| { + format!("{prebuilt_binary_kind}'s destination filename is not valid UTF-8") + })?; if source_url.ends_with(".zip") { let mut archive = zip::ZipArchive::new(std::io::Cursor::new(bytes))?; for i in 0..archive.len() { @@ -104,7 +122,7 @@ fn extract_binary( unimplemented!() } Err(anyhow::anyhow!( - "Failed to find the `pavexc` binary in the downloaded archive" + "Failed to find the `{prebuilt_binary_kind}` binary in the downloaded archive" )) } diff --git a/libs/pavex_cli/src/state.rs b/libs/pavex_cli/src/state.rs index 966791a6a..ce94ca2b2 100644 --- a/libs/pavex_cli/src/state.rs +++ b/libs/pavex_cli/src/state.rs @@ -38,9 +38,7 @@ impl State { Some(current_state) => Ok(current_state.toolchain), None => { // We default to the toolchain that matches the current version of the CLI. - let cli_version = semver::Version::parse(version()) - .context("Failed to parse the current version of the CLI.")?; - Ok(cli_version) + Ok(version()) } } } diff --git a/libs/pavex_cli/src/utils.rs b/libs/pavex_cli/src/utils.rs new file mode 100644 index 000000000..c4df16f8d --- /dev/null +++ b/libs/pavex_cli/src/utils.rs @@ -0,0 +1,20 @@ +use std::fmt::{Display, Formatter}; + +pub fn anyhow2miette(err: anyhow::Error) -> miette::Error { + #[derive(Debug, miette::Diagnostic)] + struct InteropError(anyhow::Error); + + impl Display for InteropError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } + } + + impl std::error::Error for InteropError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.0.source() + } + } + + miette::Error::from(InteropError(err)) +} diff --git a/libs/pavex_cli/src/version.rs b/libs/pavex_cli/src/version.rs new file mode 100644 index 000000000..3e638bcc3 --- /dev/null +++ b/libs/pavex_cli/src/version.rs @@ -0,0 +1,24 @@ +use anyhow::Context; +use semver::Version; + +/// Query GitHub's API to get the latest released version of Pavex. +pub fn latest_released_version() -> Result { + #[derive(serde::Deserialize)] + struct Response { + tag_name: String, + } + + let response = ureq::get("https://api.github.com/repos/LukeMathWalker/pavex/releases/latest") + .call() + .context("Failed to query GitHub's API for the latest release")?; + if response.status() < 200 || response.status() >= 300 { + anyhow::bail!( + "Failed to query GitHub's API for the latest release. It returned an error status code ({})", + response.status() + ); + } + let response: Response = response.into_json()?; + let version = Version::parse(&response.tag_name) + .context("Failed to parse the version returned by GitHub's API for the latest release")?; + Ok(version) +}