FN-8499: fix macOS embedded PostgreSQL ICU loader links

Ensure embedded PostgreSQL repairs its macOS ICU ABI loader link before startup.

- Normalize the libicuuc.68 dylib compatibility symlink from packaged patch versions.
- Cover missing, dangling, and valid ICU compatibility links in lifecycle tests.
- Add a patch changeset for the macOS startup fix.

Files changed:
 .changeset/fn-8499-macos-postgres-libicu.md        |  7 +++++
 .../__tests__/postgres/embedded-lifecycle.test.ts  | 32 ++++++++++++++++++----
 packages/core/src/postgres/embedded-lifecycle.ts   | 17 +++++++++---
 3 files changed, 47 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-8499

Fusion-Task-Lineage: 1ca453f4-f619-452c-9112-f32ff8fb8128

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-22 14:56:51 -07:00
parent 242b22dbf4
commit d0d10aae6a
3 changed files with 47 additions and 9 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Fix macOS embedded PostgreSQL startup when bundled ICU compatibility links are missing.
category: fix
dev: Repair the libicuuc loader-name symlink before initdb starts.

View File

@@ -490,12 +490,16 @@ describe("embedded-lifecycle: macOS dylib compatibility links", () => {
writeFileSync(join(libDir, "libzstd.1.5.7.dylib"), "");
writeFileSync(join(libDir, "liblz4.1.10.0.dylib"), "");
writeFileSync(join(libDir, "libz.1.3.2.dylib"), "");
// This is the loader-name pair in the packaged Darwin payload: i18n
// requests the ABI-specific uc name rather than the unversioned link.
writeFileSync(join(libDir, "libicui18n.68.2.dylib"), "");
writeFileSync(join(libDir, "libicuuc.68.2.dylib"), "");
const created = normalizeMacosEmbeddedPostgresDylibSymlinks(nativeRoot);
expect(created.map((link) => link.expected).sort()).toEqual([
"libicui18n.dylib",
"libicuuc.68.dylib",
"liblz4.1.dylib",
"libpq.5.dylib",
"libz.1.dylib",
@@ -503,6 +507,7 @@ describe("embedded-lifecycle: macOS dylib compatibility links", () => {
]);
expect(readlinkSync(join(libDir, "libpq.5.dylib"))).toBe("libpq.5.15.dylib");
expect(readlinkSync(join(libDir, "libzstd.1.dylib"))).toBe("libzstd.1.5.7.dylib");
expect(readlinkSync(join(libDir, "libicuuc.68.dylib"))).toBe("libicuuc.68.2.dylib");
// Idempotent: the second pass sees the compatibility names and creates nothing.
expect(normalizeMacosEmbeddedPostgresDylibSymlinks(nativeRoot)).toEqual([]);
@@ -511,20 +516,37 @@ describe("embedded-lifecycle: macOS dylib compatibility links", () => {
}
});
it("replaces stale broken compatibility-name symlinks", () => {
it("selects the highest matching ICU patch and repairs a dangling loader-name link", () => {
const nativeRoot = mkdtempSync(join(tmpdir(), "fusion-embedded-native-"));
try {
const libDir = join(nativeRoot, "lib");
mkdirSync(libDir, { recursive: true });
writeFileSync(join(libDir, "libpq.5.16.dylib"), "");
symlinkSync("libpq.5.15.dylib", join(libDir, "libpq.5.dylib"));
writeFileSync(join(libDir, "libicuuc.68.2.dylib"), "");
writeFileSync(join(libDir, "libicuuc.68.12.dylib"), "");
symlinkSync("libicuuc.68.1.dylib", join(libDir, "libicuuc.68.dylib"));
const created = normalizeMacosEmbeddedPostgresDylibSymlinks(nativeRoot);
expect(created).toEqual([
{ expected: "libpq.5.dylib", target: "libpq.5.16.dylib", created: true },
{ expected: "libicuuc.68.dylib", target: "libicuuc.68.12.dylib", created: true },
]);
expect(readlinkSync(join(libDir, "libpq.5.dylib"))).toBe("libpq.5.16.dylib");
expect(readlinkSync(join(libDir, "libicuuc.68.dylib"))).toBe("libicuuc.68.12.dylib");
} finally {
rmSync(nativeRoot, { recursive: true, force: true });
}
});
it("preserves a valid ICU compatibility link and treats absent library trees as no-ops", () => {
const nativeRoot = mkdtempSync(join(tmpdir(), "fusion-embedded-native-"));
try {
const libDir = join(nativeRoot, "lib");
mkdirSync(libDir, { recursive: true });
writeFileSync(join(libDir, "libicuuc.68.2.dylib"), "");
symlinkSync("libicuuc.68.2.dylib", join(libDir, "libicuuc.68.dylib"));
expect(normalizeMacosEmbeddedPostgresDylibSymlinks(nativeRoot)).toEqual([]);
expect(readlinkSync(join(libDir, "libicuuc.68.dylib"))).toBe("libicuuc.68.2.dylib");
expect(normalizeMacosEmbeddedPostgresDylibSymlinks(join(nativeRoot, "missing"))).toEqual([]);
} finally {
rmSync(nativeRoot, { recursive: true, force: true });
}

View File

@@ -750,12 +750,20 @@ export interface EmbeddedDylibNormalization {
readonly created: boolean;
}
/*
FNXC:PostgresEmbedded 2026-07-22-14:34:
The bundled libicui18n.68.2.dylib records libicuuc.68.dylib as its loader
name. macOS dyld must find that ABI-specific compatibility name before initdb
runs, so normalize it only from the packaged libicuuc.68.<patch>.dylib payload
rather than adding a broad unversioned ICU link or relying on system libraries.
*/
const MACOS_EMBEDDED_DYLIB_SYMLINKS: readonly EmbeddedDylibSymlinkSpec[] = [
{ expected: "libpq.5.dylib", candidate: /^libpq\.5\..+\.dylib$/ },
{ expected: "libzstd.1.dylib", candidate: /^libzstd\.1\..+\.dylib$/ },
{ expected: "liblz4.1.dylib", candidate: /^liblz4\.1\..+\.dylib$/ },
{ expected: "libz.1.dylib", candidate: /^libz\.1\..+\.dylib$/ },
{ expected: "libicui18n.dylib", candidate: /^libicui18n\..+\.dylib$/ },
{ expected: "libicuuc.68.dylib", candidate: /^libicuuc\.68\..+\.dylib$/ },
];
function sortDylibCandidates(files: readonly string[], candidate: RegExp): string[] {
@@ -768,10 +776,11 @@ function sortDylibCandidates(files: readonly string[], candidate: RegExp): strin
* Normalize macOS embedded-postgres library names before initdb/postgres spawn.
*
* The @embedded-postgres/darwin-* packages can contain fully-versioned dylibs
* (for example libpq.5.15.dylib, libzstd.1.5.7.dylib) while the bundled
* binaries link against ABI compatibility names such as libpq.5.dylib and
* libzstd.1.dylib via @loader_path/../lib/.... When the package postinstall
* symlink hydration is skipped or incomplete, dyld fails before initdb can run.
* (for example libpq.5.15.dylib, libzstd.1.5.7.dylib, and
* libicuuc.68.2.dylib) while the bundled binaries link against ABI compatibility
* names such as libpq.5.dylib, libzstd.1.dylib, and libicuuc.68.dylib via
* @loader_path/../lib/.... When the package postinstall symlink hydration is
* skipped or incomplete, dyld fails before initdb can run.
*
* This is intentionally local to the embedded binary package and idempotent:
* existing compatibility names are left alone; missing compatibility names are