use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
const VERSION: &str = "0.5.0";
const BASE_URL: &str = "https://radar.sanixdk.xyz/downloads";
const LINUX_X86_64_SHA256: &str =
"73f8faa5f0558c91fb8e2966005579c66c76ddb1198a8aec10f850c6d116f2d2";
const LINUX_X86_64_ASSET: &str = "radar-v0.5.0-linux-x86_64-73f8faa5.tar.gz";
fn target() -> Result<&'static str, String> {
match (env::consts::OS, env::consts::ARCH) {
("linux", "x86_64") => Ok("linux-x86_64"),
pair => Err(format!("radar has no release for {}-{}", pair.0, pair.1)),
}
}
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 run_at(directory: &Path, program: &str, arguments: &[&str]) -> Result<(), String> {
let status = Command::new(program)
.current_dir(directory)
.args(arguments)
.status()
.map_err(|error| format!("cannot run {program}: {error}"))?;
if status.success() {
Ok(())
} else {
Err(format!("{program} failed with {status}"))
}
}
fn verify(archive: &Path, checksum: &Path) -> Result<(), String> {
let archive = archive
.to_str()
.ok_or_else(|| "archive path is not UTF-8".to_string())?;
let checksum = checksum
.to_str()
.ok_or_else(|| "checksum path is not UTF-8".to_string())?;
let directory = archive
.rsplit_once('/')
.map_or(".", |(directory, _)| directory);
let name = archive.rsplit('/').next().unwrap_or(archive);
let copied = Path::new(directory).join(format!("{name}.sha256"));
fs::copy(checksum, &copied).map_err(|error| format!("cannot stage checksum: {error}"))?;
let checksum_name = copied
.file_name()
.and_then(|value| value.to_str())
.ok_or_else(|| "checksum filename is not UTF-8".to_string())?;
let result = if Command::new("sha256sum").arg("--version").output().is_ok() {
run_at(Path::new(directory), "sha256sum", &["-c", checksum_name])
} else {
run_at(
Path::new(directory),
"shasum",
&["-a", "256", "-c", checksum_name],
)
};
let _ = fs::remove_file(copied);
result?;
let expected = match target()? {
"linux-x86_64" => LINUX_X86_64_SHA256,
_ => return Err("release hash is unavailable for this target".to_string()),
};
let output = Command::new("sha256sum")
.arg(archive)
.output()
.map_err(|error| format!("cannot compute embedded checksum: {error}"))?;
if !output.status.success() {
return Err("sha256sum failed while checking embedded hash".to_string());
}
let actual = String::from_utf8_lossy(&output.stdout)
.split_whitespace()
.next()
.unwrap_or("")
.to_string();
if actual == expected {
Ok(())
} else {
Err(format!(
"embedded checksum mismatch: expected {expected}, got {actual}"
))
}
}
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 = match target {
"linux-x86_64" => LINUX_X86_64_ASSET.to_string(),
_ => return Err("release asset is unavailable for this target".to_string()),
};
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)?;
run(
"tar",
&["-xzf", archive_path, "-C", temporary.to_str().unwrap_or("")],
)?;
let extracted = temporary.join("radar");
if !extracted.is_file() {
return Err("release archive does not contain radar".to_string());
}
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{VERSION}/{target}/radar"));
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() {
if env::consts::OS == "linux" && env::consts::ARCH == "x86_64" {
assert_eq!(target().unwrap(), "linux-x86_64");
assert_eq!(LINUX_X86_64_SHA256.len(), 64);
assert!(
LINUX_X86_64_SHA256
.bytes()
.all(|byte| byte.is_ascii_hexdigit())
);
}
}
#[test]
fn version_matches_package_metadata() {
assert_eq!(VERSION, env!("CARGO_PKG_VERSION"));
}
}