Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 106 additions & 18 deletions src/cmds/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*};

Expand All @@ -19,6 +20,18 @@ pub struct Args {
pub version: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct VersionsFile {
tools: Vec<ToolVersion>,
}

#[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?;
Expand All @@ -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()?;
Expand Down Expand Up @@ -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<String>, config: &Config) -> Result<()> {
pub async fn install_tool(tool: &Tool, current_version: &Option<String>, version: &Option<String>, 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();
Expand All @@ -150,7 +173,7 @@ pub async fn install_tool(tool: &Tool, version: &Option<String>, 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
Expand All @@ -161,24 +184,89 @@ pub async fn install_tool(tool: &Tool, version: &Option<String>, 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<u32> = version_a
.split('.')
.filter_map(|s| s.parse().ok())
.collect();

let parts_b: Vec<u32> = 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<VersionsFile> {
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, &current_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(())
}
5 changes: 3 additions & 2 deletions src/cmds/show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
99 changes: 87 additions & 12 deletions src/tools.rs
Original file line number Diff line number Diff line change
@@ -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<ManifestTool>,
}

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 {
Expand All @@ -16,49 +38,102 @@ impl Tool {
}
}

use std::{path::PathBuf, sync::OnceLock};

use crate::Config;

static TOOLS: OnceLock<Vec<Tool>> = OnceLock::new();

pub fn all_tools() -> impl Iterator<Item = &'static Tool> {
fn supported_tools() -> impl Iterator<Item = &'static Tool> {
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<Item = &'static Tool> {
all_tools().filter(move |t| t.name == name)
pub async fn all_tools() -> anyhow::Result<Vec<Tool>> {
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<String> {
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<Vec<Tool>> {
all_tools().await
.map(|tools| {
tools.into_iter()
.filter(|tool| tool.name == name)
.collect()
})
}