use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::OnceLock;
pub fn ensure_worker_signed(worker_path: &Path) -> Result<(), String> {
static IN_PROCESS_DONE: OnceLock<()> = OnceLock::new();
if IN_PROCESS_DONE.get().is_some() {
return Ok(());
}
let meta = std::fs::metadata(worker_path)
.map_err(|e| format!("stat {}: {e}", worker_path.display()))?;
let worker_size = meta.len();
let worker_mtime = meta
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0);
let sentinel_marker = format!(
"size={}\nmtime={}\npath={}\n",
worker_size,
worker_mtime,
worker_path.display()
);
if let Some(sentinel) = sentinel_path() {
if let Ok(existing) = std::fs::read_to_string(&sentinel) {
if existing == sentinel_marker {
IN_PROCESS_DONE.set(()).ok();
return Ok(());
}
}
}
let plist = std::env::temp_dir().join(format!(
"supermachine-entitlements-{}.plist",
std::process::id()
));
std::fs::write(&plist, crate::assets::ENTITLEMENTS_PLIST)
.map_err(|e| format!("write entitlements plist: {e}"))?;
let status = Command::new("codesign")
.args(["-s", "-", "--entitlements"])
.arg(&plist)
.arg("--force")
.arg(worker_path)
.stdout(Stdio::null())
.stderr(Stdio::piped())
.status();
let _ = std::fs::remove_file(&plist);
match status {
Ok(s) if s.success() => {
if let Some(sentinel) = sentinel_path() {
if let Some(parent) = sentinel.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(&sentinel, sentinel_marker);
}
IN_PROCESS_DONE.set(()).ok();
Ok(())
}
Ok(s) => Err(format!(
"codesign exited with {:?} for {}",
s.code(),
worker_path.display()
)),
Err(e) => Err(format!(
"failed to spawn codesign for {}: {e}\n\
(codesign ships with macOS by default; if missing, \
reinstall Xcode Command Line Tools)",
worker_path.display()
)),
}
}
pub fn locate_worker_bin() -> Option<PathBuf> {
if let Some(p) = std::env::var_os("SUPERMACHINE_WORKER_BIN") {
let p = PathBuf::from(p);
if p.is_file() {
return Some(p);
}
}
let exe = std::env::current_exe().ok()?;
if let Some(dir) = exe.parent() {
let sibling = dir.join("supermachine-worker");
if sibling.is_file() {
return Some(sibling);
}
}
if let Ok(canonical) = std::fs::canonicalize(&exe) {
if canonical != exe {
if let Some(dir) = canonical.parent() {
let sibling = dir.join("supermachine-worker");
if sibling.is_file() {
return Some(sibling);
}
}
for ancestor in canonical.ancestors() {
let p = ancestor.join("target/release/supermachine-worker");
if p.is_file() {
return Some(p);
}
}
}
}
for ancestor in exe.ancestors() {
let p = ancestor.join("target/release/supermachine-worker");
if p.is_file() {
return Some(p);
}
}
None
}
pub fn check_self_has_hvf_entitlement() -> Result<(), String> {
static CACHED: OnceLock<Result<(), String>> = OnceLock::new();
CACHED
.get_or_init(check_self_has_hvf_entitlement_uncached)
.clone()
}
fn check_self_has_hvf_entitlement_uncached() -> Result<(), String> {
let exe = std::env::current_exe().map_err(|e| {
format!(
"could not resolve current_exe to check HVF entitlement: {e} \
(your binary may need to be codesigned with \
`cargo supermachine build`)"
)
})?;
let output = Command::new("codesign")
.args(["--display", "--entitlements", "-", "--xml"])
.arg(&exe)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output();
let output = match output {
Ok(o) => o,
Err(_) => return Ok(()), };
if !output.status.success() {
return Err(missing_entitlement_message(&exe));
}
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.contains("com.apple.security.hypervisor") {
Ok(())
} else {
Err(missing_entitlement_message(&exe))
}
}
fn missing_entitlement_message(exe: &Path) -> String {
format!(
"this binary lacks the `com.apple.security.hypervisor` entitlement, \
so `Vm::start` cannot call `hv_vm_create` (it would return HV_DENIED).\n\
\n\
Two ways to fix:\n\
\n\
(a) Use `Image::acquire` / `Image::acquire_with` instead of \
`Vm::start`. The library spawns a pre-signed \
`supermachine-worker` subprocess that handles HVF on your \
behalf, so your own binary never calls into HVF and doesn't \
need codesigning. This is the recommended path for embedders.\n\
(b) Build your binary with the bundled cargo plugin:\n\
cargo supermachine build --release\n\
which wraps `cargo build` and codesigns the output with the \
HVF entitlement. Use this if you specifically want the \
in-process VM thread (`Vm::start`).\n\
\n\
Path: {}",
exe.display()
)
}
fn sentinel_path() -> Option<PathBuf> {
let base = if let Some(d) = std::env::var_os("XDG_DATA_HOME") {
PathBuf::from(d)
} else if let Some(h) = std::env::var_os("HOME") {
PathBuf::from(h).join(".local/share")
} else {
return None;
};
Some(
base.join("supermachine")
.join(format!("v{}", env!("CARGO_PKG_VERSION")))
.join(".worker-signed"),
)
}