feat: make self-upgrade check for updates by itself

This commit is contained in:
daimond113 2024-11-16 13:46:05 +01:00
parent 0ae1797ead
commit 24ad379b7c
No known key found for this signature in database
GPG key ID: 3A8ECE51328B513C
6 changed files with 170 additions and 66 deletions

View file

@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Correctly link Wally server packages by @daimond113 - 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 ## [0.5.0-rc.8] - 2024-11-12
### Added ### Added
- Add `--index` flag to `publish` command to publish to a specific index by @daimond113 - Add `--index` flag to `publish` command to publish to a specific index by @daimond113

View file

@ -118,6 +118,9 @@ after downloading the pesde binary.
Upgrades the pesde binary to the latest version. 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` ## `pesde patch`
```sh ```sh

View file

@ -1,6 +1,8 @@
use crate::cli::{version::update_bin_exe, HOME_DIR}; use crate::cli::{version::update_bin_exe, HOME_DIR};
use anyhow::Context;
use clap::Args; use clap::Args;
use colored::Colorize; use colored::Colorize;
use std::env::current_exe;
#[derive(Debug, Args)] #[derive(Debug, Args)]
pub struct SelfInstallCommand { pub struct SelfInstallCommand {
/// Skip adding the bin directory to the PATH /// Skip adding the bin directory to the PATH
@ -68,7 +70,7 @@ and then restart your shell.
); );
} }
update_bin_exe().await?; update_bin_exe(&current_exe().context("failed to get current exe path")?).await?;
Ok(()) Ok(())
} }

View file

@ -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 clap::Args;
use colored::Colorize;
#[derive(Debug, Args)] #[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 { impl SelfUpgradeCommand {
pub async fn run(self, reqwest: reqwest::Client) -> anyhow::Result<()> { 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?; if latest_version <= current_version() {
// a call to `update_bin_exe` or other similar function *should* be here, in case new versions println!("already up to date");
// have fixes to bugs in executing other versions, but that would cause return Ok(());
// the current file to be overwritten by itself, so this needs more thought }
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(()) Ok(())
} }

View file

@ -1,9 +1,4 @@
use crate::cli::{ use crate::cli::{bin_dir, config::read_config, files::make_executable, home_dir};
bin_dir,
config::{read_config, write_config, CliConfig},
files::make_executable,
home_dir,
};
use anyhow::Context; use anyhow::Context;
use colored::Colorize; use colored::Colorize;
use fs_err::tokio as fs; use fs_err::tokio as fs;
@ -11,7 +6,10 @@ use futures::StreamExt;
use reqwest::header::ACCEPT; use reqwest::header::ACCEPT;
use semver::Version; use semver::Version;
use serde::Deserialize; use serde::Deserialize;
use std::path::PathBuf; use std::{
env::current_exe,
path::{Path, PathBuf},
};
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
pub fn current_version() -> Version { 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<Version> {
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::<Vec<Release>>()
.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); const CHECK_INTERVAL: chrono::Duration = chrono::Duration::hours(6);
pub async fn check_for_updates(reqwest: &reqwest::Client) -> anyhow::Result<()> { pub async fn check_for_updates(reqwest: &reqwest::Client) -> anyhow::Result<()> {
let (owner, repo) = get_repo();
let config = read_config().await?; let config = read_config().await?;
let version = if let Some((_, version)) = config let version = if let Some((_, version)) = config
@ -51,52 +70,66 @@ pub async fn check_for_updates(reqwest: &reqwest::Client) -> anyhow::Result<()>
{ {
version version
} else { } else {
let releases = reqwest get_latest_remote_version(reqwest).await?
.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::<Vec<Release>>()
.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
}; };
let current_version = current_version();
if version > current_version() { if version > current_version {
let name = env!("CARGO_PKG_NAME"); 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!( let width = unformatted_messages
"a new version of {} is available: {}", .iter()
name.cyan(), .map(|s| s.chars().count())
version.to_string().yellow().bold() .max()
); .unwrap()
+ 4;
let stars = "-" let column = "".bright_magenta();
.repeat(unformatted_message.len() + 4)
.bright_magenta()
.bold();
let column = "|".bright_magenta().bold();
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::<Vec<_>>()
.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(()) Ok(())
@ -169,6 +202,7 @@ pub async fn download_github_release(
pub async fn get_or_download_version( pub async fn get_or_download_version(
reqwest: &reqwest::Client, reqwest: &reqwest::Client,
version: &Version, version: &Version,
always_give_path: bool,
) -> anyhow::Result<Option<PathBuf>> { ) -> anyhow::Result<Option<PathBuf>> {
let path = home_dir()?.join("versions"); let path = home_dir()?.join("versions");
fs::create_dir_all(&path) 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 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() { if path.exists() {
return Ok(if is_requested_version { return Ok(if is_requested_version {
@ -188,7 +222,7 @@ pub async fn get_or_download_version(
} }
if is_requested_version { if is_requested_version {
fs::copy(std::env::current_exe()?, &path) fs::copy(current_exe()?, &path)
.await .await
.context("failed to copy current executable to version directory")?; .context("failed to copy current executable to version directory")?;
} else { } else {
@ -245,16 +279,39 @@ pub async fn max_installed_version() -> anyhow::Result<Version> {
Ok(max_version) Ok(max_version)
} }
pub async fn update_bin_exe() -> anyhow::Result<()> { pub async fn update_bin_exe(downloaded_file: &Path) -> anyhow::Result<()> {
let copy_to = bin_dir().await?.join(format!( let bin_exe_path = bin_dir().await?.join(format!(
"{}{}", "{}{}",
env!("CARGO_BIN_NAME"), env!("CARGO_BIN_NAME"),
std::env::consts::EXE_SUFFIX std::env::consts::EXE_SUFFIX
)); ));
let mut downloaded_file = downloaded_file.to_path_buf();
fs::copy(std::env::current_exe()?, &copy_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 .await
.context("failed to copy executable to bin folder")?; .context("failed to copy executable to bin folder")?;
make_executable(&copy_to).await make_executable(&bin_exe_path).await
} }

View file

@ -249,17 +249,17 @@ async fn run() -> anyhow::Result<()> {
.and_then(|manifest| manifest.pesde_version); .and_then(|manifest| manifest.pesde_version);
// store the current version in case it needs to be used later // store the current version in case it needs to be used later
get_or_download_version(&reqwest, &current_version()).await?; get_or_download_version(&reqwest, &current_version(), false).await?;
let exe_path = if let Some(version) = target_version { 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 { } else {
None None
}; };
let exe_path = if let Some(exe_path) = exe_path { let exe_path = if let Some(exe_path) = exe_path {
exe_path exe_path
} else { } 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 { if let Some(exe_path) = exe_path {