supermachine 0.5.0

Run any OCI/Docker image as a hardware-isolated microVM on macOS HVF (Linux KVM and Windows WHP in progress). Single library API, zero flags for the common case, sub-100 ms cold-restore from snapshot.
//! Locate the runtime assets that supermachine needs to start a
//! microVM: the Linux kernel image, the in-VM init shim source +
//! prebuilt binary, and the HVF entitlements plist.
//!
//! The CLI subcommands `supermachine bundle`, `supermachine
//! codesign`, `supermachine assets-path`, and `supermachine
//! entitlements-path` use these helpers. Embedders shipping a
//! `.app` bundle should call [`AssetPaths::from_app_bundle`] with
//! the `Contents/Resources/` dir of their bundle.
//!
//! ## Discovery order
//!
//! 1. `$SUPERMACHINE_ASSETS_DIR` if set (explicit override).
//! 2. The directory containing the running binary's executable (so
//!    a `.app/Contents/MacOS/your-app` finds assets in
//!    `Contents/Resources/`).
//! 3. `$HOME/.local/share/supermachine/` (release-tarball install).
//! 4. The dev-tree path `crates/supermachine/{kernel,oci,...}`
//!    relative to the binary's ancestors (cargo workspace dev).
//! 5. **Bundled fallback** — extract the kernel + init shim that
//!    rode inside this binary (via the `supermachine-kernel`
//!    crate's `KERNEL_BYTES` / `INIT_OCI_BYTES` constants) into
//!    `$XDG_DATA_HOME/supermachine/v{VERSION}/` and return that.
//!    First call writes the files; subsequent calls reuse them.
//!    This is what makes `cargo install supermachine` work with
//!    zero manual setup — the bytes are always available, even on
//!    a host that's never seen a supermachine release tarball or
//!    dev tree.
//!
//! The first match wins. Each helper returns `Option<PathBuf>` so
//! callers can compose their own fallback strategy.

use std::path::{Path, PathBuf};

/// Bundled HVF entitlements plist content. Embedded at compile
/// time so the CLI's `codesign` subcommand and `cargo-supermachine`
/// plugin work without an on-disk plist file. Customers signing
/// their own .app for distribution should write this to a temp
/// file and pass it to `codesign --entitlements`.
pub const ENTITLEMENTS_PLIST: &str =
    include_str!("../entitlements.plist");

/// Where supermachine looks for runtime assets, in priority order.
#[derive(Debug, Clone)]
pub struct AssetPaths {
    /// Path to the Linux kernel image. Required to start a VM.
    pub kernel: Option<PathBuf>,
    /// Path to the prebuilt in-VM init binary, if any. The CLI
    /// builds this from `oci/init-oci.c` on first bake if absent;
    /// release-tarball installs ship it pre-built.
    pub init_oci_bin: Option<PathBuf>,
    /// Path to the in-VM init source. Used as a fallback when the
    /// prebuilt binary isn't shipped (the CLI compiles it via zig).
    pub init_oci_src: Option<PathBuf>,
    /// Path to the prebuilt in-VM `supermachine-agent` binary
    /// (statically-linked aarch64-musl). Bake copies it into the
    /// delta layer at `/supermachine-agent`; init-oci forks and
    /// exec's it post-pivot for the in-guest exec / control RPCs.
    pub supermachine_agent: Option<PathBuf>,
}

impl AssetPaths {
    /// Auto-discover assets from the standard search paths.
    pub fn discover() -> Self {
        // Honored: $SUPERMACHINE_ASSETS_DIR
        if let Some(dir) = std::env::var_os("SUPERMACHINE_ASSETS_DIR") {
            let dir = PathBuf::from(dir);
            return Self::from_dir(&dir);
        }

        let exe = std::env::current_exe().ok();

        // .app bundle: <prefix>/Contents/MacOS/your-app  →  <prefix>/Contents/Resources/
        if let Some(exe) = exe.as_deref() {
            if let Some(macos_dir) = exe.parent() {
                if macos_dir.file_name().and_then(|s| s.to_str()) == Some("MacOS") {
                    if let Some(contents) = macos_dir.parent() {
                        let res = contents.join("Resources");
                        let probe = Self::from_dir(&res);
                        if probe.kernel.is_some() {
                            return probe;
                        }
                    }
                }
            }
        }

        // Tarball install: <prefix>/bin/supermachine + <prefix>/share/supermachine/
        if let Some(exe) = exe.as_deref() {
            for ancestor in exe.ancestors() {
                let share = ancestor.join("share/supermachine");
                if share.join("kernel").is_file() {
                    return Self::from_dir(&share);
                }
            }
        }

        // Dev tree: walk ancestors of the binary. Kernel lives in
        // the `supermachine-kernel` data crate; init shim sources
        // and prebuilt binaries live next to the main crate's `oci/`.
        if let Some(exe) = exe.as_deref() {
            for ancestor in exe.ancestors() {
                let kernel_crate = ancestor.join("crates/supermachine-kernel");
                let main_crate = ancestor.join("crates/supermachine");
                let agent_crate = ancestor.join("crates/supermachine-guest-agent");
                if kernel_crate.join("kernel").is_file() {
                    return Self {
                        kernel: Some(kernel_crate.join("kernel")),
                        init_oci_bin: Some(main_crate.join("oci/init-oci"))
                            .filter(|p| p.is_file()),
                        init_oci_src: Some(main_crate.join("oci/init-oci.c"))
                            .filter(|p| p.is_file()),
                        supermachine_agent: Some(agent_crate.join(
                            "target/aarch64-unknown-linux-musl/release/supermachine-agent",
                        ))
                        .filter(|p| p.is_file()),
                    };
                }
            }
        }

        // Final fallback: extract the bundled bytes (rode inside
        // this binary via supermachine-kernel) into a per-user
        // data dir. This is the path that makes
        // `cargo install supermachine` produce a working CLI with
        // zero manual setup.
        if let Some(dir) = ensure_bundled_extracted() {
            let probe = Self::from_dir(&dir);
            if probe.kernel.is_some() {
                return probe;
            }
        }

        Self {
            kernel: None,
            init_oci_bin: None,
            init_oci_src: None,
            supermachine_agent: None,
        }
    }

    /// Locate assets relative to a single directory (used by the
    /// `.app/Contents/Resources/` and tarball-install layouts).
    pub fn from_dir(dir: &Path) -> Self {
        Self {
            kernel: Some(dir.join("kernel")).filter(|p| p.is_file()),
            init_oci_bin: Some(dir.join("init-oci")).filter(|p| p.is_file()),
            init_oci_src: Some(dir.join("init-oci.c")).filter(|p| p.is_file()),
            supermachine_agent: Some(dir.join("supermachine-agent"))
                .filter(|p| p.is_file()),
        }
    }

    /// Locate assets inside a macOS `.app` bundle. Pass the path to
    /// the `.app` itself (not its `Contents/Resources/`).
    pub fn from_app_bundle(app: &Path) -> Self {
        Self::from_dir(&app.join("Contents/Resources"))
    }
}

/// Extract the bundled kernel + init-oci bytes (linked into this
/// binary via the `supermachine-kernel` crate) to a per-user data
/// dir, returning the dir path on success. First invocation writes
/// the files; subsequent invocations short-circuit and return the
/// same dir.
///
/// Layout:
///
/// ```text
/// $XDG_DATA_HOME/supermachine/v{VERSION}/
///     kernel
///     init-oci
/// ```
///
/// Falls back to `$HOME/.local/share/...` when XDG isn't set, and
/// gives up (returns `None`) when neither HOME nor XDG_DATA_HOME
/// resolves — caller will then surface a "kernel not found" error
/// the same way it would on any other host without assets.
///
/// Errors writing the files (permission denied, disk full, ...)
/// are swallowed: we return `None` so the caller's error path
/// triggers cleanly. The next discover() attempt will retry.
fn ensure_bundled_extracted() -> Option<PathBuf> {
    let dir = bundled_assets_dir()?;
    let kernel_dst = dir.join("kernel");
    let init_dst = dir.join("init-oci");
    let agent_dst = dir.join("supermachine-agent");
    if kernel_dst.is_file() && init_dst.is_file() && agent_dst.is_file() {
        return Some(dir);
    }
    if std::fs::create_dir_all(&dir).is_err() {
        return None;
    }
    if !kernel_dst.is_file() {
        // Atomic write: stage to a sibling tmp file then rename.
        // Two parallel discover() calls (e.g. CLI + library in the
        // same process tree) won't tear a half-written kernel.
        let tmp = dir.join("kernel.partial");
        if std::fs::write(&tmp, supermachine_kernel::KERNEL_BYTES).is_err() {
            let _ = std::fs::remove_file(&tmp);
            return None;
        }
        if std::fs::rename(&tmp, &kernel_dst).is_err() {
            let _ = std::fs::remove_file(&tmp);
            return None;
        }
    }
    if !init_dst.is_file() {
        let tmp = dir.join("init-oci.partial");
        if supermachine_kernel::extract_init_oci_to(&tmp).is_err() {
            let _ = std::fs::remove_file(&tmp);
            return None;
        }
        if std::fs::rename(&tmp, &init_dst).is_err() {
            let _ = std::fs::remove_file(&tmp);
            return None;
        }
    }
    if !agent_dst.is_file() {
        let tmp = dir.join("supermachine-agent.partial");
        if supermachine_kernel::extract_supermachine_agent_to(&tmp).is_err() {
            let _ = std::fs::remove_file(&tmp);
            return None;
        }
        if std::fs::rename(&tmp, &agent_dst).is_err() {
            let _ = std::fs::remove_file(&tmp);
            return None;
        }
    }
    Some(dir)
}

/// Resolve `$XDG_DATA_HOME/supermachine/v{VERSION}/`, falling back
/// to `$HOME/.local/share/supermachine/v{VERSION}/`. Versioned so
/// upgrading supermachine doesn't reuse stale assets — each crate
/// version gets its own dir.
fn bundled_assets_dir() -> 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"))),
    )
}