Skip to content

Fix CLI crash in compiled binaries from @napi-rs/keyring native binding#1610

Merged
ChiragAgg5k merged 4 commits into
mainfrom
fix/cli-keyring-bun-compile-binding
Jun 24, 2026
Merged

Fix CLI crash in compiled binaries from @napi-rs/keyring native binding#1610
ChiragAgg5k merged 4 commits into
mainfrom
fix/cli-keyring-bun-compile-binding

Conversation

@ChiragAgg5k

Copy link
Copy Markdown
Member

What

The standalone CLI binaries (the ones distributed via Homebrew) crash on every command with:

error: Cannot find native binding. npm has a bug related to optional dependencies (...)

This also breaks brew install/upgrade appwrite, because the formula runs appwrite completion bash during 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 the bun --compile binaries crash.

Root cause

@napi-rs/keyring is a native addon: the real code is a per-platform .node binary, and the npm package is a thin JS shim that resolves the right one through dynamic requires at runtime (against node_modules).

bun build --compile produces a single self-contained file, so everything must be embedded at build time. bun only embeds a .node when it is reached through a literal require it can resolve statically — it cannot follow the shim's dynamic/computed requires. So the binding is never embedded, and the eager top-level import { Entry } from "@napi-rs/keyring" throws the instant the module loads, before any command runs.

Fix

  • Load keyring lazily via a literal per-platform require (@napi-rs/keyring-<triple>), so bun embeds the matching .node for each --target and secure OS-keychain storage keeps working in the standalone binaries.
  • The process.platform/process.arch if-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. (A switch/lookup-table is not folded by bun and fails the build — confirmed.)
  • Graceful fallback chain: platform package → umbrella @napi-rs/keyring (covers npm installs) → config-file storage. A missing binding degrades instead of crashing.
  • Externalize @napi-rs/keyring-* in the esbuild bundles so the npm builds don't try to bundle the .node files.

Verification

On the compiled darwin-arm64 binary (run with no node_modules present):

  • appwrite --version / --help / completion bash / completion zsh — all work (previously all crashed).
  • Secure storage confirmed: through the real refresh-token module, setget returns the token and delete clears it. Since the config-file fallback can't store without an existing session, a successful get proves the OS keychain was used.
  • ESLint clean; esbuild esm/cjs/cli builds pass with no warnings; npm keychain path unaffected (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 install only 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.

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-apps

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR changes how the CLI loads the native keyring binding so compiled binaries do not crash at startup. The main changes are:

  • Lazy per-platform require calls for @napi-rs/keyring bindings.
  • Config-file fallback when secure credential storage is unavailable.
  • Esbuild externalization for @napi-rs/keyring-* packages.
  • A compiled Linux CLI smoke test in validation.

Confidence Score: 4/5

The 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

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex ran the initial checkout and attempted setup on the base branch, but composer install and php example.php cli failed with exit status 127 due to missing executables.
  • T-Rex observed that the head commit remained blocked with the same operational blockers.
  • T-Rex evaluated the npm-keyring bundle flow on the before-state, where php: not found occurred; npm install succeeded via the templates/cli path, and build:runtime progressed to build:cli but failed because the generated template has no cli.ts, while library bundles were emitted and import of @napi-rs/keyring was detected.
  • T-Rex evaluated the npm-keyring bundle on the after-state, where the php: not found blocker persisted for full generation; npm install succeeded, and build:runtime failed during build:lib:esm with No loader configured for .node files (lib/auth/refresh-token.ts:45).
  • T-Rex noted that no P1 contract-mismatch finding was reported because the claimed head behavior was not executable in this environment.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (1)

  1. General comment

    P1 Head npm/esbuild build still bundles platform keyring native package

    • Bug
      • In the head validation, npm run build:runtime in the CLI template fails during esbuild's ESM library build because lib/auth/refresh-token.ts requires @napi-rs/keyring-linux-x64-gnu, causing esbuild to resolve that package and attempt to load its keyring.linux-x64-gnu.node native addon. This contradicts the externalization claim for npm/esbuild bundles and prevents npm-oriented runtime builds from completing when the platform package is installed.
    • Cause
      • templates/cli/lib/auth/refresh-token.ts contains a static require("@napi-rs/keyring-linux-x64-gnu"), but templates/cli/package.json.twig / generated build scripts only externalize @napi-rs/keyring and not @napi-rs/keyring-*, so esbuild treats the platform package as bundle input and errors on the .node file.
    • Fix
      • Add an esbuild external pattern for the platform packages in the CLI package template, e.g. --external:@napi-rs/keyring-* on every npm/esbuild bundle command that can include refresh-token.ts, or make the platform-package require opaque to esbuild while preserving the intended runtime fallback chain.

    T-Rex Ran code and verified through T-Rex

Reviews (1): Last reviewed commit: "Run only the host-native CLI binary in v..." | Re-trigger Greptile

Comment on lines +304 to +307
BIN=build/appwrite-cli-linux-x64
"./$BIN" --version
"./$BIN" --help | grep -q "Usage:"
SHELL=bash "./$BIN" completion bash >/dev/null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Restore arm64 smoke test

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!

@ChiragAgg5k ChiragAgg5k merged commit 3ffded3 into main Jun 24, 2026
59 checks passed
@ChiragAgg5k ChiragAgg5k deleted the fix/cli-keyring-bun-compile-binding branch June 24, 2026 05:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant