Skip to content

Commit 12b0455

Browse files
committed
fix: cross-platform npm publish with runtime NAPI detection
- build-binary.js: use napi-rs generated index.js for platform detection instead of hardcoded triple shim. Copies all available .node files so the package works on any platform. - publish.yml: build native addons on Linux AND Windows in parallel, upload as artifacts, combine in publish job. Tests run on each platform before upload. Follows same pattern as hyperlight-js PR #36. Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
1 parent 05c2431 commit 12b0455

2 files changed

Lines changed: 84 additions & 32 deletions

File tree

.github/workflows/publish.yml

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,21 @@ env:
2121
IMAGE_NAME: ${{ github.repository }}
2222

2323
jobs:
24-
# Test on all hypervisor configurations before publishing
25-
# NOTE: Windows WHP temporarily disabled (see pr-validate.yml)
26-
test:
27-
name: Test (${{ matrix.build }})
24+
# Build native addons on each platform and upload as artifacts.
25+
# These are combined in the publish-npm job to create a cross-platform package.
26+
build-native:
27+
name: Build (${{ matrix.build }})
2828
strategy:
2929
fail-fast: true
3030
matrix:
31-
build: [linux-kvm, linux-mshv]
31+
build: [linux-kvm, windows-whp]
3232
include:
3333
- build: linux-kvm
3434
os: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd"]
3535
hypervisor: kvm
36-
- build: linux-mshv
37-
os: [self-hosted, Linux, X64, "1ES.Pool=hld-azlinux3-mshv-amd"]
38-
hypervisor: mshv
36+
- build: windows-whp
37+
os: [self-hosted, Windows, X64, "1ES.Pool=hld-win2022-amd"]
38+
hypervisor: whp
3939
runs-on: ${{ matrix.os }}
4040
steps:
4141
- uses: actions/checkout@v6
@@ -55,14 +55,28 @@ jobs:
5555

5656
- name: Build release binary
5757
run: node scripts/build-binary.js --release
58+
env:
59+
VERSION: ${{ github.event.release.tag_name || inputs.version }}
5860

5961
- name: Run tests
6062
run: just test
6163

62-
# Build and publish npm package (after tests pass)
64+
# Upload the native .node addons so the publish job can combine them
65+
- name: Upload native addons
66+
uses: actions/upload-artifact@v4
67+
with:
68+
name: native-addons-${{ matrix.build }}
69+
path: |
70+
deps/js-host-api/js-host-api.*.node
71+
src/code-validator/guest/host/hyperlight-analysis.*.node
72+
src/code-validator/guest/hyperlight-analysis.*.node
73+
if-no-files-found: error
74+
retention-days: 1
75+
76+
# Combine native addons from all platforms and publish a single npm package
6377
publish-npm:
6478
name: Publish to npmjs.org
65-
needs: [test]
79+
needs: [build-native]
6680
runs-on: ubuntu-latest
6781
steps:
6882
- uses: actions/checkout@v6
@@ -78,10 +92,17 @@ jobs:
7892
env:
7993
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
8094

95+
# Download all platform native addons into their expected locations
96+
- name: Download all native addons
97+
uses: actions/download-artifact@v4
98+
with:
99+
pattern: native-addons-*
100+
merge-multiple: true
101+
81102
- name: Setup
82103
run: just setup
83104

84-
- name: Build binary
105+
- name: Build binary (with all platform addons present)
85106
run: VERSION="${{ github.event.release.tag_name || inputs.version }}" node scripts/build-binary.js --release
86107

87108
- name: Set version from release tag
@@ -100,7 +121,7 @@ jobs:
100121
# Build and publish Docker image (after tests pass)
101122
publish-docker:
102123
name: Publish to GitHub Container Registry
103-
needs: [test]
124+
needs: [build-native]
104125
runs-on: ubuntu-latest
105126
steps:
106127
- uses: actions/checkout@v6

scripts/build-binary.js

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -217,20 +217,43 @@ if (!existsSync(analysisNode)) {
217217
process.exit(1);
218218
}
219219

220-
copyFileSync(hyperlightNode, join(LIB_DIR, `js-host-api.${napiTriple}.node`));
221-
copyFileSync(
222-
analysisNode,
223-
join(LIB_DIR, `hyperlight-analysis.${napiTriple}.node`),
224-
);
220+
// Copy .node files for ALL available platforms so the package is cross-platform.
221+
// The current platform's .node is guaranteed to exist (checked above).
222+
// Additional platform .node files are copied if present (e.g. from CI matrix builds).
223+
const ALL_TRIPLES = ["linux-x64-gnu", "linux-x64-musl", "win32-x64-msvc"];
224+
for (const triple of ALL_TRIPLES) {
225+
const hlNode = join(ROOT, `deps/js-host-api/js-host-api.${triple}.node`);
226+
const anNode = join(
227+
ROOT,
228+
`src/code-validator/guest/host/hyperlight-analysis.${triple}.node`,
229+
);
230+
if (existsSync(hlNode)) {
231+
copyFileSync(hlNode, join(LIB_DIR, `js-host-api.${triple}.node`));
232+
console.log(` ✓ js-host-api.${triple}.node`);
233+
}
234+
if (existsSync(anNode)) {
235+
copyFileSync(anNode, join(LIB_DIR, `hyperlight-analysis.${triple}.node`));
236+
console.log(` ✓ hyperlight-analysis.${triple}.node`);
237+
}
238+
}
225239

226240
// Create a proper node_modules package structure for hyperlight-analysis
227241
// so both require() and import() can resolve it in the bundled binary.
228242
const analysisPkgDir = join(LIB_DIR, "node_modules", "hyperlight-analysis");
229243
mkdirSync(analysisPkgDir, { recursive: true });
230-
copyFileSync(
231-
analysisNode,
232-
join(analysisPkgDir, `hyperlight-analysis.${napiTriple}.node`),
233-
);
244+
// Copy all available platform .node files into the package dir
245+
for (const triple of ALL_TRIPLES) {
246+
const anNode = join(
247+
ROOT,
248+
`src/code-validator/guest/host/hyperlight-analysis.${triple}.node`,
249+
);
250+
if (existsSync(anNode)) {
251+
copyFileSync(
252+
anNode,
253+
join(analysisPkgDir, `hyperlight-analysis.${triple}.node`),
254+
);
255+
}
256+
}
234257
// Copy the index.js and index.d.ts from the source package
235258
const analysisIndex = join(ROOT, "src/code-validator/guest/index.js");
236259
const analysisTypes = join(ROOT, "src/code-validator/guest/index.d.ts");
@@ -250,24 +273,30 @@ if (existsSync(analysisPkg))
250273
// Files are renamed to .cjs because the host package.json has "type": "module"
251274
// which makes Node.js treat .js as ESM — but lib.js uses require().
252275
const hyperlightLibJs = join(ROOT, "deps/js-host-api/lib.js");
276+
const hyperlightIndexJs = join(ROOT, "deps/js-host-api/index.js");
253277
const hyperlightHostApiDir = join(LIB_DIR, "js-host-api");
254278
mkdirSync(hyperlightHostApiDir, { recursive: true });
255-
copyFileSync(
256-
hyperlightNode,
257-
join(hyperlightHostApiDir, `js-host-api.${napiTriple}.node`),
258-
);
279+
// Copy all available platform .node files
280+
for (const triple of ALL_TRIPLES) {
281+
const hlNode = join(ROOT, `deps/js-host-api/js-host-api.${triple}.node`);
282+
if (existsSync(hlNode)) {
283+
copyFileSync(
284+
hlNode,
285+
join(hyperlightHostApiDir, `js-host-api.${triple}.node`),
286+
);
287+
}
288+
}
259289
// Copy lib.js as lib.cjs, patching the require('./index.js') to './index.cjs'
260290
const libJsContent = readFileSync(hyperlightLibJs, "utf-8").replace(
261291
"require('./index.js')",
262292
"require('./index.cjs')",
263293
);
264294
writeFileSync(join(hyperlightHostApiDir, "lib.cjs"), libJsContent);
265-
// Create a minimal index.cjs shim that loads the .node addon from the
266-
// same directory. Platform-specific .node file is resolved at build time.
267-
writeFileSync(
268-
join(hyperlightHostApiDir, "index.cjs"),
269-
`'use strict';\nmodule.exports = require('./js-host-api.${napiTriple}.node');\n`,
270-
);
295+
// Copy the napi-rs generated index.js as index.cjs — it already has full
296+
// platform detection (musl vs glibc, win32, darwin) and tries local .node
297+
// files first, then falls back to optional @hyperlight/ scoped packages.
298+
const indexJsContent = readFileSync(hyperlightIndexJs, "utf-8");
299+
writeFileSync(join(hyperlightHostApiDir, "index.cjs"), indexJsContent);
271300

272301
// ── Step 5: Copy runtime resources ─────────────────────────────────────
273302
console.log("📁 Copying runtime resources...");
@@ -403,7 +432,9 @@ Module._load = function(request, parent, isMain) {
403432
return originalLoad.call(this, join(LIB_DIR, 'js-host-api', 'lib.cjs'), parent, isMain);
404433
}
405434
if (request === 'hyperlight-analysis') {
406-
return originalLoad.call(this, join(LIB_DIR, 'hyperlight-analysis.${napiTriple}.node'), parent, isMain);
435+
// The hyperlight-analysis index.js already has full napi-rs platform detection.
436+
// It's copied into the node_modules structure, so just load it from there.
437+
return originalLoad.call(this, join(LIB_DIR, 'node_modules', 'hyperlight-analysis', 'index.js'), parent, isMain);
407438
}
408439
return originalLoad.apply(this, arguments);
409440
};

0 commit comments

Comments
 (0)