use std::ffi::OsString;
use std::path::{Path, PathBuf};
pub const APR_BIN_ENV: &str = "APR_BIN";
#[must_use]
pub fn apr_binary() -> PathBuf {
resolve(std::env::var_os(APR_BIN_ENV), std::env::current_exe().ok())
}
#[must_use]
pub fn resolve(override_var: Option<OsString>, current_exe: Option<PathBuf>) -> PathBuf {
if let Some(explicit) = override_var {
if !explicit.is_empty() {
return PathBuf::from(explicit);
}
}
if let Some(exe) = current_exe {
if is_apr_binary(&exe) {
return exe;
}
}
PathBuf::from("apr")
}
fn is_apr_binary(path: &Path) -> bool {
path.file_stem().is_some_and(|stem| stem == "apr")
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn write_marker_bin(path: &Path, marker: &str) {
let mut f = std::fs::File::create(path).expect("create marker bin");
writeln!(f, "#!/bin/sh").expect("shebang");
writeln!(f, "echo {marker}").expect("body");
f.sync_all().expect("sync");
drop(f);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(path).expect("stat").permissions();
perms.set_mode(0o755);
std::fs::set_permissions(path, perms).expect("chmod");
}
}
#[cfg(unix)]
fn exec_marker_bin(path: &Path) -> std::process::Output {
const ETXTBSY: i32 = 26;
let mut last = String::new();
for _ in 0..100 {
match std::process::Command::new(path).output() {
Ok(out) => return out,
Err(e) if e.raw_os_error() == Some(ETXTBSY) => {
last = e.to_string();
std::thread::sleep(std::time::Duration::from_millis(10));
}
Err(e) => panic!("spawn {}: {e}", path.display()),
}
}
panic!(
"spawn {} still busy after 100 attempts: {last}",
path.display()
);
}
#[test]
#[cfg(unix)]
fn exec_marker_bin_survives_a_transient_etxtbsy() {
let dir = scratch_dir("etxtbsy");
let shim = dir.join("apr");
write_marker_bin(&shim, "RETRY-MARKER");
let held = std::fs::OpenOptions::new()
.write(true)
.open(&shim)
.expect("hold a write fd open");
let direct = std::process::Command::new(&shim).output();
assert_eq!(
direct.err().and_then(|e| e.raw_os_error()),
Some(26),
"an open write fd must make a direct spawn fail with ETXTBSY; without that \
the retry loop is guarding nothing"
);
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(50));
drop(held);
});
let out = exec_marker_bin(&shim);
assert_eq!(
String::from_utf8_lossy(&out.stdout).trim(),
"RETRY-MARKER",
"the helper must retry through ETXTBSY and then run the shim"
);
}
fn scratch_dir(name: &str) -> PathBuf {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let dir = std::env::temp_dir().join(format!(
"aprender-mcp-apr-bin-{name}-{}-{nonce}",
std::process::id()
));
std::fs::create_dir_all(&dir).expect("mkdir scratch");
dir
}
#[test]
#[cfg(unix)]
fn resolution_executes_the_current_executable_not_a_path_lookup() {
let dir = scratch_dir("self");
let self_apr = dir.join("apr");
write_marker_bin(&self_apr, "SELF-BINARY-UNDER-TEST");
let resolved = resolve(None, Some(self_apr.clone()));
assert_eq!(
resolved,
self_apr,
"resolution must return the running executable, got {}",
resolved.display()
);
let out = exec_marker_bin(&resolved);
assert_eq!(
String::from_utf8_lossy(&out.stdout).trim(),
"SELF-BINARY-UNDER-TEST",
"the resolved program must be the current executable"
);
}
#[test]
fn resolution_keeps_the_directory_of_the_current_executable() {
let a = PathBuf::from("/opt/release-0.63.0/bin/apr");
let b = PathBuf::from("/home/user/.local/bin/apr");
assert_eq!(resolve(None, Some(a.clone())), a);
assert_eq!(resolve(None, Some(b.clone())), b);
assert_ne!(resolve(None, Some(a)), b);
}
#[test]
fn non_apr_host_process_falls_back_to_the_path_name() {
let harness = PathBuf::from("/w/target/debug/deps/aprender_mcp-1a2b3c4d");
assert_eq!(resolve(None, Some(harness)), PathBuf::from("apr"));
assert_eq!(resolve(None, None), PathBuf::from("apr"));
}
#[test]
fn similar_names_are_not_treated_as_apr() {
for name in ["apr-cli", "aprender", "apr_serve", "aprx"] {
let exe = PathBuf::from("/usr/bin").join(name);
assert_eq!(
resolve(None, Some(exe)),
PathBuf::from("apr"),
"{name} must not be mistaken for the apr binary"
);
}
}
#[test]
fn exe_suffix_is_recognised() {
let exe = PathBuf::from("/Program Files/apr/apr.exe");
assert_eq!(resolve(None, Some(exe.clone())), exe);
}
#[test]
fn explicit_override_wins_and_empty_is_ignored() {
let exe = PathBuf::from("/opt/bin/apr");
assert_eq!(
resolve(Some(OsString::from("/mock/apr")), Some(exe.clone())),
PathBuf::from("/mock/apr")
);
assert_eq!(resolve(Some(OsString::new()), Some(exe.clone())), exe);
}
}