Create docs/learnings/ for hard-won technical knowledge that isn't obvious from code or docs alone. Reference from CLAUDE.md so contributors (human and AI) consult it before working on related areas. First entry covers NixOS Electron resource path resolution, /proc/self/exe symlink behavior, testing without NixOS, and why the co-located binary approach was chosen over alternatives. Co-Authored-By: Claude <claude@anthropic.com>
3.3 KiB
NixOS / Nix Flake Learnings
Hard-won knowledge from debugging and fixing NixOS packaging issues. These are things that aren't obvious from reading the code or docs.
Electron + NixOS resource path
The core problem: On NixOS, Electron and the app live in separate
Nix store paths. Chromium computes process.resourcesPath from
/proc/self/exe, which resolves to electron-unwrapped's store path.
The app's locale files, tray icons, and other resources live in a
different store path and aren't found.
/proc/self/exe resolves symlinks. This is why symlinkJoin and
symlink-based approaches don't work. The kernel follows symlinks to
the real binary, so resourcesPath always points to
electron-unwrapped's directory. The only fix is a real copy of the
ELF binary.
The ENOENT is JS, not C++. The failure when isPackaged=true is
readFileSync loading en-US.json from process.resourcesPath at
module top-level in the minified app code — before
frame-fix-wrapper.js can correct the path. Chromium's .pak locale
files live in libexec/electron/ and libexec/electron/locales/ (not
in resources/), so C++ locale loading was never the issue.
The fix (PR #368): Copy the Electron ELF binary into a custom tree
within the derivation, then merge both Electron's and the app's
resources into the adjacent resources/ directory. Everything else
(shared libs, .pak files, locales/) is symlinked to avoid
duplication. This makes /proc/self/exe resolve to our tree, so
resourcesPath naturally contains all needed files.
The stock Electron wrapper
The nixpkgs electron package at ${electron}/bin/electron is a bash
script (generated by makeWrapper) that sets GIO_EXTRA_MODULES,
GDK_PIXBUF_MODULE_FILE, XDG_DATA_DIRS, and CHROME_DEVEL_SANDBOX
before exec-ing the unwrapped binary. Our derivation reuses this
wrapper by copying everything except the final exec line and
pointing it at our custom binary.
How other nixpkgs Electron apps work
Signal, Obsidian, Vesktop use the simple makeWrapper electron --add-flags app.asar pattern. They work because they don't critically
depend on resourcesPath for locale files at startup. Claude Desktop
is unusual in loading locale JSONs from resourcesPath at module
init time with no fallback.
There is no Electron-native env var or CLI flag to override
resourcesPath. A PR for --resources-path (electron/electron#36114)
was closed in Nov 2025 over security concerns. The property was made
read-only in Electron 28.2.1.
Testing NixOS changes without NixOS
A Fedora distrobox with the Nix package manager (Determinate Systems
installer, --init none for no-systemd containers) can build and run
the flake. The Nix derivation produces identical store paths whether
built on NixOS or standalone Nix. Start the daemon manually with
sudo nix-daemon & before building.
This is sufficient to validate build success and basic app startup, but not a substitute for real NixOS testing (system integration, desktop environment, etc.).
Nix store immutability
The Nix store (/nix/store/...) is read-only. You cannot modify
files in an existing derivation's output after build. This rules out
approaches like "add symlinks to Electron's resources dir at runtime."
Any file layout changes must happen at build time in the derivation's
installPhase.