diff --git a/src/cmds/check.rs b/src/cmds/check.rs new file mode 100644 index 0000000..4d29d95 --- /dev/null +++ b/src/cmds/check.rs @@ -0,0 +1,54 @@ +use clap::Parser; + +use crate::{Config, manifest, updates}; + +#[derive(Parser, Default)] +pub struct Args { + #[arg(short, long)] + pub silent: bool, + + #[arg(short, long)] + pub force: bool, + + #[arg(short, long)] + pub verbose: bool, +} + +fn print_update(update: &updates::Update, manifest: &manifest::Manifest) -> anyhow::Result<()> { + let tool = manifest.tool_by_name(&update.tool).unwrap(); + + if let Some(current) = update.current()? { + println!("\nYour version of {} needs to be updated 😬", tool.name); + println!(" Current version: {current}"); + println!(" Requested version: {}", update.requested); + } else { + println!("\nYour need to install {} 📦", tool.name); + } + + Ok(()) +} + +pub async fn run(args: &Args, config: &Config) -> anyhow::Result<()> { + let manifest = manifest::load_manifest(config, args.force).await?; + + let updates = updates::load_updates(&manifest, config, args.force).await?; + + if args.silent { + return Ok(()); + } + + if updates.is_empty() { + println!("You are up to date 🎉"); + return Ok(()); + } + + if !args.verbose { + println!("You have {} update/s to install 📦", updates.len()); + } else { + for update in updates { + print_update(&update, &manifest)?; + } + } + + Ok(()) +} diff --git a/src/cmds/install.rs b/src/cmds/install.rs index bdfc052..2573ed1 100644 --- a/src/cmds/install.rs +++ b/src/cmds/install.rs @@ -15,8 +15,8 @@ use std::path::PathBuf; use tar::Archive; use xz2::read::XzDecoder; -use crate::bin; use crate::manifest; +use crate::updates; use crate::{Config, manifest::*}; #[derive(Parser, Default)] @@ -163,6 +163,7 @@ pub async fn download_tool_from_asset(tool: &Tool, asset: &Asset, config: &Confi tool.name, install_dir.join(&tool.name).display() ); + println!(); Ok(()) @@ -203,23 +204,6 @@ async fn find_matching_release( Ok(None) } -async fn find_installed_version(tool: &Tool, config: &Config) -> anyhow::Result> { - if !bin::is_installed(tool, config).await? { - return Ok(None); - } - - let current_version = bin::check_current_version(tool, config).await; - - match current_version { - Ok(version) => Ok(Some(version)), - Err(_) => { - // if the version command fails, we assume there's something wrong with the - // binary and respond as if it wasn't installed - Ok(None) - } - } -} - async fn install_tool(tool: &Tool, requested: &VersionReq, config: &Config) -> anyhow::Result<()> { println!("\n> Installing {} at version {}", tool.name, requested); @@ -240,38 +224,35 @@ async fn install_tool(tool: &Tool, requested: &VersionReq, config: &Config) -> a Ok(()) } -pub async fn run(args: &Args, config: &Config) -> anyhow::Result<()> { +pub async fn run(_args: &Args, config: &Config) -> anyhow::Result<()> { let manifest = manifest::load_manifest(config, true).await?; - let to_install: Vec<_> = if let Some(filter) = &args.tool { - manifest.tools().filter(|x| x.name == *filter).collect() - } else { - manifest.tools().collect() - }; + let updates = updates::check_updates(&manifest, config).await?; - if to_install.is_empty() { - return Err(anyhow::anyhow!("No tools found to install")); + if updates.is_empty() { + println!("You are up to date 🎉"); + return Ok(()); } - for tool in to_install.iter() { - let current = find_installed_version(tool, config).await?; - let requested = VersionReq::parse(&tool.version)?; + for update in updates { + let tool = manifest.tool_by_name(&update.tool).unwrap(); - if let Some(current) = current { - if requested.matches(¤t) { - println!("\nYour version of {} is up to date 👌", tool.name); - - continue; - } else { - println!("\nYour version of {} needs to be updated 😬", tool.name); - println!(" Current version: {current}"); - println!(" Requested version: {requested}"); - } + if let Some(current) = update.current()? { + println!("\nYour version of {} needs to be updated 😬", tool.name); + println!(" Current version: {current}"); + println!(" Requested version: {}", update.requested); } else { println!("\nYour need to install {} 📦", tool.name); } - install_tool(tool, &requested, config).await?; + install_tool(tool, &update.requested()?, config).await?; + } + + // we do a second check to make sure we have the latest updates + let after = updates::check_updates(&manifest, config).await?; + + if !after.is_empty() { + println!("Seems that you still have updates to install, please run `tx3up install` again",); } Ok(()) diff --git a/src/cmds/mod.rs b/src/cmds/mod.rs index 03ca510..ff8774c 100644 --- a/src/cmds/mod.rs +++ b/src/cmds/mod.rs @@ -1,3 +1,4 @@ +pub mod check; pub mod default; pub mod install; pub mod show; diff --git a/src/main.rs b/src/main.rs index d7a817f..beb0c64 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ mod bin; mod cmds; mod manifest; mod perm_path; +mod updates; pub const BANNER: &str = color_print::cstr! { r#" @@ -32,10 +33,10 @@ struct Cli { #[derive(Subcommand)] enum Commands { - /// Install the tx3 toolchain + /// Install or update the tx3 toolchain Install(cmds::install::Args), - /// Update the tx3 toolchain to the latest version - Update, + /// Check for updates + Check(cmds::check::Args), /// Uninstall the tx3 toolchain Uninstall, /// Set the default channel @@ -84,6 +85,10 @@ impl Config { pub fn manifest_file(&self) -> PathBuf { self.channel_dir().join("manifest.json") } + + pub fn updates_file(&self) -> PathBuf { + self.channel_dir().join("updates.json") + } } #[tokio::main] @@ -100,7 +105,7 @@ async fn main() -> anyhow::Result<()> { if let Some(command) = cli.command { match command { Commands::Install(args) => cmds::install::run(&args, &config).await?, - Commands::Update => todo!(), + Commands::Check(args) => cmds::check::run(&args, &config).await?, Commands::Uninstall => todo!(), Commands::Default(args) => cmds::default::run(&args, &config).await?, Commands::Show(args) => cmds::show::run(&args, &config).await?, diff --git a/src/manifest.rs b/src/manifest.rs index 7e0c5d8..aea0ec5 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -38,6 +38,10 @@ impl Manifest { pub fn tools(&self) -> impl Iterator { self.tools.iter() } + + pub fn tool_by_name(&self, name: &str) -> Option<&Tool> { + self.tools.iter().find(|tool| tool.name == name) + } } async fn fetch_manifest_content(url: &str) -> anyhow::Result { diff --git a/src/updates.rs b/src/updates.rs new file mode 100644 index 0000000..52706ad --- /dev/null +++ b/src/updates.rs @@ -0,0 +1,123 @@ +use anyhow::Context as _; +use semver::{Version, VersionReq}; +use serde::{Deserialize, Serialize}; +use tokio::fs; + +use crate::{ + Config, bin, + manifest::{Manifest, Tool}, +}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Update { + pub tool: String, + pub current: Option, + pub requested: String, +} + +impl Update { + pub fn requested(&self) -> anyhow::Result { + VersionReq::parse(&self.requested).context("parsing requested version") + } + + pub fn current(&self) -> anyhow::Result> { + if let Some(current) = &self.current { + Ok(Some( + Version::parse(current).context("parsing current version")?, + )) + } else { + Ok(None) + } + } +} + +async fn find_installed_version(tool: &Tool, config: &Config) -> anyhow::Result> { + if !bin::is_installed(tool, config).await? { + return Ok(None); + } + + let current_version = bin::check_current_version(tool, config).await; + + match current_version { + Ok(version) => Ok(Some(version)), + Err(_) => { + // if the version command fails, we assume there's something wrong with the + // binary and respond as if it wasn't installed + Ok(None) + } + } +} + +async fn evaluate_update(tool: &Tool, config: &Config) -> anyhow::Result> { + let current = find_installed_version(tool, config).await?; + let requested = VersionReq::parse(&tool.version)?; + + if let Some(current) = ¤t { + if requested.matches(¤t) { + return Ok(None); + } + } + + Ok(Some(Update { + tool: tool.name.clone(), + current: current.map(|v| v.to_string()), + requested: requested.to_string(), + })) +} + +async fn save_updates(updates: &[Update], config: &Config) -> anyhow::Result<()> { + fs::create_dir_all(config.channel_dir()) + .await + .context("creating channel dir")?; + + fs::write( + config.updates_file(), + serde_json::to_string(&updates)?.as_bytes(), + ) + .await + .context("writing updates file")?; + + Ok(()) +} + +pub async fn clear_updates(config: &Config) -> anyhow::Result<()> { + fs::remove_file(config.updates_file()) + .await + .context("removing updates file")?; + + Ok(()) +} + +pub async fn check_updates(manifest: &Manifest, config: &Config) -> anyhow::Result> { + let mut updates = vec![]; + + for tool in manifest.tools() { + if let Some(update) = evaluate_update(tool, config).await? { + updates.push(update); + } + } + + save_updates(&updates, config).await?; + + Ok(updates) +} + +pub async fn load_updates( + manifest: &Manifest, + config: &Config, + force_check: bool, +) -> anyhow::Result> { + let updates_file = config.updates_file(); + + if !updates_file.exists() || force_check { + check_updates(manifest, config).await?; + } + + let updates = fs::read_to_string(updates_file) + .await + .context("reading updates file")?; + + let updates: Vec = serde_json::from_str(&updates)?; + + Ok(updates) +}