Skip to content

Commit 10ce3bb

Browse files
fix(desktop): stage sqlite bindings per app arch
1 parent 2361085 commit 10ce3bb

3 files changed

Lines changed: 86 additions & 30 deletions

File tree

apps/desktop/scripts/install-sqlite-bindings.cjs

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
* app needs Electron at runtime; vitest needs Node. To keep both working from a
99
* single `pnpm install`, we download both prebuilds and stash them at:
1010
*
11-
* build/Release/better_sqlite3.node-node.node (Node ABI, used by vitest)
12-
* build/Release/better_sqlite3.node-electron.node (Electron ABI, used by app)
11+
* build/Release/better_sqlite3.node-node.node (Node ABI, used by vitest)
12+
* build/Release/better_sqlite3.node-electron-x64.node (Electron ABI, x64 app)
13+
* build/Release/better_sqlite3.node-electron-arm64.node(Electron ABI, arm64 app)
14+
* build/Release/better_sqlite3.node-electron.node (legacy alias for host arch)
1315
*
1416
* snapshots-db.ts then opts into the right file via better-sqlite3's
1517
* `nativeBinding` constructor option, depending on whether process.versions.electron
@@ -19,14 +21,14 @@
1921
* recorded versions in install-sqlite-bindings.lock.json. Safe to re-run on
2022
* every install.
2123
*
22-
* SchemaVersion 1: marker for the on-disk lock format so we can migrate later
24+
* SchemaVersion 2: marker for the on-disk lock format so we can migrate later
2325
* without breaking older checkouts.
2426
*/
2527
const { execFileSync } = require('node:child_process');
2628
const fs = require('node:fs');
2729
const path = require('node:path');
2830

29-
const LOCK_SCHEMA_VERSION = 1;
31+
const LOCK_SCHEMA_VERSION = 2;
3032

3133
function log(msg) {
3234
process.stdout.write(`[sqlite-bindings] ${msg}\n`);
@@ -51,6 +53,13 @@ function resolveElectronVersion() {
5153
}
5254
}
5355

56+
function electronTargetArches(platform, hostArch) {
57+
if (platform === 'darwin' || platform === 'win32') {
58+
return ['x64', 'arm64'];
59+
}
60+
return [hostArch];
61+
}
62+
5463
function resolvePrebuildInstallEntrypoint(pkgDir) {
5564
try {
5665
return require.resolve('prebuild-install/bin.js', { paths: [pkgDir] });
@@ -135,7 +144,14 @@ function main() {
135144
const electronVersion = resolveElectronVersion();
136145

137146
const nodeBinary = path.join(releaseDir, 'better_sqlite3.node-node.node');
138-
const electronBinary = path.join(releaseDir, 'better_sqlite3.node-electron.node');
147+
const electronArches = electronTargetArches(platform, arch);
148+
const electronBinaries = Object.fromEntries(
149+
electronArches.map((targetArch) => [
150+
targetArch,
151+
path.join(releaseDir, `better_sqlite3.node-electron-${targetArch}.node`),
152+
]),
153+
);
154+
const legacyElectronBinary = path.join(releaseDir, 'better_sqlite3.node-electron.node');
139155
const lockPath = path.join(releaseDir, 'install-sqlite-bindings.lock.json');
140156

141157
const lock = (() => {
@@ -154,7 +170,13 @@ function main() {
154170
nodeVersion,
155171
electronVersion,
156172
hasNodeBinary: fs.existsSync(nodeBinary),
157-
hasElectronBinary: fs.existsSync(electronBinary),
173+
electronArches,
174+
hasElectronBinaries: Object.fromEntries(
175+
electronArches.map((targetArch) => [
176+
targetArch,
177+
fs.existsSync(electronBinaries[targetArch]),
178+
]),
179+
),
158180
};
159181

160182
const upToDate =
@@ -165,9 +187,13 @@ function main() {
165187
lock.nodeVersion === nodeVersion &&
166188
lock.electronVersion === electronVersion &&
167189
lock.hasNodeBinary === targetLock.hasNodeBinary &&
168-
lock.hasElectronBinary === targetLock.hasElectronBinary &&
190+
JSON.stringify(lock.electronArches) === JSON.stringify(targetLock.electronArches) &&
191+
JSON.stringify(lock.hasElectronBinaries) === JSON.stringify(targetLock.hasElectronBinaries) &&
169192
(!targetLock.hasNodeBinary || fs.existsSync(nodeBinary)) &&
170-
(!targetLock.hasElectronBinary || fs.existsSync(electronBinary));
193+
electronArches.every(
194+
(targetArch) =>
195+
targetLock.hasElectronBinaries[targetArch] !== true || fs.existsSync(electronBinaries[targetArch]),
196+
);
171197

172198
if (upToDate) {
173199
log(
@@ -187,23 +213,25 @@ function main() {
187213
optional: true,
188214
});
189215

190-
let hasElectronBinary = false;
216+
const hasElectronBinaries = {};
191217
if (electronVersion === null) {
192218
log('electron not installed; skipping Electron native binding (fine for prod-only installs)');
193219
} else {
194-
log(`downloading Electron prebuild (electron=${electronVersion}, ${platform}-${arch})`);
195-
hasElectronBinary = downloadPrebuild({
196-
pkgDir,
197-
runtime: 'electron',
198-
target: electronVersion,
199-
arch,
200-
platform,
201-
dest: electronBinary,
202-
optional: false,
203-
});
220+
for (const targetArch of electronArches) {
221+
log(`downloading Electron prebuild (electron=${electronVersion}, ${platform}-${targetArch})`);
222+
hasElectronBinaries[targetArch] = downloadPrebuild({
223+
pkgDir,
224+
runtime: 'electron',
225+
target: electronVersion,
226+
arch: targetArch,
227+
platform,
228+
dest: electronBinaries[targetArch],
229+
optional: false,
230+
});
231+
}
204232
}
205233

206-
if (!hasNodeBinary && !hasElectronBinary) {
234+
if (!hasNodeBinary && !electronArches.some((targetArch) => hasElectronBinaries[targetArch])) {
207235
throw new Error('Failed to stage any better-sqlite3 native bindings');
208236
}
209237

@@ -215,13 +243,21 @@ function main() {
215243
const defaultBinary = path.join(releaseDir, 'better_sqlite3.node');
216244
if (hasNodeBinary) {
217245
fs.copyFileSync(nodeBinary, defaultBinary);
218-
} else if (hasElectronBinary) {
219-
fs.copyFileSync(electronBinary, defaultBinary);
246+
} else {
247+
const firstElectronArch = electronArches.find((targetArch) => hasElectronBinaries[targetArch]);
248+
if (firstElectronArch) fs.copyFileSync(electronBinaries[firstElectronArch], defaultBinary);
249+
}
250+
251+
const hostElectronBinary = electronBinaries[arch];
252+
if (hostElectronBinary && fs.existsSync(hostElectronBinary)) {
253+
fs.copyFileSync(hostElectronBinary, legacyElectronBinary);
254+
} else if (fs.existsSync(legacyElectronBinary)) {
255+
fs.rmSync(legacyElectronBinary);
220256
}
221257

222258
fs.writeFileSync(
223259
lockPath,
224-
`${JSON.stringify({ ...targetLock, hasNodeBinary, hasElectronBinary }, null, 2)}\n`,
260+
`${JSON.stringify({ ...targetLock, hasNodeBinary, hasElectronBinaries }, null, 2)}\n`,
225261
);
226262
log('done');
227263
}

apps/desktop/src/main/snapshots-db.binding.test.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,34 @@ function makeReleaseDir() {
2020
}
2121

2222
describe('resolveNativeBindingPath', () => {
23-
it('prefers the Electron-specific binding when present', () => {
23+
it('prefers the arch-specific Electron binding when present', () => {
2424
const releaseDir = makeReleaseDir();
25-
const electronBinding = path.join(releaseDir, 'better_sqlite3.node-electron.node');
25+
const electronBinding = path.join(releaseDir, 'better_sqlite3.node-electron-x64.node');
26+
const legacyBinding = path.join(releaseDir, 'better_sqlite3.node-electron.node');
2627
const defaultBinding = path.join(releaseDir, 'better_sqlite3.node');
2728
fs.writeFileSync(electronBinding, '');
29+
fs.writeFileSync(legacyBinding, '');
2830
fs.writeFileSync(defaultBinding, '');
2931

30-
expect(resolveNativeBindingPath(releaseDir, true)).toBe(electronBinding);
32+
expect(resolveNativeBindingPath(releaseDir, true, 'x64')).toBe(electronBinding);
33+
});
34+
35+
it('falls back to the legacy Electron binding when the arch-specific one is missing', () => {
36+
const releaseDir = makeReleaseDir();
37+
const legacyBinding = path.join(releaseDir, 'better_sqlite3.node-electron.node');
38+
const defaultBinding = path.join(releaseDir, 'better_sqlite3.node');
39+
fs.writeFileSync(legacyBinding, '');
40+
fs.writeFileSync(defaultBinding, '');
41+
42+
expect(resolveNativeBindingPath(releaseDir, true, 'arm64')).toBe(legacyBinding);
3143
});
3244

3345
it('falls back to the default binding when the runtime-specific one is missing', () => {
3446
const releaseDir = makeReleaseDir();
3547
const defaultBinding = path.join(releaseDir, 'better_sqlite3.node');
3648
fs.writeFileSync(defaultBinding, '');
3749

38-
expect(resolveNativeBindingPath(releaseDir, true)).toBe(defaultBinding);
50+
expect(resolveNativeBindingPath(releaseDir, true, 'arm64')).toBe(defaultBinding);
3951
});
4052

4153
it('keeps the Node-specific path when no Node binding was staged', () => {

apps/desktop/src/main/snapshots-db.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,24 @@ let singleton: Database | null = null;
3939
/**
4040
* Resolve the .node binary that matches the active runtime ABI.
4141
*
42-
* scripts/install-sqlite-bindings.cjs stages two prebuilds side by side:
43-
* build/Release/better_sqlite3.node-node.node ← Node 22 (vitest)
44-
* build/Release/better_sqlite3.node-electron.node ← Electron (app)
42+
* scripts/install-sqlite-bindings.cjs stages the host Node prebuild plus
43+
* per-arch Electron prebuilds side by side:
44+
* build/Release/better_sqlite3.node-node.node ← Node 22 (vitest)
45+
* build/Release/better_sqlite3.node-electron-x64.node ← Electron x64 app
46+
* build/Release/better_sqlite3.node-electron-arm64.node← Electron arm64 app
47+
* build/Release/better_sqlite3.node-electron.node ← legacy host-arch alias
4548
* so that one `pnpm install` covers both runtimes without
4649
* an electron-rebuild step that toggles the single default binary.
4750
*/
4851
export function resolveNativeBindingPath(
4952
releaseDir: string,
5053
isElectron = typeof process.versions.electron === 'string',
54+
arch = process.arch,
5155
): string {
56+
if (isElectron) {
57+
const archSpecific = path.join(releaseDir, `better_sqlite3.node-electron-${arch}.node`);
58+
if (fs.existsSync(archSpecific)) return archSpecific;
59+
}
5260
const runtimeSpecific = path.join(
5361
releaseDir,
5462
isElectron ? 'better_sqlite3.node-electron.node' : 'better_sqlite3.node-node.node',

0 commit comments

Comments
 (0)