use anyhow::Result;
use clap::{Args, Subcommand};
#[cfg(feature = "self-update")]
use colored::Colorize;
use crate::install_source::InstallSource;
#[derive(Debug, Args)]
pub struct SelfNamespace {
#[command(subcommand)]
pub command: SelfCommand,
}
#[derive(Debug, Subcommand)]
pub enum SelfCommand {
#[command(alias = "upgrade")]
Update(SelfUpdateArgs),
}
#[derive(Debug, Args)]
pub struct SelfUpdateArgs {
pub target_version: Option<String>,
#[arg(long, env = "GITHUB_TOKEN")]
pub token: Option<String>,
#[arg(long)]
pub dry_run: bool,
}
pub fn handle_self_command(args: SelfNamespace) -> Result<i32> {
match args.command {
SelfCommand::Update(SelfUpdateArgs {
target_version,
token,
dry_run,
}) => handle_self_update(target_version, token, dry_run),
}
}
#[cfg(feature = "self-update")]
fn handle_self_update(
version: Option<String>,
token: Option<String>,
dry_run: bool,
) -> Result<i32> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
let result = runtime.block_on(self_update(version, token, dry_run));
runtime.shutdown_background();
result
}
#[cfg(not(feature = "self-update"))]
fn handle_self_update(
_version: Option<String>,
_token: Option<String>,
_dry_run: bool,
) -> Result<i32> {
let message = InstallSource::detect()
.map(|source| {
format!(
"r2x was installed via {} and cannot self-update. To update, run `{}`",
source.description(),
source.update_instructions()
)
})
.unwrap_or_else(|| {
"r2x was installed via an external package manager and cannot self-update. \
Please use your package manager to update r2x."
.to_string()
});
anyhow::bail!("{message}");
}
#[cfg(feature = "self-update")]
fn format_install_hint() -> String {
match InstallSource::detect() {
Some(source) => format!(
"{}{} You installed r2x via {}. To update, run `{}`",
"hint".cyan().bold(),
":".bold(),
source.description(),
source.update_instructions()
),
None => format!(
"{}{} If you installed r2x with cargo, pipx, brew, or another package manager, update r2x with `cargo install --locked r2x`, `pipx upgrade`, `brew upgrade`, or similar.",
"hint".cyan().bold(),
":".bold()
),
}
}
#[cfg(feature = "self-update")]
fn bail_not_standalone() -> Result<i32> {
eprintln!(
"{}{} Self-update is only available for r2x binaries installed via the standalone installation scripts.",
"error".red().bold(),
":".bold(),
);
eprintln!("{}", format_install_hint());
Ok(1)
}
#[cfg(feature = "self-update")]
async fn self_update(version: Option<String>, token: Option<String>, dry_run: bool) -> Result<i32> {
use axoupdater::{AxoUpdater, AxoupdateError, UpdateRequest};
use tracing::{debug, enabled};
let mut updater = AxoUpdater::new_for("r2x");
if enabled!(tracing::Level::DEBUG) {
std::env::set_var("INSTALLER_PRINT_VERBOSE", "1");
updater.enable_installer_output();
} else {
updater.disable_installer_output();
}
if let Some(ref token) = token {
updater.set_github_token(token);
}
let Ok(updater) = updater.load_receipt() else {
debug!("no receipt found; assuming r2x was installed via a package manager");
return bail_not_standalone();
};
if let Ok(version) = env!("CARGO_PKG_VERSION").parse() {
let _ = updater.set_current_version(version);
}
if !updater.check_receipt_is_for_this_executable()? {
debug!(
"receipt is not for this executable; assuming r2x was installed via a package manager"
);
return bail_not_standalone();
}
eprintln!(
"{}{} Checking for updates...",
"info".cyan().bold(),
":".bold()
);
let update_request = if let Some(version) = version {
UpdateRequest::SpecificTag(version)
} else {
UpdateRequest::Latest
};
updater.configure_version_specifier(update_request.clone());
if dry_run {
if updater.is_update_needed().await? {
let version = match update_request {
UpdateRequest::Latest | UpdateRequest::LatestMaybePrerelease => {
"the latest version".to_string()
}
UpdateRequest::SpecificTag(version) | UpdateRequest::SpecificVersion(version) => {
format!("v{version}")
}
};
eprintln!(
"Would update r2x from {} to {}",
format!("v{}", env!("CARGO_PKG_VERSION")).bold().white(),
version.bold().white(),
);
} else {
eprintln!(
"{}{} You're on the latest version of r2x ({})",
"success".green().bold(),
":".bold(),
format!("v{}", env!("CARGO_PKG_VERSION")).bold().white()
);
}
return Ok(0);
}
match updater.run().await {
Ok(Some(result)) => {
let direction = if result
.old_version
.as_ref()
.is_some_and(|old_version| *old_version > result.new_version)
{
"Downgraded"
} else {
"Upgraded"
};
let version_information = if let Some(old_version) = result.old_version {
format!(
"from {} to {}",
format!("v{old_version}").bold().white(),
format!("v{}", result.new_version).bold().white(),
)
} else {
format!("to {}", format!("v{}", result.new_version).bold().white())
};
eprintln!(
"{}{} {direction} r2x {}! {}",
"success".green().bold(),
":".bold(),
version_information,
format!(
"https://github.com/NatLabRockies/r2x-cli/releases/tag/{}",
result.new_version_tag
)
.cyan()
);
}
Ok(None) => {
eprintln!(
"{}{} You're on the latest version of r2x ({})",
"success".green().bold(),
":".bold(),
format!("v{}", env!("CARGO_PKG_VERSION")).bold().white()
);
}
Err(err) => {
return if let AxoupdateError::Reqwest(err) = err {
if err.status() == Some(http::StatusCode::FORBIDDEN) && token.is_none() {
eprintln!(
"{}{} GitHub API rate limit exceeded. Please provide a GitHub token via the {} option.",
"error".red().bold(),
":".bold(),
"`--token`".green().bold()
);
Ok(1)
} else {
Err(err.into())
}
} else {
Err(err.into())
};
}
}
Ok(0)
}
#[cfg(all(test, feature = "self-update"))]
mod tests {
use super::*;
#[test]
fn install_hint_mentions_known_source() {
let hint = format_install_hint();
assert!(hint.contains("r2x"));
}
}