Skip to content

fix(nix): expose libstdc++ so the native fs watcher loads on NixOS - #53213

Closed
bew wants to merge 1 commit into
anomalyco:devfrom
bew:nix-fix-fs-watcher
Closed

bew wants to merge 1 commit into
anomalyco:devfrom
bew:nix-fix-fs-watcher

Conversation

@bew

@bew bew commented Oct 4, 2026

Copy link
Copy Markdown

Issue for this PR

Closes #53210

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

On NixOS the packaged opencode never loads its native filesystem watcher.
The bundled @parcel/watcher addon is a prebuilt .node that links libstdc++.so.6 dynamically and has no RUNPATH, so the loader can't resolve the C++ runtime and the dlopen is dropped quietly.

In practice this shows up as skills and config edits never reloading: recursive directory watches (type:directory, parcel-backed) go silent, while the per-file watches (type:file / entries, which use node's fs.watch) keep working.

The Nix wrapper's LD_LIBRARY_PATH only carried wayland, and the devshell had no libstdc++ at all, so bun dev from the devshell failed the same way.
This adds stdenv.cc.cc.lib in both places so the addon resolves the C++ runtime when it loads.

Note

numtide's llm-agents.nix takes a different route in packages/opencode2/package.nix: it patches the wrapped binary's DT_RPATH with formatelf so the executable itself resolves libstdc++, and vendors formatelf because it isn't in nixpkgs.
I kept the wrapper approach here: formatelf would pull a new packaged tool (and its setup hook) into this repo, plain patchelf --force-rpath corrupts this bun standalone (it segfaults on launch), and the wrapper already sets LD_LIBRARY_PATH for wayland — so extending it is the smaller, single-place change.

How did you verify your code works?

I reproduced it by hand first: readelf -d watcher.node shows NEEDED libstdc++.so.6 and no RPATH, and a plain bun require of the addon fails in a clean env but loads once the gcc lib dir is on LD_LIBRARY_PATH.

Then I built the package (nix build .#opencode) and ran the wrapped binary under strace.
The embedded addon is extracted to /tmp/.bun-<uid>-<hash>.node (sha256 matches @parcel/[email protected]/watcher.node), and the libstdc++.so.6 lookup now resolves from the added gcc lib dir instead of returning ENOENT.

I didn't run the JS test suite for this — it's a Nix packaging change only.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@nkoynov

nkoynov commented Oct 7, 2026

Copy link
Copy Markdown

Same bug here on the v2.0.24 tag built with nix/opencode.nix: every type=directory watch logs "watcher backend not supported", so config and skill edits never reach a running server. The release binary loads the same addon fine on NixOS because it runs through nix-ld, which supplies libstdc++.

One concern with the wrapper route: LD_LIBRARY_PATH is inherited by everything OpenCode starts (shell tool commands, LSPs, MCP servers), and it overrides those programs' own RUNPATH. When the libstdc++ on it is older than what a program was built against, the program fails to start. For example, with an older libstdc++ first on the path, the system's nix exits with "version `CXXABI_1.3.15' not found". The flake pins its own nixpkgs, so that skew happens whenever a user's system is newer than OpenCode's pin.

Patching the addon itself avoids that. We do it after configurePhase copies node_modules and before bun build embeds the file:

postConfigure = lib.optionalString stdenvNoCC.hostPlatform.isLinux ''
  for addon in node_modules/.bun/@parcel+watcher-linux-*-glibc@*/node_modules/@parcel/watcher-linux-*-glibc/watcher.node; do
    chmod u+w "$addon"
    patchelf --add-rpath ${lib.makeLibraryPath [ stdenv.cc.cc.lib ]} "$addon"
    bun -e 'require(process.argv[1])' "$PWD/$addon"   # fails the build if it still can't load
  done
'';

(with patchelf in nativeBuildInputs). The standalone binary isn't touched, so the segfault you saw from patchelf --force-rpath on it doesn't come up. The extracted addon keeps its RUNPATH, and the gcc lib becomes a runtime reference of the output. On NixOS (VM, 2.0.24), directory watches then start with backend=inotify like the release. A skill added under ~/.config/opencode/skills is rescanned live, and the wrapper's LD_LIBRARY_PATH stays as it was.

@bew

bew commented Oct 7, 2026

Copy link
Copy Markdown
Author

One concern with the wrapper route: LD_LIBRARY_PATH is inherited by everything OpenCode starts (shell tool commands, LSPs, MCP servers), and it overrides those programs' own RUNPATH. When the libstdc++ on it is older than what a program was built against, the program can fail to start.

Right, I thought I mentioned it. 😬

We do it after configurePhase copies node_modules and before bun build embeds the file

Who is 'We' In this context? I don't see this change in the repo, are you talking about another project?

@nkoynov

nkoynov commented Oct 7, 2026

Copy link
Copy Markdown

Sorry, that was unclear: "we" is my own NixOS config. It builds OpenCode from the v2.0.24 tag with this repo's flake package and adds that postConfigure through overrideAttrs, so none of it is in this repo. Feel free to take it into this PR; I can also open a separate one if you'd rather.

For transparency: an AI agent is managing this change on my side.

@bew

bew commented Oct 29, 2026

Copy link
Copy Markdown
Author

Hello, I have an update ready based on what @nkoynov suggested, with additional check to ensure the patch is applied & useful.
I'll push it later today (french tz) when I'm back home

The glibc `@parcel/watcher` addon links libstdc++ without a RUNPATH,
so the load fails and every directory watch dies (see anomalyco#53210).
Patch it in `postConfigure`, before `buildPhase` embeds it into
the standalone library binary.

Keeping the fix in the addon avoids putting libstdc++ on
`LD_LIBRARY_PATH`, which every spawned process would inherit and which
outranks that program's own RUNPATH.
The dev shell keeps its own libstdc++ export, limited to Linux.
@bew
bew force-pushed the nix-fix-fs-watcher branch from f6d9a9a to c1afe3a Compare November 1, 2026 14:46
@nkoynov

nkoynov commented Nov 1, 2026

Copy link
Copy Markdown

Tested c1afe3a on NixOS x86_64: built it and its merge base (69269e4) with nix/opencode.nix and ran each as a throwaway opencode serve. On the base every directory watch logs "watcher backend not supported" and a skill added or edited in the config dir never shows up. With the PR the extracted watcher.node finds libstdc++ through its RUNPATH, the watches start on inotify and both changes are picked up live. Small thing: the PR title and description still describe the LD_LIBRARY_PATH wrapper.

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Automated PR Cleanup

Thank you for contributing to opencode.

Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions.

This PR was closed because it matched the following cleanup criteria:

  • The PR was created more than 1 month ago
  • The PR had fewer than 2 positive reactions
  • Positive reactions are counted as thumbs-up, heart, celebration, or rocket reactions on the PR

PRs created within the last month are not affected by this cleanup.

If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate.

Thanks again for taking the time to contribute.

@nkoynov

nkoynov commented Nov 4, 2026

Copy link
Copy Markdown

This is still needed: dev's nix/opencode.nix has no fix for it, so every recursive watch on a NixOS source build still fails with "watcher backend not supported" (#53210). The PR wasn't stale either: bew pushed c1afe3a on 11-01 and it fixes the watcher in our NixOS test. Could it be reopened?

@bew

bew commented Nov 5, 2026 •

Copy link
Copy Markdown
Author

(I'll post the reply myself, in case the bot only react if the author replies)

This is still needed: nix/opencode.nix is still broken on dev branch, so every recursive watch on a NixOS source build still fails with "watcher backend not supported" (#53210).
The PR wasn't stale either: I pushed c1afe3a a few days ago which improves the impl & fixes the watcher in people's NixOS test.

For me the PR is ready for review/merge.

👉 Could it be reopened? and get a review for merge?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NixOS: native recursive FS watcher never loads (libstdc++.so.6 not found)

2 participants