diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 80804be6..ff145fda 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,6 +48,9 @@ jobs: node-version: 20 cache: npm + - name: Test postinstall helpers + run: node --test test/postinstall.test.js + - uses: actions/setup-python@v5 with: python-version: '3.12' diff --git a/scripts/postinstall.js b/scripts/postinstall.js index 6b46dfda..32c1db0f 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -1,31 +1,33 @@ #!/usr/bin/env node -const { execSync } = require('child_process'); +const { execSync, execFileSync } = require('child_process'); const path = require('path'); const fs = require('fs'); -// Install native dependencies for Electron -try { - execSync('npx electron-builder install-app-deps', { stdio: 'inherit' }); -} catch (err) { - console.error('electron-builder install-app-deps failed:', err.message); - // Fallback: rebuild only better-sqlite3 for Electron (node-pty uses prebuilds) - console.log('Attempting fallback: rebuilding better-sqlite3 for Electron...'); +if (require.main === module) { + // Install native dependencies for Electron try { - execSync('npx @electron/rebuild -f -m . -o better-sqlite3', { stdio: 'inherit' }); - console.log('Fallback rebuild succeeded.'); - } catch (err2) { - console.error('Fallback rebuild also failed:', err2.message); + execSync('npx electron-builder install-app-deps', { stdio: 'inherit' }); + } catch (err) { + console.error('electron-builder install-app-deps failed:', err.message); + // Fallback: rebuild only better-sqlite3 for Electron (node-pty uses prebuilds) + console.log('Attempting fallback: rebuilding better-sqlite3 for Electron...'); + try { + execSync('npx @electron/rebuild -f -m . -o better-sqlite3', { stdio: 'inherit' }); + console.log('Fallback rebuild succeeded.'); + } catch (err2) { + console.error('Fallback rebuild also failed:', err2.message); + } } } // macOS/Linux: ad-hoc codesign native modules & fix node-pty permissions -if (process.platform !== 'win32') { +if (require.main === module && process.platform !== 'win32') { // Ad-hoc codesign all .node files so macOS doesn't block them try { const nodeModules = path.join(__dirname, '..', 'node_modules'); findFiles(nodeModules, '.node').forEach(file => { try { - execSync(`codesign --sign - --force "${file}"`, { stdio: 'ignore' }); + codesignFile(file); } catch {} }); } catch {} @@ -58,3 +60,9 @@ function findFiles(dir, suffix) { } catch {} return results; } + +function codesignFile(file, sign = execFileSync) { + sign('codesign', ['--sign', '-', '--force', file], { stdio: 'ignore' }); +} + +module.exports = { findFiles, codesignFile }; diff --git a/test/postinstall.test.js b/test/postinstall.test.js new file mode 100644 index 00000000..bf39bc9c --- /dev/null +++ b/test/postinstall.test.js @@ -0,0 +1,63 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + +const { findFiles, codesignFile } = require('../scripts/postinstall'); + +const SPECIAL_NAMES = ['some file.node', 'some"file.node', 'native\\addon.node']; + +function withTempDir(fn) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'postinstall-test-')); + try { + return fn(dir); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +} + +for (const name of SPECIAL_NAMES) { + test(`findFiles returns the exact, unmangled path for filename: ${name}`, { + // Quotes are invalid and backslashes are path separators on Windows. + skip: process.platform === 'win32' && /["\\]/.test(name), + }, () => { + withTempDir(dir => { + const expected = path.join(dir, name); + fs.writeFileSync(expected, ''); + const results = findFiles(dir, '.node'); + assert.deepEqual(results, [expected]); + // The regressed behavior stripped \\ and / from the name, breaking the path. + assert.ok(fs.existsSync(results[0])); + }); + }); +} + +test('findFiles descends into directories whose name contains a backslash', { + skip: process.platform === 'win32', +}, () => { + withTempDir(dir => { + const subdir = path.join(dir, 'sub\\dir'); + fs.mkdirSync(subdir); + const expected = path.join(subdir, 'addon.node'); + fs.writeFileSync(expected, ''); + const results = findFiles(dir, '.node'); + assert.deepEqual(results, [expected]); + }); +}); + +for (const name of SPECIAL_NAMES) { + test(`codesignFile passes filename unchanged to the signer for: ${name}`, () => { + // A stubbed signer only needs a path string, not a real filesystem entry. + const file = path.join(os.tmpdir(), name); + const calls = []; + const stubSign = (...args) => calls.push(args); + codesignFile(file, stubSign); + assert.equal(calls.length, 1); + assert.deepEqual(calls[0], [ + 'codesign', + ['--sign', '-', '--force', file], + { stdio: 'ignore' }, + ]); + }); +}