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_]) => ({