Skip to main content

nub_build/
lib.rs

1//! build.rs helper: cross-compile a nub bare-metal Arch guest crate
2//! for a stable bare-metal target (today: `x86_64-unknown-none`) and
3//! return the path to the resulting ELF.
4//!
5//! Today's only consumer is `nub-arch-x86`. As we add more
6//! bare-metal Arch backends (e.g. an arm/riscv guest), they live here
7//! too — this crate owns the cross-compile recipe for all of nub's
8//! bare-metal arch guests.
9//!
10//! Modeled on `build-javm`, but with two simplifications:
11//!
12//! 1. **No custom target JSON.** We use the upstream stable target
13//!    `x86_64-unknown-none` (shipped since Rust 1.71). The
14//!    `core`/`alloc`/`compiler_builtins` come pre-built; no
15//!    `-Zbuild-std` needed.
16//!
17//! 2. **No C toolchain.** Hyperlight guests that link picolibc need
18//!    a full cross-clang setup (which is why `cargo-hyperlight`
19//!    exists). Our guests use `hyperlight-guest-bin` with
20//!    `default-features = false`, dropping the picolibc dependency
21//!    entirely — pure Rust, no cc-rs, no bindgen.
22//!
23//! What `cargo-hyperlight` does that we replicate as `RUSTFLAGS`:
24//!
25//! - `--cfg=hyperlight` + `--check-cfg=cfg(hyperlight)` — the
26//!   hyperlight guest crates gate some code on this cfg.
27//! - `-Clink-args=-eentrypoint` — make the symbol `entrypoint` the
28//!   ELF entry point.
29//!
30//! What `cargo-hyperlight` does that we skip:
31//!
32//! - Building the sysroot (`-Zbuild-std`) — unnecessary for stable
33//!   `x86_64-unknown-none`.
34//! - Setting `CC_…`, `AR_…`, `CFLAGS_…` — unnecessary without C.
35
36use std::path::PathBuf;
37use std::process::Command;
38
39const TARGET_TRIPLE: &str = "x86_64-unknown-none";
40
41/// Cross-compile a hyperlight guest crate. Returns the path to the
42/// resulting ELF binary, suitable for `include_bytes!` or for
43/// passing to `hyperlight_host::GuestBinary::FilePath`.
44///
45/// `manifest_dir` is relative to the calling `build.rs`'s
46/// `CARGO_MANIFEST_DIR`. `bin_name` is the `[[bin]]` name to build.
47/// `features` is forwarded to cargo as `--features <comma-joined>`;
48/// pass `&[]` for no extras.
49///
50/// Emits `cargo:rerun-if-changed` for the guest crate's `src/` and
51/// `Cargo.toml`, plus `cargo:rerun-if-env-changed` for
52/// `SKIP_GUEST_BUILD`. Respects the `BUILD_CRATE_GUEST_BUILD` env
53/// var as a recursion guard (mirrors `build-javm`).
54pub fn build(manifest_dir: &str, bin_name: &str, features: &[&str]) -> PathBuf {
55    let manifest_dir = build_crate::resolve_manifest_dir(manifest_dir);
56    let manifest_path = manifest_dir.join("Cargo.toml");
57
58    build_crate::emit_rerun_for_dir(&manifest_dir.join("src"));
59    println!("cargo:rerun-if-changed={}", manifest_path.display());
60    println!("cargo:rerun-if-env-changed=SKIP_GUEST_BUILD");
61
62    let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
63    let target_dir = PathBuf::from(&out_dir).join("nub-arch-guest-build");
64    let elf_path = target_dir
65        .join(TARGET_TRIPLE)
66        .join("release")
67        .join(bin_name);
68
69    if std::env::var("SKIP_GUEST_BUILD").is_ok() && elf_path.exists() {
70        return elf_path;
71    }
72
73    // Custom linker script. Adjacent to the guest crate's `src/`.
74    let link_script = manifest_dir.join("link.x");
75    let link_script_arg = format!("-Clink-args=-T{}", link_script.display());
76    // The guest is a PIE (DYN ELF) linked at VA 0. The host loader
77    // (`nub-host-kvm/src/mem/elf.rs::load_at`) patches each
78    // `R_X86_64_RELATIVE` entry with `runtime_base_va + addend`, where
79    // `runtime_base_va = guest_va_base() + KERNEL_OFFSET` from
80    // `nub-host-common::layout` — env-overridable on Linux, dynamic
81    // on macOS. So the kernel boots wherever the host reserves the
82    // per-process GUEST_VA range, no hardcoded link base required.
83    let rustflags = [
84        "--cfg=hyperlight",
85        "--check-cfg=cfg(hyperlight)",
86        "-Clink-args=-eentrypoint",
87        // Force PIE output (DYN ELF) so absolute references emit
88        // `R_X86_64_RELATIVE` entries the host can patch at load
89        // time with the runtime base GVA. Without `-pie`, lld
90        // produces an EXEC binary with statically-resolved (to 0)
91        // absolute references, which would dereference garbage at
92        // runtime once mapped at a non-zero kernel base.
93        "-Clink-args=-pie",
94        link_script_arg.as_str(),
95        // PIC (not static): the linker -pie flag produces a DYN ELF
96        // with `R_X86_64_RELATIVE` entries for absolute references;
97        // the compiler must agree by emitting PIC-style code (so
98        // text-segment relocations can be rewritten as RELATIVE).
99        "-Crelocation-model=pic",
100        // x86_64-unknown-none defaults to the `kernel` code model,
101        // which assumes the kernel sits in the high-half
102        // (`0xFFFF_FFFF_8000_0000+`) where R_X86_64_32S sign-extension
103        // does the right thing. We load the guest at a low-half VA
104        // (typically `0x5001_4000_0000`), which is too far above 2 GiB
105        // for the small/kernel models — switch to `large` to emit
106        // 64-bit absolute relocations everywhere (the linker rewrites
107        // them as `R_X86_64_RELATIVE` in the PIE output).
108        "-Ccode-model=large",
109        // Smallest valid panic strategy for no_std bin
110        "-Cpanic=abort",
111    ]
112    .join("\x1f");
113
114    let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
115    let mut cmd = Command::new(cargo);
116    cmd.arg("build")
117        .arg("--release")
118        .arg("--manifest-path")
119        .arg(&manifest_path)
120        .arg("--target")
121        .arg(TARGET_TRIPLE)
122        .arg("--bin")
123        .arg(bin_name)
124        // NOTE: enabling LTO here (`--config profile.release.lto=…`) fails:
125        // the guest forces `-Ccode-model=large` (it loads at a low-half GVA,
126        // too far above 2 GiB for the default `kernel` model), but the
127        // *precompiled* `core`/`alloc` use the target-default `kernel`
128        // model, and LTO (fat *and* thin) refuses to merge modules with
129        // conflicting `Code Model` flags (`i32 4` vs `i32 2`). Making LTO
130        // work would require rebuilding std with the matching code model via
131        // `-Zbuild-std`, i.e. a nightly toolchain (or `RUSTC_BOOTSTRAP=1`) —
132        // a departure from this crate's deliberate stable / no-build-std
133        // design. Left disabled pending that decision.
134        .env("CARGO_TARGET_DIR", &target_dir)
135        .env("BUILD_CRATE_GUEST_BUILD", "1")
136        .env("CARGO_ENCODED_RUSTFLAGS", rustflags);
137    if !features.is_empty() {
138        cmd.arg("--features").arg(features.join(","));
139    }
140
141    let output = cmd
142        .output()
143        .expect("failed to spawn cargo for nub arch guest");
144
145    if !output.status.success() {
146        let stderr = String::from_utf8_lossy(&output.stderr);
147        let stdout = String::from_utf8_lossy(&output.stdout);
148        panic!(
149            "nub arch guest build failed for {}:\n--- stderr ---\n{}\n--- stdout ---\n{}",
150            manifest_dir.display(),
151            stderr,
152            stdout
153        );
154    }
155
156    assert!(
157        elf_path.exists(),
158        "Expected ELF artifact not found at: {}",
159        elf_path.display()
160    );
161    elf_path
162}