use clap::Parser;
use comfy_table::{ContentArrangement, Table, modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL};
use detect_targets::detect_targets;
use miette::{IntoDiagnostic, Result};
use reqwest::{Client, Url};
use serde::Deserialize;
use std::str::FromStr;
use target_tuples::{
CanonicalTarget, TargetRef,
pieces::{Architecture, OS},
};
use crate::actions::Context;
use super::TamanuArgs;
#[derive(Debug, Clone, Parser)]
pub struct ArtifactsArgs {
#[arg(value_name = "VERSION")]
pub version: String,
#[arg(short, long, value_name = "PLATFORM", default_value = "host")]
pub platform: Platform,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Platform {
All,
Host,
Match(String),
}
impl FromStr for Platform {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"all" => Ok(Platform::All),
"host" => Ok(Platform::Host),
s => Ok(Platform::Match(s.to_string())),
}
}
}
#[derive(Debug, Deserialize)]
pub struct Artifact {
pub artifact_type: String,
pub platform: String,
pub download_url: Url,
}
pub async fn get_artifacts(version: &str, for_platform: &Platform) -> Result<Vec<Artifact>> {
let url = format!("https://meta.tamanu.app/versions/{version}/artifacts");
let client = Client::new();
let response = client.get(&url).send().await.into_diagnostic()?;
if !response.status().is_success() {
return Err(miette::miette!(
"Failed to fetch artifacts for version {}: HTTP {}",
version,
response.status()
));
}
let mut artifacts: Vec<Artifact> = response.json().await.into_diagnostic()?;
if artifacts.is_empty() {
println!("No artifacts found for version {}", version);
return Ok(Vec::new());
}
let host_targets = detect_targets()
.await
.into_iter()
.filter_map(|target| {
TargetRef::try_parse(&target)
.map(|target| target.canonical())
.ok()
})
.collect::<Vec<CanonicalTarget>>();
artifacts.retain(|artifact| match &for_platform {
Platform::All => true,
Platform::Host => {
if artifact.platform == "any" {
return true;
}
let os = if artifact.platform.contains("linux") {
host_targets.iter().any(|t| t.sys.os() == Some(OS::Linux))
} else if artifact.platform.contains("windows") {
host_targets.iter().any(|t| t.sys.os() == Some(OS::Win32))
} else if artifact.platform.contains("macos") {
host_targets.iter().any(|t| t.sys.os() == Some(OS::Darwin))
} else {
false
};
let arch = if artifact.platform.contains("amd64") {
host_targets
.iter()
.any(|t| matches!(t.arch, Architecture::X86_64 { .. }))
} else if artifact.platform.contains("arm64") {
host_targets
.iter()
.any(|t| matches!(t.arch, Architecture::Aarch64))
} else {
!artifact.platform.contains("-")
};
os && arch
}
Platform::Match(s) => artifact.platform.contains(s),
});
artifacts.sort_by(|a, b| a.platform.cmp(&b.platform));
artifacts.sort_by(|a, b| a.artifact_type.cmp(&b.artifact_type));
Ok(artifacts)
}
pub async fn run(ctx: Context<TamanuArgs, ArtifactsArgs>) -> Result<()> {
let ArtifactsArgs { version, platform } = ctx.args_sub;
let artifacts = get_artifacts(&version, &platform).await?;
if artifacts.is_empty() {
println!(
"No artifacts found for version {} with the specified platform filter",
version
);
return Ok(());
}
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.apply_modifier(UTF8_ROUND_CORNERS)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(vec!["Type", "Platform", "Download URL"]);
for artifact in artifacts {
table.add_row(vec![
artifact.artifact_type,
artifact.platform,
artifact.download_url.to_string(),
]);
}
println!("{table}");
Ok(())
}