From 24ad379b7cf42d96049e2a9afad24ec27f529cff Mon Sep 17 00:00:00 2001 From: daimond113 <72147841+daimond113@users.noreply.github.com> Date: Sat, 16 Nov 2024 13:46:05 +0100 Subject: [PATCH] feat: make self-upgrade check for updates by itself --- CHANGELOG.md | 3 + docs/src/content/docs/reference/cli.mdx | 3 + src/cli/commands/self_install.rs | 4 +- src/cli/commands/self_upgrade.rs | 53 +++++++- src/cli/version.rs | 167 ++++++++++++++++-------- src/main.rs | 6 +- 6 files changed, 170 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc1b478..0d2b566 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Correctly link Wally server packages by @daimond113 +### Changed +- `self-upgrade` now will check for updates by itself by default by @daimond113 + ## [0.5.0-rc.8] - 2024-11-12 ### Added - Add `--index` flag to `publish` command to publish to a specific index by @daimond113 diff --git a/docs/src/content/docs/reference/cli.mdx b/docs/src/content/docs/reference/cli.mdx index ac516c5..dc3de81 100644 --- a/docs/src/content/docs/reference/cli.mdx +++ b/docs/src/content/docs/reference/cli.mdx @@ -118,6 +118,9 @@ after downloading the pesde binary. Upgrades the pesde binary to the latest version. +- `--use-cached`: Whether to use the version displayed in the "upgrade available" + message instead of checking for the latest version. + ## `pesde patch` ```sh diff --git a/src/cli/commands/self_install.rs b/src/cli/commands/self_install.rs index 7b68a41..8f529bc 100644 --- a/src/cli/commands/self_install.rs +++ b/src/cli/commands/self_install.rs @@ -1,6 +1,8 @@ use crate::cli::{version::update_bin_exe, HOME_DIR}; +use anyhow::Context; use clap::Args; use colored::Colorize; +use std::env::current_exe; #[derive(Debug, Args)] pub struct SelfInstallCommand { /// Skip adding the bin directory to the PATH @@ -68,7 +70,7 @@ and then restart your shell. ); } - update_bin_exe().await?; + update_bin_exe(¤t_exe().context("failed to get current exe path")?).await?; Ok(()) } diff --git a/src/cli/commands/self_upgrade.rs b/src/cli/commands/self_upgrade.rs index 2644d01..21f9c46 100644 --- a/src/cli/commands/self_upgrade.rs +++ b/src/cli/commands/self_upgrade.rs @@ -1,17 +1,56 @@ -use crate::cli::{config::read_config, version::get_or_download_version}; +use crate::cli::{ + config::read_config, + version::{ + current_version, get_latest_remote_version, get_or_download_version, update_bin_exe, + }, +}; +use anyhow::Context; use clap::Args; +use colored::Colorize; #[derive(Debug, Args)] -pub struct SelfUpgradeCommand {} +pub struct SelfUpgradeCommand { + /// Whether to use the version from the "upgrades available" message + #[clap(long, default_value_t = false)] + use_cached: bool, +} impl SelfUpgradeCommand { pub async fn run(self, reqwest: reqwest::Client) -> anyhow::Result<()> { - let config = read_config().await?; + let latest_version = if self.use_cached { + read_config() + .await? + .last_checked_updates + .context("no cached version found")? + .1 + } else { + get_latest_remote_version(&reqwest).await? + }; - get_or_download_version(&reqwest, &config.last_checked_updates.unwrap().1).await?; - // a call to `update_bin_exe` or other similar function *should* be here, in case new versions - // have fixes to bugs in executing other versions, but that would cause - // the current file to be overwritten by itself, so this needs more thought + if latest_version <= current_version() { + println!("already up to date"); + return Ok(()); + } + + if !inquire::prompt_confirmation(format!( + "are you sure you want to upgrade {} from {} to {}?", + env!("CARGO_BIN_NAME").cyan(), + current_version().to_string().yellow().bold(), + latest_version.to_string().yellow().bold() + ))? { + println!("cancelled upgrade"); + return Ok(()); + } + + let path = get_or_download_version(&reqwest, &latest_version, true) + .await? + .unwrap(); + update_bin_exe(&path).await?; + + println!( + "upgraded to version {}!", + latest_version.to_string().yellow().bold() + ); Ok(()) } diff --git a/src/cli/version.rs b/src/cli/version.rs index 2633491..9224ecd 100644 --- a/src/cli/version.rs +++ b/src/cli/version.rs @@ -1,9 +1,4 @@ -use crate::cli::{ - bin_dir, - config::{read_config, write_config, CliConfig}, - files::make_executable, - home_dir, -}; +use crate::cli::{bin_dir, config::read_config, files::make_executable, home_dir}; use anyhow::Context; use colored::Colorize; use fs_err::tokio as fs; @@ -11,7 +6,10 @@ use futures::StreamExt; use reqwest::header::ACCEPT; use semver::Version; use serde::Deserialize; -use std::path::PathBuf; +use std::{ + env::current_exe, + path::{Path, PathBuf}, +}; use tokio::io::AsyncReadExt; pub fn current_version() -> Version { @@ -38,11 +36,32 @@ fn get_repo() -> (String, String) { ) } +pub async fn get_latest_remote_version(reqwest: &reqwest::Client) -> anyhow::Result { + let (owner, repo) = get_repo(); + + let releases = reqwest + .get(format!( + "https://api.github.com/repos/{owner}/{repo}/releases", + )) + .send() + .await + .context("failed to send request to GitHub API")? + .error_for_status() + .context("failed to get GitHub API response")? + .json::>() + .await + .context("failed to parse GitHub API response")?; + + releases + .into_iter() + .map(|release| Version::parse(release.tag_name.trim_start_matches('v')).unwrap()) + .max() + .context("failed to find latest version") +} + const CHECK_INTERVAL: chrono::Duration = chrono::Duration::hours(6); pub async fn check_for_updates(reqwest: &reqwest::Client) -> anyhow::Result<()> { - let (owner, repo) = get_repo(); - let config = read_config().await?; let version = if let Some((_, version)) = config @@ -51,52 +70,66 @@ pub async fn check_for_updates(reqwest: &reqwest::Client) -> anyhow::Result<()> { version } else { - let releases = reqwest - .get(format!( - "https://api.github.com/repos/{owner}/{repo}/releases", - )) - .send() - .await - .context("failed to send request to GitHub API")? - .error_for_status() - .context("failed to get GitHub API response")? - .json::>() - .await - .context("failed to parse GitHub API response")?; - - let version = releases - .into_iter() - .map(|release| Version::parse(release.tag_name.trim_start_matches('v')).unwrap()) - .max() - .context("failed to find latest version")?; - - write_config(&CliConfig { - last_checked_updates: Some((chrono::Utc::now(), version.clone())), - ..config - }) - .await?; - - version + get_latest_remote_version(reqwest).await? }; + let current_version = current_version(); - if version > current_version() { - let name = env!("CARGO_PKG_NAME"); + if version > current_version { + let name = env!("CARGO_BIN_NAME"); + let changelog = format!("{}/releases/tag/v{version}", env!("CARGO_PKG_REPOSITORY"),); - let unformatted_message = format!("a new version of {name} is available: {version}"); + let unformatted_messages = [ + "".to_string(), + format!("update available! {current_version} → {version}"), + format!("changelog: {changelog}"), + format!("run `{name} self-upgrade` to upgrade"), + "".to_string(), + ]; - let message = format!( - "a new version of {} is available: {}", - name.cyan(), - version.to_string().yellow().bold() - ); + let width = unformatted_messages + .iter() + .map(|s| s.chars().count()) + .max() + .unwrap() + + 4; - let stars = "-" - .repeat(unformatted_message.len() + 4) - .bright_magenta() - .bold(); - let column = "|".bright_magenta().bold(); + let column = "│".bright_magenta(); - println!("\n{stars}\n{column} {message} {column}\n{stars}\n"); + let message = [ + "".to_string(), + format!( + "update available! {} → {}", + current_version.to_string().red(), + version.to_string().green() + ), + format!("changelog: {}", changelog.blue()), + format!( + "run `{} {}` to upgrade", + name.blue(), + "self-upgrade".yellow() + ), + "".to_string(), + ] + .into_iter() + .enumerate() + .map(|(i, s)| { + let text_length = unformatted_messages[i].chars().count(); + let padding = (width as f32 - text_length as f32) / 2f32; + let padding_l = " ".repeat(padding.floor() as usize); + let padding_r = " ".repeat(padding.ceil() as usize); + format!("{column}{padding_l}{s}{padding_r}{column}") + }) + .collect::>() + .join("\n"); + + let lines = "─".repeat(width).bright_magenta(); + + let tl = "╭".bright_magenta(); + let tr = "╮".bright_magenta(); + let bl = "╰".bright_magenta(); + let br = "╯".bright_magenta(); + + println!("\n{tl}{lines}{tr}\n{message}\n{bl}{lines}{br}\n"); } Ok(()) @@ -169,6 +202,7 @@ pub async fn download_github_release( pub async fn get_or_download_version( reqwest: &reqwest::Client, version: &Version, + always_give_path: bool, ) -> anyhow::Result> { let path = home_dir()?.join("versions"); fs::create_dir_all(&path) @@ -177,7 +211,7 @@ pub async fn get_or_download_version( let path = path.join(format!("{version}{}", std::env::consts::EXE_SUFFIX)); - let is_requested_version = *version == current_version(); + let is_requested_version = !always_give_path && *version == current_version(); if path.exists() { return Ok(if is_requested_version { @@ -188,7 +222,7 @@ pub async fn get_or_download_version( } if is_requested_version { - fs::copy(std::env::current_exe()?, &path) + fs::copy(current_exe()?, &path) .await .context("failed to copy current executable to version directory")?; } else { @@ -245,16 +279,39 @@ pub async fn max_installed_version() -> anyhow::Result { Ok(max_version) } -pub async fn update_bin_exe() -> anyhow::Result<()> { - let copy_to = bin_dir().await?.join(format!( +pub async fn update_bin_exe(downloaded_file: &Path) -> anyhow::Result<()> { + let bin_exe_path = bin_dir().await?.join(format!( "{}{}", env!("CARGO_BIN_NAME"), std::env::consts::EXE_SUFFIX )); + let mut downloaded_file = downloaded_file.to_path_buf(); - fs::copy(std::env::current_exe()?, ©_to) + if cfg!(target_os = "linux") && bin_exe_path.exists() { + fs::remove_file(&bin_exe_path) + .await + .context("failed to remove existing executable")?; + } else { + let tempfile = tempfile::Builder::new() + .make(|_| Ok(())) + .context("failed to create temporary file")?; + let path = tempfile.into_temp_path().to_path_buf(); + #[cfg(windows)] + let path = path.with_extension("exe"); + + let current_exe = current_exe().context("failed to get current exe path")?; + if current_exe == downloaded_file { + downloaded_file = path.to_path_buf(); + } + + fs::rename(&bin_exe_path, &path) + .await + .context("failed to rename current executable")?; + } + + fs::copy(downloaded_file, &bin_exe_path) .await .context("failed to copy executable to bin folder")?; - make_executable(©_to).await + make_executable(&bin_exe_path).await } diff --git a/src/main.rs b/src/main.rs index eb7b137..c9a5cb8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -249,17 +249,17 @@ async fn run() -> anyhow::Result<()> { .and_then(|manifest| manifest.pesde_version); // store the current version in case it needs to be used later - get_or_download_version(&reqwest, ¤t_version()).await?; + get_or_download_version(&reqwest, ¤t_version(), false).await?; let exe_path = if let Some(version) = target_version { - Some(get_or_download_version(&reqwest, &version).await?) + Some(get_or_download_version(&reqwest, &version, false).await?) } else { None }; let exe_path = if let Some(exe_path) = exe_path { exe_path } else { - get_or_download_version(&reqwest, &max_installed_version().await?).await? + get_or_download_version(&reqwest, &max_installed_version().await?, false).await? }; if let Some(exe_path) = exe_path {