Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
36 changes: 22 additions & 14 deletions scripts/postinstall.js
Original file line number Diff line number Diff line change
@@ -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 {}
Expand Down Expand Up @@ -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 };
63 changes: 63 additions & 0 deletions test/postinstall.test.js
Original file line number Diff line number Diff line change
@@ -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' },
]);
});
}
Loading