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 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 +}