From 8c0e63abd9b4cb583ddca56461285e46efadf064 Mon Sep 17 00:00:00 2001 From: Aleksandr Borovskii Date: Wed, 2 Sep 2026 18:32:43 +0300 Subject: [PATCH 1/7] fix(deps): bump nan to ^2.28.0 and node-abi to ^4.35.0 for Node 26 (ABI 147) nan gained Node 26 / V8 14 support in 2.27.0; the lockfile pinned 2.24.0, which fails to compile on Node 26. node-abi ^4.24.0 mapped Node 26 to abi144 (a stale dev value); ^4.35.0 maps it to 147. Also adds --target 26.8.1 to build:gyp and build:gyp-cross so the release matrix emits ABI 147. Refs TM-3894 --- package-lock.json | 14 +++++++------- package.json | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4ade15f..edd9c73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "nan": "^2.24.0", + "nan": "^2.28.0", "node-gyp-build": "^4.8.4", "plist": "^3.1.0" }, @@ -6345,9 +6345,9 @@ } }, "node_modules/nan": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.24.0.tgz", - "integrity": "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==", + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", + "integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==", "license": "MIT" }, "node_modules/neo-async": { @@ -6365,9 +6365,9 @@ "license": "MIT" }, "node_modules/node-abi": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.24.0.tgz", - "integrity": "sha512-u2EC1CeNe25uVtX3EZbdQ275c74zdZmmpzrHEQh2aIYqoVjlglfUpOX9YY85x1nlBydEKDVaSmMNhR7N82Qj8A==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.35.0.tgz", + "integrity": "sha512-ymk4aIzxdPopw2giv8Fs1Ec6vybGkjmyxUwVqhkI4MCy2tVfXdkOGGWieWVjL0THgH+7a8lRdevyupoYj3Js/Q==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 6b75e3e..982ee41 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "access": "public" }, "dependencies": { - "nan": "^2.24.0", + "nan": "^2.28.0", "node-gyp-build": "^4.8.4", "plist": "^3.1.0" }, @@ -98,15 +98,15 @@ "prepublishOnly": "npm run build", "test": "node --test", "semantic-release": "semantic-release", - "build:gyp": "prebuildify --napi false --strip --target 20.20.0 --target 21.7.3 --target 22.22.0 --target 23.11.1 --target 24.13.0 --target 25.3.0", - "build:gyp-cross": "prebuildify-cross --napi false --strip --target 20.20.0 --target 21.7.3 --target 22.22.0 --target 23.11.1 --target 24.13.0 --target 25.3.0", + "build:gyp": "prebuildify --napi false --strip --target 20.20.0 --target 21.7.3 --target 22.22.0 --target 23.11.1 --target 24.13.0 --target 25.3.0 --target 26.8.1", + "build:gyp-cross": "prebuildify-cross --napi false --strip --target 20.20.0 --target 21.7.3 --target 22.22.0 --target 23.11.1 --target 24.13.0 --target 25.3.0 --target 26.8.1", "native_install": "node-gyp-build", "install": "node scripts/install.mjs", "prepare": "is-ci || husky install" }, "overrides": { "prebuildify": { - "node-abi": "^4.24.0" + "node-abi": "^4.35.0" } } } From 6201d0134f36148e354822834e3a0c39bbb98114 Mon Sep 17 00:00:00 2001 From: Aleksandr Borovskii Date: Wed, 2 Sep 2026 18:32:47 +0300 Subject: [PATCH 2/7] fix(install): propagate the native build exit code scripts/install.mjs logged the child exit code but never propagated it, so a failed Windows native build exited 0 and only surfaced later at require() time inside the service-install path. Exit with the child's code (and non-zero on signal/spawn error). Adds an OS_SERVICE_FORCE_NATIVE_INSTALL override so the Windows-only build path is exercisable on any OS, and a mutation-checked regression test. Refs TM-3894 --- scripts/install.mjs | 10 +++++- test/e2e/install-exit-code.test.mjs | 54 +++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 test/e2e/install-exit-code.test.mjs diff --git a/scripts/install.mjs b/scripts/install.mjs index 7fc9b05..f6c56e9 100644 --- a/scripts/install.mjs +++ b/scripts/install.mjs @@ -5,7 +5,10 @@ import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); -if (platform() === 'win32') { +// The native addon is only needed on Windows, so the build normally runs only +// there. OS_SERVICE_FORCE_NATIVE_INSTALL forces it anywhere (manual rebuilds, +// CI verifying the build/exit-code path on a non-Windows runner). +if (platform() === 'win32' || process.env.OS_SERVICE_FORCE_NATIVE_INSTALL) { const npmProcess = spawn('npm', ['run', 'native_install'], { input: 'Windows detected. Installing native module.', stdio: 'inherit', @@ -19,14 +22,19 @@ if (platform() === 'win32') { npmProcess.on('exit', (code, signal) => { if (code !== null) { console.log(`Child process exited with code ${code}`); + // Propagate a failing build; a silent exit 0 ships a broken addon that + // only fails later at require() time. + process.exit(code); } else if (signal !== null) { console.log(`Child process was terminated by signal ${signal}`); + process.exit(1); } }); // Handle child process errors npmProcess.on('error', (error) => { console.error('Error occurred:', error); + process.exit(1); }); // Forward parent process signals to the child process diff --git a/test/e2e/install-exit-code.test.mjs b/test/e2e/install-exit-code.test.mjs new file mode 100644 index 0000000..8583d5c --- /dev/null +++ b/test/e2e/install-exit-code.test.mjs @@ -0,0 +1,54 @@ +import { describe, it } from 'node:test'; +import { strictEqual } from 'node:assert'; +import { spawn } from 'node:child_process'; +import { mkdtemp, mkdir, writeFile, copyFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const installScript = join(__dirname, '..', '..', 'scripts', 'install.mjs'); + +// Run install.mjs in an isolated project whose `native_install` exits with the +// given code, forcing the (normally Windows-only) build path via +// OS_SERVICE_FORCE_NATIVE_INSTALL so the exit-propagation is exercised on any OS. +function runInstall(nativeInstallExitCode) { + return new Promise((resolvePromise, reject) => { + (async () => { + const dir = await mkdtemp(join(tmpdir(), 'os-service-install-')); + try { + await mkdir(join(dir, 'scripts'), { recursive: true }); + await copyFile(installScript, join(dir, 'scripts', 'install.mjs')); + await writeFile( + join(dir, 'package.json'), + JSON.stringify({ + name: 'install-exit-code-fixture', + scripts: { native_install: `node -e "process.exit(${nativeInstallExitCode})"` } + }) + ); + const child = spawn(process.execPath, ['scripts/install.mjs'], { + cwd: dir, + env: { ...process.env, OS_SERVICE_FORCE_NATIVE_INSTALL: '1' }, + stdio: 'ignore' + }); + child.on('error', reject); + child.on('exit', (code) => { rm(dir, { recursive: true, force: true }).finally(() => resolvePromise(code)); }); + } catch (err) { + await rm(dir, { recursive: true, force: true }).catch(() => {}); + reject(err); + } + })(); + }); +} + +describe('install.mjs native build exit code', () => { + it('should exit non-zero when the native build fails', async () => { + const code = await runInstall(1); + strictEqual(code, 1); + }); + + it('should exit zero when the native build succeeds', async () => { + const code = await runInstall(0); + strictEqual(code, 0); + }); +}); From 42efb642cf045fad46630e06a07cd296becc9197 Mon Sep 17 00:00:00 2001 From: Aleksandr Borovskii Date: Wed, 2 Sep 2026 18:32:48 +0300 Subject: [PATCH 3/7] ci: add Node 25 and 26 to the test matrices; pin Windows to windows-2022 windows-latest (Server 2025 / VS 18) breaks node-gyp VS detection on older Node; pin the Windows test leg to windows-2022 for legacy coverage. Refs TM-3894 --- .github/workflows/test.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 31ed6d1..6f2f063 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,8 +17,10 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - node-version: [20, 22, 23, 24] + # windows-latest (Server 2025 / VS 18) breaks the node-gyp VS detection + # bundled with older Node; pin to windows-2022 for legacy coverage. + os: [ubuntu-latest, macos-latest, windows-2022] + node-version: [20, 22, 23, 24, 25, 26] steps: - uses: actions/checkout@v4 @@ -46,7 +48,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [20, 22, 23, 24] + node-version: [20, 22, 23, 24, 25, 26] steps: - uses: actions/checkout@v4 From 09168f966c292435e48e14513f54d40dc56e304d Mon Sep 17 00:00:00 2001 From: Aleksandr Borovskii Date: Wed, 2 Sep 2026 18:34:06 +0300 Subject: [PATCH 4/7] ci: add temporary Windows abi147 prebuild gate (DELETE BEFORE MERGE) deploy.yml runs only on release and the Windows prebuild cannot be built on the macOS dev host, so this branch-push gate builds the Node 26 (abi147) win32-x64 prebuild on windows-2022, asserts it exists and resolves for the running ABI, and uploads it. Never publishes. Must be removed before merge. Refs TM-3894 --- .../tmp-tm3894-win-prebuild-gate.yml | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/tmp-tm3894-win-prebuild-gate.yml diff --git a/.github/workflows/tmp-tm3894-win-prebuild-gate.yml b/.github/workflows/tmp-tm3894-win-prebuild-gate.yml new file mode 100644 index 0000000..9be1885 --- /dev/null +++ b/.github/workflows/tmp-tm3894-win-prebuild-gate.yml @@ -0,0 +1,57 @@ +# TEMPORARY — TM-3894 Windows prebuild gate. DELETE BEFORE MERGE. +# +# deploy.yml runs only on: release, so the Windows abi147 prebuild is +# unreachable from a PR — and it cannot be built on the macOS dev host. This +# builds it on windows-2022, asserts an abi147 binary is produced and loads, +# and uploads it. It NEVER publishes. Remove before merge. +name: TMP TM-3894 win prebuild gate + +on: + push: + branches: + - 'fix-#tm-3894/node-26-abi147-win32' + +jobs: + win-prebuild: + runs-on: windows-2022 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 26 + architecture: x64 + + - name: Install dependencies + # Node 26's Windows binary is ThinLTO-built, so node-gyp would append the + # lld-only /opt:lldltojobs to link.exe and break the MSVC build (LNK1117). + env: + npm_config_enable_thin_lto: 'false' + npm_config_enable_lto: 'false' + run: npm ci + + - name: Build Node 26 (abi147) prebuild + env: + npm_config_enable_thin_lto: 'false' + npm_config_enable_lto: 'false' + run: npx prebuildify --napi false --strip --target 26.8.1 + + - name: Assert abi147 prebuild exists and loads + shell: bash + env: + PREBUILDS_ONLY: '1' + run: | + set -euo pipefail + ls -R prebuilds + f=$(ls prebuilds/win32-x64/*abi147*.node 2>/dev/null || true) + if [ -z "$f" ]; then echo "::error::no abi147 prebuild in prebuilds/win32-x64"; exit 1; fi + echo "found: $f" + # Confirm node-gyp-build resolves THIS prebuild for the running ABI + # (no source fallback), i.e. the produced binary is correctly named. + node -e "const p=require('node-gyp-build').path('.'); if(!/win32-x64/.test(p)||!/abi147/.test(p)){throw new Error('resolved wrong binding: '+p)} console.log('resolved', p, 'for abi', process.versions.modules)" + + - uses: actions/upload-artifact@v4 + with: + name: tmp-tm3894-win-prebuilds + path: prebuilds/ + if-no-files-found: error From d613c9496c44d2c442a15fda4930e5ba603cd18a Mon Sep 17 00:00:00 2001 From: Aleksandr Borovskii Date: Thu, 3 Sep 2026 11:58:56 +0300 Subject: [PATCH 5/7] ci: set ThinLTO-off env on the Windows test install step (LNK1117) The test job's npm ci source-builds the native addon on Windows; Node 26's ThinLTO config makes node-gyp emit the lld-only /opt:lldltojobs that MSVC link.exe rejects. Set npm_config_enable_thin_lto/enable_lto=false so the Node 26 Windows test leg builds. Refs TM-3894 --- .github/workflows/test.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6f2f063..3718dd0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,6 +30,12 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install dependencies + # On Windows + Node 26 the install script source-builds the native + # addon; Node 26's ThinLTO config makes node-gyp emit the lld-only + # /opt:lldltojobs, which MSVC link.exe rejects (LNK1117). Force LTO off. + env: + npm_config_enable_thin_lto: 'false' + npm_config_enable_lto: 'false' run: npm ci - name: Ensure systemd directory exists (Linux) From 2f3c954ac0e10148e9e47b59439a247272cb24fa Mon Sep 17 00:00:00 2001 From: Aleksandr Borovskii Date: Thu, 3 Sep 2026 14:16:27 +0300 Subject: [PATCH 6/7] ci: remove temporary Windows abi147 prebuild gate The Windows abi147 prebuild is verified (win32-x64/@neuralegion+os-service.abi147.node resolved for the running ABI on windows-2022 / Node 26); the temporary branch-push gate has served its purpose. deploy.yml produces the release prebuild. Refs TM-3894 --- .../tmp-tm3894-win-prebuild-gate.yml | 57 ------------------- 1 file changed, 57 deletions(-) delete mode 100644 .github/workflows/tmp-tm3894-win-prebuild-gate.yml diff --git a/.github/workflows/tmp-tm3894-win-prebuild-gate.yml b/.github/workflows/tmp-tm3894-win-prebuild-gate.yml deleted file mode 100644 index 9be1885..0000000 --- a/.github/workflows/tmp-tm3894-win-prebuild-gate.yml +++ /dev/null @@ -1,57 +0,0 @@ -# TEMPORARY — TM-3894 Windows prebuild gate. DELETE BEFORE MERGE. -# -# deploy.yml runs only on: release, so the Windows abi147 prebuild is -# unreachable from a PR — and it cannot be built on the macOS dev host. This -# builds it on windows-2022, asserts an abi147 binary is produced and loads, -# and uploads it. It NEVER publishes. Remove before merge. -name: TMP TM-3894 win prebuild gate - -on: - push: - branches: - - 'fix-#tm-3894/node-26-abi147-win32' - -jobs: - win-prebuild: - runs-on: windows-2022 - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 26 - architecture: x64 - - - name: Install dependencies - # Node 26's Windows binary is ThinLTO-built, so node-gyp would append the - # lld-only /opt:lldltojobs to link.exe and break the MSVC build (LNK1117). - env: - npm_config_enable_thin_lto: 'false' - npm_config_enable_lto: 'false' - run: npm ci - - - name: Build Node 26 (abi147) prebuild - env: - npm_config_enable_thin_lto: 'false' - npm_config_enable_lto: 'false' - run: npx prebuildify --napi false --strip --target 26.8.1 - - - name: Assert abi147 prebuild exists and loads - shell: bash - env: - PREBUILDS_ONLY: '1' - run: | - set -euo pipefail - ls -R prebuilds - f=$(ls prebuilds/win32-x64/*abi147*.node 2>/dev/null || true) - if [ -z "$f" ]; then echo "::error::no abi147 prebuild in prebuilds/win32-x64"; exit 1; fi - echo "found: $f" - # Confirm node-gyp-build resolves THIS prebuild for the running ABI - # (no source fallback), i.e. the produced binary is correctly named. - node -e "const p=require('node-gyp-build').path('.'); if(!/win32-x64/.test(p)||!/abi147/.test(p)){throw new Error('resolved wrong binding: '+p)} console.log('resolved', p, 'for abi', process.versions.modules)" - - - uses: actions/upload-artifact@v4 - with: - name: tmp-tm3894-win-prebuilds - path: prebuilds/ - if-no-files-found: error From ea630dd3747f91dc520b4e53d2360677c292fbde Mon Sep 17 00:00:00 2001 From: Aleksandr Borovskii Date: Thu, 3 Sep 2026 14:45:46 +0300 Subject: [PATCH 7/7] ci: disable ThinLTO on the Node 26 Windows release build (LNK1117) The release build job source-builds the win32 abi147 prebuild on Windows, where Node 26's ThinLTO config makes node-gyp emit the lld-only /opt:lldltojobs that MSVC link.exe rejects. Set npm_config_enable_thin_lto/ enable_lto=false on the build job's install + prebuild steps so the release publish does not fail on Windows. Refs TM-3894 --- .github/workflows/deploy.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 449c6ee..5d7786d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,9 +29,18 @@ jobs: architecture: ${{ matrix.node_arch }} - name: Install dependencies + # Node 26's ThinLTO config makes node-gyp emit the lld-only + # /opt:lldltojobs that MSVC link.exe rejects (LNK1117); the install + # script source-builds the addon on Windows. Force LTO off. + env: + npm_config_enable_thin_lto: 'false' + npm_config_enable_lto: 'false' run: npm ci - name: Prebuild package + env: + npm_config_enable_thin_lto: 'false' + npm_config_enable_lto: 'false' run: npm run ${{ matrix.command }} -- ${{ matrix.args }} - uses: actions/upload-artifact@v4