From fad9da85eac3677b6b6bbbe4338fac79be8950ba Mon Sep 17 00:00:00 2001 From: JKRT Date: Thu, 25 Jun 2026 16:39:49 +0200 Subject: [PATCH 1/3] Add standalone binary build via Node.js SEA Adds npm run build:standalone which produces a self-contained modelica-language-server[.exe] using the Node.js Single Executable Application (SEA) API (Node >= 20). The binary bundles the Node.js runtime + server.js into one file so end users do not need Node.js installed. The two WASM files (tree-sitter-modelica.wasm, web-tree-sitter.wasm) must remain alongside the binary. Intended for use in OMEdit installer packaging where CI builds platform-specific binaries via GitHub Actions and attaches them to releases. OMEdit's LSPClient already prefers a modelica-language-server binary over server.js when both are present. Co-Authored-By: Claude Sonnet 4.6 --- server/package-lock.json | 27 ++++++++++++ server/package.json | 2 + server/scripts/build-standalone.js | 70 ++++++++++++++++++++++++++++++ server/sea-config.json | 5 +++ 4 files changed, 104 insertions(+) create mode 100644 server/scripts/build-standalone.js create mode 100644 server/sea-config.json diff --git a/server/package-lock.json b/server/package-lock.json index cf3c797..6a6c224 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -19,6 +19,7 @@ "devDependencies": { "@types/node": "^25.9.1", "esbuild": "^0.28.0", + "postject": "^1.0.0-alpha.6", "tsx": "^4.19.0", "typescript": "^6.0.0" }, @@ -478,6 +479,16 @@ "undici-types": ">=7.24.0 <7.24.7" } }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/esbuild": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", @@ -535,6 +546,22 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tsx": { "version": "4.22.4", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", diff --git a/server/package.json b/server/package.json index b269728..797f46d 100644 --- a/server/package.json +++ b/server/package.json @@ -29,12 +29,14 @@ "scripts": { "build": "node esbuild.config.js", "build:watch": "node esbuild.config.js --watch", + "build:standalone": "npm run build && node scripts/build-standalone.js", "prepack": "npm run build", "prepublishOnly": "npm run build" }, "devDependencies": { "@types/node": "^25.9.1", "esbuild": "^0.28.0", + "postject": "^1.0.0-alpha.6", "tsx": "^4.19.0", "typescript": "^6.0.0" }, diff --git a/server/scripts/build-standalone.js b/server/scripts/build-standalone.js new file mode 100644 index 0000000..48478e6 --- /dev/null +++ b/server/scripts/build-standalone.js @@ -0,0 +1,70 @@ +#!/usr/bin/env node +/** + * Build a Node.js Single Executable Application (SEA) for the language server. + * Produces out/modelica-language-server[.exe] alongside the existing WASM files. + * Requires Node.js >= 20 and postject (dev dependency). + * + * Usage: node scripts/build-standalone.js + */ + +'use strict'; + +const { execFileSync, execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const OUT_DIR = path.join(__dirname, '..', 'out'); +const SEA_CONFIG = path.join(__dirname, '..', 'sea-config.json'); +const BLOB = path.join(OUT_DIR, 'sea-prep.blob'); +const IS_WIN = process.platform === 'win32'; +const IS_MAC = process.platform === 'darwin'; +const BINARY_NAME = IS_WIN ? 'modelica-language-server.exe' : 'modelica-language-server'; +const OUT_BINARY = path.join(OUT_DIR, BINARY_NAME); +const POSTJECT = path.join(__dirname, '..', 'node_modules', '.bin', 'postject'); + +function run(cmd, args, opts) { + console.log(` $ ${cmd} ${args.join(' ')}`); + execFileSync(cmd, args, { stdio: 'inherit', ...opts }); +} + +// 1. Generate the SEA preparation blob. +console.log('\n[1/4] Generating SEA blob...'); +run(process.execPath, ['--experimental-sea-config', SEA_CONFIG]); + +// 2. Copy the current node binary as the carrier. +console.log('\n[2/4] Copying node binary...'); +fs.copyFileSync(process.execPath, OUT_BINARY); +fs.chmodSync(OUT_BINARY, 0o755); + +// 3. Remove existing code signature on macOS (required before injection). +if (IS_MAC) { + console.log('\n[2b] Removing macOS code signature...'); + run('codesign', ['--remove-signature', OUT_BINARY]); +} + +// 4. Inject the blob. +console.log('\n[3/4] Injecting SEA blob with postject...'); +const postjectArgs = [ + OUT_BINARY, + 'NODE_SEA_BLOB', + BLOB, + '--sentinel-fuse', 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2', +]; +if (IS_MAC) { + postjectArgs.push('--macho-segment-name', 'NODE_SEA'); +} +run(POSTJECT, postjectArgs); + +// 4. Re-sign on macOS (ad-hoc, sufficient for local use; CI can use a real identity). +if (IS_MAC) { + console.log('\n[4/4] Ad-hoc re-signing for macOS...'); + run('codesign', ['--sign', '-', OUT_BINARY]); +} else { + console.log('\n[4/4] Done.'); +} + +const size = (fs.statSync(OUT_BINARY).size / 1024 / 1024).toFixed(1); +console.log(`\nStandalone binary: ${OUT_BINARY} (${size} MB)`); +console.log('WASM files must remain alongside the binary:'); +console.log(' tree-sitter-modelica.wasm'); +console.log(' web-tree-sitter.wasm'); diff --git a/server/sea-config.json b/server/sea-config.json new file mode 100644 index 0000000..352ff2d --- /dev/null +++ b/server/sea-config.json @@ -0,0 +1,5 @@ +{ + "main": "out/server.js", + "output": "out/sea-prep.blob", + "disableExperimentalSEAWarning": true +} From 6cb142f1239197bdecba9a7de62c7b34267b57c0 Mon Sep 17 00:00:00 2001 From: JKRT Date: Thu, 25 Jun 2026 17:07:08 +0200 Subject: [PATCH 2/3] Added a workflow to build standalone binaries --- .github/workflows/standalone.yml | 59 ++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/workflows/standalone.yml diff --git a/.github/workflows/standalone.yml b/.github/workflows/standalone.yml new file mode 100644 index 0000000..e62e99f --- /dev/null +++ b/.github/workflows/standalone.yml @@ -0,0 +1,59 @@ +name: Build standalone binary + + on: + push: + tags: ['v*'] + workflow_dispatch: + + jobs: + build: + name: ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: server/package-lock.json + + - name: Build standalone binary + run: cd server && npm ci && npm run build:standalone + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: modelica-language-server-${{ runner.os }} + path: | + server/out/modelica-language-server* + server/out/tree-sitter-modelica.wasm + server/out/web-tree-sitter.wasm + + release: + name: Attach to release + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + path: artifacts/ + + - name: Package per-platform archives + run: | + cd artifacts + for dir in */; do + platform="${dir%/}" + tar -czf "${platform}.tar.gz" -C "$dir" . + done + + - name: Upload to GitHub release + uses: softprops/action-gh-release@v2 + with: + files: artifacts/*.tar.gz From 070e73c74757df7849b68a59713681a4f1bd0064 Mon Sep 17 00:00:00 2001 From: JKRT Date: Thu, 25 Jun 2026 17:53:01 +0200 Subject: [PATCH 3/3] Fold standalone binary build into test.yml; build always and attach on release Addresses review feedback: build the standalone binary in the existing build job on every run so the script is verified, and attach the binary plus WASM files to the GitHub release. Removes the separate standalone.yml. Co-Authored-By: JKRT Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/standalone.yml | 59 -------------------------------- .github/workflows/test.yml | 8 +++++ 2 files changed, 8 insertions(+), 59 deletions(-) delete mode 100644 .github/workflows/standalone.yml diff --git a/.github/workflows/standalone.yml b/.github/workflows/standalone.yml deleted file mode 100644 index e62e99f..0000000 --- a/.github/workflows/standalone.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Build standalone binary - - on: - push: - tags: ['v*'] - workflow_dispatch: - - jobs: - build: - name: ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: '22' - cache: 'npm' - cache-dependency-path: server/package-lock.json - - - name: Build standalone binary - run: cd server && npm ci && npm run build:standalone - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: modelica-language-server-${{ runner.os }} - path: | - server/out/modelica-language-server* - server/out/tree-sitter-modelica.wasm - server/out/web-tree-sitter.wasm - - release: - name: Attach to release - needs: build - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/download-artifact@v4 - with: - path: artifacts/ - - - name: Package per-platform archives - run: | - cd artifacts - for dir in */; do - platform="${dir%/}" - tar -czf "${platform}.tar.gz" -C "$dir" . - done - - - name: Upload to GitHub release - uses: softprops/action-gh-release@v2 - with: - files: artifacts/*.tar.gz diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 295b4fa..6c806b6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -65,11 +65,17 @@ jobs: working-directory: server run: npm pack + - name: Build standalone binary + working-directory: server + run: npm run build:standalone + - name: Stage server release files run: | mkdir server-release cp server/openmodelica-modelica-language-server-*.tgz server-release/ cp server/src/tree-sitter-modelica.wasm server-release/ + cp server/out/modelica-language-server server-release/modelica-language-server-linux-x64 + cp server/out/web-tree-sitter.wasm server-release/ - name: Archive server release files uses: actions/upload-artifact@v7 @@ -107,6 +113,8 @@ jobs: modelica-language-server-*.vsix server-release/openmodelica-modelica-language-server-*.tgz server-release/tree-sitter-modelica.wasm + server-release/web-tree-sitter.wasm + server-release/modelica-language-server-linux-x64 fail_on_unmatched_files: true generate_release_notes: true append_body: true