From 7d78a684a529fecfb0adee260f41e3ac8062dd47 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 | 33 +--- README.md | 2 + docs/extending-runtime.md | 176 +++++++---------- src/hyperlight-js/Cargo.toml | 2 + src/hyperlight-js/build.rs | 231 ++++++++++++++++------ src/hyperlight-js/tests/native_modules.rs | 43 +++- src/hyperlight-js/tests/runtime_build.rs | 83 ++++++++ 7 files changed, 381 insertions(+), 189 deletions(-) create mode 100644 src/hyperlight-js/tests/runtime_build.rs diff --git a/Justfile b/Justfile index dc19e4f0..cd64e6b1 100644 --- a/Justfile +++ b/Justfile @@ -8,12 +8,8 @@ latest-release:= if os() == "windows" {"$(git tag -l --sort=v:refname | select - PWD := replace(justfile_dir(), "\\", "/") # Set the HYPERLIGHT_CFLAGS so cargo-hyperlight applies them when building the runtimes: -# * include the stubs required by hyperlight-js-runtime # * define __wasi__ as this disables threading support in quickjs -export HYPERLIGHT_CFLAGS := \ - "-I" + PWD + "/src/hyperlight-js-runtime/include " + \ - "-D__wasi__=1 " + \ - "-D_POSIX_MONOTONIC_CLOCK " +export HYPERLIGHT_CFLAGS := "-D__wasi__=1 -D_POSIX_MONOTONIC_CLOCK" # On Windows, use Ninja generator for CMake to avoid aws-lc-sys build issues with Visual Studio generator export CMAKE_GENERATOR := if os() == "windows" { "Ninja" } else { "" } @@ -160,40 +156,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/x86_64-hyperlight-none" - -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 \ - --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 972a0604..0ed3519a 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 8a77ddbf..7d61955c 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,47 +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) -export HYPERLIGHT_JS_RUNTIME_PATH=my-custom-runtime/target/x86_64-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}; @@ -145,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") @@ -155,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])?; @@ -185,95 +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 -export HYPERLIGHT_JS_RUNTIME_PATH=my-custom-runtime/target/x86_64-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 ab76a7c0..efc08586 100644 --- a/src/hyperlight-js/Cargo.toml +++ b/src/hyperlight-js/Cargo.toml @@ -41,6 +41,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"] } @@ -58,6 +59,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 03abf368..aae7b477 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() { if env::var("DOCS_RS").is_ok() { // docs.rs runs offline, so we can't prepare the sysroot for x86_64-hyperlight-none in there. @@ -122,7 +191,7 @@ fn find_target_dir() -> PathBuf { target_dir.to_path_buf() } -fn build_js_runtime() -> PathBuf { +fn build_js_runtime(custom: Option<(PathBuf, Option)>) -> PathBuf { let profile = env::var_os("PROFILE").unwrap(); // Get the current target directory. @@ -131,7 +200,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(), @@ -142,59 +216,112 @@ 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() + let target = format!( + "{}-hyperlight-none", + env::var("CARGO_CFG_TARGET_ARCH").unwrap() ); - // 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("\\", "\\\\"); - let mut cargo_cmd = cargo_hyperlight::cargo().unwrap(); let cmd = cargo_cmd .arg("build") .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) - .env("HYPERLIGHT_CFLAGS", cflags); - + .current_dir(runtime_dir) + .env("HYPERLIGHT_CFLAGS", QUICKJS_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("x86_64-hyperlight-none") - .join(profile) - .join("hyperlight-js-runtime"); + let resource = target_dir.join(target).join(profile).join(bin); if let Ok(path) = resource.canonicalize() { path @@ -209,31 +336,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 28dc0f6e..41e83519 100644 --- a/src/hyperlight-js/tests/native_modules.rs +++ b/src/hyperlight-js/tests/native_modules.rs @@ -17,8 +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 `x86_64-hyperlight-none` 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: @@ -26,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)] @@ -135,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 00000000..c97a413e --- /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 e7e55b0dea311b93f48712dbca7221c922d1fd7b 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 efc08586..882149e7 100644 --- a/src/hyperlight-js/Cargo.toml +++ b/src/hyperlight-js/Cargo.toml @@ -38,7 +38,6 @@ windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_ [build-dependencies] cargo-hyperlight = "0.1.14" serde_json = { version = "1.0" } -serde = { version = "1.0", features = ["derive"] } [dev-dependencies] cargo-hyperlight = "0.1.14" @@ -59,7 +58,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 aae7b477..def2b375 100644 --- a/src/hyperlight-js/build.rs +++ b/src/hyperlight-js/build.rs @@ -113,11 +113,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"); @@ -127,44 +131,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 { @@ -201,8 +185,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"); @@ -216,20 +201,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 c97a413e..0db823df 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 158dfb6aca2f703b8efb557578aca6aa6b7bd57e 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 def2b375..01db2cb3 100644 --- a/src/hyperlight-js/build.rs +++ b/src/hyperlight-js/build.rs @@ -134,7 +134,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 0db823df..9479211f 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 7b60f4a3a3e706129280dc6bdd921dfdbe39bd90 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 7d61955c..2503f3f5 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 01db2cb3..d944bff8 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()), } } @@ -175,7 +155,7 @@ fn find_target_dir() -> PathBuf { target_dir.to_path_buf() } -fn build_js_runtime(custom: Option<(PathBuf, Option)>) -> PathBuf { +fn build_js_runtime(custom: Option) -> PathBuf { let profile = env::var_os("PROFILE").unwrap(); // Get the current target directory. @@ -185,9 +165,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"); @@ -212,8 +191,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. @@ -308,7 +286,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 @@ -316,11 +293,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 9479211f..e4f9010b 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 45fe09825b5e742d8fb29c00daa5ce383934f897 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 882149e7..2af50a86 100644 --- a/src/hyperlight-js/Cargo.toml +++ b/src/hyperlight-js/Cargo.toml @@ -40,7 +40,6 @@ cargo-hyperlight = "0.1.14" 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"] } @@ -82,6 +81,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 4936608d..23c306f5 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 d944bff8..7d71cd1f 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() { if env::var("DOCS_RS").is_ok() { // docs.rs runs offline, so we can't prepare the sysroot for x86_64-hyperlight-none in there. @@ -307,6 +266,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 dc4c3073..7b3961aa 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 42511e97..8e6685c6 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 2b8c6087..26f5eaa7 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 53bfcf2d..22b36989 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 041a13dc..aa1de80c 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 096d6edd..d31418db 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 a63b0a14..ca0090d9 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 00000000..838a7633 --- /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 a1928fa0..04d9c957 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 d603789e..84a1ecb5 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 62026144..fa2a37ac 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 f35d431c..29a4285b 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 510aa975..c828f1a6 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 b3f7b5f8..03c022d9 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 82d786e4..0801a26b 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 655000ac..99f9e945 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 0ce08364..fa5bf689 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 41e83519..958999cb 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 763cce21..592eb82f 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 57c572e5..22550377 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 e4f9010b..00000000 --- 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 66092acb..a4f61a5d 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 9f177f55..f92e80aa 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 46b1c769a43692254d0aad60abc827f8d60d131d 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 2503f3f5..a4ffb90c 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 7d71cd1f..b9fa5e57 100644 --- a/src/hyperlight-js/build.rs +++ b/src/hyperlight-js/build.rs @@ -243,17 +243,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 838a7633..83a81e92 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 8b36dd0967a0ec5a25d223259f9486f8b306deaa 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 b9fa5e57..af65fca5 100644 --- a/src/hyperlight-js/build.rs +++ b/src/hyperlight-js/build.rs @@ -192,7 +192,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") @@ -210,13 +210,6 @@ fn build_js_runtime(custom: Option) -> PathBuf { .current_dir(runtime_dir) .env("HYPERLIGHT_CFLAGS", QUICKJS_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" @@ -224,6 +217,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:?}")