From d45263b5ba985675c0bf65154614363aea7c6806 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 17:53:29 +0000 Subject: [PATCH 1/2] fix(packaging): resolve bare ICU install names beside the source dylib Release run 48 failed with "ENOENT: lstat packaging/electron/libicuuc.77.dylib". Postgres.app's ICU libraries record bare install names (libicuuc.77.dylib), and the relinker passed them to realpathSync, which resolved them against the build step's working directory. Resolve a non-absolute dependency next to the image that records it, and fail with a clear message if it is not there. --- .../scripts/prepare-postgres-runtime.ts | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packaging/electron/scripts/prepare-postgres-runtime.ts b/packaging/electron/scripts/prepare-postgres-runtime.ts index 3975ade9c..f89051bd2 100644 --- a/packaging/electron/scripts/prepare-postgres-runtime.ts +++ b/packaging/electron/scripts/prepare-postgres-runtime.ts @@ -379,9 +379,23 @@ function relinkDistribution( continue; } if (dependency.startsWith("@")) continue; - let destination = mappedDestination(dependency, mappings); + // ICU's own build records bare install names such as libicuuc.77.dylib. + // Resolve those beside the source image, never against the working + // directory. + const dependencyPath = path.isAbsolute(dependency) + ? dependency + : path.resolve( + path.dirname(origins.get(filePath) ?? filePath), + dependency, + ); + if (!fs.existsSync(dependencyPath)) { + throw new Error( + `PostgreSQL dependency is missing: ${path.basename(dependency)}`, + ); + } + let destination = mappedDestination(dependencyPath, mappings); if (!destination) { - const source = fs.realpathSync(dependency); + const source = fs.realpathSync(dependencyPath); // Preserve the filename used by the Mach-O load command. Homebrew // commonly exposes a compatibility symlink such as // libicudata.78.dylib whose real file has a longer versioned name. @@ -389,7 +403,7 @@ function relinkDistribution( if (!fs.existsSync(destination)) { fs.copyFileSync(source, destination); fs.chmodSync(destination, 0o755); - origins.set(destination, dependency); + origins.set(destination, dependencyPath); if (isMachO(destination)) queue.push(destination); } copyLicenseFiles(source, licensesRoot, copiedLicenses); From 8cc3d14a878d0fc67cd55ac71039359e1db6ab2e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 18:02:25 +0000 Subject: [PATCH 2/2] fix(packaging): thin Postgres.app binaries and drop unshipped files Inspecting the pinned Postgres.app 2.9.6 DMG showed three more relink failures waiting behind the ICU one: - Every Mach-O is universal (x86_64 + arm64). otool prints one block per slice, so the parsers read the slice headers as dependencies and library ids. Thin each file to the build architecture first. - Six static archives are universal too, so `file` calls them Mach-O. They are link-time only; remove them. - The PL/Python modules link /Library/Frameworks/Python.framework, which the runner does not have and Vision does not use. Remove them and their extension scripts. --- .../scripts/prepare-postgres-runtime.test.ts | 22 ++++++++++++++ .../scripts/prepare-postgres-runtime.ts | 29 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/packaging/electron/scripts/prepare-postgres-runtime.test.ts b/packaging/electron/scripts/prepare-postgres-runtime.test.ts index ea2e86912..8291ab686 100644 --- a/packaging/electron/scripts/prepare-postgres-runtime.test.ts +++ b/packaging/electron/scripts/prepare-postgres-runtime.test.ts @@ -11,6 +11,7 @@ import { commonDirectory, inspectDistribution, materializeSymlinks, + removeUnshippedFiles, } from "./prepare-postgres-runtime"; void test("packaged PostgreSQL paths retain their build-time relative layout", () => { @@ -104,3 +105,24 @@ esac await fs.promises.rm(temp, { recursive: true, force: true }); } }); + +void test("static archives and PL/Python are left out of the packaged runtime", async () => { + const temp = await fs.promises.mkdtemp( + path.join(os.tmpdir(), "vision-postgres-plpython-"), + ); + const pkglib = path.join(temp, "lib", "postgresql"); + const extension = path.join(temp, "share", "postgresql", "extension"); + try { + await fs.promises.mkdir(pkglib, { recursive: true }); + await fs.promises.mkdir(extension, { recursive: true }); + for (const name of ["plpython3.dylib", "libpgport.a", "pg_trgm.dylib"]) + await fs.promises.writeFile(path.join(pkglib, name), ""); + for (const name of ["jsonb_plpython3u.control", "pg_trgm.control"]) + await fs.promises.writeFile(path.join(extension, name), ""); + removeUnshippedFiles(temp); + assert.deepEqual(await fs.promises.readdir(pkglib), ["pg_trgm.dylib"]); + assert.deepEqual(await fs.promises.readdir(extension), ["pg_trgm.control"]); + } finally { + await fs.promises.rm(temp, { recursive: true, force: true }); + } +}); diff --git a/packaging/electron/scripts/prepare-postgres-runtime.ts b/packaging/electron/scripts/prepare-postgres-runtime.ts index f89051bd2..833d66dfa 100644 --- a/packaging/electron/scripts/prepare-postgres-runtime.ts +++ b/packaging/electron/scripts/prepare-postgres-runtime.ts @@ -188,6 +188,16 @@ export function assertNoSymlinks(root: string): void { visit(root); } +// Static archives are link-time only. PL/Python links the python.org +// framework (Postgres.app), which Vision neither uses nor ships; dropping it +// keeps the relinker from trying to vendor a Python runtime. +export function removeUnshippedFiles(root: string): void { + for (const filePath of walkFiles(root)) { + const name = path.basename(filePath); + if (name.endsWith(".a") || /plpython/i.test(name)) fs.rmSync(filePath); + } +} + function walkFiles(root: string): string[] { const files: string[] = []; function visit(current: string): void { @@ -244,6 +254,23 @@ function dynamicLibraryId(filePath: string): string | undefined { .find(Boolean); } +// Postgres.app ships universal binaries. otool prints one block per slice, +// which the parsers above do not expect, and Vision ships one architecture. +function thinToBuildArchitecture(filePath: string): void { + const architectures = run("/usr/bin/lipo", ["-archs", filePath]).split(/\s+/); + if (architectures.length < 2) return; + const architecture = process.arch === "arm64" ? "arm64" : "x86_64"; + if (!architectures.includes(architecture)) { + throw new Error( + `PostgreSQL file has no ${architecture} slice: ${path.basename(filePath)}`, + ); + } + const thinned = `${filePath}.thin`; + run("/usr/bin/lipo", [filePath, "-thin", architecture, "-output", thinned]); + fs.chmodSync(thinned, fs.statSync(filePath).mode & 0o777); + fs.renameSync(thinned, filePath); +} + function systemLibrary(dependency: string): boolean { return ( dependency.startsWith("/usr/lib/") || @@ -349,6 +376,7 @@ function relinkDistribution( ) { if (visited.has(filePath)) continue; visited.add(filePath); + thinToBuildArchitecture(filePath); const libraryId = dynamicLibraryId(filePath); for (const dependency of linkedLibraries(filePath)) { if (dependency === libraryId || systemLibrary(dependency)) continue; @@ -482,6 +510,7 @@ export function preparePostgresRuntime({ copyDirectory(source, targetDirectory); materializeSymlinks(root); assertNoSymlinks(root); + removeUnshippedFiles(root); normalizePermissions(root); const mappings = [...uniqueDirectories].map(([source, destination_]) => ({