From 43f4f6222f74313bcb8a4097075fa75537efe71b Mon Sep 17 00:00:00 2001 From: Simon Davies Date: Tue, 8 Sep 2026 13:51:29 +0100 Subject: [PATCH 1/7] Simplify custom runtime builds with cargo-hyperlight Build and embed custom runtimes from their Cargo manifests, keeping compiler configuration in the host build script. Remove the prebuilt runtime override and simplify the consumer documentation and test recipes. BREAKING CHANGE: HYPERLIGHT_JS_RUNTIME_PATH is no longer supported. Use HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH to select a custom runtime crate. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Simon Davies --- Justfile | 28 +-- README.md | 2 + docs/extending-runtime.md | 183 +++++++--------- src/hyperlight-js/Cargo.toml | 2 + src/hyperlight-js/build.rs | 243 ++++++++++++++++------ src/hyperlight-js/tests/native_modules.rs | 44 +++- src/hyperlight-js/tests/runtime_build.rs | 83 ++++++++ 7 files changed, 390 insertions(+), 195 deletions(-) create mode 100644 src/hyperlight-js/tests/runtime_build.rs diff --git a/Justfile b/Justfile index f24f275..ab63346 100644 --- a/Justfile +++ b/Justfile @@ -167,41 +167,25 @@ test target=default-target features="": (build target) # Note: We exclude test_metrics (requires process isolation, already run by `test`) # and native_modules (requires custom guest runtime, run by `test-native-modules`) test-monitors target=default-target: - cd src/hyperlight-js && cargo test --features monitor-wall-clock,monitor-cpu-time --profile={{ if target == "debug" {"dev"} else { target } }} -- --include-ignored --skip test_metrics --skip custom_native_module --skip builtin_modules_work_with_custom --skip console_log_works_with_custom + cd src/hyperlight-js && cargo test --features monitor-wall-clock,monitor-cpu-time --profile={{ if target == "debug" {"dev"} else { target } }} -- --include-ignored --skip test_metrics --skip custom_native_module --skip builtin_modules_work_with_custom --skip console_log_works_with_custom --skip custom_globals_and_host_clock test-js-host-api target=default-target features="": (build-js-host-api target features) cd src/js-host-api && npm test # Test custom native modules: # 1. Runs the runtime crate's native_modules unit/pipeline tests (native binary) -# 2. Builds the extended_runtime fixture for the hyperlight target -# 3. Rebuilds hyperlight-js with the custom guest embedded via HYPERLIGHT_JS_RUNTIME_PATH -# 4. Runs the ignored VM integration tests -# 5. Rebuilds hyperlight-js with the default guest (unsets HYPERLIGHT_JS_RUNTIME_PATH) -# -# The build.rs in hyperlight-js has `cargo:rerun-if-env-changed=HYPERLIGHT_JS_RUNTIME_PATH` -# so setting/unsetting the env var triggers a rebuild automatically. +# 2. Builds and embeds the fixture from its manifest and runs the VM tests +# 3. Rebuilds hyperlight-js with the default guest -# Base path to the extended runtime fixture target directory -extended_runtime_target := replace(justfile_dir(), "\\", "/") + "/src/hyperlight-js-runtime/tests/fixtures/extended_runtime/target/" + guest-target - -test-native-modules target=default-target: (ensure-tools) (check-fixture-lock) (_test-native-modules-unit target) (_test-native-modules-build-guest target) (_test-native-modules-vm target) (_test-native-modules-restore target) +test-native-modules target=default-target: (check-fixture-lock) (_test-native-modules-unit target) (_test-native-modules-manifest target) (_test-native-modules-restore target) [private] _test-native-modules-unit target=default-target: cargo test --manifest-path=./src/hyperlight-js-runtime/Cargo.toml --test=native_modules --profile={{ if target == "debug" {"dev"} else { target } }} [private] -_test-native-modules-build-guest target=default-target: - cargo hyperlight build \ - --target={{ guest-target }} \ - --manifest-path src/hyperlight-js-runtime/tests/fixtures/extended_runtime/Cargo.toml \ - --profile={{ if target == "debug" {"dev"} else { target } }} \ - --target-dir src/hyperlight-js-runtime/tests/fixtures/extended_runtime/target - -[private] -_test-native-modules-vm target=default-target: - {{ set-env-command }}HYPERLIGHT_JS_RUNTIME_PATH="{{extended_runtime_target}}/{{ if target == "debug" {"debug"} else { target } }}/extended-runtime" {{ if os() == "windows" { ";" } else { "&&" } }} cargo test -p hyperlight-js --test native_modules --profile={{ if target == "debug" {"dev"} else { target } }} -- --ignored --nocapture +_test-native-modules-manifest target=default-target: + {{ set-env-command }}HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH="{{PWD}}/src/hyperlight-js-runtime/tests/fixtures/extended_runtime/Cargo.toml" {{ if os() == "windows" { ";" } else { "&&" } }} cargo test -p hyperlight-js --test native_modules --test runtime_build --profile={{ if target == "debug" {"dev"} else { target } }} -- --include-ignored --nocapture [private] _test-native-modules-restore target=default-target: diff --git a/README.md b/README.md index 83de8b0..41b82bf 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Provides a capability to run JavaScript inside of Hyperlight using quickjs as th ## Documentation +- [Custom guest runtimes](docs/extending-runtime.md) - Extend with native modules and build and embed a custom guest using cargo-hyperlight + - [Execution Monitors](docs/execution-monitors.md) - Timeout and resource limit enforcement for handler execution - [Observability](docs/observability.md) - Metrics and tracing - [Crashdumps](docs/create-and-analyse-guest-crashdumps.md) - Creating and analyzing guest crash dumps diff --git a/docs/extending-runtime.md b/docs/extending-runtime.md index 6e76911..7d61955 100644 --- a/docs/extending-runtime.md +++ b/docs/extending-runtime.md @@ -18,9 +18,15 @@ code that JavaScript handlers can `import` — without forking the runtime. 2. **`native_modules!` macro** — registers custom modules into a global registry. The runtime's `NativeModuleLoader` checks custom modules first, then falls back to built-ins (io, crypto, console, require). -3. **`HYPERLIGHT_JS_RUNTIME_PATH`** — a build-time env var that tells - `hyperlight-js` to embed your custom runtime binary instead of the - default one. +3. **Build with `cargo hyperlight`** — it discovers Hyperlight's libc + headers and configures the guest compiler and sysroot. +4. **Build and embed with the host** — point `hyperlight-js` at your custom + runtime manifest. Its build script builds the guest and embeds it at + compile time. + +Custom guests must use a compatible `hyperlight-js-runtime` and Hyperlight +version with the host library/addon. Pin the guest and host to the same +release (or git revision). ## Quick Start @@ -64,51 +70,41 @@ mod math { hyperlight_js_runtime::native_modules! { "math" => js_math, } + +hyperlight_js_runtime::custom_globals! {} ``` -That's all the Rust you write for the Hyperlight guest. The macro generates +That's the guest application code. The macro generates an `init_native_modules()` function that the `NativeModuleLoader` calls automatically on first use. Built-in modules are inherited. The lib provides -all hyperlight guest infrastructure (entry point, host function dispatch, -libc stubs) — no copying files or build scripts needed. +the guest entry point and host function dispatch. Invoke both registration +macros, even when one is empty. ### 3. Build and embed in hyperlight-js -The hyperlight target has no libc, so QuickJS needs stub headers from -`hyperlight-js-runtime/include/` and `-D__wasi__=1` to disable pthreads. -Set `HYPERLIGHT_CFLAGS` before building — the one-liner below uses -`cargo metadata` to resolve the include path from your dependency tree: +Set `HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH` to the custom crate's **absolute** +`Cargo.toml` path, then build your host project normally: ```bash -# Resolve CFLAGS from hyperlight-js-runtime's include/ directory -export HYPERLIGHT_CFLAGS=$(node -e " - var m=JSON.parse(require('child_process').execSync( - 'cargo metadata --format-version 1 --manifest-path my-custom-runtime/Cargo.toml', - {encoding:'utf8',stdio:['pipe','pipe','pipe'],maxBuffer:20*1024*1024})); - var p=m.packages.find(function(p){return p.name==='hyperlight-js-runtime'}); - if(p)console.log('-I'+require('path').join( - require('path').dirname(p.manifest_path),'include')+' -D__wasi__=1'); -") - -# Build the custom runtime for the hyperlight target -cargo hyperlight build --manifest-path my-custom-runtime/Cargo.toml --release - -# Tell hyperlight-js to embed the custom runtime (not the default one). -# The guest target matches the host architecture: x86_64-hyperlight-none on -# x86_64, aarch64-hyperlight-none on Apple Silicon and other aarch64 hosts. -# Note: macOS `uname -m` prints `arm64`, so normalise it to Rust's `aarch64`. -GUEST_ARCH=$(uname -m | sed 's/^arm64$/aarch64/') -export HYPERLIGHT_JS_RUNTIME_PATH=my-custom-runtime/target/${GUEST_ARCH}-hyperlight-none/release/my-custom-runtime - -# Rebuild hyperlight-js so the embedded runtime is updated -cargo build -p hyperlight-js --release +export HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH="$(realpath my-custom-runtime/Cargo.toml)" +cargo build --release +``` + +PowerShell: + +```powershell +$env:HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH = (Resolve-Path .\my-custom-runtime\Cargo.toml).Path +cargo build --release ``` -### 4. Use from the host +This builds your custom runtime with `cargo-hyperlight` and embeds it in the +host. No additional compiler flags or include paths need to be configured. +Re-run the host build after changing your runtime. -The host-side code is **identical** to any other `hyperlight-js` usage. -Custom native modules are transparent — they're baked into the guest -binary. Your handlers just `import` from them: +### 4. Use from the Rust host + +The host-side API is unchanged. Your custom runtime is already embedded, +and handlers simply import your modules: ```rust use hyperlight_js::{SandboxBuilder, Script}; @@ -149,7 +145,9 @@ A no-op `Host` is all that's needed — it only gets called for `.js` file imports, which native modules don't use: ```rust +#[cfg(not(hyperlight))] struct NoOpHost; +#[cfg(not(hyperlight))] impl hyperlight_js_runtime::host::Host for NoOpHost { fn resolve_module(&self, _base: String, name: String) -> anyhow::Result { anyhow::bail!("Module '{name}' not found") @@ -159,6 +157,7 @@ impl hyperlight_js_runtime::host::Host for NoOpHost { } } +#[cfg(not(hyperlight))] fn main() -> anyhow::Result<()> { let args: Vec = std::env::args().collect(); let script = std::fs::read_to_string(&args[1])?; @@ -189,98 +188,66 @@ cargo run -- handler.js '{"a":6,"b":7}' See the [extended_runtime fixture](../src/hyperlight-js-runtime/tests/fixtures/extended_runtime/) for a working example with end-to-end tests. -Run `just test-native-modules` to build the fixture for the Hyperlight -target and run the full integration tests. +Run `just test-native-modules` to build and embed the fixture. +These VM tests cover custom modules, custom globals, built-ins, and the +host-backed clock. They require a supported hypervisor. Build selection +regressions are also covered by +`cargo test -p hyperlight-js --test runtime_build`. ## Using js-host-api from a Downstream Node.js Project -If your downstream project depends on `@hyperlight/js-host-api` (the -Node.js NAPI addon) and uses a custom runtime, you **cannot** use a -published version of the addon — the published binary has the default -runtime baked in via `include_bytes!()`. You need to build the NAPI -addon from source with your custom runtime embedded. - -### Why not just `npm install`? +**If you use a custom runtime, you must build the Node.js addon from source +instead of using the published `@hyperlight-dev/js-host-api` binary.** -The `js-host-api` NAPI addon links against the `hyperlight-js` Rust crate, -which embeds the runtime binary at compile time. A published npm package -would contain a `.node` binary with the **default** runtime — your custom -native modules wouldn't be present. +### Why the published addon cannot be used -### The pattern: reuse Cargo's git checkout +The NAPI addon links against the `hyperlight-js` Rust crate, which embeds +the guest runtime using `include_bytes!()` at compile time. The published +package's `.node` binary therefore already contains the **default** runtime. +Your custom native modules are not in that binary. -Your custom runtime crate already has a Cargo dependency on -`hyperlight-js-runtime`, which causes Cargo to clone the full -`hyperlight-js` workspace into `~/.cargo/git/checkouts/`. The -`js-host-api` NAPI source is included in that checkout — no separate -git clone needed. +Running `npm install` to get the published package does not rebuild it with +your guest. Neither building your custom runtime separately nor setting +`HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH` when starting Node.js changes the +runtime inside an already-compiled addon. That variable is read during the +Rust host build, not when JavaScript creates a sandbox. -#### 1. Discover the checkout path +### Build the addon with your custom runtime -Use `cargo metadata` to find where Cargo placed the hyperlight-js -workspace: +Use a `hyperlight-js` checkout matching the release or git revision used by +your custom runtime. From the checkout root, set the custom manifest's +absolute path and build the addon: -```bash -HYPERLIGHT_DIR=$(node -e " - var m=JSON.parse(require('child_process').execSync( - 'cargo metadata --format-version 1 --manifest-path my-custom-runtime/Cargo.toml', - {encoding:'utf8',stdio:['pipe','pipe','pipe'],maxBuffer:20*1024*1024})); - var p=m.packages.find(function(p){return p.name==='hyperlight-js-runtime'}); - if(p)console.log(require('path').resolve( - require('path').dirname(p.manifest_path),'..','..')); -") -echo "$HYPERLIGHT_DIR" -# e.g. /home/you/.cargo/git/checkouts/hyperlight-js-abc123/def456 +```powershell +$env:HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH = (Resolve-Path C:\path\to\my-custom-runtime\Cargo.toml).Path +just build-js-host-api release ``` -#### 2. Build the NAPI addon with your custom runtime +On Bash, use `export HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH=/absolute/path/to/my-custom-runtime/Cargo.toml` +before the same `just` command. This builds and embeds the custom guest as +part of the addon build. -```bash -# Set HYPERLIGHT_CFLAGS for the guest build -export HYPERLIGHT_CFLAGS=$(node -e " - var m=JSON.parse(require('child_process').execSync( - 'cargo metadata --format-version 1 --manifest-path my-custom-runtime/Cargo.toml', - {encoding:'utf8',stdio:['pipe','pipe','pipe'],maxBuffer:20*1024*1024})); - var p=m.packages.find(function(p){return p.name==='hyperlight-js-runtime'}); - if(p)console.log('-I'+require('path').join( - require('path').dirname(p.manifest_path),'include')+' -D__wasi__=1'); -") - -# Build your custom runtime for the hyperlight target -cargo hyperlight build --manifest-path my-custom-runtime/Cargo.toml --release - -# Point hyperlight-js at your custom runtime binary (the guest target matches -# the host architecture — aarch64-hyperlight-none on Apple Silicon). -# Note: macOS `uname -m` prints `arm64`, so normalise it to Rust's `aarch64`. -GUEST_ARCH=$(uname -m | sed 's/^arm64$/aarch64/') -export HYPERLIGHT_JS_RUNTIME_PATH=my-custom-runtime/target/${GUEST_ARCH}-hyperlight-none/release/my-custom-runtime - -# Clean stale builds so build.rs re-embeds the runtime -cd "${HYPERLIGHT_DIR}/src/hyperlight-js" && cargo clean -p hyperlight-js - -# Build the NAPI addon from the Cargo checkout -cd "${HYPERLIGHT_DIR}" && just build release -``` - -#### 3. Symlink for npm dependency resolution - -Create a symlink so npm can resolve the addon via a stable path: - -```bash -mkdir -p deps -ln -sfn "${HYPERLIGHT_DIR}/src/js-host-api" deps/js-host-api -``` +### Use the locally built addon -In your package.json, point to js-host-api via the symlink: +Point your downstream project's npm dependency at the built checkout's +`src/js-host-api` directory, rather than a published version: ```json { "dependencies": { - "@hyperlight/js-host-api": "file:deps/js-host-api" + "@hyperlight-dev/js-host-api": "file:../hyperlight-js/src/js-host-api" } } ``` -Make sure to add `deps` to your `.gitignore` since it's a symlink to a local Cargo checkout. + +Adjust the path for your layout, then run `npm install` in the downstream +project to update its dependency and lockfile. The application must use this +locally built addon, not a previously installed published copy. + +The JavaScript API is unchanged: use the usual `SandboxBuilder`, and handlers +can import the custom modules embedded in your guest. After changing the +custom runtime, rebuild the addon and refresh the downstream installation +before restarting the application. ## API Reference diff --git a/src/hyperlight-js/Cargo.toml b/src/hyperlight-js/Cargo.toml index a7e324a..bdabc5a 100644 --- a/src/hyperlight-js/Cargo.toml +++ b/src/hyperlight-js/Cargo.toml @@ -42,6 +42,7 @@ serde_json = { version = "1.0" } serde = { version = "1.0", features = ["derive"] } [dev-dependencies] +cargo-hyperlight = "0.1.14" chrono = "0.4.45" clap = { version = "4.6", features = ["derive"] } criterion = { version = "0.8.2", features = ["html_reports"] } @@ -59,6 +60,7 @@ opentelemetry-otlp = { version = "0.32.0", default-features = false, features = opentelemetry-semantic-conventions = "0.32" plotters = { version = "0.3.5", default-features = false, features = ["svg_backend"] } rand = "0.10.1" +serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0" } tempfile = "3.27" tracing-forest = { version = "0.3.1", features = ["full"] } diff --git a/src/hyperlight-js/build.rs b/src/hyperlight-js/build.rs index 7775722..3d10ccc 100644 --- a/src/hyperlight-js/build.rs +++ b/src/hyperlight-js/build.rs @@ -26,9 +26,78 @@ limitations under the License. // The source crate for the hyperlight-js-runtime binary is obtained through cargo metadata, and obtaining the manifest_path // of the hyperlight-js-runtime dependency. +use std::ffi::OsString; use std::path::{Path, PathBuf}; use std::{env, fs}; +use serde_json::Value; + +// cargo-hyperlight supplies libc headers. QuickJS still needs threading disabled +// and the monotonic clock definitions enabled. +const QUICKJS_CFLAGS: &str = "-D__wasi__=1 -D_POSIX_MONOTONIC_CLOCK"; + +#[derive(Debug, PartialEq)] +pub(crate) enum RuntimeSource { + Default, + Manifest { path: PathBuf, bin: Option }, +} + +pub(crate) fn runtime_source( + removed_binary_override: Option, + manifest: Option, + bin: Option, +) -> Result { + let nonempty = |value: &OsString| !value.to_string_lossy().trim().is_empty(); + if removed_binary_override.filter(nonempty).is_some() { + return Err( + "HYPERLIGHT_JS_RUNTIME_PATH is no longer supported; unset it and set HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH to your custom runtime's Cargo.toml" + .into(), + ); + } + let bin = bin.filter(|value| !value.trim().is_empty()); + if let Some(path) = manifest.filter(nonempty) { + return Ok(RuntimeSource::Manifest { + path: path.into(), + bin, + }); + } + if bin.is_some() { + return Err( + "HYPERLIGHT_JS_RUNTIME_BIN requires HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH".into(), + ); + } + Ok(RuntimeSource::Default) +} + +pub(crate) fn select_binary(package: &Value, requested: Option<&str>) -> Result { + let targets = package["targets"] + .as_array() + .ok_or("Guest package has no targets in cargo metadata")?; + let binaries: Vec<&str> = targets + .iter() + .filter(|target| { + target["kind"] + .as_array() + .is_some_and(|kinds| kinds.iter().any(|kind| kind == "bin")) + }) + .filter_map(|target| target["name"].as_str()) + .collect(); + if let Some(name) = requested.or_else(|| package["default_run"].as_str()) { + if binaries.contains(&name) { + return Ok(name.to_owned()); + } + return Err(format!("Guest package has no binary target named '{name}'")); + } + match binaries.as_slice() { + [name] => Ok((*name).to_owned()), + [] => Err("Custom runtime manifest must define a binary target".into()), + _ => Err( + "Custom runtime has multiple binaries; set HYPERLIGHT_JS_RUNTIME_BIN or package.default-run" + .into(), + ), + } +} + fn main() { // Mirror the hypervisor cfg aliases used by hyperlight-host so that // `#[cfg(kvm)]` etc. mean "feature enabled *and* on the platform that @@ -154,9 +223,9 @@ fn guest_target() -> String { format!("{arch}-hyperlight-none") } -fn build_js_runtime() -> PathBuf { +fn build_js_runtime(custom: Option<(PathBuf, Option)>) -> PathBuf { let profile = env::var_os("PROFILE").unwrap(); - let guest_target = guest_target(); + let target = guest_target(); // Get the current target directory. let target_dir = find_target_dir(); @@ -164,7 +233,12 @@ fn build_js_runtime() -> PathBuf { // and would result in a deadlock let target_dir = target_dir.join("hyperlight-js-runtime"); - let manifest_path = resolve_js_runtime_manifest_path(); + let is_custom = custom.is_some(); + let (manifest_path, requested_bin) = + custom.unwrap_or_else(|| (resolve_js_runtime_manifest_path(), None)); + let manifest_path = manifest_path + .canonicalize() + .expect("JS runtime manifest must point to an existing Cargo.toml"); assert!( manifest_path.is_file(), @@ -175,61 +249,122 @@ fn build_js_runtime() -> PathBuf { .parent() .expect("expected hyperlight-js-runtime manifest path to have a parent directory"); - println!("cargo:rerun-if-changed={}", runtime_dir.display()); + let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); + let output = std::process::Command::new(cargo) + .args(["metadata", "--format-version=1"]) + .arg("--manifest-path") + .arg(&manifest_path) + .output() + .expect("Failed to inspect the JS runtime manifest"); + assert!( + output.status.success(), + "Failed to inspect the JS runtime manifest: {}", + String::from_utf8_lossy(&output.stderr) + ); + let metadata: serde_json::Value = + serde_json::from_slice(&output.stdout).expect("Invalid cargo metadata"); + let packages = metadata["packages"].as_array().expect("Missing packages"); + let package = packages + .iter() + .find(|package| { + package["manifest_path"] + .as_str() + .and_then(|path| Path::new(path).canonicalize().ok()) + .as_ref() + == Some(&manifest_path) + }) + .expect("Custom runtime manifest must identify a package, not a virtual workspace"); + let bin = + select_binary(package, requested_bin.as_deref()).unwrap_or_else(|error| panic!("{error}")); + + // Track local dependencies too, including native modules outside the guest crate. + // Do not watch entire crate directories: they may contain the nested build output. + for package in packages + .iter() + .filter(|package| package["source"].is_null()) + { + let manifest = Path::new(package["manifest_path"].as_str().unwrap()); + let dir = manifest.parent().unwrap(); + println!("cargo:rerun-if-changed={}", manifest.display()); + for entry in ["src", "build.rs", ".cargo"] { + let path = dir.join(entry); + if path.exists() { + println!("cargo:rerun-if-changed={}", path.display()); + } + } + for target in package["targets"].as_array().unwrap() { + println!( + "cargo:rerun-if-changed={}", + target["src_path"].as_str().unwrap() + ); + } + } + let workspace = Path::new(metadata["workspace_root"].as_str().unwrap()); + for entry in ["Cargo.toml", "Cargo.lock", ".cargo"] { + let path = workspace.join(entry); + if path.exists() { + println!("cargo:rerun-if-changed={}", path.display()); + } + } // the PROFILE env var unfortunately only gives us 1 bit of "dev or release" let cargo_profile = if profile == "debug" { "dev" } else { "release" }; - let stubs_inc = runtime_dir.join("include"); - let cflags = format!( - "-I{} -D__wasi__=1 -D_POSIX_MONOTONIC_CLOCK", - stubs_inc.display() - ); - - // in windows escape the backslash to make bindgen happy - // TODO(jprendes): this should probably go in cargo-hyperlight instead, where - // we already do something similar, but looks like its not enough. - let cflags = cflags.replace("\\", "\\\\"); + // Use the runtime dependency's headers, not the custom guest's directory. + // The math shim must precede the sysroot headers on aarch64. + let runtime_package = packages + .iter() + .find(|package| package["name"] == "hyperlight-js-runtime") + .expect("hyperlight-js-runtime crate not found in cargo metadata"); + let include_dir = Path::new(runtime_package["manifest_path"].as_str().unwrap()) + .parent() + .unwrap() + .join("include") + .canonicalize() + .expect("hyperlight-js-runtime include directory not found"); + println!("cargo:rerun-if-changed={}", include_dir.display()); + let cflags = format!("-I{} {QUICKJS_CFLAGS}", include_dir.display()).replace("\\", "\\\\"); let mut cargo_cmd = cargo_hyperlight::cargo().unwrap(); let cmd = cargo_cmd .arg("build") - .arg("--target") - .arg(&guest_target) .arg("--profile") .arg(cargo_profile) - .arg("-v") - // Point the guest build at its own target directory. We set this *both* as a - // `--target-dir` flag and as the `CARGO_TARGET_DIR` env var below. The flag alone - // is not enough: cargo-hyperlight >= 0.1.12 strips `--target`/`--target-dir` from - // the forwarded cargo args (intending to re-inject them as env vars) but only - // re-applies `--target`, silently dropping `--target-dir`. Without the env var the - // guest build falls back to the workspace `target/` directory, which the - // host build already holds locked, causing a permanent `.cargo-lock` deadlock. + .arg("--bin") + .arg(&bin) + .arg("--target") + .arg(&target) + // The host Cargo process holds its target directory locked. Build the + // guest separately to avoid a deadlock; cargo-hyperlight forwards this flag. .arg("--target-dir") .arg(&target_dir) .arg("--manifest-path") - .arg(manifest_path) + .arg(&manifest_path) .arg("--locked") .env_clear_cargo() - // Belt-and-braces for the cargo-hyperlight arg-stripping behaviour described above: - // an explicit env var is applied last by the wrapper and reaches the inner cargo - // intact, keeping the guest build in its own directory regardless of wrapper version. - .env("CARGO_TARGET_DIR", &target_dir) + .current_dir(runtime_dir) .env("HYPERLIGHT_CFLAGS", cflags); + // Link arguments from the runtime library's build.rs do not propagate to + // downstream binaries. Preserve its clock override for custom guest builds. + if is_custom { + let mut flags = env::var_os("RUSTFLAGS").unwrap_or_default(); + flags.push(" -Clink-arg=--wrap=clock_gettime"); + cmd.env("RUSTFLAGS", flags); + } if std::env::var("CARGO_FEATURE_TRACE_GUEST").is_ok() { - cmd.arg("--features").arg("trace_guest"); + cmd.arg("--features").arg(if is_custom { + "hyperlight-js-runtime/trace_guest" + } else { + "trace_guest" + }); } cmd.status().unwrap_or_else(|e| { panic!("Could not run `cargo build` for the js runtime: {e:?}\n{cmd:?}") }); - let resource = target_dir - .join(&guest_target) - .join(profile) - .join("hyperlight-js-runtime"); + let resource = target_dir.join(target).join(profile).join(bin); if let Ok(path) = resource.canonicalize() { path @@ -244,31 +379,21 @@ fn build_js_runtime() -> PathBuf { fn bundle_runtime() { // Always rerun if the environment variable changes, even if it's currently unset. println!("cargo:rerun-if-env-changed=HYPERLIGHT_JS_RUNTIME_PATH"); - - // `HYPERLIGHT_JS_RUNTIME_PATH` may be given as either an absolute path or a - // path relative to this build script's working directory (the - // `src/hyperlight-js` crate root). It is resolved with `canonicalize()`, - // which normalises a relative path to absolute and requires the target file - // to already exist. An absolute path is recommended to avoid any ambiguity - // about the base directory. - let js_runtime_resource = match env::var("HYPERLIGHT_JS_RUNTIME_PATH") { - Ok(path) if !path.trim().is_empty() => { - let canonical = PathBuf::from(&path) - .canonicalize() - .expect("HYPERLIGHT_JS_RUNTIME_PATH must point to a valid file"); - assert!( - canonical.is_file(), - "HYPERLIGHT_JS_RUNTIME_PATH must point to a file, not a directory: {}", - canonical.display() - ); - println!( - "cargo:warning=Using custom JS runtime: {}", - canonical.display() - ); - println!("cargo:rerun-if-changed={}", canonical.display()); - canonical - } - _ => build_js_runtime(), + println!("cargo:rerun-if-env-changed=HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH"); + println!("cargo:rerun-if-env-changed=HYPERLIGHT_JS_RUNTIME_BIN"); + + // Relative manifest paths resolve from this build script's working directory + // (the hyperlight-js crate root), not the invoking host project. Prefer an + // absolute path. build_js_runtime canonicalizes it and requires it to exist. + let source = runtime_source( + env::var_os("HYPERLIGHT_JS_RUNTIME_PATH"), + env::var_os("HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH"), + env::var("HYPERLIGHT_JS_RUNTIME_BIN").ok(), + ) + .unwrap_or_else(|error| panic!("{error}")); + let js_runtime_resource = match source { + RuntimeSource::Manifest { path, bin } => build_js_runtime(Some((path, bin))), + RuntimeSource::Default => build_js_runtime(None), }; let out_dir = env::var_os("OUT_DIR").unwrap(); diff --git a/src/hyperlight-js/tests/native_modules.rs b/src/hyperlight-js/tests/native_modules.rs index d8b3596..41e8351 100644 --- a/src/hyperlight-js/tests/native_modules.rs +++ b/src/hyperlight-js/tests/native_modules.rs @@ -17,9 +17,8 @@ limitations under the License. //! Integration tests for custom native modules in the Hyperlight VM. //! //! These tests require a custom runtime (the `extended_runtime` fixture) -//! built for the host's guest target (`x86_64-hyperlight-none` on x86_64, -//! `aarch64-hyperlight-none` on aarch64) and embedded in `hyperlight-js` via -//! `HYPERLIGHT_JS_RUNTIME_PATH`. They are marked `#[ignore]` because they +//! built and embedded in `hyperlight-js` via +//! `HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH`. They are marked `#[ignore]` because they //! cannot run with a normal `cargo test`. //! //! To run them, use: @@ -27,9 +26,7 @@ limitations under the License. //! just test-native-modules //! ``` //! -//! This recipe builds the fixture with `cargo hyperlight build`, sets the -//! env var, rebuilds `hyperlight-js` with the custom guest, and runs these -//! tests. +//! This recipe builds the custom runtime automatically during the host build. #![allow(clippy::disallowed_macros)] @@ -136,3 +133,38 @@ fn console_log_works_with_custom_native_module() { assert_eq!(result, "54"); } + +#[test] +#[ignore] +fn custom_globals_and_host_clock_work_in_vm() { + let mut sandbox = SandboxBuilder::new() + .build() + .unwrap() + .load_runtime() + .unwrap(); + sandbox + .add_handler( + "globals", + Script::from_content( + "export function handler() { return { custom: CUSTOM_GLOBAL_TEST, now: Date.now() }; }", + ), + ) + .unwrap(); + let before = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis(); + let result = sandbox + .get_loaded_sandbox() + .unwrap() + .handle_event("globals", "{}".to_owned(), None) + .unwrap(); + let after = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis(); + let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert_eq!(parsed["custom"], 42); + let now = parsed["now"].as_u64().unwrap() as u128; + assert!((before..=after).contains(&now)); +} diff --git a/src/hyperlight-js/tests/runtime_build.rs b/src/hyperlight-js/tests/runtime_build.rs new file mode 100644 index 0000000..c97a413 --- /dev/null +++ b/src/hyperlight-js/tests/runtime_build.rs @@ -0,0 +1,83 @@ +/* +Copyright 2026 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#![allow(clippy::disallowed_macros)] + +#[allow(dead_code)] +#[path = "../build.rs"] +mod build_script; + +use build_script::{runtime_source, select_binary, RuntimeSource}; +use serde_json::json; + +#[test] +fn default_and_empty_overrides_preserve_embedded_runtime() { + assert_eq!( + runtime_source(None, None, None).unwrap(), + RuntimeSource::Default + ); + assert_eq!( + runtime_source(Some(" ".into()), Some("".into()), Some(" ".into())).unwrap(), + RuntimeSource::Default + ); +} + +#[test] +fn removed_binary_override_is_rejected_instead_of_silently_using_another_runtime() { + for manifest in [None, Some("Cargo.toml".into())] { + let error = runtime_source(Some("guest".into()), manifest, None).unwrap_err(); + assert!(error.contains("HYPERLIGHT_JS_RUNTIME_PATH is no longer supported")); + assert!(error.contains("HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH")); + } +} + +#[test] +fn manifest_and_optional_binary_are_selected() { + assert_eq!( + runtime_source(None, Some("Cargo.toml".into()), Some("custom".into())).unwrap(), + RuntimeSource::Manifest { + path: "Cargo.toml".into(), + bin: Some("custom".into()) + } + ); + assert!(runtime_source(None, None, Some("custom".into())).is_err()); +} + +#[test] +fn binary_selection_ignores_libraries_and_build_scripts() { + let package = json!({"targets": [ + {"name": "build-script-build", "kind": ["custom-build"]}, + {"name": "runtime_lib", "kind": ["lib"]}, + {"name": "different-from-package-name", "kind": ["bin"]} + ]}); + assert_eq!( + select_binary(&package, None).unwrap(), + "different-from-package-name" + ); + assert!(select_binary(&package, Some("missing")).is_err()); +} + +#[test] +fn ambiguous_binaries_require_selection() { + let mut package = json!({"targets": [ + {"name": "one", "kind": ["bin"]}, {"name": "two", "kind": ["bin"]} + ]}); + assert!(select_binary(&package, None).is_err()); + package["default_run"] = json!("two"); + assert_eq!(select_binary(&package, None).unwrap(), "two"); + assert_eq!(select_binary(&package, Some("one")).unwrap(), "one"); + assert!(select_binary(&json!({"targets": []}), None).is_err()); +} From a534564cd82c6ed2e111888d11c51f96d9489962 Mon Sep 17 00:00:00 2001 From: Simon Davies Date: Tue, 8 Sep 2026 14:22:19 +0100 Subject: [PATCH 2/7] Reuse cargo metadata when building the default runtime Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Simon Davies --- src/hyperlight-js/Cargo.toml | 2 - src/hyperlight-js/build.rs | 79 ++++++++---------------- src/hyperlight-js/tests/runtime_build.rs | 30 ++++++++- 3 files changed, 54 insertions(+), 57 deletions(-) diff --git a/src/hyperlight-js/Cargo.toml b/src/hyperlight-js/Cargo.toml index bdabc5a..201eefa 100644 --- a/src/hyperlight-js/Cargo.toml +++ b/src/hyperlight-js/Cargo.toml @@ -39,7 +39,6 @@ windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_ cargo-hyperlight = "0.1.14" cfg_aliases = "0.2.1" serde_json = { version = "1.0" } -serde = { version = "1.0", features = ["derive"] } [dev-dependencies] cargo-hyperlight = "0.1.14" @@ -60,7 +59,6 @@ opentelemetry-otlp = { version = "0.32.0", default-features = false, features = opentelemetry-semantic-conventions = "0.32" plotters = { version = "0.3.5", default-features = false, features = ["svg_backend"] } rand = "0.10.1" -serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0" } tempfile = "3.27" tracing-forest = { version = "0.3.1", features = ["full"] } diff --git a/src/hyperlight-js/build.rs b/src/hyperlight-js/build.rs index 3d10ccc..683c851 100644 --- a/src/hyperlight-js/build.rs +++ b/src/hyperlight-js/build.rs @@ -129,11 +129,15 @@ fn main() { bundle_runtime(); } -fn resolve_js_runtime_manifest_path() -> PathBuf { - // Use cargo metadata to obtain information about our dependencies +fn read_cargo_metadata(manifest_path: Option<&Path>) -> Value { + // Inspect the custom guest when supplied, otherwise the host dependency graph. let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); - let output = std::process::Command::new(&cargo) - .args(["metadata", "--format-version=1"]) + let mut command = std::process::Command::new(&cargo); + command.args(["metadata", "--format-version=1"]); + if let Some(path) = manifest_path { + command.arg("--manifest-path").arg(path); + } + let output = command .output() .expect("Cargo is not installed or not found in PATH"); @@ -143,44 +147,24 @@ fn resolve_js_runtime_manifest_path() -> PathBuf { String::from_utf8_lossy(&output.stderr) ); - // Cargo metadata output is in JSON format, so we use serde_json to parse it. - // The output will look like this: - // { - // "packages": [ - // ..., - // { - // "name": "hyperlight-js-runtime", - // "manifest_path": "/path/to/hyperlight-js-runtime/Cargo.toml", - // ... - // }, - // ... - // ], - // ... - // } - // We only care about the name and manifest_path fields of the packages, so we - // define a minimal struct to deserialize the output. - #[derive(serde::Deserialize)] - struct CargoMetadata { - packages: Vec, - } - - #[derive(serde::Deserialize)] - struct CargoPackage { - name: String, - manifest_path: PathBuf, - } - - let metadata: CargoMetadata = - serde_json::from_slice(&output.stdout).expect("Failed to parse cargo metadata"); + serde_json::from_slice(&output.stdout).expect("Failed to parse cargo metadata") +} - // find the package entry for hyperlight-js-runtime and get its manifest_path - let hyperlight_js_runtime = metadata - .packages - .into_iter() - .find(|pkg| pkg.name == "hyperlight-js-runtime") +pub(crate) fn resolve_js_runtime_manifest_path(metadata: &Value) -> PathBuf { + // Reuse the host metadata to locate the default runtime. The same response + // also supplies binary targets and local dependencies for the guest build. + let hyperlight_js_runtime = metadata["packages"] + .as_array() + .expect("Missing packages in cargo metadata") + .iter() + .find(|pkg| pkg["name"] == "hyperlight-js-runtime") .expect("hyperlight-js-runtime crate not found in cargo metadata"); - hyperlight_js_runtime.manifest_path + PathBuf::from( + hyperlight_js_runtime["manifest_path"] + .as_str() + .expect("Missing hyperlight-js-runtime manifest path in cargo metadata"), + ) } fn find_target_dir() -> PathBuf { @@ -234,8 +218,9 @@ fn build_js_runtime(custom: Option<(PathBuf, Option)>) -> PathBuf { let target_dir = target_dir.join("hyperlight-js-runtime"); let is_custom = custom.is_some(); + let metadata = read_cargo_metadata(custom.as_ref().map(|(path, _)| path.as_path())); let (manifest_path, requested_bin) = - custom.unwrap_or_else(|| (resolve_js_runtime_manifest_path(), None)); + custom.unwrap_or_else(|| (resolve_js_runtime_manifest_path(&metadata), None)); let manifest_path = manifest_path .canonicalize() .expect("JS runtime manifest must point to an existing Cargo.toml"); @@ -249,20 +234,6 @@ fn build_js_runtime(custom: Option<(PathBuf, Option)>) -> PathBuf { .parent() .expect("expected hyperlight-js-runtime manifest path to have a parent directory"); - let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); - let output = std::process::Command::new(cargo) - .args(["metadata", "--format-version=1"]) - .arg("--manifest-path") - .arg(&manifest_path) - .output() - .expect("Failed to inspect the JS runtime manifest"); - assert!( - output.status.success(), - "Failed to inspect the JS runtime manifest: {}", - String::from_utf8_lossy(&output.stderr) - ); - let metadata: serde_json::Value = - serde_json::from_slice(&output.stdout).expect("Invalid cargo metadata"); let packages = metadata["packages"].as_array().expect("Missing packages"); let package = packages .iter() diff --git a/src/hyperlight-js/tests/runtime_build.rs b/src/hyperlight-js/tests/runtime_build.rs index c97a413..0db823d 100644 --- a/src/hyperlight-js/tests/runtime_build.rs +++ b/src/hyperlight-js/tests/runtime_build.rs @@ -20,9 +20,37 @@ limitations under the License. #[path = "../build.rs"] mod build_script; -use build_script::{runtime_source, select_binary, RuntimeSource}; +use build_script::{ + resolve_js_runtime_manifest_path, runtime_source, select_binary, RuntimeSource, +}; use serde_json::json; +#[test] +fn default_runtime_reuses_host_metadata_for_manifest_and_binary_selection() { + let metadata = json!({"packages": [ + {"name": "host", "manifest_path": "host/Cargo.toml"}, + { + "name": "hyperlight-js-runtime", + "manifest_path": "runtime/Cargo.toml", + "targets": [{"name": "hyperlight-js-runtime", "kind": ["bin"]}] + } + ]}); + assert_eq!( + resolve_js_runtime_manifest_path(&metadata), + std::path::PathBuf::from("runtime/Cargo.toml") + ); + assert_eq!( + select_binary(&metadata["packages"][1], None).unwrap(), + "hyperlight-js-runtime" + ); +} + +#[test] +#[should_panic(expected = "hyperlight-js-runtime crate not found in cargo metadata")] +fn missing_default_runtime_is_reported() { + resolve_js_runtime_manifest_path(&json!({"packages": []})); +} + #[test] fn default_and_empty_overrides_preserve_embedded_runtime() { assert_eq!( From dc2dc0d8d2e3d963f0216b9facd6f06c19430d07 Mon Sep 17 00:00:00 2001 From: Simon Davies Date: Tue, 8 Sep 2026 14:47:36 +0100 Subject: [PATCH 3/7] Remove low-value runtime build tests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Simon Davies --- src/hyperlight-js/build.rs | 2 +- src/hyperlight-js/tests/runtime_build.rs | 34 +++--------------------- 2 files changed, 4 insertions(+), 32 deletions(-) diff --git a/src/hyperlight-js/build.rs b/src/hyperlight-js/build.rs index 683c851..2d5a127 100644 --- a/src/hyperlight-js/build.rs +++ b/src/hyperlight-js/build.rs @@ -150,7 +150,7 @@ fn read_cargo_metadata(manifest_path: Option<&Path>) -> Value { serde_json::from_slice(&output.stdout).expect("Failed to parse cargo metadata") } -pub(crate) fn resolve_js_runtime_manifest_path(metadata: &Value) -> PathBuf { +fn resolve_js_runtime_manifest_path(metadata: &Value) -> PathBuf { // Reuse the host metadata to locate the default runtime. The same response // also supplies binary targets and local dependencies for the guest build. let hyperlight_js_runtime = metadata["packages"] diff --git a/src/hyperlight-js/tests/runtime_build.rs b/src/hyperlight-js/tests/runtime_build.rs index 0db823d..9479211 100644 --- a/src/hyperlight-js/tests/runtime_build.rs +++ b/src/hyperlight-js/tests/runtime_build.rs @@ -20,39 +20,11 @@ limitations under the License. #[path = "../build.rs"] mod build_script; -use build_script::{ - resolve_js_runtime_manifest_path, runtime_source, select_binary, RuntimeSource, -}; +use build_script::{runtime_source, select_binary, RuntimeSource}; use serde_json::json; #[test] -fn default_runtime_reuses_host_metadata_for_manifest_and_binary_selection() { - let metadata = json!({"packages": [ - {"name": "host", "manifest_path": "host/Cargo.toml"}, - { - "name": "hyperlight-js-runtime", - "manifest_path": "runtime/Cargo.toml", - "targets": [{"name": "hyperlight-js-runtime", "kind": ["bin"]}] - } - ]}); - assert_eq!( - resolve_js_runtime_manifest_path(&metadata), - std::path::PathBuf::from("runtime/Cargo.toml") - ); - assert_eq!( - select_binary(&metadata["packages"][1], None).unwrap(), - "hyperlight-js-runtime" - ); -} - -#[test] -#[should_panic(expected = "hyperlight-js-runtime crate not found in cargo metadata")] -fn missing_default_runtime_is_reported() { - resolve_js_runtime_manifest_path(&json!({"packages": []})); -} - -#[test] -fn default_and_empty_overrides_preserve_embedded_runtime() { +fn absent_or_empty_overrides_select_default_source() { assert_eq!( runtime_source(None, None, None).unwrap(), RuntimeSource::Default @@ -73,7 +45,7 @@ fn removed_binary_override_is_rejected_instead_of_silently_using_another_runtime } #[test] -fn manifest_and_optional_binary_are_selected() { +fn manifest_selects_custom_source_and_binary_requires_manifest() { assert_eq!( runtime_source(None, Some("Cargo.toml".into()), Some("custom".into())).unwrap(), RuntimeSource::Manifest { From 193e1eac3422c3c0e4ff432167d09297c4b2c6b1 Mon Sep 17 00:00:00 2001 From: Simon Davies Date: Tue, 8 Sep 2026 14:58:43 +0100 Subject: [PATCH 4/7] Require a single binary in runtime manifests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Simon Davies --- docs/extending-runtime.md | 1 + src/hyperlight-js/build.rs | 42 +++++----------------- src/hyperlight-js/tests/runtime_build.rs | 44 ++++++++++++------------ 3 files changed, 32 insertions(+), 55 deletions(-) diff --git a/docs/extending-runtime.md b/docs/extending-runtime.md index 7d61955..2503f3f 100644 --- a/docs/extending-runtime.md +++ b/docs/extending-runtime.md @@ -99,6 +99,7 @@ cargo build --release This builds your custom runtime with `cargo-hyperlight` and embeds it in the host. No additional compiler flags or include paths need to be configured. +The custom runtime manifest must define exactly one binary target. Re-run the host build after changing your runtime. ### 4. Use from the Rust host diff --git a/src/hyperlight-js/build.rs b/src/hyperlight-js/build.rs index 2d5a127..05a53a2 100644 --- a/src/hyperlight-js/build.rs +++ b/src/hyperlight-js/build.rs @@ -39,13 +39,12 @@ const QUICKJS_CFLAGS: &str = "-D__wasi__=1 -D_POSIX_MONOTONIC_CLOCK"; #[derive(Debug, PartialEq)] pub(crate) enum RuntimeSource { Default, - Manifest { path: PathBuf, bin: Option }, + Manifest { path: PathBuf }, } pub(crate) fn runtime_source( removed_binary_override: Option, manifest: Option, - bin: Option, ) -> Result { let nonempty = |value: &OsString| !value.to_string_lossy().trim().is_empty(); if removed_binary_override.filter(nonempty).is_some() { @@ -54,22 +53,13 @@ pub(crate) fn runtime_source( .into(), ); } - let bin = bin.filter(|value| !value.trim().is_empty()); if let Some(path) = manifest.filter(nonempty) { - return Ok(RuntimeSource::Manifest { - path: path.into(), - bin, - }); - } - if bin.is_some() { - return Err( - "HYPERLIGHT_JS_RUNTIME_BIN requires HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH".into(), - ); + return Ok(RuntimeSource::Manifest { path: path.into() }); } Ok(RuntimeSource::Default) } -pub(crate) fn select_binary(package: &Value, requested: Option<&str>) -> Result { +pub(crate) fn select_binary(package: &Value) -> Result { let targets = package["targets"] .as_array() .ok_or("Guest package has no targets in cargo metadata")?; @@ -82,19 +72,9 @@ pub(crate) fn select_binary(package: &Value, requested: Option<&str>) -> Result< }) .filter_map(|target| target["name"].as_str()) .collect(); - if let Some(name) = requested.or_else(|| package["default_run"].as_str()) { - if binaries.contains(&name) { - return Ok(name.to_owned()); - } - return Err(format!("Guest package has no binary target named '{name}'")); - } match binaries.as_slice() { [name] => Ok((*name).to_owned()), - [] => Err("Custom runtime manifest must define a binary target".into()), - _ => Err( - "Custom runtime has multiple binaries; set HYPERLIGHT_JS_RUNTIME_BIN or package.default-run" - .into(), - ), + _ => Err("Runtime manifest must define exactly one binary target".into()), } } @@ -207,7 +187,7 @@ fn guest_target() -> String { format!("{arch}-hyperlight-none") } -fn build_js_runtime(custom: Option<(PathBuf, Option)>) -> PathBuf { +fn build_js_runtime(custom: Option) -> PathBuf { let profile = env::var_os("PROFILE").unwrap(); let target = guest_target(); @@ -218,9 +198,8 @@ fn build_js_runtime(custom: Option<(PathBuf, Option)>) -> PathBuf { let target_dir = target_dir.join("hyperlight-js-runtime"); let is_custom = custom.is_some(); - let metadata = read_cargo_metadata(custom.as_ref().map(|(path, _)| path.as_path())); - let (manifest_path, requested_bin) = - custom.unwrap_or_else(|| (resolve_js_runtime_manifest_path(&metadata), None)); + let metadata = read_cargo_metadata(custom.as_deref()); + let manifest_path = custom.unwrap_or_else(|| resolve_js_runtime_manifest_path(&metadata)); let manifest_path = manifest_path .canonicalize() .expect("JS runtime manifest must point to an existing Cargo.toml"); @@ -245,8 +224,7 @@ fn build_js_runtime(custom: Option<(PathBuf, Option)>) -> PathBuf { == Some(&manifest_path) }) .expect("Custom runtime manifest must identify a package, not a virtual workspace"); - let bin = - select_binary(package, requested_bin.as_deref()).unwrap_or_else(|error| panic!("{error}")); + let bin = select_binary(package).unwrap_or_else(|error| panic!("{error}")); // Track local dependencies too, including native modules outside the guest crate. // Do not watch entire crate directories: they may contain the nested build output. @@ -351,7 +329,6 @@ fn bundle_runtime() { // Always rerun if the environment variable changes, even if it's currently unset. println!("cargo:rerun-if-env-changed=HYPERLIGHT_JS_RUNTIME_PATH"); println!("cargo:rerun-if-env-changed=HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH"); - println!("cargo:rerun-if-env-changed=HYPERLIGHT_JS_RUNTIME_BIN"); // Relative manifest paths resolve from this build script's working directory // (the hyperlight-js crate root), not the invoking host project. Prefer an @@ -359,11 +336,10 @@ fn bundle_runtime() { let source = runtime_source( env::var_os("HYPERLIGHT_JS_RUNTIME_PATH"), env::var_os("HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH"), - env::var("HYPERLIGHT_JS_RUNTIME_BIN").ok(), ) .unwrap_or_else(|error| panic!("{error}")); let js_runtime_resource = match source { - RuntimeSource::Manifest { path, bin } => build_js_runtime(Some((path, bin))), + RuntimeSource::Manifest { path } => build_js_runtime(Some(path)), RuntimeSource::Default => build_js_runtime(None), }; diff --git a/src/hyperlight-js/tests/runtime_build.rs b/src/hyperlight-js/tests/runtime_build.rs index 9479211..e4f9010 100644 --- a/src/hyperlight-js/tests/runtime_build.rs +++ b/src/hyperlight-js/tests/runtime_build.rs @@ -25,12 +25,9 @@ use serde_json::json; #[test] fn absent_or_empty_overrides_select_default_source() { + assert_eq!(runtime_source(None, None).unwrap(), RuntimeSource::Default); assert_eq!( - runtime_source(None, None, None).unwrap(), - RuntimeSource::Default - ); - assert_eq!( - runtime_source(Some(" ".into()), Some("".into()), Some(" ".into())).unwrap(), + runtime_source(Some(" ".into()), Some("".into())).unwrap(), RuntimeSource::Default ); } @@ -38,22 +35,20 @@ fn absent_or_empty_overrides_select_default_source() { #[test] fn removed_binary_override_is_rejected_instead_of_silently_using_another_runtime() { for manifest in [None, Some("Cargo.toml".into())] { - let error = runtime_source(Some("guest".into()), manifest, None).unwrap_err(); + let error = runtime_source(Some("guest".into()), manifest).unwrap_err(); assert!(error.contains("HYPERLIGHT_JS_RUNTIME_PATH is no longer supported")); assert!(error.contains("HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH")); } } #[test] -fn manifest_selects_custom_source_and_binary_requires_manifest() { +fn manifest_selects_custom_source() { assert_eq!( - runtime_source(None, Some("Cargo.toml".into()), Some("custom".into())).unwrap(), + runtime_source(None, Some("Cargo.toml".into())).unwrap(), RuntimeSource::Manifest { - path: "Cargo.toml".into(), - bin: Some("custom".into()) + path: "Cargo.toml".into() } ); - assert!(runtime_source(None, None, Some("custom".into())).is_err()); } #[test] @@ -64,20 +59,25 @@ fn binary_selection_ignores_libraries_and_build_scripts() { {"name": "different-from-package-name", "kind": ["bin"]} ]}); assert_eq!( - select_binary(&package, None).unwrap(), + select_binary(&package).unwrap(), "different-from-package-name" ); - assert!(select_binary(&package, Some("missing")).is_err()); } #[test] -fn ambiguous_binaries_require_selection() { - let mut package = json!({"targets": [ - {"name": "one", "kind": ["bin"]}, {"name": "two", "kind": ["bin"]} - ]}); - assert!(select_binary(&package, None).is_err()); - package["default_run"] = json!("two"); - assert_eq!(select_binary(&package, None).unwrap(), "two"); - assert_eq!(select_binary(&package, Some("one")).unwrap(), "one"); - assert!(select_binary(&json!({"targets": []}), None).is_err()); +fn runtime_requires_exactly_one_binary() { + for package in [ + json!({"targets": [{"name": "runtime_lib", "kind": ["lib"]}]}), + json!({"targets": [ + {"name": "one", "kind": ["bin"]}, {"name": "two", "kind": ["bin"]} + ]}), + json!({"default_run": "two", "targets": [ + {"name": "one", "kind": ["bin"]}, {"name": "two", "kind": ["bin"]} + ]}), + ] { + assert_eq!( + select_binary(&package).unwrap_err(), + "Runtime manifest must define exactly one binary target" + ); + } } From 6e3f6601c07ce830a17ced4e7c13df53b2ef30e4 Mon Sep 17 00:00:00 2001 From: Simon Davies Date: Tue, 8 Sep 2026 16:10:17 +0100 Subject: [PATCH 5/7] Separate runtime build helpers and scope assertion lint to release library Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Simon Davies --- src/hyperlight-js/Cargo.toml | 9 +- src/hyperlight-js/benches/benchmarks.rs | 1 - src/hyperlight-js/build.rs | 48 +------ src/hyperlight-js/clippy.toml | 6 +- .../examples/execution_stats/main.rs | 2 - src/hyperlight-js/examples/interrupt/main.rs | 2 - src/hyperlight-js/examples/metrics/main.rs | 1 - .../examples/run_handler/main.rs | 1 - src/hyperlight-js/examples/tracing/main.rs | 1 - .../examples/user_modules/main.rs | 2 - src/hyperlight-js/runtime_build.rs | 128 ++++++++++++++++++ src/hyperlight-js/src/lib.rs | 2 +- src/hyperlight-js/tests/builtin_crypto.rs | 2 - src/hyperlight-js/tests/builtin_globals.rs | 1 - src/hyperlight-js/tests/builtin_modules.rs | 2 - src/hyperlight-js/tests/execution_stats.rs | 1 - src/hyperlight-js/tests/handlers.rs | 2 - src/hyperlight-js/tests/host_functions.rs | 2 - src/hyperlight-js/tests/module_loader.rs | 2 - src/hyperlight-js/tests/monitors.rs | 1 - src/hyperlight-js/tests/native_modules.rs | 2 - src/hyperlight-js/tests/printing.rs | 2 - src/hyperlight-js/tests/runtime.rs | 2 - src/hyperlight-js/tests/runtime_build.rs | 83 ------------ src/hyperlight-js/tests/termination.rs | 2 - src/hyperlight-js/tests/user_modules.rs | 2 - 26 files changed, 144 insertions(+), 165 deletions(-) create mode 100644 src/hyperlight-js/runtime_build.rs delete mode 100644 src/hyperlight-js/tests/runtime_build.rs diff --git a/src/hyperlight-js/Cargo.toml b/src/hyperlight-js/Cargo.toml index 201eefa..c54d636 100644 --- a/src/hyperlight-js/Cargo.toml +++ b/src/hyperlight-js/Cargo.toml @@ -41,7 +41,6 @@ cfg_aliases = "0.2.1" serde_json = { version = "1.0" } [dev-dependencies] -cargo-hyperlight = "0.1.14" chrono = "0.4.45" clap = { version = "4.6", features = ["derive"] } criterion = { version = "0.8.2", features = ["html_reports"] } @@ -87,6 +86,14 @@ monitor-cpu-time = ["dep:libc", "dep:windows-sys"] [package.metadata.cargo-machete] ignored = ["hyperlight-js-runtime"] +[lints.clippy] +# lib.rs enables this restriction for release library builds, not tests or tools. +disallowed_macros = "allow" + +[[test]] +name = "runtime_build" +path = "runtime_build.rs" + [[example]] name = "run_handler" path = "examples/run_handler/main.rs" diff --git a/src/hyperlight-js/benches/benchmarks.rs b/src/hyperlight-js/benches/benchmarks.rs index 4936608..23c306f 100644 --- a/src/hyperlight-js/benches/benchmarks.rs +++ b/src/hyperlight-js/benches/benchmarks.rs @@ -15,7 +15,6 @@ limitations under the License. */ // this is benchmarks, assert macros are fine -#![allow(clippy::disallowed_macros)] use std::time::{Duration, Instant}; diff --git a/src/hyperlight-js/build.rs b/src/hyperlight-js/build.rs index 05a53a2..5f7e42c 100644 --- a/src/hyperlight-js/build.rs +++ b/src/hyperlight-js/build.rs @@ -13,7 +13,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -#![allow(clippy::disallowed_macros)] // allow assert!(..) // build.rs @@ -26,58 +25,18 @@ limitations under the License. // The source crate for the hyperlight-js-runtime binary is obtained through cargo metadata, and obtaining the manifest_path // of the hyperlight-js-runtime dependency. -use std::ffi::OsString; use std::path::{Path, PathBuf}; use std::{env, fs}; use serde_json::Value; +mod runtime_build; +use runtime_build::{runtime_source, select_binary, RuntimeSource}; + // cargo-hyperlight supplies libc headers. QuickJS still needs threading disabled // and the monotonic clock definitions enabled. const QUICKJS_CFLAGS: &str = "-D__wasi__=1 -D_POSIX_MONOTONIC_CLOCK"; -#[derive(Debug, PartialEq)] -pub(crate) enum RuntimeSource { - Default, - Manifest { path: PathBuf }, -} - -pub(crate) fn runtime_source( - removed_binary_override: Option, - manifest: Option, -) -> Result { - let nonempty = |value: &OsString| !value.to_string_lossy().trim().is_empty(); - if removed_binary_override.filter(nonempty).is_some() { - return Err( - "HYPERLIGHT_JS_RUNTIME_PATH is no longer supported; unset it and set HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH to your custom runtime's Cargo.toml" - .into(), - ); - } - if let Some(path) = manifest.filter(nonempty) { - return Ok(RuntimeSource::Manifest { path: path.into() }); - } - Ok(RuntimeSource::Default) -} - -pub(crate) fn select_binary(package: &Value) -> Result { - let targets = package["targets"] - .as_array() - .ok_or("Guest package has no targets in cargo metadata")?; - let binaries: Vec<&str> = targets - .iter() - .filter(|target| { - target["kind"] - .as_array() - .is_some_and(|kinds| kinds.iter().any(|kind| kind == "bin")) - }) - .filter_map(|target| target["name"].as_str()) - .collect(); - match binaries.as_slice() { - [name] => Ok((*name).to_owned()), - _ => Err("Runtime manifest must define exactly one binary target".into()), - } -} - fn main() { // Mirror the hypervisor cfg aliases used by hyperlight-host so that // `#[cfg(kvm)]` etc. mean "feature enabled *and* on the platform that @@ -350,6 +309,7 @@ fn bundle_runtime() { fs::write(dest_path, contents).unwrap(); println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=runtime_build.rs"); } fn bundle_dummy() { diff --git a/src/hyperlight-js/clippy.toml b/src/hyperlight-js/clippy.toml index dc4c307..7b3961a 100644 --- a/src/hyperlight-js/clippy.toml +++ b/src/hyperlight-js/clippy.toml @@ -1,5 +1,5 @@ disallowed-macros = [ - { path = "std::assert", reason = "no asserts in release builds" }, - { path = "std::assert_eq", reason = "no asserts in release builds" }, - { path = "std::assert_ne", reason = "no asserts in release builds" }, + { path = "std::assert", reason = "no asserts in release library builds" }, + { path = "std::assert_eq", reason = "no asserts in release library builds" }, + { path = "std::assert_ne", reason = "no asserts in release library builds" }, ] \ No newline at end of file diff --git a/src/hyperlight-js/examples/execution_stats/main.rs b/src/hyperlight-js/examples/execution_stats/main.rs index 42511e9..8e6685c 100644 --- a/src/hyperlight-js/examples/execution_stats/main.rs +++ b/src/hyperlight-js/examples/execution_stats/main.rs @@ -30,8 +30,6 @@ limitations under the License. //! Or via Just: //! just run-examples -#![allow(clippy::disallowed_macros)] - use std::time::Duration; use anyhow::Result; diff --git a/src/hyperlight-js/examples/interrupt/main.rs b/src/hyperlight-js/examples/interrupt/main.rs index 2b8c608..26f5eaa 100644 --- a/src/hyperlight-js/examples/interrupt/main.rs +++ b/src/hyperlight-js/examples/interrupt/main.rs @@ -22,8 +22,6 @@ limitations under the License. //! //! Run with: cargo run --example interrupt -#![allow(clippy::disallowed_macros)] - use std::sync::{Arc, Barrier}; use std::thread; use std::time::Duration; diff --git a/src/hyperlight-js/examples/metrics/main.rs b/src/hyperlight-js/examples/metrics/main.rs index 53bfcf2..22b3698 100644 --- a/src/hyperlight-js/examples/metrics/main.rs +++ b/src/hyperlight-js/examples/metrics/main.rs @@ -13,7 +13,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -#![allow(clippy::disallowed_macros)] use std::thread::{spawn, JoinHandle}; use hyperlight_js::{LoadedJSSandbox, Result, SandboxBuilder, Script}; diff --git a/src/hyperlight-js/examples/run_handler/main.rs b/src/hyperlight-js/examples/run_handler/main.rs index 041a13d..aa1de80 100644 --- a/src/hyperlight-js/examples/run_handler/main.rs +++ b/src/hyperlight-js/examples/run_handler/main.rs @@ -13,7 +13,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -#![allow(clippy::disallowed_macros)] use std::collections::HashMap; use std::path::PathBuf; use std::{env, fs}; diff --git a/src/hyperlight-js/examples/tracing/main.rs b/src/hyperlight-js/examples/tracing/main.rs index 096d6ed..d31418d 100644 --- a/src/hyperlight-js/examples/tracing/main.rs +++ b/src/hyperlight-js/examples/tracing/main.rs @@ -13,7 +13,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -#![allow(clippy::disallowed_macros)] extern crate hyperlight_js; use std::collections::HashMap; use std::path::PathBuf; diff --git a/src/hyperlight-js/examples/user_modules/main.rs b/src/hyperlight-js/examples/user_modules/main.rs index a63b0a1..ca0090d 100644 --- a/src/hyperlight-js/examples/user_modules/main.rs +++ b/src/hyperlight-js/examples/user_modules/main.rs @@ -28,8 +28,6 @@ limitations under the License. //! cargo run --example user_modules //! ``` -#![allow(clippy::disallowed_macros)] - use anyhow::Result; use hyperlight_js::{SandboxBuilder, Script}; diff --git a/src/hyperlight-js/runtime_build.rs b/src/hyperlight-js/runtime_build.rs new file mode 100644 index 0000000..838a763 --- /dev/null +++ b/src/hyperlight-js/runtime_build.rs @@ -0,0 +1,128 @@ +/* +Copyright 2026 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +use std::ffi::OsString; +use std::path::PathBuf; + +use serde_json::Value; + +#[derive(Debug, PartialEq)] +pub(crate) enum RuntimeSource { + Default, + Manifest { path: PathBuf }, +} + +pub(crate) fn runtime_source( + removed_binary_override: Option, + manifest: Option, +) -> Result { + let nonempty = |value: &OsString| !value.to_string_lossy().trim().is_empty(); + if removed_binary_override.filter(nonempty).is_some() { + return Err( + "HYPERLIGHT_JS_RUNTIME_PATH is no longer supported; unset it and set HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH to your custom runtime's Cargo.toml" + .into(), + ); + } + if let Some(path) = manifest.filter(nonempty) { + return Ok(RuntimeSource::Manifest { path: path.into() }); + } + Ok(RuntimeSource::Default) +} + +pub(crate) fn select_binary(package: &Value) -> Result { + let targets = package["targets"] + .as_array() + .ok_or("Guest package has no targets in cargo metadata")?; + let binaries: Vec<&str> = targets + .iter() + .filter(|target| { + target["kind"] + .as_array() + .is_some_and(|kinds| kinds.iter().any(|kind| kind == "bin")) + }) + .filter_map(|target| target["name"].as_str()) + .collect(); + match binaries.as_slice() { + [name] => Ok((*name).to_owned()), + _ => Err("Runtime manifest must define exactly one binary target".into()), + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::{runtime_source, select_binary, RuntimeSource}; + + #[test] + fn absent_or_empty_overrides_select_default_source() { + assert_eq!(runtime_source(None, None).unwrap(), RuntimeSource::Default); + assert_eq!( + runtime_source(Some(" ".into()), Some("".into())).unwrap(), + RuntimeSource::Default + ); + } + + #[test] + fn removed_binary_override_is_rejected_instead_of_silently_using_another_runtime() { + for manifest in [None, Some("Cargo.toml".into())] { + let error = runtime_source(Some("guest".into()), manifest).unwrap_err(); + assert!(error.contains("HYPERLIGHT_JS_RUNTIME_PATH is no longer supported")); + assert!(error.contains("HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH")); + } + } + + #[test] + fn manifest_selects_custom_source() { + assert_eq!( + runtime_source(None, Some("Cargo.toml".into())).unwrap(), + RuntimeSource::Manifest { + path: "Cargo.toml".into() + } + ); + } + + #[test] + fn binary_selection_ignores_libraries_and_build_scripts() { + let package = json!({"targets": [ + {"name": "build-script-build", "kind": ["custom-build"]}, + {"name": "runtime_lib", "kind": ["lib"]}, + {"name": "different-from-package-name", "kind": ["bin"]} + ]}); + assert_eq!( + select_binary(&package).unwrap(), + "different-from-package-name" + ); + } + + #[test] + fn runtime_requires_exactly_one_binary() { + for package in [ + json!({"targets": [{"name": "runtime_lib", "kind": ["lib"]}]}), + json!({"targets": [ + {"name": "one", "kind": ["bin"]}, {"name": "two", "kind": ["bin"]} + ]}), + json!({"default_run": "two", "targets": [ + {"name": "one", "kind": ["bin"]}, {"name": "two", "kind": ["bin"]} + ]}), + ] { + assert_eq!( + select_binary(&package).unwrap_err(), + "Runtime manifest must define exactly one binary target" + ); + } + } +} diff --git a/src/hyperlight-js/src/lib.rs b/src/hyperlight-js/src/lib.rs index a1928fa..04d9c95 100644 --- a/src/hyperlight-js/src/lib.rs +++ b/src/hyperlight-js/src/lib.rs @@ -18,7 +18,7 @@ limitations under the License. #![cfg_attr(not(any(test, debug_assertions)), warn(clippy::panic))] #![cfg_attr(not(any(test, debug_assertions)), warn(clippy::expect_used))] #![cfg_attr(not(any(test, debug_assertions)), warn(clippy::unwrap_used))] -#![cfg_attr(any(test, debug_assertions), allow(clippy::disallowed_macros))] +#![cfg_attr(not(any(test, debug_assertions)), warn(clippy::disallowed_macros))] mod resolver; mod script; diff --git a/src/hyperlight-js/tests/builtin_crypto.rs b/src/hyperlight-js/tests/builtin_crypto.rs index d603789..84a1ecb 100644 --- a/src/hyperlight-js/tests/builtin_crypto.rs +++ b/src/hyperlight-js/tests/builtin_crypto.rs @@ -15,8 +15,6 @@ limitations under the License. */ //! Test the built-in crypto module -#![allow(clippy::disallowed_macros)] - use hyperlight_js::{SandboxBuilder, Script}; #[test] diff --git a/src/hyperlight-js/tests/builtin_globals.rs b/src/hyperlight-js/tests/builtin_globals.rs index 6202614..fa2a37a 100644 --- a/src/hyperlight-js/tests/builtin_globals.rs +++ b/src/hyperlight-js/tests/builtin_globals.rs @@ -13,7 +13,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -#![allow(clippy::disallowed_macros)] use hyperlight_js::{SandboxBuilder, Script}; diff --git a/src/hyperlight-js/tests/builtin_modules.rs b/src/hyperlight-js/tests/builtin_modules.rs index f35d431..29a4285 100644 --- a/src/hyperlight-js/tests/builtin_modules.rs +++ b/src/hyperlight-js/tests/builtin_modules.rs @@ -15,8 +15,6 @@ limitations under the License. */ //! Tests for the built-in (native) modules -#![allow(clippy::disallowed_macros)] - use std::collections::{HashMap, HashSet}; use hyperlight_js::{SandboxBuilder, Script}; diff --git a/src/hyperlight-js/tests/execution_stats.rs b/src/hyperlight-js/tests/execution_stats.rs index 510aa97..c828f1a 100644 --- a/src/hyperlight-js/tests/execution_stats.rs +++ b/src/hyperlight-js/tests/execution_stats.rs @@ -20,7 +20,6 @@ limitations under the License. //! and without execution monitors. #![cfg(feature = "guest-call-stats")] -#![allow(clippy::disallowed_macros)] use std::time::Duration; diff --git a/src/hyperlight-js/tests/handlers.rs b/src/hyperlight-js/tests/handlers.rs index b3f7b5f..03c022d 100644 --- a/src/hyperlight-js/tests/handlers.rs +++ b/src/hyperlight-js/tests/handlers.rs @@ -15,8 +15,6 @@ limitations under the License. */ //! Test the behaviour of JavaScript handlers -#![allow(clippy::disallowed_macros)] - use hyperlight_js::{SandboxBuilder, Script}; #[test] diff --git a/src/hyperlight-js/tests/host_functions.rs b/src/hyperlight-js/tests/host_functions.rs index 82d786e..0801a26 100644 --- a/src/hyperlight-js/tests/host_functions.rs +++ b/src/hyperlight-js/tests/host_functions.rs @@ -15,8 +15,6 @@ limitations under the License. */ //! Test for host modules / functions. -#![allow(clippy::disallowed_macros)] - use hyperlight_js::{SandboxBuilder, Script}; #[test] diff --git a/src/hyperlight-js/tests/module_loader.rs b/src/hyperlight-js/tests/module_loader.rs index 655000a..99f9e94 100644 --- a/src/hyperlight-js/tests/module_loader.rs +++ b/src/hyperlight-js/tests/module_loader.rs @@ -15,8 +15,6 @@ limitations under the License. */ //! Tests for the module loader that import files from the embedded filesystem. -#![allow(clippy::disallowed_macros)] - use hyperlight_js::{embed_modules, SandboxBuilder, Script}; #[test] diff --git a/src/hyperlight-js/tests/monitors.rs b/src/hyperlight-js/tests/monitors.rs index 0ce0836..fa5bf68 100644 --- a/src/hyperlight-js/tests/monitors.rs +++ b/src/hyperlight-js/tests/monitors.rs @@ -16,7 +16,6 @@ limitations under the License. //! Execution Monitor Integration Tests #![cfg(any(feature = "monitor-wall-clock", feature = "monitor-cpu-time"))] -#![allow(clippy::disallowed_macros)] use std::time::{Duration, Instant}; diff --git a/src/hyperlight-js/tests/native_modules.rs b/src/hyperlight-js/tests/native_modules.rs index 41e8351..958999c 100644 --- a/src/hyperlight-js/tests/native_modules.rs +++ b/src/hyperlight-js/tests/native_modules.rs @@ -28,8 +28,6 @@ limitations under the License. //! //! This recipe builds the custom runtime automatically during the host build. -#![allow(clippy::disallowed_macros)] - use hyperlight_js::{SandboxBuilder, Script}; /// Test that a custom native module ("math") can be imported and used diff --git a/src/hyperlight-js/tests/printing.rs b/src/hyperlight-js/tests/printing.rs index 763cce2..592eb82 100644 --- a/src/hyperlight-js/tests/printing.rs +++ b/src/hyperlight-js/tests/printing.rs @@ -15,8 +15,6 @@ limitations under the License. */ //! Tests for output printing from the sandbox -#![allow(clippy::disallowed_macros)] - use std::sync::mpsc::channel; use hyperlight_js::{SandboxBuilder, Script}; diff --git a/src/hyperlight-js/tests/runtime.rs b/src/hyperlight-js/tests/runtime.rs index 57c572e..2255037 100644 --- a/src/hyperlight-js/tests/runtime.rs +++ b/src/hyperlight-js/tests/runtime.rs @@ -15,8 +15,6 @@ limitations under the License. */ //! Test some key aspects of the JavaScript runtime -#![allow(clippy::disallowed_macros)] - use hyperlight_js::{SandboxBuilder, Script}; #[test] diff --git a/src/hyperlight-js/tests/runtime_build.rs b/src/hyperlight-js/tests/runtime_build.rs deleted file mode 100644 index e4f9010..0000000 --- a/src/hyperlight-js/tests/runtime_build.rs +++ /dev/null @@ -1,83 +0,0 @@ -/* -Copyright 2026 The Hyperlight Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -#![allow(clippy::disallowed_macros)] - -#[allow(dead_code)] -#[path = "../build.rs"] -mod build_script; - -use build_script::{runtime_source, select_binary, RuntimeSource}; -use serde_json::json; - -#[test] -fn absent_or_empty_overrides_select_default_source() { - assert_eq!(runtime_source(None, None).unwrap(), RuntimeSource::Default); - assert_eq!( - runtime_source(Some(" ".into()), Some("".into())).unwrap(), - RuntimeSource::Default - ); -} - -#[test] -fn removed_binary_override_is_rejected_instead_of_silently_using_another_runtime() { - for manifest in [None, Some("Cargo.toml".into())] { - let error = runtime_source(Some("guest".into()), manifest).unwrap_err(); - assert!(error.contains("HYPERLIGHT_JS_RUNTIME_PATH is no longer supported")); - assert!(error.contains("HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH")); - } -} - -#[test] -fn manifest_selects_custom_source() { - assert_eq!( - runtime_source(None, Some("Cargo.toml".into())).unwrap(), - RuntimeSource::Manifest { - path: "Cargo.toml".into() - } - ); -} - -#[test] -fn binary_selection_ignores_libraries_and_build_scripts() { - let package = json!({"targets": [ - {"name": "build-script-build", "kind": ["custom-build"]}, - {"name": "runtime_lib", "kind": ["lib"]}, - {"name": "different-from-package-name", "kind": ["bin"]} - ]}); - assert_eq!( - select_binary(&package).unwrap(), - "different-from-package-name" - ); -} - -#[test] -fn runtime_requires_exactly_one_binary() { - for package in [ - json!({"targets": [{"name": "runtime_lib", "kind": ["lib"]}]}), - json!({"targets": [ - {"name": "one", "kind": ["bin"]}, {"name": "two", "kind": ["bin"]} - ]}), - json!({"default_run": "two", "targets": [ - {"name": "one", "kind": ["bin"]}, {"name": "two", "kind": ["bin"]} - ]}), - ] { - assert_eq!( - select_binary(&package).unwrap_err(), - "Runtime manifest must define exactly one binary target" - ); - } -} diff --git a/src/hyperlight-js/tests/termination.rs b/src/hyperlight-js/tests/termination.rs index 66092ac..a4f61a5 100644 --- a/src/hyperlight-js/tests/termination.rs +++ b/src/hyperlight-js/tests/termination.rs @@ -15,8 +15,6 @@ limitations under the License. */ //! Test manual termination of the sandbox (i.e., without using a monitor) -#![allow(clippy::disallowed_macros)] - use std::sync::{Arc, Barrier}; use std::thread; use std::time::Duration; diff --git a/src/hyperlight-js/tests/user_modules.rs b/src/hyperlight-js/tests/user_modules.rs index 9f177f5..f92e80a 100644 --- a/src/hyperlight-js/tests/user_modules.rs +++ b/src/hyperlight-js/tests/user_modules.rs @@ -18,8 +18,6 @@ limitations under the License. //! These tests exercise the full lifecycle: host-side registration → guest-side //! lazy compilation → handler import → execution. -#![allow(clippy::disallowed_macros)] - use hyperlight_js::{SandboxBuilder, Script}; // ── Basic import ───────────────────────────────────────────────────── From 902f90f420d01a81f42e482c6ce304dece41a667 Mon Sep 17 00:00:00 2001 From: Simon Davies Date: Wed, 9 Sep 2026 10:40:05 +0100 Subject: [PATCH 6/7] Remove obsolete runtime override migration guard Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Simon Davies --- docs/extending-runtime.md | 7 ++++++ src/hyperlight-js/build.rs | 7 +----- src/hyperlight-js/runtime_build.rs | 34 ++++++------------------------ 3 files changed, 15 insertions(+), 33 deletions(-) diff --git a/docs/extending-runtime.md b/docs/extending-runtime.md index 2503f3f..a4ffb90 100644 --- a/docs/extending-runtime.md +++ b/docs/extending-runtime.md @@ -82,6 +82,13 @@ macros, even when one is empty. ### 3. Build and embed in hyperlight-js +**Breaking change:** `HYPERLIGHT_JS_RUNTIME_PATH` is no longer read. +Prebuilt guest binary embedding is no longer supported. Replace that setting +with `HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH` pointing to the custom crate's +`Cargo.toml`, then rebuild the host or Node.js addon from source. +Without a custom manifest, the default runtime is built and embedded, even +if the old variable is still set. + Set `HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH` to the custom crate's **absolute** `Cargo.toml` path, then build your host project normally: diff --git a/src/hyperlight-js/build.rs b/src/hyperlight-js/build.rs index 5f7e42c..7a9b410 100644 --- a/src/hyperlight-js/build.rs +++ b/src/hyperlight-js/build.rs @@ -286,17 +286,12 @@ fn build_js_runtime(custom: Option) -> PathBuf { fn bundle_runtime() { // Always rerun if the environment variable changes, even if it's currently unset. - println!("cargo:rerun-if-env-changed=HYPERLIGHT_JS_RUNTIME_PATH"); println!("cargo:rerun-if-env-changed=HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH"); // Relative manifest paths resolve from this build script's working directory // (the hyperlight-js crate root), not the invoking host project. Prefer an // absolute path. build_js_runtime canonicalizes it and requires it to exist. - let source = runtime_source( - env::var_os("HYPERLIGHT_JS_RUNTIME_PATH"), - env::var_os("HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH"), - ) - .unwrap_or_else(|error| panic!("{error}")); + let source = runtime_source(env::var_os("HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH")); let js_runtime_resource = match source { RuntimeSource::Manifest { path } => build_js_runtime(Some(path)), RuntimeSource::Default => build_js_runtime(None), diff --git a/src/hyperlight-js/runtime_build.rs b/src/hyperlight-js/runtime_build.rs index 838a763..83a81e9 100644 --- a/src/hyperlight-js/runtime_build.rs +++ b/src/hyperlight-js/runtime_build.rs @@ -25,21 +25,12 @@ pub(crate) enum RuntimeSource { Manifest { path: PathBuf }, } -pub(crate) fn runtime_source( - removed_binary_override: Option, - manifest: Option, -) -> Result { +pub(crate) fn runtime_source(manifest: Option) -> RuntimeSource { let nonempty = |value: &OsString| !value.to_string_lossy().trim().is_empty(); - if removed_binary_override.filter(nonempty).is_some() { - return Err( - "HYPERLIGHT_JS_RUNTIME_PATH is no longer supported; unset it and set HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH to your custom runtime's Cargo.toml" - .into(), - ); - } if let Some(path) = manifest.filter(nonempty) { - return Ok(RuntimeSource::Manifest { path: path.into() }); + return RuntimeSource::Manifest { path: path.into() }; } - Ok(RuntimeSource::Default) + RuntimeSource::Default } pub(crate) fn select_binary(package: &Value) -> Result { @@ -68,27 +59,16 @@ mod tests { use super::{runtime_source, select_binary, RuntimeSource}; #[test] - fn absent_or_empty_overrides_select_default_source() { - assert_eq!(runtime_source(None, None).unwrap(), RuntimeSource::Default); - assert_eq!( - runtime_source(Some(" ".into()), Some("".into())).unwrap(), - RuntimeSource::Default - ); - } - - #[test] - fn removed_binary_override_is_rejected_instead_of_silently_using_another_runtime() { - for manifest in [None, Some("Cargo.toml".into())] { - let error = runtime_source(Some("guest".into()), manifest).unwrap_err(); - assert!(error.contains("HYPERLIGHT_JS_RUNTIME_PATH is no longer supported")); - assert!(error.contains("HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH")); + fn absent_or_empty_manifest_selects_default_source() { + for manifest in [None, Some("".into()), Some(" ".into())] { + assert_eq!(runtime_source(manifest), RuntimeSource::Default); } } #[test] fn manifest_selects_custom_source() { assert_eq!( - runtime_source(None, Some("Cargo.toml".into())).unwrap(), + runtime_source(Some("Cargo.toml".into())), RuntimeSource::Manifest { path: "Cargo.toml".into() } From 5207db72c177875cb0fadff527d1c40ba1c28dc9 Mon Sep 17 00:00:00 2001 From: Simon Davies Date: Wed, 9 Sep 2026 20:51:30 +0100 Subject: [PATCH 7/7] Scope custom guest clock linker flag to the guest binary Use cargo rustc for custom guests so the clock wrapper flag is not inherited by cargo-hyperlight's native sysroot wrapper build. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Simon Davies --- src/hyperlight-js/build.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/hyperlight-js/build.rs b/src/hyperlight-js/build.rs index 7a9b410..a99881e 100644 --- a/src/hyperlight-js/build.rs +++ b/src/hyperlight-js/build.rs @@ -235,7 +235,7 @@ fn build_js_runtime(custom: Option) -> PathBuf { let mut cargo_cmd = cargo_hyperlight::cargo().unwrap(); let cmd = cargo_cmd - .arg("build") + .arg(if is_custom { "rustc" } else { "build" }) .arg("--profile") .arg(cargo_profile) .arg("--bin") @@ -253,13 +253,6 @@ fn build_js_runtime(custom: Option) -> PathBuf { .current_dir(runtime_dir) .env("HYPERLIGHT_CFLAGS", cflags); - // Link arguments from the runtime library's build.rs do not propagate to - // downstream binaries. Preserve its clock override for custom guest builds. - if is_custom { - let mut flags = env::var_os("RUSTFLAGS").unwrap_or_default(); - flags.push(" -Clink-arg=--wrap=clock_gettime"); - cmd.env("RUSTFLAGS", flags); - } if std::env::var("CARGO_FEATURE_TRACE_GUEST").is_ok() { cmd.arg("--features").arg(if is_custom { "hyperlight-js-runtime/trace_guest" @@ -267,6 +260,12 @@ fn build_js_runtime(custom: Option) -> PathBuf { "trace_guest" }); } + // Dependency build scripts do not pass linker arguments to this binary. + // Scope the clock override to the guest: RUSTFLAGS would also affect + // cargo-hyperlight's native sysroot wrappers. + if is_custom { + cmd.arg("--").arg("-Clink-arg=--wrap=clock_gettime"); + } cmd.status().unwrap_or_else(|e| { panic!("Could not run `cargo build` for the js runtime: {e:?}\n{cmd:?}")