use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
const NATIVE_VERSION: &str = "0.5.16";
const BASE_URL: &str = "https://github.com/Sanix-Darker/radar/releases/download/v0.5.16";
const LINUX_X86_64_SHA256: &str =
"451ca17c05fe9c5b7a86e162b992efd3c2568b29b363590f37693d50e1b8092b";
const LINUX_X86_64_ASSET: &str = "radar-v0.5.16-linux-x86_64.tar.gz";
const LINUX_AARCH64_SHA256: &str =
"6acf2b563e5987a75e96b407a029881e1ab85923cfbd26829b4066df99faab18";
const LINUX_AARCH64_ASSET: &str = "radar-v0.5.16-linux-aarch64.tar.gz";
const MACOS_X86_64_SHA256: &str =
"c22846fc8f764b85b3b2267aee739634f985c2bb6c61ee85f9204ef8a4de330a";
const MACOS_X86_64_ASSET: &str = "radar-v0.5.16-macos-x86_64.tar.gz";
const MACOS_AARCH64_SHA256: &str =
"a765c224c657215d6bc4c7e874f4a173d18ad452a9064534fe9aeae43310af8a";
const MACOS_AARCH64_ASSET: &str = "radar-v0.5.16-macos-aarch64.tar.gz";
const WINDOWS_X86_64_SHA256: &str =
"66a1c11a4e96acbaca31475eb5178917c2985b23151f1436c1c4be1ea95f3ec3";
const WINDOWS_X86_64_ASSET: &str = "radar-v0.5.16-windows-x86_64.zip";
const WINDOWS_AARCH64_SHA256: &str =
"03ccc1906a395f2dabf7f1c75bab009cea3e053492f0d73d4ca67e1ced56c0da";
const WINDOWS_AARCH64_ASSET: &str = "radar-v0.5.16-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.16");
assert_eq!(env!("CARGO_PKG_VERSION"), "0.5.16");
}
}