mod archive;
mod binstall;
mod checksum;
mod github;
mod gitlab;
mod quickinstall;
use std::{
collections::HashSet,
path::{Path, PathBuf},
};
use snafu::{IntoError, ResultExt};
use self::archive::{
ArchiveFormat, extract_binary_at_archive_relative_path, extract_binary_by_candidate_names,
};
pub(super) use self::{
binstall::BinstallProvider, github::GithubProvider, gitlab::GitlabProvider,
quickinstall::QuickinstallProvider,
};
use crate::{
Result, bin_resolver::ConclusiveResolution, config::BinaryProvider, crate_resolver::ResolvedCrate,
downloader::DownloadedCrate, error, target::TargetTriple,
};
pub(super) trait Provider {
fn try_resolve(&self, krate: &DownloadedCrate, target: &TargetTriple) -> Result<ConclusiveResolution>;
fn kind(&self) -> BinaryProvider;
}
fn recreate_staging_work_dir(staging_dir: &Path, krate: &ResolvedCrate) -> Result<PathBuf> {
let work_dir = staging_dir.join(format!("{}-{}", krate.name, krate.version));
if work_dir.exists() {
std::fs::remove_dir_all(&work_dir).with_context(|_| error::IoSnafu {
path: work_dir.clone(),
})?;
}
std::fs::create_dir_all(&work_dir).with_context(|_| error::IoSnafu {
path: work_dir.clone(),
})?;
Ok(work_dir)
}
fn stage_extracted_binary(
work_dir: &Path,
binary_name: &str,
host_target: &TargetTriple,
extracted_binary_path: &Path,
) -> Result<PathBuf> {
let staged_path = work_dir.join(format!("{}{}", binary_name, host_target.binary_ext()));
std::fs::rename(extracted_binary_path, &staged_path).with_context(|_| error::RenameFileSnafu {
src: extracted_binary_path.to_path_buf(),
dst: staged_path.clone(),
})?;
Ok(staged_path)
}
fn provider_asset_preparation_failed(
provider: BinaryProvider,
url: &str,
source: error::Error,
) -> error::Error {
error::ProviderAssetPreparationFailedSnafu {
provider,
url: url.to_string(),
}
.into_error(Box::new(source))
}
fn expected_binary_names(
primary: &str,
matched_candidate_name: Option<&str>,
crate_name: &str,
) -> Vec<String> {
let mut names = Vec::new();
push_expected_binary_name(&mut names, primary);
if let Some(name) = matched_candidate_name {
push_expected_binary_name(&mut names, name);
}
push_expected_binary_name(&mut names, crate_name);
names
}
fn push_expected_binary_name(names: &mut Vec<String>, name: &str) {
if !name.is_empty() && !names.iter().any(|existing| existing == name) {
names.push(name.to_string());
}
}
struct CandidateFilename {
pub filename: String,
pub binary_basename: String,
pub format: ArchiveFormat,
pub target: TargetTriple,
}
fn generate_candidate_filenames(
crate_name: &str,
extra_binary_names: &[&str],
version: &str,
target: &TargetTriple,
) -> Vec<CandidateFilename> {
let formats = ArchiveFormat::all_formats();
let platform_groups = target.compatible_asset_platform_alias_groups();
let mut names: Vec<String> = Vec::new();
names.push(crate_name.to_string());
names.extend(extra_binary_names.iter().map(|&n| n.to_string()));
if target.is_windows() {
names.push(format!("{}.exe", crate_name));
names.extend(extra_binary_names.iter().map(|n| format!("{}.exe", n)));
}
let mut candidates = Vec::new();
for (group_target, platform_tokens) in &platform_groups {
for name in &names {
for platform_token in platform_tokens {
for &(format, suffix) in formats {
push_candidate_patterns(
&mut candidates,
name,
version,
platform_token.as_ref(),
format,
suffix,
group_target,
);
}
}
}
}
let mut seen = HashSet::new();
candidates.retain(|c| seen.insert(c.filename.clone()));
candidates
}
fn push_candidate_patterns(
candidates: &mut Vec<CandidateFilename>,
name: &str,
version: &str,
platform: &str,
format: ArchiveFormat,
suffix: &str,
target: &TargetTriple,
) {
let patterns = [
format!("{}-{}-v{}{}", name, platform, version, suffix),
format!("{}-{}-{}{}", name, platform, version, suffix),
format!("{}-v{}-{}{}", name, version, platform, suffix),
format!("{}-{}-{}{}", name, version, platform, suffix),
format!("{}_{}_v{}{}", name, platform, version, suffix),
format!("{}_{}_{}{}", name, platform, version, suffix),
format!("{}_v{}_{}{}", name, version, platform, suffix),
format!("{}_{}_{}{}", name, version, platform, suffix),
format!("{}-{}{}", name, platform, suffix),
format!("{}_{}{}", name, platform, suffix),
];
candidates.extend(patterns.into_iter().map(|filename| CandidateFilename {
filename,
binary_basename: name.to_string(),
format,
target: target.clone(),
}));
}
fn generate_candidate_tags(crate_name: &str, version: &str) -> Vec<String> {
vec![
format!("v{}", version),
version.to_string(),
format!("{}-v{}", crate_name, version),
format!("{}-{}", crate_name, version),
format!("{}/v{}", crate_name, version),
format!("{}/{}", crate_name, version),
]
}
fn tag_url_path_segment(tag: &str) -> String {
tag.replace('/', "%2F")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testdata::target_triple;
fn filenames(candidates: &[CandidateFilename]) -> Vec<&str> {
candidates.iter().map(|c| c.filename.as_str()).collect()
}
#[test]
fn binary_name_short_platform_and_naked_gz_combine() {
let target = target_triple("x86_64-unknown-linux-gnu");
let candidates = generate_candidate_filenames("taplo-cli", &["taplo"], "0.10.0", &target);
let target = candidates
.iter()
.find(|c| c.filename == "taplo-linux-x86_64.gz")
.expect("expected a taplo-linux-x86_64.gz candidate");
assert_eq!(target.format, ArchiveFormat::Gz);
}
#[test]
fn crate_name_full_triple_is_first_candidate() {
let target = target_triple("x86_64-unknown-linux-gnu");
let candidates = generate_candidate_filenames("taplo-cli", &["taplo"], "0.10.0", &target);
assert_eq!(
candidates[0].filename,
"taplo-cli-x86_64-unknown-linux-gnu-v0.10.0.tar"
);
}
#[test]
fn crate_name_candidates_precede_binary_name_candidates_within_target_group() {
let target = target_triple("x86_64-unknown-linux-gnu");
let candidates = generate_candidate_filenames("taplo-cli", &["taplo"], "0.10.0", &target);
let host_names: Vec<&str> = candidates
.iter()
.filter(|c| c.target == target)
.map(|c| c.filename.as_str())
.collect();
let last_crate = host_names
.iter()
.rposition(|n| n.starts_with("taplo-cli"))
.unwrap();
let first_binary = host_names
.iter()
.position(|n| n.starts_with("taplo-") && !n.starts_with("taplo-cli"))
.unwrap();
assert!(
last_crate < first_binary,
"within the host group, all taplo-cli candidates should precede the taplo (binary) candidates"
);
}
#[test]
fn candidates_are_deduplicated() {
let target = target_triple("x86_64-unknown-linux-gnu");
let candidates = generate_candidate_filenames("foo", &["foo"], "1.0.0", &target);
let count = candidates.len();
let mut names = filenames(&candidates);
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), count, "candidate filenames should be unique");
}
#[test]
fn matched_candidate_carries_binary_basename() {
let target = target_triple("x86_64-unknown-linux-musl");
let candidates = generate_candidate_filenames("ripgrep", &["rg"], "15.1.0", &target);
let candidate = candidates
.iter()
.find(|candidate| candidate.filename == "ripgrep-15.1.0-x86_64-unknown-linux-musl.tar.gz")
.unwrap();
assert_eq!(candidate.binary_basename, "ripgrep");
}
#[test]
fn ripgrep_15_1_0_release_asset_names_are_generated() {
let cases = [
(
"x86_64-unknown-linux-musl",
"ripgrep-15.1.0-x86_64-unknown-linux-musl.tar.gz",
ArchiveFormat::TarGz,
),
(
"aarch64-unknown-linux-gnu",
"ripgrep-15.1.0-aarch64-unknown-linux-gnu.tar.gz",
ArchiveFormat::TarGz,
),
(
"aarch64-apple-darwin",
"ripgrep-15.1.0-aarch64-apple-darwin.tar.gz",
ArchiveFormat::TarGz,
),
(
"x86_64-pc-windows-msvc",
"ripgrep-15.1.0-x86_64-pc-windows-msvc.zip",
ArchiveFormat::Zip,
),
];
for (platform, asset_name, format) in cases {
let target = target_triple(platform);
let candidates = generate_candidate_filenames("ripgrep", &["rg"], "15.1.0", &target);
let candidate = candidates
.iter()
.find(|candidate| candidate.filename == asset_name)
.unwrap();
assert_eq!(candidate.format, format);
assert_eq!(candidate.binary_basename, "ripgrep");
}
}
#[test]
fn expected_binary_names_are_deduplicated_in_priority_order() {
assert_eq!(
expected_binary_names("rg", Some("ripgrep"), "ripgrep"),
vec!["rg".to_string(), "ripgrep".to_string()]
);
assert_eq!(
expected_binary_names("ripgrep", Some("ripgrep"), "ripgrep"),
vec!["ripgrep".to_string()]
);
}
#[test]
fn windows_adds_exe_name_variant() {
let target = target_triple("x86_64-pc-windows-msvc");
let candidates = generate_candidate_filenames("mytool", &[], "1.0.0", &target);
let names = filenames(&candidates);
assert!(
names.iter().any(|n| n.starts_with("mytool.exe")),
"expected a mytool.exe-prefixed candidate on Windows"
);
}
#[test]
fn gnu_host_generates_musl_fallback_after_exact_host() {
let target = target_triple("x86_64-unknown-linux-gnu");
let candidates = generate_candidate_filenames("ripgrep", &["rg"], "15.1.0", &target);
let names = filenames(&candidates);
let gnu = names
.iter()
.position(|n| *n == "ripgrep-15.1.0-x86_64-unknown-linux-gnu.tar.gz")
.expect("expected an exact-host gnu candidate");
let musl = names
.iter()
.position(|n| *n == "ripgrep-15.1.0-x86_64-unknown-linux-musl.tar.gz")
.expect("expected a musl fallback candidate");
assert!(
gnu < musl,
"the exact-host gnu candidate must precede the musl fallback"
);
}
#[test]
fn windows_msvc_host_generates_gnu_exe_fallback() {
let target = target_triple("x86_64-pc-windows-msvc");
let candidates = generate_candidate_filenames("eza", &[], "0.23.1", &target);
let names = filenames(&candidates);
assert!(
names.iter().any(|n| *n == "eza.exe_x86_64-pc-windows-gnu.tar.gz"),
"expected an eza.exe_x86_64-pc-windows-gnu.tar.gz fallback candidate"
);
}
#[test]
fn macos_host_generates_universal_fallback() {
let target = target_triple("aarch64-apple-darwin");
let candidates = generate_candidate_filenames("mytool", &[], "1.0.0", &target);
let names = filenames(&candidates);
assert!(
names.iter().any(|n| n.contains("universal-apple-darwin")),
"expected a universal-apple-darwin fallback candidate"
);
}
#[test]
fn exact_host_candidates_precede_fallback_target_candidates() {
let target = target_triple("x86_64-unknown-linux-gnu");
let candidates = generate_candidate_filenames("ripgrep", &["rg"], "15.1.0", &target);
let names = filenames(&candidates);
let host_binary_name = names
.iter()
.position(|n| *n == "rg-15.1.0-x86_64-unknown-linux-gnu.tar.gz")
.expect("expected an exact-host binary-name candidate");
let fallback_crate_name = names
.iter()
.position(|n| *n == "ripgrep-15.1.0-x86_64-unknown-linux-musl.tar.gz")
.expect("expected a musl fallback crate-name candidate");
assert!(
host_binary_name < fallback_crate_name,
"exact-host candidate (index {host_binary_name}) must precede fallback-target candidate (index \
{fallback_crate_name})"
);
}
#[test]
fn aarch64_target_generates_arm64_spelling_candidates() {
let target = target_triple("aarch64-apple-darwin");
let candidates = generate_candidate_filenames("foo", &[], "1.0.0", &target);
let names = filenames(&candidates);
assert!(
names.iter().any(|n| *n == "foo-arm64-darwin.tar.gz"),
"expected a foo-arm64-darwin.tar.gz candidate"
);
}
#[test]
fn candidate_tags_are_generated_in_priority_order() {
assert_eq!(
generate_candidate_tags("cargo-nextest", "0.9.140"),
vec![
"v0.9.140",
"0.9.140",
"cargo-nextest-v0.9.140",
"cargo-nextest-0.9.140",
"cargo-nextest/v0.9.140",
"cargo-nextest/0.9.140",
]
);
}
#[test]
fn tag_url_path_segment_encodes_only_slash() {
assert_eq!(
tag_url_path_segment("cargo-nextest/v1.2.3"),
"cargo-nextest%2Fv1.2.3"
);
assert_eq!(tag_url_path_segment("cargo-nextest-1.2.3"), "cargo-nextest-1.2.3");
assert_eq!(tag_url_path_segment("v1.0.0+build.1"), "v1.0.0+build.1");
}
}