mod cache;
mod eligibility;
mod install;
mod release;
use std::path::PathBuf;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use anyhow::{Context, Result, bail};
use semver::Version;
use serde::{Deserialize, Serialize};
use tokio::sync::watch;
pub(crate) use cache::{background_check_due, cached_update, check_for_update};
pub(crate) use eligibility::install_plan;
pub(crate) use install::install_direct;
pub(crate) const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
pub(crate) const REPOSITORY: &str = "raine/aven";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Release {
pub(crate) version: Version,
pub(crate) tag: String,
pub(crate) archive_name: String,
pub(crate) archive_url: String,
pub(crate) checksum_url: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum InstallMethod {
Direct { target: PathBuf },
Homebrew,
Cargo,
Nix,
Development,
Unsupported { reason: String },
Unwritable { target: PathBuf },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct InstallPlan {
pub(crate) release: Release,
pub(crate) method: InstallMethod,
}
impl InstallPlan {
pub(crate) fn direct_target(&self) -> Option<&std::path::Path> {
match &self.method {
InstallMethod::Direct { target } => Some(target),
_ => None,
}
}
pub(crate) fn guidance(&self) -> Option<Vec<String>> {
let current = format!("aven v{CURRENT_VERSION}");
let latest = format!("v{}", self.release.version);
let lines = match &self.method {
InstallMethod::Direct { .. } => return None,
InstallMethod::Homebrew => vec![
format!("{current} can update to {latest}."),
"This installation is managed by Homebrew.".to_string(),
"Run: brew update && brew upgrade aven".to_string(),
"The formula may take a little longer than the GitHub release to update."
.to_string(),
],
InstallMethod::Cargo => vec![
format!("{current} can update to {latest}."),
"This installation lives in Cargo's bin directory.".to_string(),
format!(
"Run: cargo install --git https://github.com/{REPOSITORY} --tag {} --locked --force",
self.release.tag
),
],
InstallMethod::Nix => vec![
format!("{current} can update to {latest}."),
"This installation is managed by Nix.".to_string(),
"Update the flake or profile that provides aven, then rebuild it.".to_string(),
],
InstallMethod::Development => vec![
format!("{current} can update to {latest}."),
"This executable is a development build.".to_string(),
"Pull the repository changes and rebuild aven from source.".to_string(),
],
InstallMethod::Unsupported { reason } => vec![
format!("{current} can update to {latest}."),
reason.clone(),
format!("Build from source: cargo install --git https://github.com/{REPOSITORY}"),
],
InstallMethod::Unwritable { target } => vec![
format!("{current} can update to {latest}."),
format!("Aven cannot replace {} from the TUI.", target.display()),
"Use Homebrew or reinstall to a writable directory such as ~/.local/bin."
.to_string(),
],
};
Some(lines)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CheckOutcome {
Current { version: Version, cached: bool },
Available { release: Release, cached: bool },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum UpdatePhase {
Downloading,
Verifying,
Extracting,
Staging,
Installing,
}
impl UpdatePhase {
pub(crate) fn label(self) -> &'static str {
match self {
Self::Downloading => "Downloading release",
Self::Verifying => "Verifying checksum",
Self::Extracting => "Extracting aven",
Self::Staging => "Validating new binary",
Self::Installing => "Installing update",
}
}
pub(crate) fn cancellable(self) -> bool {
!matches!(self, Self::Installing)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct UpdateProgress {
pub(crate) phase: UpdatePhase,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct InstallSuccess {
pub(crate) version: Version,
}
pub(crate) fn current_version() -> Version {
Version::parse(CURRENT_VERSION).expect("package version must be valid semver")
}
pub(crate) fn client() -> Result<reqwest::Client> {
reqwest::Client::builder()
.connect_timeout(std::time::Duration::from_secs(5))
.timeout(std::time::Duration::from_secs(30))
.user_agent(format!("aven/{CURRENT_VERSION}"))
.build()
.context("build update client")
}
pub(crate) fn automatic_checks_disabled() -> bool {
std::env::var("AVEN_NO_UPDATE_CHECK")
.ok()
.is_some_and(|value| {
matches!(
value.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes"
)
})
}
pub(crate) fn ensure_not_cancelled(cancelled: &Arc<AtomicBool>) -> Result<()> {
if cancelled.load(Ordering::Relaxed) {
bail!("update cancelled");
}
Ok(())
}
pub(crate) fn progress_channel() -> (
watch::Sender<UpdateProgress>,
watch::Receiver<UpdateProgress>,
) {
watch::channel(UpdateProgress {
phase: UpdatePhase::Downloading,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn package_version_is_semver() {
assert_eq!(current_version().to_string(), CURRENT_VERSION);
}
#[test]
fn cancellation_is_reported() {
let cancelled = Arc::new(AtomicBool::new(true));
assert_eq!(
ensure_not_cancelled(&cancelled).unwrap_err().to_string(),
"update cancelled"
);
}
}