use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use arcbox_constants::paths::labels;
const DAEMON_BINARY: &str = "arcbox-daemon";
pub fn current_executable() -> Result<PathBuf> {
let exe = std::env::current_exe().context("could not determine current executable")?;
std::fs::canonicalize(&exe).with_context(|| format!("could not resolve {}", exe.display()))
}
pub fn detect_bundle_xbin() -> Option<PathBuf> {
let exe = current_executable().ok()?;
let xbin = bundle_contents(&exe)?.join("MacOS/xbin");
xbin.is_dir().then_some(xbin)
}
pub fn locate_daemon(data_dir: &Path) -> Result<PathBuf> {
let exe = std::env::current_exe().context("could not determine current executable")?;
locate_daemon_from(&exe, data_dir, std::env::var_os("PATH").as_deref())
}
fn locate_daemon_from(
invoked: &Path,
data_dir: &Path,
path_var: Option<&OsStr>,
) -> Result<PathBuf> {
let exe = std::fs::canonicalize(invoked)
.with_context(|| format!("could not resolve {}", invoked.display()))?;
let candidates = daemon_candidates(&exe, data_dir, path_var);
if let Some(found) = candidates.iter().find(|candidate| candidate.is_file()) {
return Ok(found.clone());
}
bail!(
"could not find {DAEMON_BINARY}: not next to {}, not in its app bundle, not at {}, and not on PATH.\n\
Install ArcBox.app (brew install --cask arcbox), or put {DAEMON_BINARY} on PATH.",
exe.display(),
data_dir.join("bin").join(DAEMON_BINARY).display(),
)
}
fn daemon_candidates(exe: &Path, data_dir: &Path, path_var: Option<&OsStr>) -> Vec<PathBuf> {
let mut candidates = Vec::new();
if let Some(dir) = exe.parent() {
candidates.push(dir.join(DAEMON_BINARY));
}
if let Some(contents) = bundle_contents(exe) {
for label in [labels::DAEMON, labels::DEVELOPMENT_DAEMON] {
candidates.push(
contents
.join("Frameworks")
.join(format!("{label}.app"))
.join("Contents/MacOS")
.join(label),
);
candidates.push(contents.join("Helpers").join(label));
}
}
candidates.push(data_dir.join("bin").join(DAEMON_BINARY));
if let Some(path) = path_var {
candidates.extend(std::env::split_paths(path).map(|dir| dir.join(DAEMON_BINARY)));
}
candidates
}
fn bundle_contents(exe: &Path) -> Option<&Path> {
let contents = exe.parent()?.parent()?.parent()?;
(contents.file_name() == Some(OsStr::new("Contents"))).then_some(contents)
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::{Path, PathBuf};
use super::locate_daemon_from;
fn touch(path: &Path) -> PathBuf {
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, b"#!/bin/sh\n").unwrap();
path.to_path_buf()
}
fn same_file(a: &Path, b: &Path) {
assert_eq!(
fs::canonicalize(a).unwrap(),
fs::canonicalize(b).unwrap(),
"{} should be {}",
a.display(),
b.display()
);
}
#[test]
fn the_bundled_daemon_is_found_through_a_symlinked_abctl() {
let root = tempfile::tempdir().unwrap();
let contents = root.path().join("Applications/ArcBox.app/Contents");
let abctl = touch(&contents.join("MacOS/bin/abctl"));
let daemon = touch(&contents.join(
"Frameworks/com.arcboxlabs.desktop.daemon.app/Contents/MacOS/com.arcboxlabs.desktop.daemon",
));
let link = root.path().join("homebrew/bin/abctl");
fs::create_dir_all(link.parent().unwrap()).unwrap();
std::os::unix::fs::symlink(&abctl, &link).unwrap();
let data_dir = root.path().join("home/.arcbox");
let found = locate_daemon_from(&link, &data_dir, None).unwrap();
same_file(&found, &daemon);
fs::remove_file(&daemon).unwrap();
let legacy = touch(&contents.join("Helpers/com.arcboxlabs.desktop.daemon"));
same_file(
&locate_daemon_from(&link, &data_dir, None).unwrap(),
&legacy,
);
}
#[test]
fn a_sibling_daemon_wins_and_the_data_dir_and_path_are_fallbacks() {
let root = tempfile::tempdir().unwrap();
let abctl = touch(&root.path().join("release/abctl"));
let data_dir = root.path().join("home/.arcbox");
let on_path = root.path().join("path-dir");
let path_var = std::env::join_paths([&on_path]).unwrap();
let err = locate_daemon_from(&abctl, &data_dir, Some(&path_var)).unwrap_err();
let message = format!("{err:#}");
assert!(message.contains("arcbox-daemon"), "{message}");
assert!(message.contains("brew install --cask arcbox"), "{message}");
let from_path = touch(&on_path.join("arcbox-daemon"));
same_file(
&locate_daemon_from(&abctl, &data_dir, Some(&path_var)).unwrap(),
&from_path,
);
let from_data_dir = touch(&data_dir.join("bin/arcbox-daemon"));
same_file(
&locate_daemon_from(&abctl, &data_dir, Some(&path_var)).unwrap(),
&from_data_dir,
);
let sibling = touch(&root.path().join("release/arcbox-daemon"));
same_file(
&locate_daemon_from(&abctl, &data_dir, Some(&path_var)).unwrap(),
&sibling,
);
}
}