Skip to content

fix: hydrate symlinks at runtime when postinstall is skipped#27

Open
adelin-b wants to merge 1 commit intoleinelissen:mainfrom
adelin-b:fix/runtime-symlink-hydration
Open

fix: hydrate symlinks at runtime when postinstall is skipped#27
adelin-b wants to merge 1 commit intoleinelissen:mainfrom
adelin-b:fix/runtime-symlink-hydration

Conversation

@adelin-b
Copy link
Copy Markdown

Problem

npm does not preserve symlinks when publishing packages. The platform packages (e.g. @embedded-postgres/darwin-arm64) ship a pg-symlinks.json manifest and a postinstall script (hydrate-symlinks.js) to restore symlinks after installation.

However, several common tools and configurations skip lifecycle scripts, leaving the dylib/so symlinks missing:

  • bunx — always skips postinstall
  • pnpm with --ignore-scripts
  • yarn PnP — doesn't run postinstall in certain configurations
  • npm with --ignore-scripts

Without these symlinks, the postgres binaries crash at startup with errors like:

dyld: Library not loaded: @loader_path/../lib/libzstd.1.dylib
  Reason: no such file

This is the same class of issue as #21 (Linux shared libraries), but affecting all platforms where symlinks exist in the native directory.

Solution

Add runtime symlink hydration in getBinaries() (packages/embedded-postgres/src/binary.ts). After importing the platform package, it:

  1. Locates pg-symlinks.json relative to the binary path
  2. Checks each symlink target with lstat
  3. Creates only missing symlinks using relative paths (matching the existing hydrate-symlinks.js behavior)

The check is:

  • Idempotent — skips already-existing symlinks
  • Safe on read-only filesystems — silently swallows errors
  • Zero overhead when symlinks exist — just a series of lstat calls that hit the fs cache

The existing postinstall script is kept as-is for the fast path when lifecycle scripts do run.

Test plan

  • Verified on macOS darwin-arm64 with bunx paperclipai@latest (which uses embedded-postgres internally)
  • Before fix: initdb and postgres crash with dyld missing library errors
  • After fix: all 30+ symlinks from pg-symlinks.json are hydrated and postgres starts successfully"
    true

npm does not preserve symlinks when publishing packages. The platform
packages include pg-symlinks.json and a postinstall script to restore
them. However, tools like bunx, pnpm with --ignore-scripts, and yarn
PnP skip lifecycle scripts, leaving the dylib/so symlinks missing.
This causes postgres binaries to crash at startup with dyld/ld errors
like "Library not loaded: libzstd.1.dylib".

This commit adds runtime symlink hydration in getBinaries() — after
importing the platform package, it checks pg-symlinks.json and creates
any missing symlinks before returning the binary paths. The check is
idempotent and silently skips already-existing symlinks or read-only
filesystems.

Fixes the same class of issue as leinelissen#21 (Linux) but for all platforms.
@leinelissen
Copy link
Copy Markdown
Owner

Hi @adelin-b. This looks promising! Thanks for your contribution. Could you perhaps add some tests that verify that the symlinks are correctly applied at runtime? I would also really appreciate it if we could also run the the tests for bun and Deno in CI to see if we run into any issues into the future. Let me know if that could work out for you :)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants