## Summary Packaged Fusion desktop Local mode failed after the SQLite→Postgres cutover: 1. **Embedded Postgres** could not start from `app.asar` — platform packages resolve `initdb`/`postgres` via `import.meta.url` into the asar virtual path, and `spawn` fails with `ENOTDIR`. 2. **After Postgres was fixed**, Local mode still fell back to the mode chooser because `@fusion-plugin-examples/omp-runtime` was never built into `dist/` (dashboard imports it from `runtime-provider-probes.ts`). This PR makes packaged Local mode boot embedded Postgres reliably and keep the dashboard shell up. ### Changes - **CJS bootstrap** (`main-bootstrap.cjs`) as Electron `main`: patches `child_process.spawn` / `fs.promises.stat|chmod` before the ESM main loads so asar binary paths rewrite to real files. - **Materialize** the full native PG install (`bin` + `lib` + `share`) under `~/.fusion/embedded-postgres/runtime-bin/<plat-arch>/`. - **electron-builder**: full `asarUnpack` of embedded-postgres packages; allowlist PG deps and `@fusion-plugin-examples/**/*` (+ plugin-sdk / ACP SDK). - **Build** `fusion-plugin-omp-runtime` with the other dashboard-static runtime plugins; export `DASHBOARD_RUNTIME_PLUGIN_PACKAGES` for tests. - Unit coverage for asar path rewrite, packaging allowlists, and omp build inclusion. ## Test plan - [x] `pnpm --filter @fusion/core test:embedded-postgres` (23/23) - [x] Desktop packaging unit tests (`build-bundling`, `electron-builder-config`) - [x] Packaged macOS `Fusion.app` Local mode: - [x] `embedded postgres: ready on port … (database "fusion")` - [x] `desktopMode` stays `"local"` (no chooser fallback) - [x] `GET /api/health` → `status: ok`, `database.healthy: true`, `engine.available: true` - [x] Linux embedded binary lifecycle smoke (Docker aarch64, `@embedded-postgres/linux-arm64`) — initdb/start/persist/restart - [ ] CI release desktop jobs (macOS/Linux) when this lands - [ ] Windows packaged desktop Local + PG (separate agent / host) ## Verification notes | Platform | Embedded Postgres | Packaged Local shell | |----------|-------------------|----------------------| | macOS | Working | Working after this PR | | Linux | Native binary smoke pass | Full AppImage not built on this host | | Windows | Out of scope here | Separate verification | <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved embedded PostgreSQL reliability in Electron-packaged apps by rewriting bundled `app.asar` binary paths to their unpacked/materialized locations. * Ensured embedded PostgreSQL runtime binaries resolve correctly across platforms/architectures, with best-effort executable permissions and macOS dylib link normalization. * **Packaging** * Updated the desktop Electron entry to use a bootstrap module for embedded PostgreSQL binary resolution. * Expanded Electron Builder inclusion and asar-unpack rules for embedded-postgres and related packages, plus required runtime plugin/sdk assets. * **Tests** * Updated and added checks to match the new packaging and plugin/runtime expectations. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
86 lines
3.1 KiB
JavaScript
86 lines
3.1 KiB
JavaScript
/*
|
|
FNXC:DesktopEmbeddedPostgres 2026-07-14-18:50:
|
|
Packaged Electron loads @fusion/core + embedded-postgres as ESM after app.asar is mounted.
|
|
Platform package entrypoints resolve native binaries via import.meta.url to
|
|
`.../app.asar/.../native/bin/postgres`, and spawn against that path fails with ENOTDIR.
|
|
CJS mutation of child_process.spawn must run BEFORE any ESM importer binds spawn.
|
|
This bootstrap is the package main: patch builtins, then dynamically import the ESM main.
|
|
|
|
FNXC:DesktopEmbeddedPostgres 2026-07-15-03:11:
|
|
After CJS reassignment of child_process.spawn / fs.promises.stat|chmod, already-resolved
|
|
ESM named imports keep the pre-patch functions until module.syncBuiltinESMExports() runs.
|
|
Call it after every CJS mutation so the subsequent dynamic import of main.js (and any
|
|
ESM embedded-postgres import) observes the rewritten paths.
|
|
*/
|
|
"use strict";
|
|
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const os = require("os");
|
|
const cp = require("child_process");
|
|
const fsp = require("fs/promises");
|
|
const { syncBuiltinESMExports } = require("module");
|
|
|
|
const BIN_NAMES = new Set([
|
|
"postgres",
|
|
"initdb",
|
|
"pg_ctl",
|
|
"postgres.exe",
|
|
"initdb.exe",
|
|
"pg_ctl.exe",
|
|
]);
|
|
|
|
function runtimeBinRoot() {
|
|
return path.join(os.homedir(), ".fusion", "embedded-postgres", "runtime-bin", `${process.platform}-${process.arch}`);
|
|
}
|
|
|
|
function resolveAsarUnpacked(filePath) {
|
|
if (!filePath || typeof filePath !== "string") return filePath;
|
|
const base = path.basename(filePath);
|
|
if (BIN_NAMES.has(base) && filePath.includes(`${path.sep}app.asar`)) {
|
|
const materialized = path.join(runtimeBinRoot(), "bin", base);
|
|
if (fs.existsSync(materialized)) return materialized;
|
|
}
|
|
if (filePath.includes(`${path.sep}app.asar.unpacked${path.sep}`)) return filePath;
|
|
const marker = `${path.sep}app.asar${path.sep}`;
|
|
const index = filePath.indexOf(marker);
|
|
if (index === -1) return filePath;
|
|
const unpacked =
|
|
filePath.slice(0, index) +
|
|
`${path.sep}app.asar.unpacked${path.sep}` +
|
|
filePath.slice(index + marker.length);
|
|
return fs.existsSync(unpacked) ? unpacked : filePath;
|
|
}
|
|
|
|
function installSpawnPatch() {
|
|
const originalSpawn = cp.spawn.bind(cp);
|
|
cp.spawn = function patchedSpawn(command, ...rest) {
|
|
const fixed = typeof command === "string" ? resolveAsarUnpacked(command) : command;
|
|
return originalSpawn(fixed, ...rest);
|
|
};
|
|
|
|
const originalStat = fsp.stat.bind(fsp);
|
|
fsp.stat = function patchedStat(p, ...rest) {
|
|
const fixed = typeof p === "string" ? resolveAsarUnpacked(p) : p;
|
|
return originalStat(fixed, ...rest);
|
|
};
|
|
|
|
const originalChmod = fsp.chmod.bind(fsp);
|
|
fsp.chmod = function patchedChmod(p, ...rest) {
|
|
const fixed = typeof p === "string" ? resolveAsarUnpacked(p) : p;
|
|
return originalChmod(fixed, ...rest);
|
|
};
|
|
|
|
// Propagate CJS mutations to ESM named exports before main.js loads.
|
|
syncBuiltinESMExports();
|
|
}
|
|
|
|
installSpawnPatch();
|
|
|
|
// Load the ESM Electron main after builtins are patched.
|
|
const { pathToFileURL } = require("url");
|
|
import(pathToFileURL(path.join(__dirname, "main.js")).href).catch((err) => {
|
|
console.error("[desktop/bootstrap] failed to load main.js", err);
|
|
process.exit(1);
|
|
});
|