diff --git a/src/cmds/install.rs b/src/cmds/install.rs index 22687c9..85f6689 100644 --- a/src/cmds/install.rs +++ b/src/cmds/install.rs @@ -10,6 +10,7 @@ use std::path::Path; use std::path::PathBuf; use tar::Archive; use xz2::read::XzDecoder; +use serde::{Deserialize, Serialize}; use crate::{Config, tools::*}; @@ -19,6 +20,18 @@ pub struct Args { pub version: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +struct VersionsFile { + tools: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ToolVersion { + repo_name: String, + repo_owner: String, + version: String, +} + pub async fn download_binary(url: &str, path: &PathBuf) -> Result<()> { let client = Client::new(); let mut response = client.get(url).send().await?; @@ -33,7 +46,7 @@ pub async fn download_binary(url: &str, path: &PathBuf) -> Result<()> { if total_size > 0 { let progress = (downloaded as f64 / total_size as f64) * 100.0; print!( - "\rDownloading: {:.1}% ({}/{})", + "\r> Downloading: {:.1}% ({}/{})", progress, downloaded, total_size ); std::io::stdout().flush()?; @@ -120,27 +133,37 @@ fn find_arch_asset<'a>(tool_name: &str, assets: &'a [Asset]) -> Option<&'a Asset assets.iter().find(|asset| asset.name.contains(&target)) } -pub async fn install_tool(tool: &Tool, version: &Option, config: &Config) -> Result<()> { +pub async fn install_tool(tool: &Tool, current_version: &Option, version: &Option, config: &Config) -> Result<()> { + let new_version = version.clone().unwrap_or(tool.min_version.clone()); + + if let Some(current_version) = current_version { + if !version_compare(&new_version, current_version) { + println!("{} is up to date", tool.name); + return Ok(()); + } + } + + println!("A new version of {} is available! 🎉", tool.name); + println!(" Current version: {}", current_version.clone().unwrap_or("-".to_string())); + println!(" Latest version: {}", new_version); + println!("\n> Installing..."); + let octocrab = Octocrab::builder().build()?; let owner = tool.repo_owner.clone(); let repo = tool.repo_name.clone(); let repo = octocrab.repos(owner, repo); let releases = repo.releases(); + + let release = releases.get_by_tag(&format!("v{}", new_version)).await + .map_err(|e| anyhow::anyhow!("Release not found: {}", e))?; - // Get the latest release or specific version - let release = if let Some(version) = version { - releases.get_by_tag(&format!("v{}", version)).await? - } else { - releases.get_latest().await? - }; - - println!("Found release: {}", release.tag_name); + println!("> Found release: {}", release.tag_name); // Find the binary asset let binary_asset = find_arch_asset(&tool.name, &release.assets) .context("No binary asset found for current platform")?; - println!("Downloading binary: {}", binary_asset.name); + println!("> Downloading binary: {}", binary_asset.name); // Create installation directory let install_dir = config.bin_dir().clone(); @@ -150,7 +173,7 @@ pub async fn install_tool(tool: &Tool, version: &Option, config: &Config let binary_path = install_dir.join(&binary_asset.name); download_binary(binary_asset.browser_download_url.as_ref(), &binary_path).await?; - println!("Extracting binary..."); + println!("> Extracting binary..."); extract_binary(&binary_path, &install_dir, &tool.name)?; // Clean up the tar.gz file @@ -161,24 +184,89 @@ pub async fn install_tool(tool: &Tool, version: &Option, config: &Config tool.name, install_dir.join(&tool.name).display() ); + println!(); + + Ok(()) +} + +fn version_compare(version_a: &str, version_b: &str) -> bool { + let parts_a: Vec = version_a + .split('.') + .filter_map(|s| s.parse().ok()) + .collect(); + + let parts_b: Vec = version_b + .split('.') + .filter_map(|s| s.parse().ok()) + .collect(); + + let max_len = parts_a.len().max(parts_b.len()); + + for i in 0..max_len { + let a = parts_a.get(i).copied().unwrap_or(0); + let b = parts_b.get(i).copied().unwrap_or(0); + + if a > b { + return true; + } else if a < b { + return false; + } + } + + false +} + +fn load_versions_file(config: &Config) -> Result { + let content = std::fs::read_to_string(config.versions_file()) + .map_err(|e| anyhow::anyhow!("Failed to read versions file: {}", e))?; + + let versions: VersionsFile = serde_json::from_str(&content) + .map_err(|e| anyhow::anyhow!("Failed to parse versions file: {}", e))?; + + Ok(versions) +} + +fn update_versions_file(versions: VersionsFile, config: &Config) -> Result<()> { + let content = serde_json::to_string_pretty(&versions) + .map_err(|e| anyhow::anyhow!("Failed to serialize versions info: {}", e))?; + + std::fs::write(config.versions_file(), content) + .map_err(|e| anyhow::anyhow!("Failed to write versions file: {}", e))?; Ok(()) } pub async fn run(args: &Args, config: &Config) -> anyhow::Result<()> { - let tools: Vec<_> = match &args.tool { - Some(tool) => tool_by_name(tool).collect(), - None => all_tools().collect(), + let tools = match &args.tool { + Some(tool) => tool_by_name(tool).await?, + None => all_tools().await?, }; if tools.is_empty() { return Err(anyhow::anyhow!("No tools found to install")); } + + let mut versions = load_versions_file(config)?; - for tool in tools { - println!("Installing {}...", tool.name); - install_tool(tool, &args.version, config).await?; + for tool in &tools { + let mut current_version = None; + if let Some(t) = versions.tools.iter().find(|t| t.repo_name == tool.repo_name && t.repo_owner == tool.repo_owner) { + current_version = Some(t.version.clone()); + } + install_tool(&tool, ¤t_version, &args.version, config).await?; } + for tool in versions.tools.iter_mut() { + if let Some(t) = tools.iter().find(|t| t.repo_name == tool.repo_name && t.repo_owner == tool.repo_owner) { + if tool.version != t.min_version { + tool.version = t.min_version.clone(); + } + } + } + + update_versions_file(versions, config)?; + + println!(); + Ok(()) } diff --git a/src/cmds/show.rs b/src/cmds/show.rs index b6da5d1..d8de6d7 100644 --- a/src/cmds/show.rs +++ b/src/cmds/show.rs @@ -34,11 +34,12 @@ fn print_tool(tool: &crate::tools::Tool, config: &Config) -> anyhow::Result<()> pub async fn run(_args: &Args, config: &Config) -> anyhow::Result<()> { // for each tool, trigger a shell command to print the version - for tool in crate::tools::all_tools() { + for tool in crate::tools::all_tools().await? { println!("{}: {}", tool.name, tool.description); println!("min version: {}", tool.min_version); + println!("max version: {}", tool.max_version); - let ok = print_tool(tool, config); + let ok = print_tool(&tool, config); if let Err(e) = ok { println!("error: {}", e); diff --git a/src/main.rs b/src/main.rs index d509aac..6cc557e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -79,6 +79,10 @@ impl Config { pub fn bin_dir(&self) -> PathBuf { self.channel_dir().join("bin") } + + pub fn versions_file(&self) -> PathBuf { + self.root_dir().join("versions.json") + } } #[tokio::main] diff --git a/src/tools.rs b/src/tools.rs index d9442c6..0182b3d 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -1,9 +1,31 @@ +use std::{path::PathBuf, vec}; +use std::sync::OnceLock; +use octocrab::Octocrab; +use reqwest::Client; +use serde::{Deserialize, Serialize}; + +use crate::Config; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ManifestTool { + repo_name: String, + repo_owner: String, + min_version: String, + max_version: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Manifest { + tools: Vec, +} + pub struct Tool { pub name: String, pub description: String, - pub min_version: String, pub repo_owner: String, pub repo_name: String, + pub min_version: String, + pub max_version: String, } impl Tool { @@ -16,49 +38,102 @@ impl Tool { } } -use std::{path::PathBuf, sync::OnceLock}; - -use crate::Config; - static TOOLS: OnceLock> = OnceLock::new(); -pub fn all_tools() -> impl Iterator { +fn supported_tools() -> impl Iterator { TOOLS .get_or_init(|| { vec![ Tool { name: "trix".to_string(), description: "The tx3 package manager".to_string(), - min_version: "0.1.0".to_string(), repo_owner: "tx3-lang".to_string(), repo_name: "trix".to_string(), + min_version: "".to_string(), + max_version: "".to_string(), }, Tool { name: "tx3-lsp".to_string(), description: "A language server for tx3".to_string(), - min_version: "0.1.0".to_string(), repo_owner: "tx3-lang".to_string(), repo_name: "lsp".to_string(), + min_version: "".to_string(), + max_version: "".to_string(), }, Tool { name: "dolos".to_string(), description: "A lightweight Cardano data node".to_string(), - min_version: "0.1.0".to_string(), repo_owner: "txpipe".to_string(), repo_name: "dolos".to_string(), + min_version: "".to_string(), + max_version: "".to_string(), }, Tool { name: "cshell".to_string(), description: "A terminal wallet for Cardano".to_string(), - min_version: "0.1.0".to_string(), repo_owner: "txpipe".to_string(), repo_name: "cshell".to_string(), + min_version: "".to_string(), + max_version: "".to_string(), }, ] }) .iter() } -pub fn tool_by_name(name: &str) -> impl Iterator { - all_tools().filter(move |t| t.name == name) +pub async fn all_tools() -> anyhow::Result> { + let octocrab = Octocrab::builder().build() + .map_err(|e| anyhow::anyhow!("Failed to create Octocrab client: {}", e))?; + + let repo = octocrab.repos("tx3-lang", "toolchain"); + + let release = repo.releases().get_latest().await + .map_err(|e| anyhow::anyhow!("Failed to fetch latest release: {}", e))?; + + let manifest_asset = release.assets.iter() + .find(|asset| asset.name == "manifest.json") + .ok_or_else(|| anyhow::anyhow!("No manifest asset found in latest release"))?; + + let manifest_content = fetch_manifest_content(manifest_asset.browser_download_url.as_ref()).await + .map_err(|e| anyhow::anyhow!("Failed to fetch manifest: {}", e))?; + + let manifest: Manifest = serde_json::from_str(&manifest_content) + .map_err(|e| anyhow::anyhow!("Failed to parse manifest file: {}", e))?; + + let mut tools = vec![]; + for tool in manifest.tools { + let t = supported_tools() + .find(|t| t.repo_name == tool.repo_name && t.repo_owner == tool.repo_owner); + + if let Some(t) = t { + tools.push(Tool { + name: t.name.clone(), + description: t.description.clone(), + repo_owner: tool.repo_owner, + repo_name: tool.repo_name, + min_version: tool.min_version, + max_version: tool.max_version, + }); + } + } + + Ok(tools) +} + +async fn fetch_manifest_content(url: &str) -> anyhow::Result { + let client = Client::new(); + let response = client.get(url).send().await + .map_err(|e| anyhow::anyhow!("Failed to fetch manifest: {}", e))?; + let data = response.text().await + .map_err(|e| anyhow::anyhow!("Failed to read manifest response: {}", e))?; + Ok(data) +} + +pub async fn tool_by_name(name: &str) -> anyhow::Result> { + all_tools().await + .map(|tools| { + tools.into_iter() + .filter(|tool| tool.name == name) + .collect() + }) }