From 1edb8591e4669a17ab9957791791c22fc444fa0b Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 21 May 2026 20:29:52 +0000 Subject: [PATCH] Fix embedded Postgres native library bootstrap Co-Authored-By: Paperclip --- cli/src/commands/routines.ts | 2 + cli/src/commands/worktree.ts | 2 + .../db/src/embedded-postgres-native.test.ts | 43 ++++++++++ packages/db/src/embedded-postgres-native.ts | 85 +++++++++++++++++++ packages/db/src/index.ts | 4 + packages/db/src/migration-runtime.ts | 2 + packages/db/src/test-embedded-postgres.ts | 2 + server/src/index.ts | 2 + 8 files changed, 142 insertions(+) create mode 100644 packages/db/src/embedded-postgres-native.test.ts create mode 100644 packages/db/src/embedded-postgres-native.ts diff --git a/cli/src/commands/routines.ts b/cli/src/commands/routines.ts index 4c5a3ca9677..63ff89d81f3 100644 --- a/cli/src/commands/routines.ts +++ b/cli/src/commands/routines.ts @@ -9,6 +9,7 @@ import { createEmbeddedPostgresLogBuffer, ensurePostgresDatabase, formatEmbeddedPostgresError, + prepareEmbeddedPostgresNativeRuntime, routines, } from "@paperclipai/db"; import { eq, inArray } from "drizzle-orm"; @@ -116,6 +117,7 @@ async function ensureEmbeddedPostgres(dataDir: string, preferredPort: number): P "Embedded PostgreSQL support requires dependency `embedded-postgres`. Reinstall dependencies and try again.", ); } + await prepareEmbeddedPostgresNativeRuntime(); const postmasterPidFile = path.resolve(dataDir, "postmaster.pid"); const runningPid = readRunningPostmasterPid(postmasterPidFile); diff --git a/cli/src/commands/worktree.ts b/cli/src/commands/worktree.ts index fea6f2c3ec8..db0ca008147 100644 --- a/cli/src/commands/worktree.ts +++ b/cli/src/commands/worktree.ts @@ -45,6 +45,7 @@ import { runDatabaseRestore, createEmbeddedPostgresLogBuffer, formatEmbeddedPostgresError, + prepareEmbeddedPostgresNativeRuntime, } from "@paperclipai/db"; import type { Command } from "commander"; import { ensureAgentJwtSecret, loadPaperclipEnvFile, mergePaperclipEnvEntries, readPaperclipEnvEntries, resolvePaperclipEnvFile } from "../config/env.js"; @@ -1059,6 +1060,7 @@ async function ensureEmbeddedPostgres(dataDir: string, preferredPort: number): P "Embedded PostgreSQL support requires dependency `embedded-postgres`. Reinstall dependencies and try again.", ); } + await prepareEmbeddedPostgresNativeRuntime(); const postmasterPidFile = path.resolve(dataDir, "postmaster.pid"); const runningPid = readRunningPostmasterPid(postmasterPidFile); diff --git a/packages/db/src/embedded-postgres-native.test.ts b/packages/db/src/embedded-postgres-native.test.ts new file mode 100644 index 00000000000..7335f2c6492 --- /dev/null +++ b/packages/db/src/embedded-postgres-native.test.ts @@ -0,0 +1,43 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { ensureLinuxSharedLibraryAliases } from "./embedded-postgres-native.js"; + +describe("embedded Postgres native runtime", () => { + const tempDirs: string[] = []; + + afterEach(() => { + for (const tempDir of tempDirs.splice(0)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it.runIf(process.platform !== "win32")("creates soname aliases for bundled patch-level shared libraries", async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-embedded-pg-libs-")); + tempDirs.push(tempDir); + fs.writeFileSync(path.join(tempDir, "libicuuc.so.60.2"), ""); + fs.writeFileSync(path.join(tempDir, "libicui18n.so.60.2"), ""); + fs.writeFileSync(path.join(tempDir, "README.md"), ""); + + const created = await ensureLinuxSharedLibraryAliases(tempDir); + + expect(created.map((file) => path.basename(file)).sort()).toEqual([ + "libicui18n.so.60", + "libicuuc.so.60", + ]); + expect(fs.readlinkSync(path.join(tempDir, "libicuuc.so.60"))).toBe("libicuuc.so.60.2"); + }); + + it.runIf(process.platform !== "win32")("is idempotent when aliases already exist", async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-embedded-pg-libs-")); + tempDirs.push(tempDir); + fs.writeFileSync(path.join(tempDir, "libicuuc.so.60.2"), ""); + + await ensureLinuxSharedLibraryAliases(tempDir); + const second = await ensureLinuxSharedLibraryAliases(tempDir); + + expect(second).toEqual([]); + expect(fs.readlinkSync(path.join(tempDir, "libicuuc.so.60"))).toBe("libicuuc.so.60.2"); + }); +}); diff --git a/packages/db/src/embedded-postgres-native.ts b/packages/db/src/embedded-postgres-native.ts new file mode 100644 index 00000000000..a304f8a18ec --- /dev/null +++ b/packages/db/src/embedded-postgres-native.ts @@ -0,0 +1,85 @@ +import { promises as fs } from "node:fs"; +import { createRequire } from "node:module"; +import path from "node:path"; + +const require = createRequire(import.meta.url); + +function resolveNativePackageName(): string | null { + if (process.platform !== "linux") return null; + + switch (process.arch) { + case "arm64": + return "linux-arm64"; + case "arm": + return "linux-arm"; + case "ia32": + return "linux-ia32"; + case "ppc64": + return "linux-ppc64"; + case "x64": + return "linux-x64"; + default: + return null; + } +} + +async function pathExists(value: string): Promise { + try { + await fs.stat(value); + return true; + } catch { + return false; + } +} + +function resolveEmbeddedPostgresPackageRoot(): string | null { + try { + const entry = require.resolve("embedded-postgres"); + return path.dirname(path.dirname(entry)); + } catch { + return null; + } +} + +function prependPathEnv(name: string, value: string): void { + const current = process.env[name] ?? ""; + const parts = current.split(path.delimiter).filter(Boolean); + if (parts.includes(value)) return; + process.env[name] = [value, ...parts].join(path.delimiter); +} + +export async function ensureLinuxSharedLibraryAliases(libDir: string): Promise { + const entries = await fs.readdir(libDir, { withFileTypes: true }); + const created: string[] = []; + + for (const entry of entries) { + if (!entry.isFile()) continue; + const match = entry.name.match(/^(lib.+\.so\.\d+)\.\d+(?:\.\d+)?$/); + if (!match) continue; + + const aliasName = match[1]; + const aliasPath = path.join(libDir, aliasName); + try { + await fs.symlink(entry.name, aliasPath); + created.push(aliasPath); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "EEXIST") continue; + throw error; + } + } + + return created; +} + +export async function prepareEmbeddedPostgresNativeRuntime(): Promise { + const nativePackageName = resolveNativePackageName(); + const packageRoot = resolveEmbeddedPostgresPackageRoot(); + if (!nativePackageName || !packageRoot) return; + + const nativeRoot = path.resolve(packageRoot, "..", "@embedded-postgres", nativePackageName); + const libDir = path.join(nativeRoot, "native", "lib"); + if (!(await pathExists(libDir))) return; + + prependPathEnv("LD_LIBRARY_PATH", libDir); + await ensureLinuxSharedLibraryAliases(libDir); +} diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 3bf4d22b57c..a1c5bce8b26 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -30,6 +30,10 @@ export { createEmbeddedPostgresLogBuffer, formatEmbeddedPostgresError, } from "./embedded-postgres-error.js"; +export { + ensureLinuxSharedLibraryAliases, + prepareEmbeddedPostgresNativeRuntime, +} from "./embedded-postgres-native.js"; export { issueRelations } from "./schema/issue_relations.js"; export { issueReferenceMentions } from "./schema/issue_reference_mentions.js"; export * from "./schema/index.js"; diff --git a/packages/db/src/migration-runtime.ts b/packages/db/src/migration-runtime.ts index 5aa2b6a27d0..3823b6d02e0 100644 --- a/packages/db/src/migration-runtime.ts +++ b/packages/db/src/migration-runtime.ts @@ -3,6 +3,7 @@ import { createServer } from "node:net"; import path from "node:path"; import { ensurePostgresDatabase, getPostgresDataDirectory } from "./client.js"; import { createEmbeddedPostgresLogBuffer, formatEmbeddedPostgresError } from "./embedded-postgres-error.js"; +import { prepareEmbeddedPostgresNativeRuntime } from "./embedded-postgres-native.js"; import { resolveDatabaseTarget } from "./runtime-config.js"; type EmbeddedPostgresInstance = { @@ -92,6 +93,7 @@ async function ensureEmbeddedPostgresConnection( preferredPort: number, ): Promise { const EmbeddedPostgres = await loadEmbeddedPostgresCtor(); + await prepareEmbeddedPostgresNativeRuntime(); const selectedPort = await findAvailablePort(preferredPort); const postmasterPidFile = path.resolve(dataDir, "postmaster.pid"); const pgVersionFile = path.resolve(dataDir, "PG_VERSION"); diff --git a/packages/db/src/test-embedded-postgres.ts b/packages/db/src/test-embedded-postgres.ts index 87db5cde962..929d17c5f7e 100644 --- a/packages/db/src/test-embedded-postgres.ts +++ b/packages/db/src/test-embedded-postgres.ts @@ -3,6 +3,7 @@ import net from "node:net"; import os from "node:os"; import path from "node:path"; import { applyPendingMigrations, ensurePostgresDatabase } from "./client.js"; +import { prepareEmbeddedPostgresNativeRuntime } from "./embedded-postgres-native.js"; type EmbeddedPostgresInstance = { initialise(): Promise; @@ -48,6 +49,7 @@ function getReservedTestPorts(): Set { async function getEmbeddedPostgresCtor(): Promise { const mod = await import("embedded-postgres"); + await prepareEmbeddedPostgresNativeRuntime(); return mod.default as EmbeddedPostgresCtor; } diff --git a/server/src/index.ts b/server/src/index.ts index c039f7584f1..9e7e3a00360 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -15,6 +15,7 @@ import { inspectMigrations, applyPendingMigrations, createEmbeddedPostgresLogBuffer, + prepareEmbeddedPostgresNativeRuntime, reconcilePendingMigrationHistory, formatDatabaseBackupResult, runDatabaseBackup, @@ -316,6 +317,7 @@ export async function startServer(): Promise { "Embedded PostgreSQL mode requires dependency `embedded-postgres`. Reinstall dependencies (without omitting required packages), or set DATABASE_URL for external Postgres.", ); } + await prepareEmbeddedPostgresNativeRuntime(); const dataDir = resolve(config.embeddedPostgresDataDir); const configuredPort = config.embeddedPostgresPort;