Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/heimdall

guests/verify-bundle/tools/guest_build.rs

// SPDX-License-Identifier: AGPL-3.0-only
//! Building the guest for wasm32 and turning it into a component, shared by
//! the tests (tests/common/guests.rs) and the release tools (examples/dist.rs,
//! examples/sign.rs), so the modules the tests load and the modules a release
//! ships come from one recipe. Heimdall is a defensive sandbox.
//!
//! Four builds of this crate: the single-instance `job` guest (default
//! features), the split VERIFIER (`--no-default-features --features split`,
//! no C), the DECODER (`--features decoder`) and the TEST-ONLY lying decoder
//! (`--features evil-decoder`, its mode patched into the module per test).
//! Each build has its own target directory, so they never rebuild each other.

#![allow(dead_code)]

use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::Command;

/// Which module.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Build {
    Job,
    Verifier,
    Decoder,
    EvilDecoder,
}

/// The three builds a release ships, in the order a release lists them.
pub const RELEASE: [Build; 3] = [Build::Job, Build::Verifier, Build::Decoder];

impl Build {
    pub fn args(self) -> &'static [&'static str] {
        match self {
            Build::Job => &[],
            Build::Verifier => &["--no-default-features", "--features", "split"],
            Build::Decoder => &["--features", "decoder"],
            Build::EvilDecoder => &["--features", "evil-decoder"],
        }
    }

    pub fn dir(self) -> &'static str {
        match self {
            Build::Job => "guest-wasm",
            Build::Verifier => "guest-wasm-verifier",
            Build::Decoder => "guest-wasm-decoder",
            Build::EvilDecoder => "guest-wasm-evil",
        }
    }

    /// The component's file name in a release. The test-only lying decoder
    /// has none: it is never shipped.
    pub fn release_name(self) -> Option<&'static str> {
        match self {
            Build::Job => Some("heimdall-verify-bundle.wasm"),
            Build::Verifier => Some("heimdall-verify-bundle-verifier.wasm"),
            Build::Decoder => Some("heimdall-verify-bundle-decoder.wasm"),
            Build::EvilDecoder => None,
        }
    }
}

/// Build one core module for wasm32-unknown-unknown with its own target
/// directory under `target_root`; returns the module's bytes. The caller's
/// `RUSTFLAGS` are replaced by the [`remaps`] only, so the bytes depend on
/// the sources, the lock and the toolchain only — not on the checkout path,
/// `CARGO_HOME`, vendoring or the target directory (build.rs maps the C
/// sources' paths away the same way, see `-ffile-prefix-map`).
pub fn build_core(b: Build, target_root: &Path) -> Vec<u8> {
    build_core_in(Path::new(env!("CARGO_MANIFEST_DIR")), &[], b, target_root)
}

/// [`build_core`] for the guest crate checked out at `crate_dir`, with
/// `env` set on every cargo and rustc call (e.g. another `CARGO_HOME`).
pub fn build_core_in(
    crate_dir: &Path,
    env: &[(&str, &OsStr)],
    b: Build,
    target_root: &Path,
) -> Vec<u8> {
    let manifest = crate_dir.join("Cargo.toml");
    let target = target_root.join(b.dir());
    let st = cargo(env)
        .args([
            "build",
            "--release",
            "--locked",
            "--lib",
            "--target",
            "wasm32-unknown-unknown",
            "--manifest-path",
        ])
        .arg(&manifest)
        .args(b.args())
        .arg("--target-dir")
        .arg(&target)
        .env_remove("RUSTFLAGS")
        .env(
            "CARGO_ENCODED_RUSTFLAGS",
            rustflags(&remaps(crate_dir, env, &target)),
        )
        .status()
        .expect("cargo could not be started");
    assert!(st.success(), "building the {b:?} guest for wasm32 failed");
    std::fs::read(target.join("wasm32-unknown-unknown/release/heimdall_guest_verify_bundle.wasm"))
        .unwrap()
}

/// Where a dependency's sources appear inside a module:
/// `<DEP_ROOT>/<name>-<version>`, wherever cargo found them (registry,
/// git checkout, a vendor directory with or without versioned names, a
/// path). `/stage` is the release build's own layout (dwarves-slsa stages
/// every crate there with versioned names), so a release maps onto itself
/// and a plain `cargo run --example dist` builds the release's bytes.
pub const DEP_ROOT: &str = "/stage";
/// The guest crate's own directory inside a module (and in build.rs's C).
pub const CRATE_ROOT: &str = "heimdall-guest-verify-bundle";

/// The `--remap-path-prefix` pairs for building the guest at `crate_dir`
/// into `target`: every package of the wasm32 build (from `cargo metadata`)
/// to [`DEP_ROOT`]`/<name>-<version>` (the guest itself to [`CRATE_ROOT`]),
/// then as backstops `CARGO_HOME`, the rustc sysroot and the target
/// directory to fixed names. Shortest prefix first: rustc applies the LAST
/// matching pair, so the most specific one wins.
pub fn remaps(crate_dir: &Path, env: &[(&str, &OsStr)], target: &Path) -> Vec<(PathBuf, String)> {
    let manifest = crate_dir.join("Cargo.toml");
    let out = cargo(env)
        .args([
            "metadata",
            "--format-version",
            "1",
            "--locked",
            "--filter-platform",
            "wasm32-unknown-unknown",
            "--manifest-path",
        ])
        .arg(&manifest)
        .stderr(std::process::Stdio::inherit())
        .output()
        .expect("cargo metadata could not be started");
    assert!(
        out.status.success(),
        "cargo metadata failed for {}",
        manifest.display()
    );
    let meta: serde_json::Value = serde_json::from_slice(&out.stdout).expect("cargo metadata JSON");
    let root = meta["resolve"]["root"].as_str().map(str::to_owned);
    let mut pairs: Vec<(PathBuf, String)> = Vec::new();
    for p in meta["packages"]
        .as_array()
        .expect("cargo metadata packages")
    {
        let dir = Path::new(p["manifest_path"].as_str().unwrap())
            .parent()
            .unwrap()
            .to_path_buf();
        let to = if p["id"].as_str().map(str::to_owned) == root {
            CRATE_ROOT.to_owned()
        } else {
            format!(
                "{DEP_ROOT}/{}-{}",
                p["name"].as_str().unwrap(),
                p["version"].as_str().unwrap()
            )
        };
        pairs.push((dir, to));
    }
    let var = |k: &str| -> Option<PathBuf> {
        env.iter()
            .find(|(n, _)| *n == k)
            .map(|(_, v)| PathBuf::from(v))
            .or_else(|| std::env::var_os(k).map(PathBuf::from))
    };
    if let Some(home) = var("CARGO_HOME").or_else(|| var("HOME").map(|h| h.join(".cargo"))) {
        pairs.push((home, "/cargo-home".to_owned()));
    }
    let rustc = var("RUSTC").unwrap_or_else(|| PathBuf::from("rustc"));
    let mut sysroot = Command::new(rustc);
    for (k, v) in env {
        sysroot.env(k, v);
    }
    if let Ok(o) = sysroot.args(["--print", "sysroot"]).output() {
        let s = String::from_utf8_lossy(&o.stdout).trim().to_owned();
        if o.status.success() && !s.is_empty() {
            pairs.push((PathBuf::from(s), "/rustc-sysroot".to_owned()));
        }
    }
    pairs.push((target.to_path_buf(), "target".to_owned()));
    // Shortest first, a prefix listed once (its last mapping kept).
    pairs.reverse();
    let mut seen = std::collections::HashSet::new();
    pairs.retain(|(from, _)| seen.insert(from.clone()));
    pairs.sort_by_key(|(from, _)| from.as_os_str().len());
    pairs
}

/// The remaps as `CARGO_ENCODED_RUSTFLAGS` (0x1f-separated, so a path may
/// hold spaces).
fn rustflags(pairs: &[(PathBuf, String)]) -> String {
    pairs
        .iter()
        .map(|(from, to)| format!("--remap-path-prefix={}={to}", from.display()))
        .collect::<Vec<_>>()
        .join("\x1f")
}

fn cargo(env: &[(&str, &OsStr)]) -> Command {
    let mut c = Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()));
    for (k, v) in env {
        c.env(k, v);
    }
    c
}

/// A core module turned into a component (wit-component, pinned in the lock).
pub fn componentize(core: &[u8]) -> Vec<u8> {
    wit_component::ComponentEncoder::default()
        .module(core)
        .unwrap()
        .validate(true)
        .encode()
        .unwrap()
}