use std::path::PathBuf;
use crate::{Error, Result};
pub(crate) fn resolve_generator_binary() -> Result<PathBuf> {
if let Ok(path) = std::env::var("PEAKRDL_RUST_BINARY") {
let p = PathBuf::from(path);
if p.exists() {
return Ok(p);
}
return Err(Error::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!(
"PEAKRDL_RUST_BINARY is set to '{}' but that path does not exist",
p.display()
),
)));
}
#[cfg(not(feature = "download-bin"))]
{
Err(Error::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"PEAKRDL_RUST_BINARY is not set; please set it or enable the 'download-bin' feature",
)))
}
#[cfg(feature = "download-bin")]
{
let (asset_name, exe_name) = download::platform_info()?;
let cache_path = download::cache_binary_path(exe_name)?;
if cache_path.exists() {
return Ok(cache_path);
}
download::download_binary(asset_name, exe_name, &cache_path)?;
Ok(cache_path)
}
}
#[cfg(feature = "download-bin")]
pub(crate) mod download {
use crate::{Error, Result};
use flate2::read::GzDecoder;
use sha2::Digest as _;
use std::{
io::{Read as _, Write},
path::PathBuf,
};
use tar::Archive;
const GITHUB_RELEASES_BASE: &str = "https://github.com/darsor/PeakRDL-rust/releases/download";
const PLATFORM_ASSETS: &[(&str, &str, &str)] = &[
("linux", "x86_64", "peakrdl-rust-linux-x86_64.tar.gz"),
("linux", "aarch64", "peakrdl-rust-linux-aarch64.tar.gz"),
("macos", "x86_64", "peakrdl-rust-darwin-x86_64.tar.gz"),
("macos", "aarch64", "peakrdl-rust-darwin-aarch64.tar.gz"),
("windows", "x86_64", "peakrdl-rust-windows-x86_64.tar.gz"),
];
const fn peakrdl_rust_version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
pub(crate) fn platform_info() -> Result<(&'static str, &'static str)> {
let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
for &(p_os, p_arch, asset) in PLATFORM_ASSETS {
if os == p_os && arch == p_arch {
let exe = if os == "windows" {
"peakrdl-rust.exe"
} else {
"peakrdl-rust"
};
return Ok((asset, exe));
}
}
Err(Error::UnsupportedPlatform { os, arch })
}
pub(crate) fn cache_binary_path(exe_name: &str) -> Result<PathBuf> {
let dir = dirs::cache_dir()
.ok_or(Error::NoCacheDir)?
.join("peakrdl-rust-build")
.join(peakrdl_rust_version());
std::fs::create_dir_all(&dir)?;
Ok(dir.join(exe_name))
}
pub(crate) fn download_binary(asset_name: &str, exe_name: &str, dest: &PathBuf) -> Result<()> {
let expected_sha256 = fetch_checksum(asset_name)?;
let url = format!(
"{}/v{}/{}",
GITHUB_RELEASES_BASE,
peakrdl_rust_version(),
asset_name
);
eprintln!("cargo:warning=Downloading peakrdl-rust binary from {url}");
let response = ureq::get(&url).call().map_err(|e| Error::DownloadFailed {
url: url.clone(),
reason: e.to_string(),
})?;
let mut archive_bytes: Vec<u8> = Vec::new();
response
.into_body()
.into_reader()
.read_to_end(&mut archive_bytes)
.map_err(|e| Error::DownloadFailed {
url: url.clone(),
reason: e.to_string(),
})?;
let actual_sha256 = hex::encode(sha2::Sha256::digest(&archive_bytes));
if actual_sha256 != expected_sha256 {
return Err(Error::ChecksumMismatch {
expected: expected_sha256,
actual: actual_sha256,
});
}
let cursor = std::io::Cursor::new(archive_bytes);
let mut archive = Archive::new(GzDecoder::new(cursor));
let mut binary_data: Option<Vec<u8>> = None;
for entry in archive.entries().map_err(Error::Io)? {
let mut entry = entry.map_err(Error::Io)?;
let path = entry.path().map_err(Error::Io)?;
if path.file_name().is_some_and(|n| n == exe_name) {
let mut data = Vec::new();
std::io::Read::read_to_end(&mut entry, &mut data)?;
binary_data = Some(data);
break;
}
}
let binary_data = binary_data.ok_or_else(|| {
Error::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("'{exe_name}' not found in downloaded archive"),
))
})?;
let tmp = dest.with_extension("tmp");
{
let mut f = std::fs::File::create(&tmp)?;
f.write_all(&binary_data)?;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&tmp)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&tmp, perms)?;
}
std::fs::rename(&tmp, dest)?;
Ok(())
}
fn fetch_checksum(asset_name: &str) -> Result<String> {
let url = format!(
"{}/v{}/{}.sha256",
GITHUB_RELEASES_BASE,
peakrdl_rust_version(),
asset_name
);
let response = ureq::get(&url).call().map_err(|e| Error::DownloadFailed {
url: url.clone(),
reason: e.to_string(),
})?;
let body = response
.into_body()
.read_to_string()
.map_err(|e| Error::DownloadFailed {
url: url.clone(),
reason: e.to_string(),
})?;
body.split_whitespace()
.next()
.ok_or_else(|| Error::DownloadFailed {
url,
reason: "checksum file was empty or malformed".to_string(),
})
.map(str::to_string)
}
}