use leo_errors::{Backtraced, Result};
use aleo_std;
use colored::Colorize;
use self_update::{Status, backends::github, get_target, version::bump_is_greater};
use std::{
fmt::Write as _,
fs,
path::{Path, PathBuf},
time::{Duration, SystemTime, UNIX_EPOCH},
};
pub struct Updater;
impl Updater {
const LEO_BIN_NAME: &'static str = "leo";
const LEO_CACHE_LAST_CHECK_FILE: &'static str = "leo_cache_last_update_check";
const LEO_CACHE_VERSION_FILE: &'static str = "leo_cache_latest_version";
const LEO_REPO_NAME: &'static str = "leo";
const LEO_REPO_OWNER: &'static str = "ProvableHQ";
const LEO_UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60);
pub fn show_available_releases() -> Result<String> {
let releases = github::ReleaseList::configure()
.repo_owner(Self::LEO_REPO_OWNER)
.repo_name(Self::LEO_REPO_NAME)
.with_target(get_target())
.build()
.map_err(crate::errors::self_update_error)?
.fetch()
.map_err(crate::errors::could_not_fetch_versions)?;
let mut output = format!(
"\nList of available versions for: {}.\nUse the quoted name to select specific releases.\n\n",
get_target()
);
for release in releases {
let _ = writeln!(output, " * {} | '{}'", release.version, release.name);
}
Ok(output)
}
pub fn update(show_output: bool, version: Option<String>) -> Result<Status> {
let mut update = github::Update::configure();
update
.repo_owner(Self::LEO_REPO_OWNER)
.repo_name(Self::LEO_REPO_NAME)
.bin_name(Self::LEO_BIN_NAME)
.current_version(env!("CARGO_PKG_VERSION"))
.show_download_progress(show_output)
.no_confirm(true)
.show_output(show_output);
if let Some(version) = version {
update.target_version_tag(&version);
}
let status = update
.build()
.map_err(crate::errors::self_update_build_error)?
.update()
.map_err(crate::errors::self_update_error)?;
Ok(status)
}
pub fn update_bundled_plugins(show_output: bool, version: Option<&str>) {
const BUNDLED_PLUGINS: &[&str] = &["leo-fmt"];
let install_dir = match std::env::current_exe().ok().and_then(|p| p.parent().map(Path::to_path_buf)) {
Some(dir) => dir,
None => {
tracing::warn!("Could not determine leo install directory; skipping plugin update");
return;
}
};
for plugin in BUNDLED_PLUGINS {
if show_output {
tracing::info!("Updating bundled plugin '{plugin}'...");
}
let mut update = github::Update::configure();
update
.repo_owner(Self::LEO_REPO_OWNER)
.repo_name(Self::LEO_REPO_NAME)
.bin_name(plugin)
.bin_install_path(&install_dir)
.current_version(env!("CARGO_PKG_VERSION"))
.show_download_progress(show_output)
.no_confirm(true)
.show_output(show_output);
if let Some(ver) = version {
update.target_version_tag(ver);
}
match update.build().and_then(|u| u.update()) {
Ok(_) => {
if show_output {
tracing::info!("Successfully updated '{plugin}'");
}
}
Err(e) => {
tracing::warn!("Failed to update bundled plugin '{plugin}': {e}");
}
}
}
}
pub fn update_available() -> Result<String> {
let updater = github::Update::configure()
.repo_owner(Self::LEO_REPO_OWNER)
.repo_name(Self::LEO_REPO_NAME)
.bin_name(Self::LEO_BIN_NAME)
.current_version(env!("CARGO_PKG_VERSION"))
.build()
.map_err(crate::errors::self_update_error)?;
let current_version = updater.current_version();
let latest_release = updater.get_latest_release().map_err(crate::errors::self_update_error)?;
if bump_is_greater(¤t_version, &latest_release.version).map_err(crate::errors::self_update_error)? {
Ok(latest_release.version)
} else {
Err(crate::errors::old_release_version(current_version, latest_release.version).into())
}
}
pub fn read_latest_version() -> Result<Option<String>, Backtraced> {
let version_file_path = Self::get_version_file_path();
match fs::read_to_string(version_file_path) {
Ok(version) => Ok(Some(version.trim().to_string())),
Err(_) => Ok(None),
}
}
pub fn get_cli_string() -> Result<Option<String>, Backtraced> {
if let Some(latest_version) = Self::read_latest_version()? {
let colorized_message = format!(
"\n🟢 {} {} {}",
"A new version is available! Run".bold().green(),
"`leo update`".bold().white(),
format!("to update to v{latest_version}.").bold().green()
);
Ok(Some(colorized_message))
} else {
Ok(None)
}
}
pub fn print_cli() -> Result<(), Backtraced> {
if let Some(message) = Self::get_cli_string()? {
println!("{message}");
}
Ok(())
}
pub fn check_for_updates(force: bool) -> Result<bool, Backtraced> {
let cache_dir = Self::get_cache_dir();
let last_check_file = cache_dir.join(Self::LEO_CACHE_LAST_CHECK_FILE);
let version_file = Self::get_version_file_path();
let should_check = force || Self::should_check_for_updates(&last_check_file)?;
if should_check {
match Self::update_available() {
Ok(latest_version) => {
Self::update_check_files(&cache_dir, &last_check_file, &version_file, &latest_version)?;
Ok(true)
}
Err(_) => {
Self::update_check_files(&cache_dir, &last_check_file, &version_file, env!("CARGO_PKG_VERSION"))?;
Ok(false)
}
}
} else if version_file.exists() {
if let Ok(stored_version) = fs::read_to_string(&version_file) {
let current_version = env!("CARGO_PKG_VERSION");
Ok(bump_is_greater(current_version, stored_version.trim()).map_err(crate::errors::self_update_error)?)
} else {
Ok(false)
}
} else {
Ok(false)
}
}
fn update_check_files(
cache_dir: &Path,
last_check_file: &Path,
version_file: &Path,
latest_version: &str,
) -> Result<(), Backtraced> {
fs::create_dir_all(cache_dir).map_err(crate::errors::cli_io_error)?;
let current_time = Self::get_current_time()?;
fs::write(last_check_file, current_time.to_string()).map_err(crate::errors::cli_io_error)?;
fs::write(version_file, latest_version).map_err(crate::errors::cli_io_error)?;
Ok(())
}
fn should_check_for_updates(last_check_file: &Path) -> Result<bool, Backtraced> {
match fs::read_to_string(last_check_file) {
Ok(contents) => {
let last_check = contents
.parse::<u64>()
.map_err(|e| crate::errors::cli_runtime_error(format!("Failed to parse last check time: {e}")))?;
let current_time = Self::get_current_time()?;
Ok(current_time.saturating_sub(last_check) > Self::LEO_UPDATE_CHECK_INTERVAL.as_secs())
}
Err(_) => Ok(true),
}
}
fn get_current_time() -> Result<u64, Backtraced> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| crate::errors::cli_runtime_error(format!("System time error: {e}")))
.map(|duration| duration.as_secs())
}
fn get_version_file_path() -> PathBuf {
Self::get_cache_dir().join(Self::LEO_CACHE_VERSION_FILE)
}
fn get_cache_dir() -> PathBuf {
aleo_std::aleo_dir().join("leo")
}
}