Fix CLI crash in compiled binaries from @napi-rs/keyring native binding#1610
Conversation
The standalone CLI binaries built with `bun build --compile` crash on every command with "Cannot find native binding", which also breaks `brew install` (it runs `appwrite completion bash` during install). Root cause: @napi-rs/keyring is a native addon whose JS shim resolves its platform-specific .node binding through dynamic requires at runtime. bun's single-file compiler can only embed a .node when it is reached through a literal require it can resolve statically, so the binding is never embedded and the eager top-level import throws at module load. Fix: - Load @napi-rs/keyring lazily via a literal per-platform require so bun embeds the matching .node for each --target; secure (OS keychain) storage keeps working in the standalone binaries. The platform/arch if-chain is dead-code-eliminated by bun per target, so each binary embeds only its own binding and each target builds with only its own platform package present. - Fall back to the umbrella @napi-rs/keyring package (npm install) and then to config-file storage, so a missing binding degrades gracefully instead of crashing. - Externalize @napi-rs/keyring-* in the esbuild bundles so the npm builds do not try to bundle the .node files. Note for release builds: each `bun --compile --target=<t>` embeds a binding only if @napi-rs/keyring-<t> is installed at build time. Build each target on its native runner, or install the per-target platform package before its build, to keep secure storage on every binary.
The validation workflow built the standalone bun binaries but never ran them; the only smoke test (node dist/cli.cjs --help) exercises the npm build, which is unaffected. A native binding that fails to embed crashes the compiled binary at startup (and breaks brew install, which runs appwrite completion bash) while still building cleanly. Run the host-native binary so this is caught.
Loop the compiled Linux binaries (x64 native, arm64 via qemu-user-static) through the startup smoke test instead of just linux-x64.
qemu-user cannot start the dynamically-linked arm64 bun binary on the x64 runner (missing aarch64 loader). The embedding failure is platform-agnostic, so the natively-runnable linux-x64 binary is enough to catch it; native arm64 coverage would need an arm64 runner.
Greptile SummaryThis PR changes how the CLI loads the native keyring binding so compiled binaries do not crash at startup. The main changes are:
Confidence Score: 4/5The change is mostly safe, but the validation workflow no longer executes one of the compiled binaries it still builds. The implementation addresses the startup crash path and keeps npm builds externalized, while the remaining CI coverage gap could miss a target-specific compiled binary failure. .github/workflows/validation.yml
What T-Rex did
|
| BIN=build/appwrite-cli-linux-x64 | ||
| "./$BIN" --version | ||
| "./$BIN" --help | grep -q "Usage:" | ||
| SHELL=bash "./$BIN" completion bash >/dev/null |
There was a problem hiding this comment.
This test now only executes build/appwrite-cli-linux-x64, while the workflow still builds appwrite-cli-linux-arm64. The keyring fix uses platform-specific literal requires, so a missing or wrong @napi-rs/keyring-linux-arm64-gnu path can still leave the arm64 binary crashing on startup while CI passes. Please execute the arm64 binary as well, either by restoring the QEMU loop or by running this check on an arm64 runner.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
What
The standalone CLI binaries (the ones distributed via Homebrew) crash on every command with:
This also breaks
brew install/upgrade appwrite, because the formula runsappwrite completion bashduring install — so the upgrade aborts and rolls back. Introduced in 22.2.0 alongside@napi-rs/keyring(OAuth refresh tokens in the OS keychain). The npm-installed CLI is unaffected; only thebun --compilebinaries crash.Root cause
@napi-rs/keyringis a native addon: the real code is a per-platform.nodebinary, and the npm package is a thin JS shim that resolves the right one through dynamicrequires at runtime (againstnode_modules).bun build --compileproduces a single self-contained file, so everything must be embedded at build time. bun only embeds a.nodewhen it is reached through a literalrequireit can resolve statically — it cannot follow the shim's dynamic/computed requires. So the binding is never embedded, and the eager top-levelimport { Entry } from "@napi-rs/keyring"throws the instant the module loads, before any command runs.Fix
require(@napi-rs/keyring-<triple>), so bun embeds the matching.nodefor each--targetand secure OS-keychain storage keeps working in the standalone binaries.process.platform/process.archif-chain is the only form bun dead-code-eliminates per target — so each binary embeds just its own binding, and each target builds with only its own platform package installed. (Aswitch/lookup-table is not folded by bun and fails the build — confirmed.)@napi-rs/keyring(covers npm installs) → config-file storage. A missing binding degrades instead of crashing.@napi-rs/keyring-*in the esbuild bundles so the npm builds don't try to bundle the.nodefiles.Verification
On the compiled
darwin-arm64binary (run with nonode_modulespresent):appwrite --version/--help/completion bash/completion zsh— all work (previously all crashed).refresh-tokenmodule,set→getreturns the token anddeleteclears it. Since the config-file fallback can't store without an existing session, a successfulgetproves the OS keychain was used.require("@napi-rs/keyring")retained, externalized).Release-build note
Each
bun --compile --target=<t>embeds a binding only if@napi-rs/keyring-<t>is installed at build time.bun installonly fetches the host's platform package, so cross-compiling all targets on one machine yields binaries that fall back to config-file storage. Build each target on its native runner, or install the per-target platform package before its build, to keep secure storage on every binary. Linux coverage is glibc (-gnu), matching bun's default Linux targets and Homebrew-on-Linux.