use crate::Error;
use std::path::{Path, PathBuf};
#[cfg(not(target_os = "windows"))]
const RENDERER_BIN: &str = "plushie-renderer";
#[cfg(target_os = "windows")]
const RENDERER_BIN: &str = "plushie-renderer.exe";
fn target_dir() -> PathBuf {
std::env::var_os("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join("target")
})
}
fn download_name() -> String {
let ext = if cfg!(target_os = "windows") {
".exe"
} else {
""
};
let os = if cfg!(target_os = "linux") {
"linux"
} else if cfg!(target_os = "macos") {
"darwin"
} else if cfg!(target_os = "windows") {
"windows"
} else {
"unknown"
};
let arch = if cfg!(target_arch = "x86_64") {
"x86_64"
} else if cfg!(target_arch = "aarch64") {
"aarch64"
} else {
"unknown"
};
format!("plushie-renderer-{os}-{arch}{ext}")
}
pub(crate) fn discover_renderer() -> Result<String, Error> {
let resolved = resolve_candidate()?;
validate_architecture(Path::new(&resolved));
Ok(resolved)
}
fn resolve_candidate() -> Result<String, Error> {
if let Ok(path) = std::env::var("PLUSHIE_BINARY_PATH") {
let trimmed = path.trim();
if !trimmed.is_empty() {
if Path::new(trimmed).is_file() {
return Ok(trimmed.to_string());
}
return Err(Error::BinaryNotFound {
hint: format!("PLUSHIE_BINARY_PATH is set to `{trimmed}` but no file exists there"),
});
}
}
let target = target_dir();
for profile in ["release", "debug"] {
if let Some(path) = find_custom_build_binary(&target, profile) {
return Ok(path);
}
}
let download_path = target.join("plushie/bin").join(download_name());
if is_executable(&download_path) {
return Ok(download_path.to_string_lossy().into_owned());
}
if let Some(path) = find_on_path(RENDERER_BIN) {
return Ok(path);
}
Err(Error::BinaryNotFound {
hint: not_found_message(),
})
}
#[cfg(unix)]
fn validate_architecture(path: &Path) {
use std::process::Command;
let output = match Command::new("file").arg(path).output() {
Ok(o) if o.status.success() => o,
_ => return,
};
let stdout = String::from_utf8_lossy(&output.stdout);
let detected = detect_arch(&stdout);
let expected = std::env::consts::ARCH;
let expected_canonical = canonicalize_arch(expected);
if let (Some(got), Some(expected)) = (detected, expected_canonical)
&& got != expected
{
log::warn!(
"architecture mismatch: binary `{}` is {got}, host is {expected}. \
Rebuild for the correct architecture or set PLUSHIE_BINARY_PATH \
to the matching binary.",
path.display()
);
}
}
#[cfg(not(unix))]
fn validate_architecture(_path: &Path) {}
#[cfg(any(unix, test))]
fn detect_arch(output: &str) -> Option<&'static str> {
let lower = output.to_ascii_lowercase();
if lower.contains("x86-64") || lower.contains("x86_64") || lower.contains("amd64") {
Some("x86_64")
} else if lower.contains("aarch64") || lower.contains("arm64") {
Some("aarch64")
} else {
None
}
}
#[cfg(any(unix, test))]
fn canonicalize_arch(arch: &str) -> Option<&'static str> {
match arch {
"x86_64" => Some("x86_64"),
"aarch64" => Some("aarch64"),
_ => None,
}
}
fn find_custom_build_binary(target: &Path, profile: &str) -> Option<String> {
let profile_dir = target.join("plushie-renderer/target").join(profile);
if !profile_dir.is_dir() {
return None;
}
let ext = if cfg!(target_os = "windows") {
"exe"
} else {
""
};
let entries = std::fs::read_dir(&profile_dir).ok()?;
for entry in entries.flatten() {
let path = entry.path();
if !is_executable(&path) {
continue;
}
let has_ext = path.extension().map(|e| e.to_string_lossy().into_owned());
let matches_ext = if ext.is_empty() {
has_ext.as_deref() != Some("rlib") && has_ext.as_deref() != Some("d")
} else {
has_ext.as_deref() == Some(ext)
};
if matches_ext {
return Some(path.to_string_lossy().into_owned());
}
}
None
}
fn find_on_path(name: &str) -> Option<String> {
let path_var = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&path_var) {
let candidate = dir.join(name);
if is_executable(&candidate) {
return Some(candidate.to_string_lossy().into_owned());
}
}
None
}
#[cfg(unix)]
fn is_executable(path: &std::path::Path) -> bool {
use std::os::unix::fs::PermissionsExt;
match std::fs::metadata(path) {
Ok(meta) => meta.is_file() && (meta.permissions().mode() & 0o111) != 0,
Err(_) => false,
}
}
#[cfg(not(unix))]
fn is_executable(path: &std::path::Path) -> bool {
path.is_file()
}
pub(crate) fn not_found_message() -> String {
format!(
"renderer binary not found. Try one of:\n \
cargo plushie build (widget-aware custom build)\n \
cargo plushie download (precompiled stock binary)\n \
cargo install plushie-renderer (build stock from source)\n\
or set PLUSHIE_BINARY_PATH to an existing binary. Searched \
PLUSHIE_BINARY_PATH, target/plushie-renderer/target/{{release,debug}}/, \
target/plushie/bin/{download_name}, and PATH for `{RENDERER_BIN}`.",
download_name = download_name()
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_executable_says_no_for_missing_file() {
assert!(!is_executable(std::path::Path::new(
"/nonexistent/plushie-renderer-xyz"
)));
}
#[test]
fn find_on_path_returns_none_for_missing_binary() {
let needle = "plushie-renderer-definitely-not-installed-xyz-12345";
assert!(find_on_path(needle).is_none());
}
#[test]
fn not_found_message_mentions_all_install_paths() {
let msg = not_found_message();
assert!(msg.contains("cargo plushie build"));
assert!(msg.contains("cargo plushie download"));
assert!(msg.contains("cargo install plushie-renderer"));
assert!(msg.contains("PLUSHIE_BINARY_PATH"));
}
#[test]
fn download_name_is_well_formed() {
let name = download_name();
assert!(name.starts_with("plushie-renderer-"));
}
#[test]
fn detect_arch_recognises_x86_64_variants() {
let samples = [
"ELF 64-bit LSB pie executable, x86-64, version 1",
"ELF 64-bit LSB executable, x86_64, version 1 (GNU/Linux)",
"Mach-O 64-bit executable x86_64",
"Mach-O universal binary with 2 architectures: [x86_64] [arm64]",
"PE32+ executable (console) x86-64, for MS Windows",
];
for sample in samples {
let trimmed = sample.replace("arm64", "");
assert_eq!(detect_arch(&trimmed), Some("x86_64"), "sample: {sample}");
}
}
#[test]
fn detect_arch_recognises_aarch64_variants() {
let samples = [
"ELF 64-bit LSB executable, ARM aarch64, version 1 (GNU/Linux)",
"Mach-O 64-bit executable arm64",
];
for sample in samples {
assert_eq!(detect_arch(sample), Some("aarch64"), "sample: {sample}");
}
}
#[test]
fn detect_arch_returns_none_for_unknown_output() {
assert_eq!(detect_arch("this is not an executable"), None);
assert_eq!(detect_arch(""), None);
}
#[test]
fn canonicalize_arch_known_values() {
assert_eq!(canonicalize_arch("x86_64"), Some("x86_64"));
assert_eq!(canonicalize_arch("aarch64"), Some("aarch64"));
assert_eq!(canonicalize_arch("riscv64"), None);
}
}