From 41c532e2ca75a904f51e80b5b59deb556414c2d6 Mon Sep 17 00:00:00 2001 From: seb Date: Tue, 27 Oct 2026 00:24:55 +0100 Subject: [PATCH] u --- Dockerfile | 11 +++---- docker-compose.yml | 2 +- scripts/buildSea.js | 80 ++++++++++++++++++++++++++++++++++++--------- src/sea-entry.cjs | 43 ++++++++++++++---------- 4 files changed, 95 insertions(+), 41 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1481381..bd35a5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,15 +15,14 @@ COPY --from=syslibs /usr/lib/x86_64-linux-gnu/libfontconfig.so* /usr/lib/x86_64- COPY --from=syslibs /usr/lib/x86_64-linux-gnu/libxml2.so* /usr/lib/x86_64-linux-gnu/ COPY --from=syslibs /usr/lib/x86_64-linux-gnu/libfribidi.so* /usr/lib/x86_64-linux-gnu/ -# Sibling dependencies for local symlink resolution -COPY dashdbschema /dashdbschema -COPY llmorbiter /llmorbiter - # The standalone SEA binary contains Node.js runtime, src/, package.json, and all app logic COPY dashback/dist/dashback ./dist/dashback -# Native C++ addons and dependencies needed by the OS dynamic linker (dlopen) -COPY dashback/node_modules ./node_modules +# Only native C++ addon packages required by OS dynamic linker (dlopen) +COPY dashback/node_modules/better-sqlite3 ./node_modules/better-sqlite3 +COPY dashback/node_modules/canvas ./node_modules/canvas +COPY dashback/node_modules/@img ./node_modules/@img +COPY dashback/node_modules/sharp ./node_modules/sharp # Set production defaults: port 4202 ENV NODE_ENV=production \ diff --git a/docker-compose.yml b/docker-compose.yml index 5e20bcf..db695ec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ services: read_only: true user: "${UID:-1000}:${GID:-1000}" tmpfs: - - /tmp:rw,noexec,nosuid,size=64m + - /tmp:rw,noexec,nosuid,size=512m - /run:rw,noexec,nosuid,size=16m networks: - dash_net diff --git a/scripts/buildSea.js b/scripts/buildSea.js index 8ae4384..0781cb8 100644 --- a/scripts/buildSea.js +++ b/scripts/buildSea.js @@ -11,40 +11,88 @@ const srcDir = path.join(rootDir, 'src'); const configPath = path.join(rootDir, 'sea-config.json'); const payloadPath = path.join(distDir, 'app-payload.bin'); -// Ensure dist directory exists +const dbschemaDir = path.resolve(rootDir, '../dashdbschema'); +const llmorbiterDir = path.resolve(rootDir, '../llmorbiter'); + if (!fs.existsSync(distDir)) { fs.mkdirSync(distDir, { recursive: true }); } -console.log('Packaging application code into SEA payload...'); +console.log('Packaging application, siblings, and JavaScript dependencies into SEA payload...'); -function packDirectory(dir, baseDir = dir) { - const result = {}; - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const full = path.join(dir, entry.name); - const rel = path.relative(baseDir, full); - if (entry.isDirectory()) { - Object.assign(result, packDirectory(full, baseDir)); - } else if (entry.isFile()) { - result[rel] = fs.readFileSync(full).toString('base64'); +// Native C++ addon folders that will be loaded from the host filesystem via dlopen +const NATIVE_ADDON_PKGS = new Set(['better-sqlite3', 'canvas', 'sharp', '@img']); + +function packDirectory(dir, prefix, result) { + if (!fs.existsSync(dir)) return; + try { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + const rel = path.join(prefix, entry.name); + if (entry.isDirectory()) { + if (entry.name === 'test' || entry.name === 'tests' || entry.name === 'docs' || entry.name === '.github' || entry.name === '.git') continue; + packDirectory(full, rel, result); + } else if (entry.isFile()) { + // Ignore compiled native binary extensions which are linked from the host + if (entry.name.endsWith('.node') || entry.name.endsWith('.so') || entry.name.endsWith('.dylib') || entry.name.endsWith('.dll') || entry.name.endsWith('.a')) continue; + result[rel] = fs.readFileSync(full).toString('base64'); + } } - } - return result; + } catch {} } -// Pack src/ and package.json -const payload = packDirectory(srcDir); +const payload = {}; + +// 1. Pack dashback src and package.json +packDirectory(srcDir, 'src', payload); const pkgPath = path.join(rootDir, 'package.json'); if (fs.existsSync(pkgPath)) { payload['package.json'] = fs.readFileSync(pkgPath).toString('base64'); } +// 2. Pack sibling dependencies (libdashstore and libllmorbiter) directly into node_modules +if (fs.existsSync(path.join(dbschemaDir, 'src'))) { + packDirectory(path.join(dbschemaDir, 'src'), 'node_modules/libdashstore/src', payload); + const dbschemaPkg = path.join(dbschemaDir, 'package.json'); + if (fs.existsSync(dbschemaPkg)) { + payload['node_modules/libdashstore/package.json'] = fs.readFileSync(dbschemaPkg).toString('base64'); + } +} + +if (fs.existsSync(path.join(llmorbiterDir, 'src'))) { + packDirectory(path.join(llmorbiterDir, 'src'), 'node_modules/libllmorbiter/src', payload); + const llmorbiterPkg = path.join(llmorbiterDir, 'package.json'); + if (fs.existsSync(llmorbiterPkg)) { + payload['node_modules/libllmorbiter/package.json'] = fs.readFileSync(llmorbiterPkg).toString('base64'); + } +} + +// 3. Pack all non-native node_modules dependencies +function packNodeModules(nmPath) { + if (!fs.existsSync(nmPath)) return; + for (const name of fs.readdirSync(nmPath)) { + if (NATIVE_ADDON_PKGS.has(name) || name.startsWith('.') || name === 'libdashstore' || name === 'libllmorbiter') continue; + const pkgDir = path.join(nmPath, name); + if (name.startsWith('@')) { + for (const sub of fs.readdirSync(pkgDir)) { + if (NATIVE_ADDON_PKGS.has(sub) || NATIVE_ADDON_PKGS.has(`${name}/${sub}`)) continue; + packDirectory(path.join(pkgDir, sub), `node_modules/${name}/${sub}`, payload); + } + } else { + packDirectory(pkgDir, `node_modules/${name}`, payload); + } + } +} + +packNodeModules(path.join(rootDir, 'node_modules')); +packNodeModules(path.join(dbschemaDir, 'node_modules')); + const compressedPayload = zlib.deflateSync(Buffer.from(JSON.stringify(payload))); fs.writeFileSync(payloadPath, compressedPayload); console.log(`Packed ${Object.keys(payload).length} files (${(compressedPayload.length / 1024 / 1024).toFixed(2)} MB compressed)`); -// Prepare sea-config.json with payload asset +// Prepare sea-config.json const config = { main: 'src/sea-entry.cjs', output: 'dist/dashback', diff --git a/src/sea-entry.cjs b/src/sea-entry.cjs index 15f4087..841923c 100644 --- a/src/sea-entry.cjs +++ b/src/sea-entry.cjs @@ -2,9 +2,8 @@ * SEA Entry point for dashback. * Embedded inside the Node.js single executable binary. * - * When built with embedded assets, it unpacks the bundled src/ and package.json - * to a runtime directory and links the host node_modules, so native C++ addons - * and dependencies resolve seamlessly without needing src/ in the filesystem. + * Unpacks the bundled application, sibling packages, and JavaScript dependencies + * to a runtime directory, and links only the native C++ addons from the host. */ const sea = require('node:sea'); const fs = require('node:fs'); @@ -17,28 +16,36 @@ function getProjectRoot() { // If embedded application payload is present in SEA if (sea.isSea && sea.isSea() && sea.getAssetKeys && sea.getAssetKeys().includes('app-payload.bin')) { const runtimeDir = process.env.DASHBACK_RUNTIME_DIR || path.join(os.tmpdir(), 'dashback-runtime'); + const markerFile = path.join(runtimeDir, '.unpacked'); - const compressed = Buffer.from(sea.getRawAsset('app-payload.bin')); - const json = zlib.inflateSync(compressed).toString('utf8'); - const files = JSON.parse(json); + if (!fs.existsSync(markerFile)) { + const compressed = Buffer.from(sea.getRawAsset('app-payload.bin')); + const json = zlib.inflateSync(compressed).toString('utf8'); + const files = JSON.parse(json); - for (const [relPath, b64] of Object.entries(files)) { - const targetFile = relPath === 'package.json' - ? path.join(runtimeDir, 'package.json') - : path.join(runtimeDir, 'src', relPath); + for (const [relPath, b64] of Object.entries(files)) { + const targetFile = path.join(runtimeDir, relPath); + fs.mkdirSync(path.dirname(targetFile), { recursive: true }); + fs.writeFileSync(targetFile, Buffer.from(b64, 'base64')); + } - fs.mkdirSync(path.dirname(targetFile), { recursive: true }); - fs.writeFileSync(targetFile, Buffer.from(b64, 'base64')); + fs.writeFileSync(markerFile, '1'); } - // Link host node_modules so dependencies & native addons resolve + // Link host native addons into runtime node_modules const hostNodeModules = path.resolve(process.cwd(), 'node_modules'); const runtimeNodeModules = path.join(runtimeDir, 'node_modules'); - if (fs.existsSync(hostNodeModules) && !fs.existsSync(runtimeNodeModules)) { - try { - fs.symlinkSync(hostNodeModules, runtimeNodeModules, 'junction'); - } catch { - // Ignore symlink failure if already exists + fs.mkdirSync(runtimeNodeModules, { recursive: true }); + + const NATIVE_ADDON_DIRS = ['better-sqlite3', 'canvas', 'sharp', '@img']; + for (const addon of NATIVE_ADDON_DIRS) { + const hostAddon = path.join(hostNodeModules, addon); + const targetAddon = path.join(runtimeNodeModules, addon); + if (fs.existsSync(hostAddon) && !fs.existsSync(targetAddon)) { + try { + fs.mkdirSync(path.dirname(targetAddon), { recursive: true }); + fs.symlinkSync(hostAddon, targetAddon, 'junction'); + } catch {} } }