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()
}