Skip to content
Merged
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
82 changes: 69 additions & 13 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,30 @@ env:
IMAGE_NAME: ${{ github.repository }}

jobs:
# Test on all hypervisor configurations before publishing
# NOTE: Windows WHP temporarily disabled (see pr-validate.yml)
test:
name: Test (${{ matrix.build }})
# Build native addons on each platform and upload as artifacts.
# These are combined in the publish-npm job to create a cross-platform package.
#
# gnu and win32 builds run tests natively on their platform.
# musl is cross-compiled from the glibc runner (can't run tests on glibc host).
build-native:
name: Build (${{ matrix.build }})
strategy:
fail-fast: true
matrix:
build: [linux-kvm, linux-mshv]
build: [linux-kvm, linux-musl, windows-whp]
include:
- build: linux-kvm
os: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd"]
hypervisor: kvm
- build: linux-mshv
os: [self-hosted, Linux, X64, "1ES.Pool=hld-azlinux3-mshv-amd"]
hypervisor: mshv
run_tests: true
- build: linux-musl
os: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd"]
hypervisor: kvm
run_tests: false # musl .node can't run on glibc host
- build: windows-whp
os: [self-hosted, Windows, X64, "1ES.Pool=hld-win2022-amd"]
hypervisor: whp
run_tests: true
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
Expand All @@ -53,17 +62,55 @@ jobs:
- name: Setup
run: just setup

- name: Install musl tools and rebuild for musl target
if: matrix.build == 'linux-musl'
run: |
sudo apt-get update && sudo apt-get install -y musl-tools
rustup target add x86_64-unknown-linux-musl

# Rebuild hyperlight-js NAPI addon targeting musl
hl_dir=$(just resolve-hyperlight-dir)
cd "${hl_dir}/src/js-host-api"
npx napi build --platform --target x86_64-unknown-linux-musl

# Rebuild hyperlight-analysis NAPI addon targeting musl
cd "$GITHUB_WORKSPACE/src/code-validator/guest"
npx napi build --platform --target x86_64-unknown-linux-musl --manifest-path host/Cargo.toml
node -e "require('fs').readdirSync('host').filter(f=>f.endsWith('.node')).forEach(f=>require('fs').copyFileSync('host/'+f,f))"

# Verify musl .node files were actually produced
ls -la "${hl_dir}/src/js-host-api/"*.linux-x64-musl.node
ls -la "$GITHUB_WORKSPACE/src/code-validator/guest/"*linux-x64-musl* || ls -la "$GITHUB_WORKSPACE/src/code-validator/guest/host/"*linux-x64-musl*

- name: Build release binary
if: matrix.run_tests
run: node scripts/build-binary.js --release
env:
VERSION: ${{ github.event.release.tag_name || inputs.version }}

- name: Run tests
if: matrix.run_tests
run: just test

# Build and publish npm package (after tests pass)
# Upload the native .node addons so the publish job can combine them
- name: Upload native addons
uses: actions/upload-artifact@v4
with:
name: native-addons-${{ matrix.build }}
path: |
deps/js-host-api/js-host-api.*.node
src/code-validator/guest/host/hyperlight-analysis.*.node
src/code-validator/guest/hyperlight-analysis.*.node
if-no-files-found: error
retention-days: 1

# Combine native addons from all platforms and publish a single npm package.
# Runs on a self-hosted Linux runner (not ubuntu-latest) because just setup
# needs to build the Rust runtime which requires hyperlight toolchain.
publish-npm:
name: Publish to npmjs.org
needs: [test]
runs-on: ubuntu-latest
needs: [build-native]
runs-on: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd"]
steps:
- uses: actions/checkout@v6

Expand All @@ -81,7 +128,16 @@ jobs:
- name: Setup
run: just setup

- name: Build binary
# Download AFTER setup so artifacts land in the symlink/junction target
# that build-hyperlight creates (deps/js-host-api → Cargo checkout).
# Downloading before setup would be clobbered when setup re-creates the link.
- name: Download all native addons
uses: actions/download-artifact@v4
with:
pattern: native-addons-*
merge-multiple: true

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

- name: Set version from release tag
Expand All @@ -100,7 +156,7 @@ jobs:
# Build and publish Docker image (after tests pass)
publish-docker:
name: Publish to GitHub Container Registry
needs: [test]
needs: [build-native]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
Expand Down
4 changes: 2 additions & 2 deletions builtin-modules/ooxml-core.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"description": "Shared OOXML infrastructure - units, colors, themes, Content_Types, relationships",
"author": "system",
"mutable": false,
"sourceHash": "sha256:1e939013c13555bc",
"dtsHash": "sha256:9f88e7c59a56854c",
"sourceHash": "sha256:b5f017fe2d4e2ed3",
"dtsHash": "sha256:6aac85502082bf89",
"importStyle": "named",
"hints": {
"overview": "Low-level OOXML infrastructure. Most users should use ha:pptx instead.",
Expand Down
4 changes: 2 additions & 2 deletions builtin-modules/pptx-charts.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"description": "OOXML DrawingML chart generation - bar, pie, line charts for PPTX presentations",
"author": "system",
"mutable": false,
"sourceHash": "sha256:5c521ce93ff39626",
"dtsHash": "sha256:5f653830226c3554",
"sourceHash": "sha256:4174b6f03be2e0fb",
"dtsHash": "sha256:4353b8263dc99405",
"importStyle": "named",
"hints": {
"overview": "Chart generation for PPTX. Always used with ha:pptx.",
Expand Down
4 changes: 2 additions & 2 deletions builtin-modules/pptx-tables.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"description": "Styled tables for PPTX presentations - headers, borders, alternating rows",
"author": "system",
"mutable": false,
"sourceHash": "sha256:0739a7db5a8ab428",
"dtsHash": "sha256:82d903ffbf4dfb1e",
"sourceHash": "sha256:2d58934ed7df9fe1",
"dtsHash": "sha256:3ba75bbc44353467",
"importStyle": "named",
"hints": {
"overview": "Table generation for PPTX. Always used with ha:pptx.",
Expand Down
4 changes: 2 additions & 2 deletions builtin-modules/pptx.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"description": "PowerPoint PPTX presentation builder - slides, text, shapes, themes, layouts",
"author": "system",
"mutable": false,
"sourceHash": "sha256:093b19522e994756",
"dtsHash": "sha256:2107e369816b4bd5",
"sourceHash": "sha256:23569540a0f8622f",
"dtsHash": "sha256:27520514e4401465",
"importStyle": "named",
"hints": {
"overview": "Core PPTX slide building. Charts in ha:pptx-charts, tables in ha:pptx-tables.",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"fmt:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\" \"plugins/**/*.ts\" \"builtin-modules/**/*.js\"",
"check": "npm run fmt:check && npm run typecheck && npm run test",
"prepare": "node -e \"if(require('fs').existsSync('scripts/build-modules.js'))require('child_process').execSync('npm run build:modules',{stdio:'inherit'})\"",
"postinstall": "node -e \"var fs=require('fs'),cp=require('child_process');if(fs.existsSync('scripts/patch-vscode-jsonrpc.js')){cp.execSync('node scripts/patch-vscode-jsonrpc.js',{stdio:'inherit'});cp.execSync('node scripts/check-native-runtime.js',{stdio:'inherit'})}\""
"postinstall": "node -e \"var fs=require('fs'),cp=require('child_process');if(fs.existsSync('scripts/patch-vscode-jsonrpc.js')){cp.execSync('node scripts/patch-vscode-jsonrpc.js',{stdio:'inherit'});cp.execSync('node scripts/check-native-runtime.js',{stdio:'inherit'});}\""
},
"dependencies": {
"@github/copilot-sdk": "^0.1.32",
Expand Down
103 changes: 80 additions & 23 deletions scripts/build-binary.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,20 +217,43 @@ if (!existsSync(analysisNode)) {
process.exit(1);
}

copyFileSync(hyperlightNode, join(LIB_DIR, `js-host-api.${napiTriple}.node`));
copyFileSync(
analysisNode,
join(LIB_DIR, `hyperlight-analysis.${napiTriple}.node`),
);
// Copy .node files for ALL available platforms so the package is cross-platform.
// The current platform's .node is guaranteed to exist (checked above).
// Additional platform .node files are copied if present (e.g. from CI matrix builds).
const ALL_TRIPLES = ["linux-x64-gnu", "linux-x64-musl", "win32-x64-msvc"];
for (const triple of ALL_TRIPLES) {
const hlNode = join(ROOT, `deps/js-host-api/js-host-api.${triple}.node`);
const anNode = join(
ROOT,
`src/code-validator/guest/host/hyperlight-analysis.${triple}.node`,
);
if (existsSync(hlNode)) {
copyFileSync(hlNode, join(LIB_DIR, `js-host-api.${triple}.node`));
console.log(` ✓ js-host-api.${triple}.node`);
}
if (existsSync(anNode)) {
copyFileSync(anNode, join(LIB_DIR, `hyperlight-analysis.${triple}.node`));
console.log(` ✓ hyperlight-analysis.${triple}.node`);
}
}

// Create a proper node_modules package structure for hyperlight-analysis
// so both require() and import() can resolve it in the bundled binary.
const analysisPkgDir = join(LIB_DIR, "node_modules", "hyperlight-analysis");
mkdirSync(analysisPkgDir, { recursive: true });
copyFileSync(
analysisNode,
join(analysisPkgDir, `hyperlight-analysis.${napiTriple}.node`),
);
// Copy all available platform .node files into the package dir
for (const triple of ALL_TRIPLES) {
const anNode = join(
ROOT,
`src/code-validator/guest/host/hyperlight-analysis.${triple}.node`,
);
if (existsSync(anNode)) {
copyFileSync(
anNode,
join(analysisPkgDir, `hyperlight-analysis.${triple}.node`),
);
}
}
// Copy the index.js and index.d.ts from the source package
const analysisIndex = join(ROOT, "src/code-validator/guest/index.js");
const analysisTypes = join(ROOT, "src/code-validator/guest/index.d.ts");
Expand All @@ -250,24 +273,30 @@ if (existsSync(analysisPkg))
// Files are renamed to .cjs because the host package.json has "type": "module"
// which makes Node.js treat .js as ESM — but lib.js uses require().
const hyperlightLibJs = join(ROOT, "deps/js-host-api/lib.js");
const hyperlightIndexJs = join(ROOT, "deps/js-host-api/index.js");
const hyperlightHostApiDir = join(LIB_DIR, "js-host-api");
mkdirSync(hyperlightHostApiDir, { recursive: true });
copyFileSync(
hyperlightNode,
join(hyperlightHostApiDir, `js-host-api.${napiTriple}.node`),
);
// Copy all available platform .node files
for (const triple of ALL_TRIPLES) {
const hlNode = join(ROOT, `deps/js-host-api/js-host-api.${triple}.node`);
if (existsSync(hlNode)) {
copyFileSync(
hlNode,
join(hyperlightHostApiDir, `js-host-api.${triple}.node`),
);
}
}
// Copy lib.js as lib.cjs, patching the require('./index.js') to './index.cjs'
const libJsContent = readFileSync(hyperlightLibJs, "utf-8").replace(
"require('./index.js')",
"require('./index.cjs')",
);
writeFileSync(join(hyperlightHostApiDir, "lib.cjs"), libJsContent);
// Create a minimal index.cjs shim that loads the .node addon from the
// same directory. Platform-specific .node file is resolved at build time.
writeFileSync(
join(hyperlightHostApiDir, "index.cjs"),
`'use strict';\nmodule.exports = require('./js-host-api.${napiTriple}.node');\n`,
);
// Copy the napi-rs generated index.js as index.cjs — it already has full
// platform detection (musl vs glibc, win32, darwin) and tries local .node
// files first, then falls back to optional @hyperlight/ scoped packages.
const indexJsContent = readFileSync(hyperlightIndexJs, "utf-8");
writeFileSync(join(hyperlightHostApiDir, "index.cjs"), indexJsContent);

// ── Step 5: Copy runtime resources ─────────────────────────────────────
console.log("📁 Copying runtime resources...");
Expand Down Expand Up @@ -403,7 +432,35 @@ Module._load = function(request, parent, isMain) {
return originalLoad.call(this, join(LIB_DIR, 'js-host-api', 'lib.cjs'), parent, isMain);
}
if (request === 'hyperlight-analysis') {
return originalLoad.call(this, join(LIB_DIR, 'hyperlight-analysis.${napiTriple}.node'), parent, isMain);
// Load the correct platform-specific .node directly, with musl detection.
// The index.js loader doesn't distinguish musl vs glibc, so we handle it here.
const fs = require('fs');
const hyperlightDir = join(LIB_DIR, 'node_modules', 'hyperlight-analysis');
const platformArch = process.platform + '-' + process.arch;
const candidates = [];
if (platformArch === 'linux-x64') {
// Detect musl vs glibc — try musl first on musl systems, then glibc
let isMusl = false;
try {
const r = require('child_process').spawnSync('ldd', ['--version'],
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
isMusl = ((r.stdout || '') + (r.stderr || '')).includes('musl');
} catch {}
if (isMusl) {
candidates.push(join(hyperlightDir, 'hyperlight-analysis.linux-x64-musl.node'));
}
candidates.push(join(hyperlightDir, 'hyperlight-analysis.linux-x64-gnu.node'));
} else if (platformArch === 'win32-x64') {
candidates.push(join(hyperlightDir, 'hyperlight-analysis.win32-x64-msvc.node'));
}
// Fall back to index.js loader
candidates.push(join(hyperlightDir, 'index.js'));
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return originalLoad.call(this, candidate, parent, isMain);
}
}
return originalLoad.apply(this, arguments);
}
return originalLoad.apply(this, arguments);
};
Expand All @@ -426,10 +483,10 @@ writeFileSync(launcherCjsPath, launcherCjs);
// Node.js launcher (works everywhere, used as npm bin entry)
const nodeLauncher = `#!/usr/bin/env node
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { fileURLToPath, pathToFileURL } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const cjs = join(__dirname, '..', 'lib', 'hyperagent-launcher.cjs');
await import('file://' + cjs.replace(/\\\\/g, '/'));
await import(pathToFileURL(cjs).href);
`;
const nodeLauncherPath = join(BIN_DIR, "hyperagent");
writeFileSync(nodeLauncherPath, nodeLauncher);
Expand Down Expand Up @@ -494,7 +551,7 @@ To run (option 3 - add to PATH permanently via System Properties):
${launcherPath}

To run (option 2 - add to PATH):
export PATH="${BIN_DIR}:\\$PATH"
export PATH="${BIN_DIR}:$PATH"
hyperagent

To run (option 3 - symlink):
Expand Down
1 change: 1 addition & 0 deletions src/code-validator/guest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"binaryName": "hyperlight-analysis",
"targets": [
"x86_64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
"x86_64-pc-windows-msvc"
]
},
Expand Down
36 changes: 35 additions & 1 deletion tests/dts-sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@

import { describe, it, expect } from "vitest";
import { createHash } from "crypto";
import { readdirSync, readFileSync, existsSync, rmSync } from "fs";
import {
readdirSync,
readFileSync,
existsSync,
rmSync,
writeFileSync,
} from "fs";
import { join } from "path";
import { execSync } from "child_process";

Expand Down Expand Up @@ -230,6 +236,34 @@ describe("TypeScript source consistency", () => {
rmSync(tmpDir, { recursive: true, force: true });
}
});

it("ha-modules.d.ts matches regenerated output", () => {
// Regenerate ha-modules.d.ts and compare with the committed version.
// Catches drift where a module's exports/types changed but the generator wasn't re-run.
const haModulesPath = join(SRC_DIR, "types", "ha-modules.d.ts");
if (!existsSync(haModulesPath)) return;

const committed = readFileSync(haModulesPath, "utf-8");

// Regenerate in-place (the script always writes to the same path)
execSync("npx tsx scripts/generate-ha-modules-dts.ts", {
cwd: join(import.meta.dirname, ".."),
stdio: "pipe",
});

const regenerated = readFileSync(haModulesPath, "utf-8");

// Restore the committed version so the test doesn't have side effects
if (regenerated !== committed) {
writeFileSync(haModulesPath, committed);
}

expect(
regenerated,
"ha-modules.d.ts is out of date with compiled .d.ts files. " +
"Run: npx tsx scripts/generate-ha-modules-dts.ts",
).toBe(committed);
});
});

/**
Expand Down
Loading
Loading