use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
const NATIVE_VERSION: &str = "0.5.18";
const BASE_URL: &str = "https://github.com/Sanix-Darker/radar/releases/download/v0.5.18";
const LINUX_X86_64_SHA256: &str =
"f02024f290f685cdb23ea167d89f3719cc4fdaf8d84691aabccf736f6ea8053f";
const LINUX_X86_64_ASSET: &str = "radar-v0.5.18-linux-x86_64.tar.gz";
const LINUX_AARCH64_SHA256: &str =
"c3b7ace392780d43d51e8944f1a67a64bb53a8883f911a20218a606b0369a4d3";
const LINUX_AARCH64_ASSET: &str = "radar-v0.5.18-linux-aarch64.tar.gz";
const MACOS_X86_64_SHA256: &str =
"415c4bf10938d9420682b8eb67db651516228d61ebf33e5d42c761589896986a";
const MACOS_X86_64_ASSET: &str = "radar-v0.5.18-macos-x86_64.tar.gz";
const MACOS_AARCH64_SHA256: &str =
"151b3875634cae265f74f9f72749bae5f84e8794ec6fde4d3611a1ad98416d46";
const MACOS_AARCH64_ASSET: &str = "radar-v0.5.18-macos-aarch64.tar.gz";
const WINDOWS_X86_64_SHA256: &str =
"dc3dce5669007e64c6bea469911e68267972f73cd702918e6f5749b29bcc3fb1";
const WINDOWS_X86_64_ASSET: &str = "radar-v0.5.18-windows-x86_64.zip";
const WINDOWS_AARCH64_SHA256: &str =
"4d1850a58237b3686f42661d60c77529ca178f880346a2de041d52b54874e6a5";
const WINDOWS_AARCH64_ASSET: &str = "radar-v0.5.18-windows-aarch64.zip";
fn target() -> Result<&'static str, String> {
match (env::consts::OS, env::consts::ARCH) {
("linux", "x86_64") => Ok("linux-x86_64"),
("linux", "aarch64") => Ok("linux-aarch64"),
("macos", "x86_64") => Ok("macos-x86_64"),
("macos", "aarch64") => Ok("macos-aarch64"),
("windows", "x86_64") => Ok("windows-x86_64"),
("windows", "aarch64") => Ok("windows-aarch64"),
pair => Err(format!("radar has no release for {}-{}", pair.0, pair.1)),
}
}
fn asset(target: &str) -> Result<&'static str, String> {
match target {
"linux-x86_64" => Ok(LINUX_X86_64_ASSET),
"linux-aarch64" => Ok(LINUX_AARCH64_ASSET),
"macos-x86_64" => Ok(MACOS_X86_64_ASSET),
"macos-aarch64" => Ok(MACOS_AARCH64_ASSET),
"windows-x86_64" => Ok(WINDOWS_X86_64_ASSET),
"windows-aarch64" => Ok(WINDOWS_AARCH64_ASSET),
_ => Err("release asset is unavailable for this target".to_string()),
}
}
fn expected_sha256(target: &str) -> Result<&'static str, String> {
match target {
"linux-x86_64" => Ok(LINUX_X86_64_SHA256),
"linux-aarch64" => Ok(LINUX_AARCH64_SHA256),
"macos-x86_64" => Ok(MACOS_X86_64_SHA256),
"macos-aarch64" => Ok(MACOS_AARCH64_SHA256),
"windows-x86_64" => Ok(WINDOWS_X86_64_SHA256),
"windows-aarch64" => Ok(WINDOWS_AARCH64_SHA256),
_ => Err("release hash is unavailable for this target".to_string()),
}
}
fn binary_name(target: &str) -> &'static str {
if target.starts_with("windows-") {
"radar.exe"
} else {
"radar"
}
}
fn cache_root() -> Result<PathBuf, String> {
if let Some(path) = env::var_os("XDG_CACHE_HOME") {
return Ok(PathBuf::from(path).join("rdar"));
}
env::var_os("HOME")
.map(PathBuf::from)
.map(|home| home.join(".cache/rdar"))
.ok_or_else(|| "HOME and XDG_CACHE_HOME are unset".to_string())
}
fn run(program: &str, arguments: &[&str]) -> Result<(), String> {
let status = Command::new(program)
.args(arguments)
.status()
.map_err(|error| format!("cannot run {program}: {error}"))?;
if status.success() {
Ok(())
} else {
Err(format!("{program} failed with {status}"))
}
}
fn command_sha256(program: &str, arguments: &[&str]) -> Result<String, String> {
let output = Command::new(program)
.args(arguments)
.output()
.map_err(|error| format!("cannot run {program}: {error}"))?;
if !output.status.success() {
return Err(format!("{program} failed while computing SHA-256"));
}
Ok(String::from_utf8_lossy(&output.stdout)
.split_whitespace()
.next()
.unwrap_or("")
.to_ascii_lowercase())
}
fn compute_sha256(archive: &Path) -> Result<String, String> {
let archive = archive
.to_str()
.ok_or_else(|| "archive path is not UTF-8".to_string())?;
if cfg!(windows) {
return command_sha256(
"powershell",
&[
"-NoProfile",
"-NonInteractive",
"-Command",
"(Get-FileHash -Algorithm SHA256 -LiteralPath $args[0]).Hash.ToLowerInvariant()",
archive,
],
);
}
if Command::new("sha256sum").arg("--version").output().is_ok() {
command_sha256("sha256sum", &[archive])
} else {
command_sha256("shasum", &["-a", "256", archive])
}
}
fn verify(archive: &Path, checksum: &Path, target: &str) -> Result<(), String> {
let expected = expected_sha256(target)?;
let remote = fs::read_to_string(checksum)
.map_err(|error| format!("cannot read remote checksum: {error}"))?
.split_whitespace()
.next()
.unwrap_or("")
.to_ascii_lowercase();
if remote != expected {
return Err(format!(
"remote checksum mismatch: expected {expected}, got {remote}"
));
}
let actual = compute_sha256(archive)?;
if actual == expected {
Ok(())
} else {
Err(format!(
"embedded checksum mismatch: expected {expected}, got {actual}"
))
}
}
fn extract(archive: &Path, temporary: &Path, target: &str) -> Result<PathBuf, String> {
let archive = archive
.to_str()
.ok_or_else(|| "cache path is not UTF-8".to_string())?;
let temporary = temporary
.to_str()
.ok_or_else(|| "cache path is not UTF-8".to_string())?;
if target.starts_with("windows-") {
run("tar", &["-xf", archive, "-C", temporary])?;
} else {
run("tar", &["-xzf", archive, "-C", temporary])?;
}
let extracted = Path::new(temporary).join(binary_name(target));
if extracted.is_file() {
Ok(extracted)
} else {
Err(format!(
"release archive does not contain {}",
binary_name(target)
))
}
}
fn install(binary: &Path, target: &str) -> Result<(), String> {
let parent = binary
.parent()
.ok_or_else(|| "invalid cache path".to_string())?;
fs::create_dir_all(parent).map_err(|error| format!("cannot create cache: {error}"))?;
let temporary = parent.join(format!("install-{}", std::process::id()));
if temporary.exists() {
fs::remove_dir_all(&temporary)
.map_err(|error| format!("cannot clear temporary install: {error}"))?;
}
fs::create_dir(&temporary)
.map_err(|error| format!("cannot create temporary install: {error}"))?;
let asset = asset(target)?;
let archive = temporary.join(asset);
let checksum = temporary.join(format!("{asset}.sha256.remote"));
let archive_url = format!("{BASE_URL}/{asset}");
let checksum_url = format!("{archive_url}.sha256");
let archive_path = archive
.to_str()
.ok_or_else(|| "cache path is not UTF-8".to_string())?;
let checksum_path = checksum
.to_str()
.ok_or_else(|| "cache path is not UTF-8".to_string())?;
run(
"curl",
&[
"-fsSL",
"--proto",
"=https",
"-o",
archive_path,
&archive_url,
],
)?;
run(
"curl",
&[
"-fsSL",
"--proto",
"=https",
"-o",
checksum_path,
&checksum_url,
],
)?;
verify(&archive, &checksum, target)?;
let extracted = extract(&archive, &temporary, target)?;
fs::rename(extracted, binary).map_err(|error| format!("cannot install radar: {error}"))?;
fs::remove_dir_all(temporary)
.map_err(|error| format!("cannot remove temporary install: {error}"))?;
Ok(())
}
fn execute(binary: &Path) -> Result<ExitCode, String> {
let status = Command::new(binary)
.args(env::args_os().skip(1))
.status()
.map_err(|error| format!("cannot start radar: {error}"))?;
Ok(ExitCode::from(
status.code().unwrap_or(1).clamp(0, 255) as u8
))
}
fn main() -> ExitCode {
let result = (|| {
let target = target()?;
let binary = cache_root()?.join(format!(
"v{NATIVE_VERSION}/{target}/{}",
binary_name(target)
));
if !binary.is_file() {
install(&binary, target)?;
}
execute(&binary)
})();
match result {
Ok(code) => code,
Err(error) => {
eprintln!("radar installer: {error}");
eprintln!("manual downloads: https://radar.sanixdk.xyz/#start");
ExitCode::FAILURE
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn supported_target_has_a_pinned_archive_hash() {
for target in [
"linux-x86_64",
"linux-aarch64",
"macos-x86_64",
"macos-aarch64",
"windows-x86_64",
"windows-aarch64",
] {
let hash = expected_sha256(target).unwrap();
assert_eq!(hash.len(), 64);
assert!(hash.bytes().all(|byte| byte.is_ascii_hexdigit()));
assert!(
asset(target)
.unwrap()
.starts_with(&format!("radar-v{NATIVE_VERSION}-"))
);
}
}
#[test]
fn native_version_matches_the_pinned_asset() {
for target in [
"linux-x86_64",
"linux-aarch64",
"macos-x86_64",
"macos-aarch64",
"windows-x86_64",
"windows-aarch64",
] {
assert!(
asset(target)
.unwrap()
.starts_with(&format!("radar-v{NATIVE_VERSION}-"))
);
}
assert_eq!(NATIVE_VERSION, "0.5.18");
assert_eq!(env!("CARGO_PKG_VERSION"), "0.5.18");
}
}