Skip to content

fix(npm): Arborist reify fails on compiled binary — Bun pre-resolves node-gyp path at build time #20778

@sjawhar

Description

@sjawhar

Bug

Plugin installation via @npmcli/arborist fails on any machine that isn't the build machine. The compiled binary contains a hardcoded absolute path to node-gyp from the build environment.

Error

Cannot find module '/home/runner/work/opencode/node_modules/.bun/node-gyp@12.2.0/node_modules/node-gyp/bin/node-gyp.js' from '/$bunfs/root/src/index.js'

Root Cause

@npmcli/run-script/lib/make-spawn-args.js line 29:

npm_config_node_gyp = require.resolve('node-gyp/bin/node-gyp.js')

Bun's bundler pre-resolves require.resolve() calls with static string arguments at compile time, baking the build machine's absolute path into the binary. At runtime on any other machine, this path doesn't exist.

This was introduced in #18308 (merged Apr 1) which replaced BunProc.install() (subprocess-based bun add) with bundled @npmcli/arborist. The old approach spawned bun as a child process with no hardcoded paths. The new approach bundles the npm resolver, which brings along @npmcli/run-script and its node-gyp resolution.

Impact

  • Affects every compiled binary distributed to users (not just self-built)
  • Not yet visible because refactor: replace BunProc with Npm module using @npmcli/arborist #18308 merged after v1.3.13 was released
  • Will break plugin installation for any plugin whose transitive deps trigger arborist.reify() lifecycle script evaluation
  • Currently affects: oh-my-opencode, and likely any plugin with native optional dependencies

Fix

Two changes needed:

  1. packages/opencode/src/npm/index.ts: Add ignoreScripts: true to both Arborist constructors. opencode installs packages for plugins/formatters/LSPs — it should not run arbitrary lifecycle scripts. Packages with native deps already ship prebuilt platform binaries via optionalDependencies.

  2. packages/opencode/script/build.ts: Add external: ["node-gyp"] to Bun.build() options to prevent Bun from pre-resolving the require.resolve('node-gyp/...') path at compile time. This is defense-in-depth — even if ignoreScripts is somehow bypassed, the path won't be hardcoded.

Diff

--- a/packages/opencode/src/npm/index.ts
+++ b/packages/opencode/src/npm/index.ts
@@ Npm.add() Arborist constructor
       binLinks: true,
       progress: false,
       savePrefix: "",
+      ignoreScripts: true,
     })

@@ Npm.install() Arborist constructor
         binLinks: true,
         progress: false,
         savePrefix: "",
+        ignoreScripts: true,
       })

--- a/packages/opencode/script/build.ts
+++ b/packages/opencode/script/build.ts
@@ Bun.build() options
     entrypoints: [...],
+    external: ["node-gyp"],
     define: {

Repro

  1. Build opencode on machine A: bun run script/build.ts
  2. Copy the compiled binary to machine B
  3. Configure a plugin (e.g., @sjawhar/oh-my-opencode@latest)
  4. Run opencode --print-logs — observe failed to install plugin with the hardcoded path error

Related

Metadata

Metadata

Assignees

Labels

coreAnything pertaining to core functionality of the application (opencode server stuff)

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions