bux-sys 0.1.2

Raw FFI bindings to libkrun — lightweight VM engine for sandboxed code execution
Documentation
//! Build script for bux-sys.
//!
//! 1. Locates or downloads the pre-built `libkrun` dynamic library.
//! 2. Optionally runs `bindgen` to regenerate Rust bindings (feature `regenerate`).
//! 3. Configures the linker for dynamic linking.
//!
//! # Environment variables
//!
//! - `BUX_DEPS_DIR` — Path to a local directory containing pre-built libraries.
//!   When set, skips downloading. Primary flow for local development.
//!
//! - `BUX_DEPS_VERSION` — Override the deps release version to download.
//!   Defaults to the crate version from `Cargo.toml`.
//!
//! - `BUX_UPDATE_BINDINGS` — When set alongside the `regenerate` feature, the
//!   freshly generated `bindings.rs` is copied back to `src/bindings.rs` so it
//!   can be committed to the repository.

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

/// Header URL pinned to a release tag on the containers/libkrun fork.
/// Version **must** match `LIBKRUN_VERSION` in `.github/workflows/deps-build.yml`.
#[cfg(feature = "regenerate")]
const HEADER_URL: &str =
    "https://raw.githubusercontent.com/containers/libkrun/v1.17.4/include/libkrun.h";

/// GitHub repository for downloading pre-built library releases.
const GITHUB_REPO: &str = "qntx/bux";

fn main() {
    println!("cargo:rerun-if-env-changed=BUX_DEPS_DIR");
    println!("cargo:rerun-if-env-changed=BUX_DEPS_VERSION");
    println!("cargo:rerun-if-env-changed=BUX_UPDATE_BINDINGS");
    println!("cargo:rerun-if-env-changed=DOCS_RS");

    // docs.rs: no network, no native libs — pre-generated bindings suffice.
    if env::var("DOCS_RS").is_ok() {
        return;
    }

    let target = env::var("TARGET").expect("TARGET not set");
    let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));

    // Optionally regenerate bindings from the remote header.
    #[cfg(feature = "regenerate")]
    {
        let header = download_header(&out_dir);
        generate_bindings(&header, &out_dir);
    }

    // Only link on supported platforms.
    if !is_supported_target(&target) {
        return;
    }

    let lib_dir = obtain_libraries(&target, &out_dir);
    println!("cargo:rustc-link-search=native={}", lib_dir.display());
    println!("cargo:rustc-link-lib=dylib=krun");
    println!("cargo:LIB_DIR={}", lib_dir.display());
}

// ===========================================================================
// Bindings generation (only compiled with `regenerate` feature)
// ===========================================================================

/// Download `libkrun.h` from the pinned fork into `$OUT_DIR`.
#[cfg(feature = "regenerate")]
fn download_header(out_dir: &Path) -> PathBuf {
    let path = out_dir.join("libkrun.h");
    if path.exists() {
        return path;
    }

    eprintln!("bux-sys: downloading header from {HEADER_URL}");
    let resp = ureq::get(HEADER_URL)
        .call()
        .unwrap_or_else(|e| panic!("Failed to download libkrun.h: {e}"));

    let mut buf = Vec::new();
    std::io::Read::read_to_end(&mut resp.into_body().into_reader(), &mut buf)
        .expect("Failed to read header");
    fs::write(&path, &buf).expect("Failed to write libkrun.h");
    path
}

/// Run `bindgen` on the header to produce `$OUT_DIR/bindings.rs`.
#[cfg(feature = "regenerate")]
fn generate_bindings(header: &Path, out_dir: &Path) {
    let out_file = out_dir.join("bindings.rs");

    let bindings = bindgen::Builder::default()
        .header(header.to_str().expect("path is not valid UTF-8"))
        .use_core()
        .allowlist_function("krun_.*")
        .allowlist_var("KRUN_.*")
        .allowlist_var("NET_.*")
        .allowlist_var("COMPAT_.*")
        .allowlist_var("VIRGLRENDERER_.*")
        .derive_debug(true)
        .derive_default(true)
        .derive_eq(true)
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        .generate()
        .expect("bindgen failed to generate bindings from libkrun.h");

    bindings
        .write_to_file(&out_file)
        .expect("Failed to write bindings.rs");

    // Copy back to src/ when requested, so it can be committed.
    if env::var("BUX_UPDATE_BINDINGS").is_ok() {
        let manifest =
            PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"));
        let committed = manifest.join("src").join("bindings.rs");
        fs::copy(&out_file, &committed).expect("Failed to copy bindings.rs to src/");
        println!(
            "cargo:warning=Updated committed bindings: {}",
            committed.display()
        );
    }
}

// ===========================================================================
// Library acquisition
// ===========================================================================

/// Obtain the pre-built dynamic library — local directory or GitHub Releases.
fn obtain_libraries(target: &str, out_dir: &Path) -> PathBuf {
    if let Ok(dir) = env::var("BUX_DEPS_DIR") {
        eprintln!("bux-sys: using local deps: {dir}");
        return PathBuf::from(dir);
    }

    let version = env::var("BUX_DEPS_VERSION")
        .unwrap_or_else(|_| env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION not set"));
    let lib_dir = out_dir.join("lib");

    if !lib_dir.join(lib_filename(target)).exists() {
        download_libs(&version, target, &lib_dir);
    }
    lib_dir
}

fn is_supported_target(target: &str) -> bool {
    let linux =
        target.contains("linux") && (target.contains("x86_64") || target.contains("aarch64"));
    let macos = target.contains("apple") && target.contains("aarch64");
    linux || macos
}

fn lib_filename(target: &str) -> &'static str {
    if target.contains("apple") {
        "libkrun.dylib"
    } else {
        "libkrun.so"
    }
}

fn download_libs(version: &str, target: &str, dest: &Path) {
    let url = format!(
        "https://github.com/{GITHUB_REPO}/releases/download/deps-v{version}/bux-deps-{target}.tar.gz"
    );
    eprintln!("bux-sys: downloading {url}");

    let resp = ureq::get(&url)
        .call()
        .unwrap_or_else(|e| panic!("Failed to download deps: {e}"));

    fs::create_dir_all(dest).expect("Failed to create lib dir");
    tar::Archive::new(flate2::read::GzDecoder::new(resp.into_body().into_reader()))
        .unpack(dest)
        .expect("Failed to extract archive");

    assert!(
        dest.join(lib_filename(target)).exists(),
        "Library not found after extraction. Check GitHub Release deps-v{version}."
    );
}