Files
claude-desktop-debian/docs/learnings/nix.md
aaddrick 879a700a7d docs: add learnings directory with NixOS packaging knowledge
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>
2026-04-01 06:00:08 -04:00

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.