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..a4ffb90c 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,49 @@ 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: +**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: ```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 ``` -### 4. Use from the host +PowerShell: + +```powershell +$env:HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH = (Resolve-Path .\my-custom-runtime\Cargo.toml).Path +cargo build --release +``` -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: +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 + +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 +153,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 +165,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 +196,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`? - -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. +**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 pattern: reuse Cargo's git checkout +### Why the published addon cannot be used -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. +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. -#### 1. Discover the checkout path +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. -Use `cargo metadata` to find where Cargo placed the hyperlight-js -workspace: +### Build the addon with your custom runtime -```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 -``` - -#### 2. Build the NAPI addon with your custom runtime +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 -# 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 +```powershell +$env:HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH = (Resolve-Path C:\path\to\my-custom-runtime\Cargo.toml).Path +just build-js-host-api release ``` -#### 3. Symlink for npm dependency resolution +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. -Create a symlink so npm can resolve the addon via a stable path: +### Use the locally built addon -```bash -mkdir -p deps -ln -sfn "${HYPERLIGHT_DIR}/src/js-host-api" deps/js-host-api -``` - -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..2af50a86 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] chrono = "0.4.45" @@ -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 03abf368..af65fca5 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 @@ -29,6 +28,15 @@ limitations under the License. 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"; + 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. @@ -44,11 +52,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"); @@ -58,44 +70,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") +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 { @@ -122,7 +114,7 @@ fn find_target_dir() -> PathBuf { target_dir.to_path_buf() } -fn build_js_runtime() -> PathBuf { +fn build_js_runtime(custom: Option) -> PathBuf { let profile = env::var_os("PROFILE").unwrap(); // Get the current target directory. @@ -131,7 +123,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 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"); assert!( manifest_path.is_file(), @@ -142,59 +139,96 @@ 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 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).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(if is_custom { "rustc" } else { "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); 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" + }); + } + // 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:?}") }); - 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 @@ -208,32 +242,15 @@ 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"); + + // 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_MANIFEST_PATH")); + let js_runtime_resource = match source { + RuntimeSource::Manifest { path } => build_js_runtime(Some(path)), + RuntimeSource::Default => build_js_runtime(None), }; let out_dir = env::var_os("OUT_DIR").unwrap(); @@ -243,6 +260,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..83a81e92 --- /dev/null +++ b/src/hyperlight-js/runtime_build.rs @@ -0,0 +1,108 @@ +/* +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(manifest: Option) -> RuntimeSource { + let nonempty = |value: &OsString| !value.to_string_lossy().trim().is_empty(); + if let Some(path) = manifest.filter(nonempty) { + return RuntimeSource::Manifest { path: path.into() }; + } + 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_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(Some("Cargo.toml".into())), + 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 28dc0f6e..958999cb 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,11 +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. - -#![allow(clippy::disallowed_macros)] +//! This recipe builds the custom runtime automatically during the host build. use hyperlight_js::{SandboxBuilder, Script}; @@ -135,3 +131,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/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/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 ─────────────────────────────────────────────────────