Files
fusion/packages/desktop/src/main-bootstrap.cjs
gsxdsm 7568705244 fix(desktop): boot embedded Postgres in packaged app and ship omp dist (#2106)
## 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 -->
2026-07-14 20:44:09 -07:00

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);
});