diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 15424dc..8a27bb8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/builtin-modules/ooxml-core.json b/builtin-modules/ooxml-core.json index d291414..7143b04 100644 --- a/builtin-modules/ooxml-core.json +++ b/builtin-modules/ooxml-core.json @@ -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.", diff --git a/builtin-modules/pptx-charts.json b/builtin-modules/pptx-charts.json index 2f92c5b..591fe56 100644 --- a/builtin-modules/pptx-charts.json +++ b/builtin-modules/pptx-charts.json @@ -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.", diff --git a/builtin-modules/pptx-tables.json b/builtin-modules/pptx-tables.json index 09e0bfc..140c43b 100644 --- a/builtin-modules/pptx-tables.json +++ b/builtin-modules/pptx-tables.json @@ -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.", diff --git a/builtin-modules/pptx.json b/builtin-modules/pptx.json index 8f3bd72..6d35ce1 100644 --- a/builtin-modules/pptx.json +++ b/builtin-modules/pptx.json @@ -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.", diff --git a/package.json b/package.json index fae1070..8527e7b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/build-binary.js b/scripts/build-binary.js index 193a04c..a5a4e1a 100644 --- a/scripts/build-binary.js +++ b/scripts/build-binary.js @@ -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"); @@ -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..."); @@ -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); }; @@ -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); @@ -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): diff --git a/src/code-validator/guest/package.json b/src/code-validator/guest/package.json index 89e4280..ca455b4 100644 --- a/src/code-validator/guest/package.json +++ b/src/code-validator/guest/package.json @@ -13,6 +13,7 @@ "binaryName": "hyperlight-analysis", "targets": [ "x86_64-unknown-linux-gnu", + "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc" ] }, diff --git a/tests/dts-sync.test.ts b/tests/dts-sync.test.ts index bed1c2e..0539031 100644 --- a/tests/dts-sync.test.ts +++ b/tests/dts-sync.test.ts @@ -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"; @@ -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); + }); }); /** diff --git a/tests/pattern-loader.test.ts b/tests/pattern-loader.test.ts index 879b89c..a36f3bc 100644 --- a/tests/pattern-loader.test.ts +++ b/tests/pattern-loader.test.ts @@ -23,8 +23,16 @@ describe("pattern-loader", () => { afterEach(() => { try { rmSync(TMP_DIR, { recursive: true, force: true }); - } catch { - // Windows: Defender/indexer may hold a lock — not worth failing the test + } catch (err: unknown) { + // Windows Defender/indexer can hold file locks — only swallow those + const code = (err as NodeJS.ErrnoException).code; + if ( + process.platform === "win32" && + (code === "EBUSY" || code === "EPERM") + ) { + return; + } + throw err; } });