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 placing the kernel at the high "negative
74    // 2 GiB" VA. Adjacent to the guest crate's `src/`.
75    let link_script = manifest_dir.join("link.x");
76    let link_script_arg = format!("-Clink-args=-T{}", link_script.display());
77    // Non-PIE: with a fixed link base we don't need relocations, and
78    // R_X86_64_RELATIVE entries from a PIE binary would carry the
79    // wrong runtime address (the host applies them against the GPA
80    // load_addr, not the high GVA).
81    let rustflags = [
82        "--cfg=hyperlight",
83        "--check-cfg=cfg(hyperlight)",
84        "-Clink-args=-eentrypoint",
85        link_script_arg.as_str(),
86        "-Crelocation-model=static",
87        // The x86_64-unknown-none target defaults to the `kernel`
88        // code model, which assumes the kernel sits in the high-half
89        // (`0xFFFF_FFFF_8000_0000+`) where R_X86_64_32S sign-extension
90        // does the right thing. We now link the guest at a low-half VA
91        // (`0x5001_4000_0000`), which is too far above 2 GiB for the
92        // small/kernel models — switch to `large` to emit 64-bit
93        // absolute relocations everywhere.
94        "-Ccode-model=large",
95        // Smallest valid panic strategy for no_std bin
96        "-Cpanic=abort",
97    ]
98    .join("\x1f");
99
100    let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
101    let mut cmd = Command::new(cargo);
102    cmd.arg("build")
103        .arg("--release")
104        .arg("--manifest-path")
105        .arg(&manifest_path)
106        .arg("--target")
107        .arg(TARGET_TRIPLE)
108        .arg("--bin")
109        .arg(bin_name)
110        .env("CARGO_TARGET_DIR", &target_dir)
111        .env("BUILD_CRATE_GUEST_BUILD", "1")
112        .env("CARGO_ENCODED_RUSTFLAGS", rustflags);
113    if !features.is_empty() {
114        cmd.arg("--features").arg(features.join(","));
115    }
116
117    let output = cmd
118        .output()
119        .expect("failed to spawn cargo for nub arch guest");
120
121    if !output.status.success() {
122        let stderr = String::from_utf8_lossy(&output.stderr);
123        let stdout = String::from_utf8_lossy(&output.stdout);
124        panic!(
125            "nub arch guest build failed for {}:\n--- stderr ---\n{}\n--- stdout ---\n{}",
126            manifest_dir.display(),
127            stderr,
128            stdout
129        );
130    }
131
132    assert!(
133        elf_path.exists(),
134        "Expected ELF artifact not found at: {}",
135        elf_path.display()
136    );
137    elf_path
138}