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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 31ed6d1..3718dd0 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 @@ -28,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) @@ -46,7 +54,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 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" } } } 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); + }); +});