Skip to content

Commit decc101

Browse files
authored
Fix desktop sidecar packaging and release pipeline (#935)
- Stage the libsql and keyring native bindings next to the compiled sidecar binary and point their loaders at them via a first-import bootstrap, matching the CLI build. - Embed the legacy v1 migration chain (drizzle-legacy-v1) in the sidecar instead of the v2 chain so the v1->v2 data migration can replay it. - Run the compiled-sidecar smoke test in the desktop publish workflow on native-arch legs, and refresh the smoke script for the v2 integration/connection/tool model. - Merge the per-arch mac update manifests in the release job so latest-mac.yml lists both arm64 and x64 artifacts. - Let the --arch flag drive win/linux electron-builder targets instead of config-level arch pins, so installers only ship sidecar binaries built for their own arch. - Show an error dialog when the sidecar fails to start (instead of quitting silently) and stage any available update so a broken install can recover on the next launch.
1 parent 27f49a7 commit decc101

8 files changed

Lines changed: 471 additions & 75 deletions

File tree

‎.github/workflows/publish-desktop.yml‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,22 +29,29 @@ jobs:
2929
fail-fast: false
3030
matrix:
3131
include:
32+
# smoke: run the compiled-sidecar smoke test on legs whose target
33+
# matches the runner (the mac x64 leg cross-compiles on an arm64
34+
# runner, so its binary can't be executed natively there).
3235
- os: macos-latest
3336
arch: arm64
3437
platform: mac
3538
bun-target: bun-darwin-arm64
39+
smoke: true
3640
- os: macos-latest
3741
arch: x64
3842
platform: mac
3943
bun-target: bun-darwin-x64
44+
smoke: false
4045
- os: ubuntu-latest
4146
arch: x64
4247
platform: linux
4348
bun-target: bun-linux-x64
49+
smoke: true
4450
- os: windows-latest
4551
arch: x64
4652
platform: win
4753
bun-target: bun-windows-x64
54+
smoke: true
4855

4956
runs-on: ${{ matrix.os }}
5057

@@ -90,6 +97,15 @@ jobs:
9097
run: bun ./scripts/build-sidecar.ts
9198
working-directory: apps/desktop
9299

100+
# Gate the release on the compiled binary actually booting. v1.5.0/.1
101+
# shipped sidecars that died on launch (missing libsql native binding) —
102+
# a regression dev mode can't catch because `bun run` resolves
103+
# node_modules that `bun build --compile` does not bundle.
104+
- name: Smoke test compiled sidecar
105+
if: matrix.smoke
106+
run: bun run test:smoke
107+
working-directory: apps/desktop
108+
93109
- name: Build Electron main/preload/renderer
94110
run: bunx --bun electron-vite build
95111
working-directory: apps/desktop
@@ -124,6 +140,16 @@ jobs:
124140
run: bunx --bun electron-builder --${{ matrix.platform }} --${{ matrix.arch }} --publish never --config electron-builder.config.ts
125141
working-directory: apps/desktop
126142

143+
# The two mac legs each emit a latest-mac.yml listing only their own
144+
# arch. Rename per-arch here; the release job merges them back into the
145+
# single latest-mac.yml electron-updater clients fetch. Without this,
146+
# merge-multiple in the release job lets one arch clobber the other and
147+
# every Mac gets pointed at whichever leg uploaded last.
148+
- name: Rename mac update manifest per arch
149+
if: matrix.platform == 'mac'
150+
shell: bash
151+
run: mv apps/desktop/dist/latest-mac.yml "apps/desktop/dist/latest-mac-${{ matrix.arch }}.yml"
152+
127153
- name: Upload artifacts
128154
uses: actions/upload-artifact@v4
129155
with:
@@ -166,6 +192,15 @@ jobs:
166192
path: artifacts
167193
merge-multiple: true
168194

195+
- name: Merge mac update manifests
196+
run: |
197+
set -euo pipefail
198+
bun scripts/merge-latest-mac-yml.ts \
199+
artifacts/latest-mac-x64.yml \
200+
artifacts/latest-mac-arm64.yml \
201+
artifacts/latest-mac.yml
202+
rm artifacts/latest-mac-x64.yml artifacts/latest-mac-arm64.yml
203+
169204
- name: Upload to GitHub Release
170205
env:
171206
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

‎apps/desktop/electron-builder.config.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,22 @@ const config: Configuration = {
4343
entitlementsInherit: "build/entitlements.mac.plist",
4444
notarize: true,
4545
},
46+
// Same arch rule as mac (see comment above): never pin `arch:` in the
47+
// target objects. The win/linux pins used to force both archs out of a
48+
// single x64 matrix leg, embedding an x64 sidecar binary inside the
49+
// "arm64" installers — DOA on linux-arm64, emulated on win-arm64. Each
50+
// workflow leg's --x64/--arm64 flag decides what gets built, so an arm64
51+
// artifact only exists once a leg stages an arm64 sidecar for it.
4652
win: {
47-
target: [{ target: "nsis", arch: ["x64", "arm64"] }],
53+
target: ["nsis"],
4854
},
4955
nsis: {
5056
oneClick: true,
5157
perMachine: false,
5258
},
5359
linux: {
5460
category: "Development",
55-
target: [
56-
{ target: "AppImage", arch: ["x64", "arm64"] },
57-
{ target: "deb", arch: ["x64", "arm64"] },
58-
{ target: "rpm", arch: ["x64", "arm64"] },
59-
],
61+
target: ["AppImage", "deb", "rpm"],
6062
},
6163
publish: {
6264
provider: "github",

‎apps/desktop/scripts/build-sidecar.ts‎

Lines changed: 132 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,112 @@ const targetIsWindows = BUN_TARGET.includes("windows") || process.platform === "
3737
const binaryName = targetIsWindows ? "executor-sidecar.exe" : "executor-sidecar";
3838
const sidecarBinary = resolve(SIDECAR_OUT_DIR, binaryName);
3939

40+
/**
41+
* Normalized `<os>-<arch>[-<abi>]` key for the compile target, derived from
42+
* BUN_TARGET (`bun` = the runner's own platform). Matches the keys used by
43+
* apps/cli/src/build.ts's native-binding maps.
44+
*/
45+
const targetKey =
46+
BUN_TARGET === "bun"
47+
? `${process.platform}-${process.arch}`
48+
: BUN_TARGET.replace(/^bun-/, "").replace(/^windows-/, "win32-");
49+
50+
const targetIsCurrentPlatform = targetKey === `${process.platform}-${process.arch}`;
51+
52+
// `bun build --compile` does not bundle `.node` native addons into bunfs, so
53+
// the sidecar's eager `require('@libsql/<plat>')` (and the keychain plugin's
54+
// lazy keyring load) would fail at runtime. We stage each binding next to the
55+
// binary; src/sidecar/native-bindings.ts (the sidecar's first import) points
56+
// the loaders at them via EXECUTOR_LIBSQL_NATIVE_PATH /
57+
// EXECUTOR_KEYRING_NATIVE_PATH. Mirrors apps/cli/src/build.ts.
58+
const LIBSQL_NATIVE_VERSION = "0.5.29";
59+
const resolveLibsqlNative = (): string => {
60+
const platformMap: Record<string, string> = {
61+
"darwin-arm64": "darwin-arm64",
62+
"darwin-x64": "darwin-x64",
63+
// The compiled binary runs on Bun, which libSQL's loader treats as glibc
64+
// (its musl->gnu workaround), so non-musl linux targets need the -gnu binding.
65+
"linux-arm64": "linux-arm64-gnu",
66+
"linux-x64": "linux-x64-gnu",
67+
"win32-arm64": "win32-arm64-msvc",
68+
"win32-x64": "win32-x64-msvc",
69+
};
70+
const target = platformMap[targetKey];
71+
if (!target) {
72+
// oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: build-time fatal
73+
throw new Error(`No @libsql native binding mapping for target ${targetKey}`);
74+
}
75+
const pkg = `@libsql/${target}`;
76+
// oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: build-time resolution falls back to bun's store layout
77+
try {
78+
const req = createRequire(join(APPS_LOCAL, "package.json"));
79+
return join(dirname(req.resolve(`${pkg}/package.json`)), "index.node");
80+
} catch {
81+
const bunPath = join(
82+
REPO_ROOT,
83+
`node_modules/.bun/${pkg.replace("/", "+")}@${LIBSQL_NATIVE_VERSION}/node_modules/${pkg}/index.node`,
84+
);
85+
if (!existsSync(bunPath)) {
86+
// oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: build-time fatal
87+
throw new Error(
88+
`Cannot resolve ${pkg} for the sidecar. Run \`bun install --cpu=* --os=*\` so cross-target native bindings are present.`,
89+
);
90+
}
91+
return bunPath;
92+
}
93+
};
94+
95+
const resolveKeyringNative = (): string => {
96+
const platformMap: Record<string, { pkg: string; node: string }> = {
97+
"darwin-arm64": {
98+
pkg: "@napi-rs/keyring-darwin-arm64",
99+
node: "keyring.darwin-arm64.node",
100+
},
101+
"darwin-x64": {
102+
pkg: "@napi-rs/keyring-darwin-x64",
103+
node: "keyring.darwin-x64.node",
104+
},
105+
"linux-arm64": {
106+
pkg: "@napi-rs/keyring-linux-arm64-gnu",
107+
node: "keyring.linux-arm64-gnu.node",
108+
},
109+
"linux-x64": {
110+
pkg: "@napi-rs/keyring-linux-x64-gnu",
111+
node: "keyring.linux-x64-gnu.node",
112+
},
113+
"win32-arm64": {
114+
pkg: "@napi-rs/keyring-win32-arm64-msvc",
115+
node: "keyring.win32-arm64-msvc.node",
116+
},
117+
"win32-x64": {
118+
pkg: "@napi-rs/keyring-win32-x64-msvc",
119+
node: "keyring.win32-x64-msvc.node",
120+
},
121+
};
122+
const entry = platformMap[targetKey];
123+
if (!entry) {
124+
// oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: build-time fatal
125+
throw new Error(`No @napi-rs/keyring native binding mapping for target ${targetKey}`);
126+
}
127+
// oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: build-time resolution falls back to bun's store layout
128+
try {
129+
const req = createRequire(join(REPO_ROOT, "node_modules", "@napi-rs/keyring", "package.json"));
130+
return join(dirname(req.resolve(`${entry.pkg}/package.json`)), entry.node);
131+
} catch {
132+
const bunPath = join(
133+
REPO_ROOT,
134+
`node_modules/.bun/${entry.pkg.replace("/", "+")}@1.2.0/node_modules/${entry.pkg}/${entry.node}`,
135+
);
136+
if (!existsSync(bunPath)) {
137+
// oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: build-time fatal
138+
throw new Error(
139+
`Cannot resolve ${entry.pkg} for the sidecar. Run \`bun install --cpu=* --os=*\` so cross-target native bindings are present.`,
140+
);
141+
}
142+
return bunPath;
143+
}
144+
};
145+
40146
// QuickJS ships its WASM as a side asset; `bun build --compile` can't pull
41147
// it into bunfs, so we stage it next to the binary and the sidecar entry
42148
// preloads it via `setQuickJSModule` before any server import.
@@ -54,11 +160,15 @@ const resolveQuickJsWasmPath = (): string => {
54160
return wasmPath;
55161
};
56162

57-
// Drizzle's migrator takes a folder path at runtime. The compiled sidecar
58-
// cannot rely on apps/local/drizzle existing on disk, so inline every migration
59-
// as text and let apps/local extract them to a temp folder during startup.
163+
// The v1→v2 data migration replays the legacy v1 drizzle chain
164+
// (apps/local/drizzle-legacy-v1) before reading a legacy database. The
165+
// compiled sidecar cannot rely on that folder existing on disk, so inline
166+
// every migration as text and let apps/local extract them to a temp folder
167+
// during startup. Mirrors apps/cli/src/build.ts — embedding the wrong dir
168+
// (e.g. the v2 chain in drizzle/) makes the sidecar treat every real legacy
169+
// database as "history does not match" and skip the replay.
60170
const createEmbeddedMigrationsSource = async () => {
61-
const migrationsDir = resolve(APPS_LOCAL, "drizzle");
171+
const migrationsDir = resolve(APPS_LOCAL, "drizzle-legacy-v1");
62172
const files = (await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: migrationsDir })))
63173
.map((file) => file.replaceAll("\\", "/"))
64174
.sort();
@@ -86,6 +196,20 @@ if (!existsSync(APPS_LOCAL_DIST)) {
86196
);
87197
}
88198

199+
// Cross-target builds (e.g. the mac x64 leg on an arm64 runner) need the other
200+
// platform's optional native packages on disk before we can stage them.
201+
// `--cpu=* --os=*` extracts them all without modifying the lockfile. Mirrors
202+
// apps/cli/src/build.ts.
203+
if (!targetIsCurrentPlatform) {
204+
console.log("[build-sidecar] installing optional native deps for all platforms...");
205+
await $`bun install --frozen-lockfile --cpu=* --os=*`.cwd(REPO_ROOT);
206+
}
207+
208+
// Resolve the native bindings up front so a missing platform package fails the
209+
// build before the (slow) compile, and cross-target builds get a clear message.
210+
const libsqlNativePath = resolveLibsqlNative();
211+
const keyringNativePath = resolveKeyringNative();
212+
89213
await rm(SIDECAR_OUT_DIR, { recursive: true, force: true });
90214
await rm(WEB_UI_OUT_DIR, { recursive: true, force: true });
91215
await mkdir(SIDECAR_OUT_DIR, { recursive: true });
@@ -108,6 +232,10 @@ try {
108232
console.log(`[build-sidecar] staging QuickJS WASM → ${SIDECAR_OUT_DIR}`);
109233
await cp(resolveQuickJsWasmPath(), join(SIDECAR_OUT_DIR, "emscripten-module.wasm"));
110234

235+
console.log(`[build-sidecar] staging native bindings (${targetKey}) → ${SIDECAR_OUT_DIR}`);
236+
await cp(libsqlNativePath, join(SIDECAR_OUT_DIR, "libsql.node"));
237+
await cp(keyringNativePath, join(SIDECAR_OUT_DIR, "keyring.node"));
238+
111239
console.log(`[build-sidecar] staging web UI → ${WEB_UI_OUT_DIR}`);
112240
await cp(APPS_LOCAL_DIST, WEB_UI_OUT_DIR, { recursive: true });
113241
} finally {

0 commit comments

Comments
 (0)