Skip to content

Launcher node pin uses versioned Homebrew Cellar path — breaks on routine brew upgrade/cleanup; pin /opt/homebrew/opt/node instead #61

Description

@vishk23

Summary

The launcher's node pin (install-runtime.jsonnodePath) records the fully-versioned Homebrew keg path, e.g. /opt/homebrew/Cellar/node/26.0.0/bin/node. Homebrew treats versioned kegs as disposable — brew upgrade node creates a new keg and brew cleanup deletes old ones — so the pin's target routinely disappears or, worse, silently rots. Suggestion: on Homebrew installs, pin the stable opt symlink /opt/homebrew/opt/node/bin/node instead, and rely on an ABI check for safety.

What happened (real-world failure, macOS arm64, almanac 0.2.x)

  1. Almanac was installed under Homebrew node 26.0.0; install-runtime.json pinned /opt/homebrew/Cellar/node/26.0.0/bin/node (ABI 147).
  2. Later, brew upgrade installed node 26.5.0_1 and upgraded ada-url 3.4.4 → 4.0.0 (dylib major bump: libada.3.dyliblibada.4.dylib), then cleanup removed the old ada-url keg — but not the old node keg.
  3. Result: the pinned binary still exists and is executable, so isUsablePinnedNode() passes — but it can no longer run, because its dynamic dependency is gone. Homebrew only guarantees dependency compatibility for the current keg of a formula; an orphaned old keg is outside that guarantee.
  4. Every almanac invocation then dies with a raw dyld error instead of the friendly formatMissingPinnedNodeMessage repair hint:
dyld[48946]: Library not loaded: /opt/homebrew/opt/ada-url/lib/libada.3.dylib
  Referenced from: /opt/homebrew/Cellar/node/26.0.0/bin/node
  Reason: tried: '/opt/homebrew/opt/ada-url/lib/libada.3.dylib' (no such file), ...

This is easy to misdiagnose as a globally broken node install, when in fact node on PATH (26.5.0_1) worked fine the whole time. The actual repair (npm install -g codealmanac@latest) is non-obvious from the error.

The existence check also means the other common sequence (brew upgrade node + brew cleanup, which deletes the pinned keg) breaks almanac on every node upgrade cycle, even though the new node is ABI-compatible (node keeps one ABI per major, so every 26.x is ABI 147).

Suggestion

When the install-time process.execPath resolves under a Homebrew Cellar (/opt/homebrew/Cellar/node/<ver>/... or /usr/local/Cellar/node/<ver>/...), pin the corresponding stable symlink instead:

  • /opt/homebrew/opt/node/bin/node (arm64) / /usr/local/opt/node/bin/node (intel)

That path survives upgrades and cleanups, and always points at the keg whose dependencies Homebrew currently guarantees. To keep the safety property the pin exists for (protecting the better-sqlite3 native binding), record nodeAbi as today and verify it at launch — e.g. spawn <pinned> -p process.versions.modules (or check process.versions.modules after re-exec) and emit the existing "repair: npm install -g codealmanac@latest" message on mismatch. ABI mismatch through the opt symlink can only happen on a node major bump, which is exactly when a rebuild is genuinely needed — versus today, where the pin breaks on every minor/patch keg rotation.

The same reasoning applies to other version-manager layouts (nvm's versions/node/vX.Y.Z/bin/node is similarly disposable), but Homebrew is the case where the failure mode is both routine and misleading.

Happy to PR this if the approach sounds right.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions