diff --git a/Dockerfile b/Dockerfile index 9cfa189..5934831 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1 FROM debian:12-slim AS syslibs RUN apt-get update && apt-get install -y --no-install-recommends \ - libatomic1 libuuid1 libfontconfig1 libxml2 libfribidi0 \ + libatomic1 libuuid1 libfontconfig1 libfribidi0 \ && rm -rf /var/lib/apt/lists/* FROM gcr.io/distroless/cc-debian12 @@ -12,7 +12,6 @@ WORKDIR /app COPY --from=syslibs /usr/lib/x86_64-linux-gnu/libatomic.so* /usr/lib/x86_64-linux-gnu/ COPY --from=syslibs /usr/lib/x86_64-linux-gnu/libuuid.so* /usr/lib/x86_64-linux-gnu/ COPY --from=syslibs /usr/lib/x86_64-linux-gnu/libfontconfig.so* /usr/lib/x86_64-linux-gnu/ -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/ # Set production defaults: port 4208 diff --git a/scripts/buildSea.js b/scripts/buildSea.js index 2008fcf..72068e6 100644 --- a/scripts/buildSea.js +++ b/scripts/buildSea.js @@ -18,7 +18,55 @@ if (!fs.existsSync(distDir)) { fs.mkdirSync(distDir, { recursive: true }); } -console.log('Packaging application, siblings, and dependencies into SEA payload...'); +console.log('Packaging application, siblings, and JavaScript dependencies into SEA payload (excluding native binaries)...'); + +const nativeNodes = {}; + +function findNearestPackageJson(startDir) { + let dir = startDir; + while (dir && dir !== path.dirname(dir)) { + const pkgFile = path.join(dir, 'package.json'); + if (fs.existsSync(pkgFile)) { + try { + return JSON.parse(fs.readFileSync(pkgFile, 'utf8')); + } catch {} + } + dir = path.dirname(dir); + } + return null; +} + +function getVersionedNodeFilename(fullPath) { + const base = path.basename(fullPath, '.node'); + const pkg = findNearestPackageJson(path.dirname(fullPath)); + if (!pkg || !pkg.name || !pkg.version) { + return `${base}.node`; + } + const shortName = pkg.name.replace(/^@[^/]+\//, ''); + if (base.includes(pkg.version)) { + return `${base}.node`; + } + if (base === shortName) { + return `${shortName}-${process.platform}-${process.arch}-${pkg.version}.node`; + } + return `${shortName}-${base}-${pkg.version}.node`; +} + +function isNativeBinary(name) { + return ( + name.endsWith('.node') || + name.endsWith('.so') || + name.includes('.so.') || + name.endsWith('.dylib') || + name.endsWith('.dll') || + name.endsWith('.a') || + name.endsWith('.o') || + name.endsWith('.cc') || + name.endsWith('.cpp') || + name.endsWith('.c') || + name.endsWith('.h') + ); +} function packDirectory(dir, prefix, result) { if (!fs.existsSync(dir)) return; @@ -30,7 +78,11 @@ function packDirectory(dir, prefix, result) { if (entry.name === 'test' || entry.name === 'tests' || entry.name === 'docs' || entry.name === '.github' || entry.name === '.git' || entry.name === 'obj.target') continue; packDirectory(full, rel, result); } else if (entry.isFile()) { - if (entry.name.endsWith('.a') || entry.name.endsWith('.o') || entry.name.endsWith('.cc') || entry.name.endsWith('.cpp') || entry.name.endsWith('.c') || entry.name.endsWith('.h')) continue; + if (entry.name.endsWith('.node')) { + nativeNodes[rel] = getVersionedNodeFilename(full); + continue; + } + if (isNativeBinary(entry.name)) continue; result[rel] = fs.readFileSync(full).toString('base64'); } } @@ -82,6 +134,8 @@ function packNodeModules(nmPath) { packNodeModules(path.join(rootDir, 'node_modules')); packNodeModules(path.join(dbschemaDir, 'node_modules')); +payload['.native-nodes.json'] = Buffer.from(JSON.stringify(nativeNodes)).toString('base64'); + const compressedPayload = zlib.deflateSync(Buffer.from(JSON.stringify(payload))); fs.writeFileSync(payloadPath, compressedPayload); diff --git a/src/api/index.js b/src/api/index.js index 07f8240..adc91d3 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -316,13 +316,9 @@ export async function runSelfTest() { apiLog.info(`Config: host=${env.host} port=${env.port} dbProvider=${env.dbProvider}`); // 2. Test database & store initialization - try { - await ensureConnection(); - const schemaVer = await getSchemaVersion(); - apiLog.info(`Database & store: OK (provider: ${env.dbProvider}, schema v${schemaVer})`); - } catch (err) { - apiLog.warn(`Database connection warning: ${err.message}`); - } + await ensureConnection(); + const schemaVer = await getSchemaVersion(); + apiLog.info(`Database & store: OK (provider: ${env.dbProvider}, schema v${schemaVer})`); // 3. Build API app and register all route modules const app = await buildApiApp(); diff --git a/src/sea-entry.cjs b/src/sea-entry.cjs index f9b8596..5365c9b 100644 --- a/src/sea-entry.cjs +++ b/src/sea-entry.cjs @@ -15,23 +15,65 @@ const { createRequire } = require('node:module'); function getProjectRoot() { // If embedded application payload is present in SEA if (sea.isSea && sea.isSea() && sea.getAssetKeys && sea.getAssetKeys().includes('app-payload.bin')) { + const distDir = path.dirname(process.execPath); const runtimeDir = process.env.DASHBACK_RUNTIME_DIR || path.join(os.tmpdir(), 'dashback-runtime'); const markerFile = path.join(runtimeDir, '.unpacked'); + const nativeMapFile = path.join(runtimeDir, '.native-nodes.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); + const nativeNodes = files['.native-nodes.json'] + ? JSON.parse(Buffer.from(files['.native-nodes.json'], 'base64').toString('utf8')) + : {}; for (const [relPath, b64] of Object.entries(files)) { + if (relPath === '.native-nodes.json') continue; const targetFile = path.join(runtimeDir, relPath); fs.mkdirSync(path.dirname(targetFile), { recursive: true }); fs.writeFileSync(targetFile, Buffer.from(b64, 'base64')); } + for (const [relPath, distFileName] of Object.entries(nativeNodes)) { + const targetLink = path.join(runtimeDir, relPath); + const distTarget = path.join(distDir, distFileName); + fs.mkdirSync(path.dirname(targetLink), { recursive: true }); + try { + if (fs.lstatSync(targetLink)) fs.unlinkSync(targetLink); + } catch {} + try { + fs.symlinkSync(distTarget, targetLink, 'file'); + } catch {} + } + + fs.writeFileSync(nativeMapFile, JSON.stringify(nativeNodes)); fs.writeFileSync(markerFile, '1'); } + const nativeNodes = fs.existsSync(nativeMapFile) + ? JSON.parse(fs.readFileSync(nativeMapFile, 'utf8')) + : {}; + + // Redirect native .node loading to distDir and preload libvips from distDir if present + const origDlopen = process.dlopen; + process.dlopen = function (mod, filename, flags) { + const relFromRuntime = path.relative(runtimeDir, filename); + const mappedName = nativeNodes[relFromRuntime] || path.basename(filename); + const distCandidate = path.join(distDir, mappedName); + const target = fs.existsSync(distCandidate) ? distCandidate : filename; + if (mappedName.startsWith('sharp-')) { + const vipsPath = path.join(distDir, 'libvips-cpp.so.8.18.7'); + if (fs.existsSync(vipsPath)) { + try { + origDlopen.call(process, { exports: {} }, vipsPath, os.constants.dlopen.RTLD_NOW | os.constants.dlopen.RTLD_GLOBAL); + } catch {} + } + } + return flags !== undefined ? origDlopen.call(process, mod, target, flags) : origDlopen.call(process, mod, target); + }; + // Link host .env if present so config/env.js can find it const hostEnv = path.resolve(process.cwd(), '.env'); const runtimeEnv = path.join(runtimeDir, '.env');