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}