use std::{
env::consts::{ARCH, OS},
fs,
ops::RangeInclusive,
path::{Path, PathBuf},
};
use semver::{Version, VersionReq};
use url::Url;
use crate::{
Cacher, ClangTool, DownloadError,
downloader::{download, hashing::HashAlgorithm},
utils::lock_path,
};
#[derive(Debug, thiserror::Error)]
pub enum StaticDistDownloadError {
#[error("Failed to download static binary: {0}")]
DownloadError(#[from] DownloadError),
#[error("The requested version does not match any available versions")]
UnsupportedVersion,
#[error("The static binaries are not built for {OS} {ARCH} architecture")]
UnsupportedArchitecture,
#[error("Failed to parse the URL: {0}")]
UrlParseError(#[from] url::ParseError),
#[error("Failed to read or write cache file: {0}")]
IoError(#[from] std::io::Error),
#[error("Failed to parse the SHA512 sum file")]
Sha512Corruption,
}
const MIN_CLANG_TOOLS_VERSION: &str = env!("MIN_CLANG_TOOLS_VERSION");
pub(crate) const MAX_CLANG_TOOLS_VERSION: &str = env!("MAX_CLANG_TOOLS_VERSION");
const CLANG_TOOLS_REPO: &str = "https://github.com/cpp-linter/clang-tools-static-binaries";
const CLANG_TOOLS_TAG: &str = env!("CLANG_TOOLS_TAG");
pub struct StaticDistDownloader;
impl Cacher for StaticDistDownloader {}
impl StaticDistDownloader {
pub fn get_major_version_range() -> RangeInclusive<u8> {
MIN_CLANG_TOOLS_VERSION.parse().unwrap_or(11)
..=MAX_CLANG_TOOLS_VERSION.parse().unwrap_or(22)
}
fn find_suitable_version(req_ver: &VersionReq) -> Option<Version> {
let clang_tools_versions: RangeInclusive<u8> = Self::get_major_version_range();
clang_tools_versions
.map(|v| Version::new(v as u64, 0, 0))
.rev()
.find(|ver| req_ver.matches(ver))
}
fn verify_sha512(file_path: &Path, sha512_path: &Path) -> Result<(), StaticDistDownloadError> {
let checksum_file_content = fs::read_to_string(sha512_path)?;
let expected = checksum_file_content
.split(' ')
.next()
.ok_or(StaticDistDownloadError::Sha512Corruption)?;
HashAlgorithm::Sha512(expected.to_string()).verify(file_path)?;
Ok(())
}
pub async fn download_tool(
tool: &ClangTool,
requested_version: &VersionReq,
directory: Option<&PathBuf>,
) -> Result<PathBuf, StaticDistDownloadError> {
#[cfg(any(
// Windows support is only for x86_64 architecture (for now)
all(target_os = "windows", not(target_arch = "x86_64")),
// Linux and macOS support only x86_64 and aarch64 architectures
all(
any(target_os = "linux", target_os = "macos"),
not(any(target_arch = "x86_64", target_arch = "aarch64"))
),
// Any OS other than Windows, Linux, or macOS is unsupported
not(any(target_os = "windows", target_os = "linux", target_os = "macos")),
))]
return Err(StaticDistDownloadError::UnsupportedArchitecture);
let ver = Self::find_suitable_version(requested_version)
.ok_or(StaticDistDownloadError::UnsupportedVersion)?;
let ver_str = ver.major.to_string();
let arch = if cfg!(target_arch = "aarch64") {
"arm64"
} else {
"amd64"
};
let platform = if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else {
"linux"
};
let base_url = format!(
"{CLANG_TOOLS_REPO}/releases/download/{CLANG_TOOLS_TAG}/{tool}-{ver_str}_{platform}-{arch}",
);
let suffix = if cfg!(target_os = "windows") {
".exe"
} else {
""
};
let url = Url::parse(format!("{base_url}{suffix}").as_str())?;
let cache_path = Self::get_cache_dir();
let bin_name = format!("{tool}-{ver_str}{suffix}");
let download_path = match directory {
None => cache_path.join("bin").join(&bin_name),
Some(dir) => dir.join(&bin_name),
};
let file_lock = lock_path(&download_path)?;
if download_path.exists() {
log::info!(
"Using cached static binary for {tool} version {ver_str} from {:?}",
download_path.to_string_lossy()
);
} else {
log::info!("Downloading static binary for {tool} version {ver_str} from {url}");
download(&url, &download_path, 60 * 2).await?;
#[cfg(unix)]
super::chmod_file(&download_path, None)?;
}
let sha512_cache_path = cache_path
.join("static_dist")
.join(format!("{tool}-{ver_str}.sha512"));
if sha512_cache_path.exists() {
log::info!(
"Using cached SHA512 checksum for {tool} version {ver_str} from {:?}",
sha512_cache_path.to_string_lossy()
);
} else {
let sha512_url = Url::parse(format!("{base_url}{suffix}.sha512sum").as_str())?;
log::info!(
"Downloading SHA512 checksum for {tool} version {ver_str} from {sha512_url}"
);
download(&sha512_url, &sha512_cache_path, 10).await?;
}
Self::verify_sha512(&download_path, &sha512_cache_path)?;
file_lock.unlock()?;
Ok(download_path)
}
}
#[cfg(test)]
mod tests {
use super::StaticDistDownloader;
use semver::VersionReq;
#[test]
fn find_none() {
let req_ver = VersionReq::parse("=8").unwrap();
let suitable_version = StaticDistDownloader::find_suitable_version(&req_ver);
assert_eq!(suitable_version, None);
}
}