use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::path::Path;
use std::slice::Iter;
use anyhow::{anyhow, Result};
use semver::Version;
use clyde::app::App;
use clyde::arch_os::{
ArchOs, ANY, ARCH_AARCH64, ARCH_X86, ARCH_X86_64, OS_LINUX, OS_MACOS, OS_WINDOWS,
};
use clyde::checksum::compute_checksum;
use clyde::file_cache::FileCache;
use clyde::package::{Asset, Package, Release};
use clyde::ui::Ui;
type MatchingPair = (&'static str, &'static str);
lazy_static! {
static ref ARCH_VEC: Vec<MatchingPair> = vec![
("x86_64", ARCH_X86_64),
("amd64", ARCH_X86_64),
("x64", ARCH_X86_64),
("x86", ARCH_X86),
("386", ARCH_X86),
("686", ARCH_X86),
("aarch64", ARCH_AARCH64),
("arm64", ARCH_AARCH64),
("32bit", ARCH_X86),
("64bit", ARCH_X86_64),
("universal", ANY),
];
static ref OS_VEC: Vec<MatchingPair> = vec![
("linux", OS_LINUX),
("darwin", OS_MACOS),
("apple", OS_MACOS),
("macos", OS_MACOS),
("windows", OS_WINDOWS),
("win32", OS_WINDOWS),
("win", OS_WINDOWS),
];
static ref UNSUPPORTED_EXTS : HashSet<&'static str> = HashSet::from(["deb", "rpm", "msi", "apk", "asc", "sha256", "sbom", "txt", "dmg", "sh"]);
static ref PACKER_EXTENSIONS: Vec<&'static str> = vec![
"zip", "gz", "bz2", "xz"
];
static ref LIBC_NAMES: Vec<&'static str> = vec![
"gnu", "musl", "msvc"
];
}
fn compute_url_checksum(ui: &Ui, cache: &FileCache, url: &str) -> Result<String> {
let path = cache.download(ui, url)?;
ui.info("Computing checksum");
compute_checksum(&path)
}
fn find_in_iter(iter: Iter<'_, (&'static str, &'static str)>, name: &str) -> Option<&'static str> {
for (token, key) in iter {
if name.contains(token) {
return Some(key);
}
}
None
}
fn extract_arch_os(name: &str) -> Option<ArchOs> {
let arch = find_in_iter(ARCH_VEC.iter(), name)?;
let os = find_in_iter(OS_VEC.iter(), name)?;
Some(ArchOs::new(arch, os))
}
fn get_extension(name: &str) -> Option<&str> {
name.rsplit_once('.').map(|x| x.1)
}
fn get_lname(url: &str) -> Result<String> {
let (_, name) = url
.rsplit_once('/')
.ok_or_else(|| anyhow!("Can't find archive name in URL {}", url))?;
Ok(name.to_ascii_lowercase())
}
fn is_supported_name(name: &str) -> bool {
let ext = match get_extension(name) {
Some(x) => x,
None => return true,
};
if UNSUPPORTED_EXTS.contains(ext) {
return false;
}
true
}
pub fn add_asset(
ui: &Ui,
cache: &FileCache,
release: &mut Release,
arch_os: &ArchOs,
url: &str,
) -> Result<()> {
let checksum = compute_url_checksum(ui, cache, url)?;
let asset = Asset {
url: url.to_string(),
sha256: checksum,
};
release.insert(arch_os.clone(), asset);
Ok(())
}
fn get_pack_score(name: &str) -> usize {
let ext = match get_extension(name) {
Some(x) => x,
None => return 0,
};
match PACKER_EXTENSIONS.iter().position(|&x| x == ext) {
Some(idx) => idx + 1,
None => 0,
}
}
fn get_libc_score(name: &str) -> usize {
match LIBC_NAMES.iter().position(|&x| name.contains(x)) {
Some(idx) => idx + 1,
None => 0,
}
}
fn select_best_url<'a>(ui: &Ui, u1: &'a str, u2: &'a str) -> &'a str {
let n1 = get_lname(u1).unwrap();
let n2 = get_lname(u2).unwrap();
let u1_libc_score = get_libc_score(&n1);
let u2_libc_score = get_libc_score(&n2);
match u1_libc_score.cmp(&u2_libc_score) {
Ordering::Greater => return u1,
Ordering::Less => return u2,
Ordering::Equal => (),
};
let u1_pack_score = get_pack_score(&n1);
let u2_pack_score = get_pack_score(&n2);
match u1_pack_score.cmp(&u2_pack_score) {
Ordering::Greater => u1,
Ordering::Less => u2,
Ordering::Equal => {
ui.warn(&format!(
"Don't know which of '{n1}' and '{n2}' is the best, picking the first one"
));
u1
}
}
}
pub fn select_best_urls(ui: &Ui, urls: &Vec<String>) -> Result<HashMap<ArchOs, String>> {
let mut best_urls = HashMap::<ArchOs, String>::new();
for url in urls {
let lname = get_lname(url)?;
if !is_supported_name(&lname) {
continue;
}
let arch_os = match extract_arch_os(&lname) {
Some(x) => x,
None => {
ui.warn(&format!("Can't extract arch-os from {lname}, skipping"));
continue;
}
};
let url = match best_urls.get(&arch_os) {
Some(current_url) => select_best_url(ui, current_url, url),
None => url,
};
best_urls.insert(arch_os, url.to_string());
}
Ok(best_urls)
}
pub fn add_assets(
app: &App,
ui: &Ui,
path: &Path,
version: &Version,
arch_os: &Option<String>,
urls: &Vec<String>,
) -> Result<()> {
let package = Package::from_file(path)?;
let mut release = match package.releases.get(version) {
Some(x) => x.clone(),
None => Release::new(),
};
if let Some(arch_os) = arch_os {
if urls.len() > 1 {
return Err(anyhow!("When using --arch-os, only one URL can be passed"));
}
let url = urls.first().unwrap();
let arch_os = ArchOs::parse(arch_os)?;
add_asset(ui, &app.download_cache, &mut release, &arch_os, url)?;
} else {
let urls_for_arch_os = select_best_urls(ui, urls)?;
for (arch_os, url) in urls_for_arch_os {
ui.info(&format!("{arch_os}: {url}"));
let result = add_asset(
&ui.nest(),
&app.download_cache,
&mut release,
&arch_os,
&url,
);
if let Err(err) = result {
ui.error(&format!(
"Can't add {:?} build from {}: {}",
arch_os, url, err
));
return Err(err);
};
}
}
let new_package = package.replace_release(version, release);
new_package.to_file(path)?;
Ok(())
}
pub fn add_assets_cmd(
app: &App,
ui: &Ui,
path: &Path,
version: &str,
arch_os: &Option<String>,
urls: &Vec<String>,
) -> Result<()> {
let version = Version::parse(version)?;
add_assets(app, ui, path, &version, arch_os, urls)
}
#[cfg(test)]
mod tests {
use super::*;
fn check_extract_arch_os(filename: &str, expected: Option<ArchOs>) {
let result = extract_arch_os(filename);
assert_eq!(result, expected);
}
#[test]
fn test_extract_arch_os() {
check_extract_arch_os(
"foo-1.2-linux-arm64.tar.gz",
Some(ArchOs::new(ARCH_AARCH64, OS_LINUX)),
);
check_extract_arch_os(
"node-v16.16.0-win-x86.zip",
Some(ArchOs::new(ARCH_X86, OS_WINDOWS)),
);
check_extract_arch_os(
"node-v16.16.0-darwin-x64.tar.gz",
Some(ArchOs::new(ARCH_X86_64, OS_MACOS)),
);
check_extract_arch_os(
"bat-v0.21.0-i686-pc-windows-msvc.zip",
Some(ArchOs::new(ARCH_X86, OS_WINDOWS)),
);
check_extract_arch_os(
"cmake-3.24.0-rc5-macos10.10-universal.tar.gz",
Some(ArchOs::new(ANY, OS_MACOS)),
);
check_extract_arch_os("bar-3.14.tar.gz", None);
}
#[test]
fn test_is_supported_name() {
assert!(is_supported_name("foo.tar.gz"));
assert!(is_supported_name("foo.zip"));
assert!(is_supported_name("foo.exe"));
assert!(is_supported_name("foo-x86_64-linux"));
assert!(is_supported_name("foo.gz"));
assert!(is_supported_name("foo.exe.xz"));
assert!(is_supported_name("foo.bz2"));
assert!(!is_supported_name("foo.deb"));
assert!(!is_supported_name("foo.rpm"));
assert!(!is_supported_name("foo.msi"));
}
#[test]
fn test_select_best_url() {
let ui = Ui::default();
assert_eq!(
select_best_url(&ui, "https://example.com/foo.gz", "https://example.com/foo"),
"https://example.com/foo.gz"
);
assert_eq!(
select_best_url(
&ui,
"https://example.com/foo.gz",
"https://example.com/foo.xz"
),
"https://example.com/foo.xz"
);
assert_eq!(
select_best_url(
&ui,
"https://example.com/foo-musl",
"https://example.com/foo-glibc.gz"
),
"https://example.com/foo-musl"
);
}
}