Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions .github/workflows/dep_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,18 @@ jobs:
strategy:
fail-fast: true
matrix:
build: [windows-2025-debug, linux-kvm-debug, linux-kvm-arm64-debug, linux-hyperv3-debug, macos-hvf-debug, windows-2025-release, linux-kvm-release, linux-kvm-arm64-release, linux-hyperv3-release, macos-hvf-release]
build: [windows-2025-debug, windows-arm64-debug, linux-kvm-debug, linux-kvm-arm64-debug, linux-hyperv3-debug, macos-hvf-debug, windows-2025-release, windows-arm64-release, linux-kvm-release, linux-kvm-arm64-release, linux-hyperv3-release, macos-hvf-release]
include:
- build: windows-2025-debug
os: [self-hosted, Windows, X64, "1ES.Pool=hld-win2025-amd"]
hypervisor: whp
arch: X64
config: debug
- build: windows-arm64-debug
os: [self-hosted, Windows, ARM64, "1ES.Pool=hl-windows-arm"]
hypervisor: whp
arch: arm64
config: debug
- build: linux-kvm-debug
os: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd"]
hypervisor: kvm
Expand All @@ -51,6 +56,11 @@ jobs:
hypervisor: whp
arch: X64
config: release
- build: windows-arm64-release
os: [self-hosted, Windows, ARM64, "1ES.Pool=hl-windows-arm"]
hypervisor: whp
arch: arm64
config: release
- build: linux-kvm-release
os: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd"]
hypervisor: kvm
Expand All @@ -71,7 +81,10 @@ jobs:
hypervisor: hvf
arch: arm64
config: release
runs-on: ${{ fromJson(matrix.hypervisor == 'hvf'
runs-on: ${{ fromJson(matrix.hypervisor == 'whp' && matrix.arch == 'arm64'
&& format('["self-hosted", "Windows", "ARM64", "1ES.Pool=hl-windows-arm", "JobId=build-{0}-{1}-{2}-{3}"]',
matrix.build, github.run_id, github.run_number, github.run_attempt)
|| matrix.hypervisor == 'hvf'
&& '["self-hosted", "macos", "arm64", "hvf"]'
|| matrix.arch == 'arm64'
&& '["self-hosted", "Linux", "arm64", "kvm"]'
Expand Down Expand Up @@ -137,6 +150,7 @@ jobs:
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7
with:
node-version: '22'
architecture: ${{ matrix.arch == 'arm64' && 'arm64' || 'x64' }}
cache: 'npm'
cache-dependency-path: 'src/js-host-api/package-lock.json'

Expand Down
192 changes: 160 additions & 32 deletions .github/workflows/npm-publish.yml

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,12 @@ set-version version:
cargo update \
--manifest-path src/hyperlight-js-runtime/tests/fixtures/extended_runtime/Cargo.toml \
-p hyperlight-js-runtime -p hyperlight-js-common
# npm: main + the 5 platform package.json versions (--ignore-scripts avoids needing node_modules)
# npm: main + the 6 platform package.json versions (--ignore-scripts avoids needing node_modules)
cd src/js-host-api && npm version {{ version }} --no-git-tag-version --allow-same-version --ignore-scripts
cd src/js-host-api/npm/linux-x64-gnu && npm version {{ version }} --no-git-tag-version --allow-same-version --ignore-scripts
cd src/js-host-api/npm/linux-x64-musl && npm version {{ version }} --no-git-tag-version --allow-same-version --ignore-scripts
cd src/js-host-api/npm/win32-x64-msvc && npm version {{ version }} --no-git-tag-version --allow-same-version --ignore-scripts
cd src/js-host-api/npm/win32-arm64-msvc && npm version {{ version }} --no-git-tag-version --allow-same-version --ignore-scripts
cd src/js-host-api/npm/darwin-arm64 && npm version {{ version }} --no-git-tag-version --allow-same-version --ignore-scripts
cd src/js-host-api/npm/linux-arm64-gnu && npm version {{ version }} --no-git-tag-version --allow-same-version --ignore-scripts
# Verify the npm lockfile
Expand Down
11 changes: 1 addition & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,23 +57,14 @@ to `PATH`, or add the active Rust toolchain's LLVM directory:
$env:Path = "$(rustc --print sysroot)\lib\rustlib\$hostTriple\bin;$env:Path"
```

In addition on Linux and macOS you will need to install the bare-metal target matching
your architecture, which `cargo-hyperlight` derives the guest target from:

```bash
# x86_64 hosts
rustup target add x86_64-unknown-none
# Apple Silicon / other aarch64 hosts
rustup target add aarch64-unknown-none
```

## Supported platforms

| Host OS | Architecture | Hypervisor |
| ------- | ------------ | ---------- |
| Linux | x86_64 | KVM or MSHV |
| Linux | aarch64 | KVM |
| Windows | x86_64 | Windows Hypervisor Platform (WHP) |
| Windows | aarch64 | Windows Hypervisor Platform (WHP) |
| macOS | aarch64 | Hypervisor.framework |

The guest runs on the same architecture as the host, so an aarch64 host builds
Expand Down
4 changes: 3 additions & 1 deletion docs/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Do this with the `just set-version` recipe. **Always use this instead of bumping

- every workspace crate's `version` and the root `Cargo.lock`,
- the excluded `extended_runtime` fixture's own `Cargo.lock` (a bare `cargo set-version` can't reach it, and a stale one fails the `native_modules --locked` build),
- the npm main package and the five platform packages,
- the npm main package and the six platform packages,
- the root package version in `src/js-host-api/package-lock.json`.

Keep `optionalDependencies` on the latest published version so `npm ci` can use
Expand Down Expand Up @@ -73,6 +73,7 @@ This must be done for all 6 packages:
- `@hyperlight-dev/js-host-api-linux-x64-gnu`
- `@hyperlight-dev/js-host-api-linux-x64-musl`
- `@hyperlight-dev/js-host-api-win32-x64-msvc`
- `@hyperlight-dev/js-host-api-win32-arm64-msvc`
- `@hyperlight-dev/js-host-api-darwin-arm64`
- `@hyperlight-dev/js-host-api-linux-arm64-gnu`

Expand Down Expand Up @@ -120,6 +121,7 @@ If you need to publish npm packages manually via `workflow_dispatch`, you'll nee
- `@hyperlight-dev/js-host-api-linux-x64-gnu`
- `@hyperlight-dev/js-host-api-linux-x64-musl`
- `@hyperlight-dev/js-host-api-win32-x64-msvc`
- `@hyperlight-dev/js-host-api-win32-arm64-msvc`
- `@hyperlight-dev/js-host-api-darwin-arm64`
- `@hyperlight-dev/js-host-api-linux-arm64-gnu`

Expand Down
8 changes: 2 additions & 6 deletions src/hyperlight-js/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,7 @@ 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";
use runtime_build::{guest_cflags, runtime_source, select_binary, RuntimeSource};

fn main() {
// Mirror the hypervisor cfg aliases used by hyperlight-host so that
Expand Down Expand Up @@ -230,7 +226,7 @@ fn build_js_runtime(custom: Option<PathBuf>) -> PathBuf {
.canonicalize()
.expect("hyperlight-js-runtime include directory not found");
println!("cargo:rerun-if-changed={}", include_dir.display());
let cflags = format!("-I{} {QUICKJS_CFLAGS}", include_dir.display()).replace("\\", "\\\\");
let cflags = guest_cflags(&include_dir);

let mut cargo_cmd = cargo_hyperlight::cargo().unwrap();
let cmd = cargo_cmd
Expand Down
34 changes: 32 additions & 2 deletions src/hyperlight-js/runtime_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,20 @@ limitations under the License.
*/

use std::ffi::OsString;
use std::path::PathBuf;
use std::path::{Path, PathBuf};

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";

pub(crate) fn guest_cflags(include_dir: &Path) -> String {
// Clang must see the runtime's math shim before the sysroot headers.
// Use forward slashes, as in the Justfile: escaped backslashes lose the include path.
format!("-I{} {QUICKJS_CFLAGS}", include_dir.display()).replace('\\', "/")
}

#[derive(Debug, PartialEq)]
pub(crate) enum RuntimeSource {
Default,
Expand Down Expand Up @@ -56,7 +66,27 @@ pub(crate) fn select_binary(package: &Value) -> Result<String, String> {
mod tests {
use serde_json::json;

use super::{runtime_source, select_binary, RuntimeSource};
use super::{guest_cflags, runtime_source, select_binary, RuntimeSource};

#[test]
fn guest_cflags_normalize_canonical_windows_include_path() {
assert_eq!(
guest_cflags(std::path::Path::new(
r"\\?\C:\repo\src\hyperlight-js-runtime\include"
)),
"-I//?/C:/repo/src/hyperlight-js-runtime/include -D__wasi__=1 -D_POSIX_MONOTONIC_CLOCK"
);
}

#[test]
fn guest_cflags_preserve_unix_include_path() {
assert_eq!(
guest_cflags(std::path::Path::new(
"/repo/src/hyperlight-js-runtime/include"
)),
"-I/repo/src/hyperlight-js-runtime/include -D__wasi__=1 -D_POSIX_MONOTONIC_CLOCK"
);
}

#[test]
fn absent_or_empty_manifest_selects_default_source() {
Expand Down
1 change: 1 addition & 0 deletions src/js-host-api/.npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ src/
Cargo.toml
test-examples.sh
test-pack.sh
test-pack.ps1

# Exclude artifacts directory (only used during CI)
artifacts/
Expand Down
8 changes: 8 additions & 0 deletions src/js-host-api/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ just build-all
just test-all release
```

Tests register runtime and loaded sandboxes with `trackSandbox()` from
`tests/test-helpers.js` so they are disposed after each test, including wrappers
returned by `unload()`. Do not rely on JavaScript garbage collection to release
VMs. Windows ARM64 WHP currently supports only 64 concurrent partitions; Vitest
also runs test files serially on that host because Hyperlight's partition counter
is process-local.

## Publishing to npm

The package is published to npmjs.com as `@hyperlight-dev/js-host-api` with platform-specific binary packages using [npm trusted publishing (OIDC)](https://docs.npmjs.com/trusted-publishers).
Expand All @@ -66,6 +73,7 @@ The npm release consists of the following packages:
| `@hyperlight-dev/js-host-api-linux-x64-gnu` | Linux x86_64 (glibc) native binary |
| `@hyperlight-dev/js-host-api-linux-x64-musl` | Linux x86_64 (musl/Alpine) native binary |
| `@hyperlight-dev/js-host-api-win32-x64-msvc` | Windows x86_64 native binary |
| `@hyperlight-dev/js-host-api-win32-arm64-msvc` | Windows aarch64 native binary (WHP) |
| `@hyperlight-dev/js-host-api-darwin-arm64` | macOS aarch64 (Apple Silicon) native binary |
| `@hyperlight-dev/js-host-api-linux-arm64-gnu` | Linux aarch64 (glibc) native binary |

Expand Down
2 changes: 1 addition & 1 deletion src/js-host-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -833,7 +833,7 @@ dependencies, custom namespaces, multiple handlers sharing a module, and
## Requirements

- **Node.js** >= 18
- **Linux** (x86_64, glibc or musl), **Windows** (x86_64), or **macOS** (Apple Silicon / aarch64)
- **Linux** (x86_64, glibc or musl; aarch64, glibc), **Windows** (x86_64 or aarch64), or **macOS** (Apple Silicon / aarch64)

## License

Expand Down
27 changes: 27 additions & 0 deletions src/js-host-api/npm/win32-arm64-msvc/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@hyperlight-dev/js-host-api-win32-arm64-msvc",
"version": "0.4.0",
"os": [
"win32"
],
"cpu": [
"arm64"
],
"main": "js-host-api.win32-arm64-msvc.node",
"files": [
"js-host-api.win32-arm64-msvc.node"
],
"description": "Node.js API bindings for Hyperlight JS - Windows ARM64 msvc",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/hyperlight-dev/hyperlight-js.git"
},
"homepage": "https://github.com/hyperlight-dev/hyperlight-js#readme",
"bugs": {
"url": "https://github.com/hyperlight-dev/hyperlight-js/issues"
},
"engines": {
"node": ">= 18"
}
}
1 change: 1 addition & 0 deletions src/js-host-api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/js-host-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"x86_64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
"x86_64-pc-windows-msvc",
"aarch64-pc-windows-msvc",
"aarch64-apple-darwin",
"aarch64-unknown-linux-gnu"
]
Expand All @@ -41,6 +42,7 @@
"@hyperlight-dev/js-host-api-linux-arm64-gnu": "0.4.0",
"@hyperlight-dev/js-host-api-linux-x64-gnu": "0.4.0",
"@hyperlight-dev/js-host-api-linux-x64-musl": "0.4.0",
"@hyperlight-dev/js-host-api-win32-arm64-msvc": "0.4.0",
"@hyperlight-dev/js-host-api-win32-x64-msvc": "0.4.0"
},
"devDependencies": {
Expand Down
65 changes: 65 additions & 0 deletions src/js-host-api/test-pack.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Validate Windows ARM64 release tarballs on a native ARM64 host with WHP enabled.
$ErrorActionPreference = "Stop"
$PSNativeCommandUseErrorActionPreference = $true

$platform = node -p "process.platform + '-' + process.arch"
if ($LASTEXITCODE -ne 0 -or $platform -ne "win32-arm64") {
throw "Windows ARM64 package validation requires native ARM64 Node.js on Windows"
}

$packageDir = Join-Path $PSScriptRoot "npm\win32-arm64-msvc"
$binaryName = "js-host-api.win32-arm64-msvc.node"
$binaryPath = Join-Path $packageDir $binaryName
if (-not (Test-Path $binaryPath)) {
Copy-Item (Join-Path $PSScriptRoot $binaryName) $binaryPath
}

$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("hyperlight-npm-test-" + [guid]::NewGuid())
$null = New-Item -ItemType Directory -Path $tempDir
Push-Location $PSScriptRoot
try {
$platformPack = npm pack $packageDir --pack-destination $tempDir --json | ConvertFrom-Json
if ($LASTEXITCODE -ne 0) { throw "Failed to pack Windows ARM64 package" }
if ($binaryName -notin $platformPack.files.path) {
throw "Windows ARM64 package is missing $binaryName"
}

$mainPack = npm pack --pack-destination $tempDir --json | ConvertFrom-Json
if ($LASTEXITCODE -ne 0) { throw "Failed to pack main package" }
foreach ($file in @("package.json", "lib.js", "lib.mjs", "index.js", "index.d.ts", "lib.d.ts")) {
if ($file -notin $mainPack.files.path) {
throw "Main package is missing $file"
}
}
foreach ($file in $mainPack.files.path) {
if ($file -match '^(src|tests|node_modules|target)/|^Cargo\.toml$|\.node$') {
throw "Main package contains unexpected file: $file"
}
}

Set-Location $tempDir
npm init -y --silent
if ($LASTEXITCODE -ne 0) { throw "Failed to initialize consumer project" }
npm install (Join-Path $tempDir $platformPack.filename) (Join-Path $tempDir $mainPack.filename) --ignore-scripts
if ($LASTEXITCODE -ne 0) { throw "Failed to install release tarballs" }

node -e @'
const assert = require('node:assert/strict');
const { SandboxBuilder } = require('@hyperlight-dev/js-host-api');
async function main() {
assert.equal(typeof SandboxBuilder, 'function');
const proto = await new SandboxBuilder().build();
const sandbox = await proto.loadRuntime();
sandbox.addHandler('hello', 'function handler(event) { event.greeting = "Hello from Hyperlight!"; return event; }');
const loaded = await sandbox.getLoadedSandbox();
const result = await loaded.callHandler('hello', {}, { gc: false });
assert.equal(result.greeting, 'Hello from Hyperlight!');
console.log('Windows ARM64 release tarballs loaded and executed a WHP sandbox successfully');
}
main().catch(error => { console.error(error); process.exit(1); });
'@
if ($LASTEXITCODE -ne 0) { throw "Windows ARM64 WHP smoke test failed" }
} finally {
Pop-Location
Remove-Item -LiteralPath $tempDir -Recurse -Force
}
14 changes: 7 additions & 7 deletions src/js-host-api/tests/host-functions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// sandboxed code can call via `import * as <module> from "host:<module>"`.
import { describe, it, expect, beforeEach } from 'vitest';
import { SandboxBuilder } from '../lib.js';
import { expectThrowsWithCode } from './test-helpers.js';
import { expectThrowsWithCode, trackSandbox } from './test-helpers.js';

// ── Helpers ──────────────────────────────────────────────────────────

Expand All @@ -20,9 +20,9 @@ import { expectThrowsWithCode } from './test-helpers.js';
async function buildLoadedSandbox(registerFns, handlerScript) {
const proto = await new SandboxBuilder().build();
registerFns(proto);
const sandbox = await proto.loadRuntime();
const sandbox = await trackSandbox(proto.loadRuntime());
sandbox.addHandler('handler', handlerScript);
return sandbox.getLoadedSandbox();
return trackSandbox(sandbox.getLoadedSandbox());
}

// ── HostModule ────────────────────────────────────────────────
Expand Down Expand Up @@ -96,9 +96,9 @@ describe('Host function invocation', () => {
]) {
const proto = await builder.build();
proto.hostModule('host').register('echo', (value) => value);
const sandbox = await proto.loadRuntime();
const sandbox = await trackSandbox(proto.loadRuntime());
sandbox.addHandler('handler', source);
const loaded = await sandbox.getLoadedSandbox();
const loaded = await trackSandbox(sandbox.getLoadedSandbox());

expect(await loaded.callHandler('handler', { value: payload })).toEqual({
value: payload,
Expand Down Expand Up @@ -406,9 +406,9 @@ describe('Multi-sandbox isolation', () => {
async function buildTaggedSandbox(tag, hostFnImpl, handlerScript) {
const proto = await new SandboxBuilder().build();
hostFnImpl(proto);
const sandbox = await proto.loadRuntime();
const sandbox = await trackSandbox(proto.loadRuntime());
sandbox.addHandler('handler', handlerScript);
const loaded = await sandbox.getLoadedSandbox();
const loaded = await trackSandbox(sandbox.getLoadedSandbox());
return { loaded, tag };
}

Expand Down
Loading
Loading