From a075f38714807c513266695de683008cba041e6e Mon Sep 17 00:00:00 2001 From: adelinb Date: Sat, 11 Apr 2026 20:28:26 +0200 Subject: [PATCH] fix: hydrate symlinks at runtime when postinstall is skipped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 #21 (Linux) but for all platforms. --- packages/embedded-postgres/src/binary.ts | 92 +++++++++++++++++++++--- 1 file changed, 81 insertions(+), 11 deletions(-) diff --git a/packages/embedded-postgres/src/binary.ts b/packages/embedded-postgres/src/binary.ts index 43fa7b5..c597e50 100644 --- a/packages/embedded-postgres/src/binary.ts +++ b/packages/embedded-postgres/src/binary.ts @@ -1,4 +1,6 @@ import os from 'os'; +import fs from 'fs/promises'; +import path from 'path'; export type PostgresBinaries = { postgres: string; @@ -6,45 +8,113 @@ export type PostgresBinaries = { initdb: string; } -function getBinaries(): Promise { +/** + * npm does not preserve symlinks when publishing packages. The platform + * packages include a `pg-symlinks.json` file that records the original + * symlinks, and a `postinstall` script (`hydrate-symlinks.js`) to restore + * them. However, some package managers and tools (e.g. bunx, pnpm with + * --ignore-scripts, yarn PnP) skip lifecycle scripts, so the symlinks may + * never be restored. + * + * This function ensures symlinks are hydrated at runtime if they are missing, + * so the postgres binaries work regardless of how the package was installed. + */ +async function hydrateSymlinksIfNeeded(binPath: string): Promise { + // Resolve the native directory from the binary path (bin -> native -> pg-symlinks.json) + const nativeDir = path.dirname(path.dirname(binPath)); + const symlinkFile = path.join(nativeDir, 'pg-symlinks.json'); + + // Read the symlinks manifest + let symlinks: { source: string; target: string }[]; + try { + const content = await fs.readFile(symlinkFile, { encoding: 'utf-8' }); + symlinks = JSON.parse(content); + } catch { + // No symlinks file found — nothing to do + return; + } + + // The paths in pg-symlinks.json are relative to the package root (parent of native/) + const packageRoot = path.dirname(nativeDir); + + for (const { source, target } of symlinks) { + const absoluteTarget = path.resolve(packageRoot, target); + + // Only create symlinks that don't already exist + try { + await fs.lstat(absoluteTarget); + } catch { + // Target doesn't exist — create the symlink + const absoluteSource = path.resolve(packageRoot, source); + const dirname = path.dirname(absoluteTarget); + const relSource = path.relative(dirname, absoluteSource); + + try { + await fs.symlink(relSource, absoluteTarget); + } catch { + // Swallow errors (e.g. read-only filesystem, race conditions) + } + } + } +} + +async function getBinaries(): Promise { const arch = os.arch(); const platform = os.platform(); - + + let binaries: PostgresBinaries; + switch (platform) { case 'darwin': switch(arch) { case 'arm64': - return import('@embedded-postgres/darwin-arm64'); + binaries = await import('@embedded-postgres/darwin-arm64'); + break; case 'x64': - return import('@embedded-postgres/darwin-x64'); + binaries = await import('@embedded-postgres/darwin-x64'); + break; default: throw new Error(`Unsupported arch "${arch}" for platform "${platform}"`); } + break; case 'linux': switch(arch) { case 'arm64': - return import('@embedded-postgres/linux-arm64'); + binaries = await import('@embedded-postgres/linux-arm64'); + break; case 'arm': - return import('@embedded-postgres/linux-arm'); + binaries = await import('@embedded-postgres/linux-arm'); + break; case 'ia32': - return import('@embedded-postgres/linux-ia32'); + binaries = await import('@embedded-postgres/linux-ia32'); + break; case 'ppc64': - return import('@embedded-postgres/linux-ppc64'); + binaries = await import('@embedded-postgres/linux-ppc64'); + break; case 'x64': - return import('@embedded-postgres/linux-x64'); + binaries = await import('@embedded-postgres/linux-x64'); + break; default: - throw new Error(`Unsupported arch "${arch}" for platform "${platform}"`); + throw new Error(`Unsupported arch "${arch}" for platform "${platform}"`); } + break; case 'win32': switch(arch) { case 'x64': - return import('@embedded-postgres/windows-x64'); + binaries = await import('@embedded-postgres/windows-x64'); + break; default: throw new Error(`Unsupported arch "${arch}" for platform "${platform}"`); } + break; default: throw new Error(`Unsupported platform "${platform}"`); } + + // Ensure symlinks are hydrated before returning binary paths + await hydrateSymlinksIfNeeded(binaries.postgres); + + return binaries; } export default getBinaries; \ No newline at end of file