Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,17 @@
"setup:mingit": "node scripts/setup-mingit.js",
"setup:python-runtime": "node scripts/setup-python-runtime.js",
"openclaw:ensure": "node scripts/ensure-openclaw-version.cjs",
"openclaw:patch": "node scripts/apply-openclaw-patches.cjs",
"openclaw:plugins": "node scripts/ensure-openclaw-plugins.cjs",
"openclaw:bundle": "node scripts/bundle-openclaw-gateway.cjs",
"openclaw:precompile": "node scripts/precompile-openclaw-extensions.cjs",
"openclaw:runtime:host": "node scripts/openclaw-runtime-host.cjs",
"openclaw:runtime:mac-arm64": "npm run openclaw:ensure && node scripts/run-build-openclaw-runtime.cjs mac-arm64 && node scripts/sync-openclaw-runtime-current.cjs mac-arm64 && npm run openclaw:plugins",
"openclaw:runtime:mac-x64": "npm run openclaw:ensure && node scripts/run-build-openclaw-runtime.cjs mac-x64 && node scripts/sync-openclaw-runtime-current.cjs mac-x64 && npm run openclaw:plugins",
"openclaw:runtime:win-x64": "npm run openclaw:ensure && node scripts/run-build-openclaw-runtime.cjs win-x64 && node scripts/sync-openclaw-runtime-current.cjs win-x64 && npm run openclaw:plugins",
"openclaw:runtime:win-arm64": "npm run openclaw:ensure && node scripts/run-build-openclaw-runtime.cjs win-arm64 && node scripts/sync-openclaw-runtime-current.cjs win-arm64 && npm run openclaw:plugins",
"openclaw:runtime:linux-x64": "npm run openclaw:ensure && node scripts/run-build-openclaw-runtime.cjs linux-x64 && node scripts/sync-openclaw-runtime-current.cjs linux-x64 && npm run openclaw:plugins",
"openclaw:runtime:linux-arm64": "npm run openclaw:ensure && node scripts/run-build-openclaw-runtime.cjs linux-arm64 && node scripts/sync-openclaw-runtime-current.cjs linux-arm64 && npm run openclaw:plugins",
"openclaw:runtime:mac-arm64": "npm run openclaw:ensure && npm run openclaw:patch && node scripts/run-build-openclaw-runtime.cjs mac-arm64 && node scripts/sync-openclaw-runtime-current.cjs mac-arm64 && npm run openclaw:bundle && npm run openclaw:plugins && npm run openclaw:precompile",
"openclaw:runtime:mac-x64": "npm run openclaw:ensure && npm run openclaw:patch && node scripts/run-build-openclaw-runtime.cjs mac-x64 && node scripts/sync-openclaw-runtime-current.cjs mac-x64 && npm run openclaw:bundle && npm run openclaw:plugins && npm run openclaw:precompile",
"openclaw:runtime:win-x64": "npm run openclaw:ensure && npm run openclaw:patch && node scripts/run-build-openclaw-runtime.cjs win-x64 && node scripts/sync-openclaw-runtime-current.cjs win-x64 && npm run openclaw:bundle && npm run openclaw:plugins && npm run openclaw:precompile",
"openclaw:runtime:win-arm64": "npm run openclaw:ensure && npm run openclaw:patch && node scripts/run-build-openclaw-runtime.cjs win-arm64 && node scripts/sync-openclaw-runtime-current.cjs win-arm64 && npm run openclaw:bundle && npm run openclaw:plugins && npm run openclaw:precompile",
"openclaw:runtime:linux-x64": "npm run openclaw:ensure && npm run openclaw:patch && node scripts/run-build-openclaw-runtime.cjs linux-x64 && node scripts/sync-openclaw-runtime-current.cjs linux-x64 && npm run openclaw:bundle && npm run openclaw:plugins && npm run openclaw:precompile",
"openclaw:runtime:linux-arm64": "npm run openclaw:ensure && npm run openclaw:patch && node scripts/run-build-openclaw-runtime.cjs linux-arm64 && node scripts/sync-openclaw-runtime-current.cjs linux-arm64 && npm run openclaw:bundle && npm run openclaw:plugins && npm run openclaw:precompile",
"regenerate:icon": "bash scripts/regenerate-mac-icon.sh",
"fix:mac-icon": "bash scripts/fix-mac-icon-display.sh"
},
Expand All @@ -92,7 +95,6 @@
"@types/uuid": "^10.0.0",
"bufferutil": "^4.1.0",
"cron-parser": "^5.5.0",

"dompurify": "^3.3.1",
"electron-log": "^5.4.3",
"extract-zip": "^2.0.1",
Expand Down Expand Up @@ -141,6 +143,7 @@
"cross-env": "^7.0.3",
"electron": "40.2.1",
"electron-builder": "^24.12.0",
"esbuild": "^0.21.5",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
Expand Down
132 changes: 132 additions & 0 deletions scripts/apply-openclaw-patches.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
'use strict';

/**
* Apply LobsterAI patches to the openclaw source tree.
*
* These patches add a dedicated gateway entry point that skips the full CLI
* infrastructure, dramatically reducing startup time inside Electron's
* utilityProcess (~15s instead of ~120s).
*
* Usage:
* node scripts/apply-openclaw-patches.cjs [openclaw-src-dir]
*
* If openclaw-src-dir is not specified, defaults to ../openclaw relative to
* the LobsterAI project root.
*
* Safe to run multiple times — already-applied patches are skipped.
*/

const { execFileSync } = require('child_process');
const fs = require('fs');
const path = require('path');

const rootDir = path.resolve(__dirname, '..');
const openclawSrc = process.argv[2]
? path.resolve(process.argv[2])
: path.resolve(rootDir, '..', 'openclaw');

const patchesDir = path.join(rootDir, 'scripts', 'patches');

if (!fs.existsSync(openclawSrc)) {
console.error(`[apply-openclaw-patches] openclaw source not found: ${openclawSrc}`);
process.exit(1);
}

if (!fs.existsSync(path.join(openclawSrc, 'package.json'))) {
console.error(`[apply-openclaw-patches] Not an openclaw project: ${openclawSrc}`);
process.exit(1);
}

const patchFiles = fs.readdirSync(patchesDir)
.filter(f => f.endsWith('.patch'))
.sort();

if (patchFiles.length === 0) {
console.log('[apply-openclaw-patches] No patches found, nothing to do.');
process.exit(0);
}

let applied = 0;
let skipped = 0;

for (const patchFile of patchFiles) {
const patchPath = path.join(patchesDir, patchFile);

// Check if patch is already applied.
//
// Strategy:
// 1. Try `git apply --check --reverse` — if it succeeds the patch is applied.
// 2. Try `git apply --check` (forward) — if it succeeds the patch is NOT applied.
// 3. If BOTH fail, the patch is partially/fully applied (e.g. new files already
// exist and modified hunks already match). Treat as already applied.
//
// This avoids fragile regex parsing of patch contents and works regardless of
// line-ending differences (CRLF vs LF).

let reverseOk = false;
try {
execFileSync('git', ['apply', '--check', '--reverse', patchPath], {
cwd: openclawSrc,
stdio: 'pipe',
});
reverseOk = true;
} catch {
// reverse check failed — patch may or may not be applied
}

if (reverseOk) {
console.log(`[apply-openclaw-patches] Already applied: ${patchFile}`);
skipped++;
continue;
}

// Try forward apply check.
let forwardErr = null;
try {
execFileSync('git', ['apply', '--check', patchPath], {
cwd: openclawSrc,
stdio: 'pipe',
});
} catch (err) {
forwardErr = err;
}

if (forwardErr) {
// Both reverse and forward checks failed. This typically means the patch
// is already applied but git can't cleanly reverse it (e.g. new files are
// untracked, or the working tree has the changes but they aren't committed).
const stderr = forwardErr.stderr ? forwardErr.stderr.toString() : '';
const alreadyExists = stderr.includes('already exists in working directory');
const patchDoesNotApply = stderr.includes('patch does not apply');

if (alreadyExists || patchDoesNotApply) {
console.log(`[apply-openclaw-patches] Already applied (forward check confirms): ${patchFile}`);
skipped++;
continue;
}

// Genuinely cannot apply — report error.
console.error(`[apply-openclaw-patches] Patch does not apply cleanly: ${patchFile}`);
console.error(`[apply-openclaw-patches] This usually means the openclaw version has changed.`);
console.error(`[apply-openclaw-patches] Regenerate patches or update to match the new source.`);
if (stderr) console.error(stderr);
process.exit(1);
}

// Apply the patch.
try {
execFileSync('git', ['apply', patchPath], {
cwd: openclawSrc,
stdio: 'pipe',
});
console.log(`[apply-openclaw-patches] Applied: ${patchFile}`);
applied++;
} catch (err) {
console.error(`[apply-openclaw-patches] Failed to apply: ${patchFile}`);
const stderr = err.stderr ? err.stderr.toString() : '';
if (stderr) console.error(stderr);
process.exit(1);
}
}

console.log(`[apply-openclaw-patches] Done. Applied: ${applied}, Skipped (already applied): ${skipped}`);
25 changes: 22 additions & 3 deletions scripts/build-openclaw-runtime.sh
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ try {
READVER
)

# Compute a fingerprint of all patch files so the build is invalidated when patches change.
PATCHES_DIR="$ELECTRON_ROOT/scripts/patches"
PATCH_HASH=""
if [[ -d "$PATCHES_DIR" ]]; then
PATCH_HASH=$(cat "$PATCHES_DIR"/*.patch 2>/dev/null | sha256sum | cut -d' ' -f1)
fi

if [[ -n "$DESIRED_VERSION" && "${OPENCLAW_FORCE_BUILD:-}" != "1" ]]; then
BUILD_INFO="$OUT_DIR/runtime-build-info.json"
if [[ -f "$BUILD_INFO" ]]; then
Expand All @@ -101,11 +108,21 @@ try {
} catch {}
READBI
)
if [[ "$BUILT_VERSION" == "$DESIRED_VERSION" ]]; then
echo "[openclaw-runtime] Already built for $DESIRED_VERSION (target=$TARGET_ID), skipping."
BUILT_PATCH_HASH=$(node - "$BUILD_INFO" <<'READPH'
try {
const info = require(process.argv[2]);
console.log(info.patchHash || '');
} catch {}
READPH
)
if [[ "$BUILT_VERSION" == "$DESIRED_VERSION" && "$BUILT_PATCH_HASH" == "$PATCH_HASH" ]]; then
echo "[openclaw-runtime] Already built for $DESIRED_VERSION (target=$TARGET_ID, patchHash=${PATCH_HASH:0:12}…), skipping."
echo "[openclaw-runtime] Use OPENCLAW_FORCE_BUILD=1 to force rebuild."
exit 0
fi
if [[ "$BUILT_VERSION" == "$DESIRED_VERSION" && "$BUILT_PATCH_HASH" != "$PATCH_HASH" ]]; then
echo "[openclaw-runtime] Patches changed (was=${BUILT_PATCH_HASH:0:12}…, now=${PATCH_HASH:0:12}…), rebuilding."
fi
fi
echo "[openclaw-runtime] Pinned version: $DESIRED_VERSION (current build: ${BUILT_VERSION:-none})"
fi
Expand Down Expand Up @@ -146,14 +163,15 @@ cp -R "$PKG_DIR" "$OUT_DIR"

# Save build metadata for traceability.
# Use `node -` so stdin is treated as script and the following args remain user args.
node - "$OUT_DIR" "$OPENCLAW_SRC" "$TARGET_ID" "$ELECTRON_ROOT" <<'NODE'
node - "$OUT_DIR" "$OPENCLAW_SRC" "$TARGET_ID" "$ELECTRON_ROOT" "$PATCH_HASH" <<'NODE'
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const outDir = process.argv[2];
const src = process.argv[3];
const target = process.argv[4];
const electronRoot = process.argv[5];
const patchHash = process.argv[6] || '';

// Read pinned version from package.json
let openclawVersion = '';
Expand All @@ -178,6 +196,7 @@ const meta = {
target,
openclawVersion,
openclawCommit,
patchHash,
};
fs.writeFileSync(path.join(outDir, 'runtime-build-info.json'), JSON.stringify(meta, null, 2) + '\n');
NODE
Expand Down
133 changes: 133 additions & 0 deletions scripts/bundle-openclaw-gateway.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
'use strict';

/**
* Bundle the openclaw gateway entry point into a single file using esbuild.
*
* This eliminates the expensive ESM module resolution overhead (~1100 files)
* that causes Electron's utilityProcess to take 80-100s to start the gateway.
* The single-file bundle loads in ~2-12s instead.
*
* Usage:
* node scripts/bundle-openclaw-gateway.cjs [runtime-dir]
*
* If runtime-dir is not specified, defaults to vendor/openclaw-runtime/current.
*/

const fs = require('fs');
const path = require('path');

const rootDir = path.resolve(__dirname, '..');
const runtimeDir = process.argv[2]
? path.resolve(process.argv[2])
: path.join(rootDir, 'vendor', 'openclaw-runtime', 'current');

const bundleOutPath = path.join(runtimeDir, 'gateway-bundle.mjs');

// Prefer gateway-entry.js (dedicated gateway entry, skips CLI overhead).
// Fall back to entry.js (full CLI entry) if gateway-entry.js doesn't exist.
const gatewayEntryPath = path.join(runtimeDir, 'dist', 'gateway-entry.js');
const fullEntryPath = path.join(runtimeDir, 'dist', 'entry.js');
const entryPath = fs.existsSync(gatewayEntryPath) ? gatewayEntryPath : fullEntryPath;

if (!fs.existsSync(entryPath)) {
console.error(`[bundle-openclaw-gateway] Entry point not found: ${entryPath}`);
console.error(`[bundle-openclaw-gateway] Make sure the openclaw runtime is built first.`);
process.exit(1);
}

// Skip if bundle is already up-to-date (newer than the entry point).
if (fs.existsSync(bundleOutPath)) {
const bundleStat = fs.statSync(bundleOutPath);
const entryStat = fs.statSync(entryPath);
if (bundleStat.mtimeMs > entryStat.mtimeMs) {
console.log(`[bundle-openclaw-gateway] Bundle is up-to-date, skipping.`);
process.exit(0);
}
}

console.log(`[bundle-openclaw-gateway] Bundling: ${path.relative(runtimeDir, entryPath)}`);
console.log(`[bundle-openclaw-gateway] Output: ${path.relative(runtimeDir, bundleOutPath)}`);

// Native addons and heavy optional deps that must NOT be bundled.
// These are resolved at runtime from node_modules/.
const EXTERNAL_PACKAGES = [
// Native image processing
'sharp', '@img/*',
// Native terminal
'@lydell/*',
// Native clipboard
'@mariozechner/*',
// Native canvas
'@napi-rs/*',
// Native audio (davey)
'@snazzah/*',
// Native FFI
'koffi',
// Electron (provided by host)
'electron',
// LLM runtime (large, optional)
'node-llama-cpp',
// FFmpeg binary (large, optional)
'ffmpeg-static',
// Browser automation (large, optional)
'chromium-bidi', 'playwright-core', 'playwright',
// Native SQLite
'better-sqlite3',
// TypeScript runtime compiler — uses dynamic require("../dist/babel.cjs")
// that esbuild can't rewrite correctly (resolves relative to bundle instead
// of the original jiti module location).
'jiti',
];

let esbuild;
try {
esbuild = require('esbuild');
} catch {
console.error('[bundle-openclaw-gateway] esbuild not found. Run: npm install --save-dev esbuild');
process.exit(1);
}

const t0 = Date.now();

esbuild
.build({
entryPoints: [entryPath],
bundle: true,
platform: 'node',
format: 'esm',
outfile: bundleOutPath,
external: EXTERNAL_PACKAGES,
// Inject createRequire so that esbuild's __require shim works in ESM context.
// Without this, CJS modules (e.g. @smithy/*) that call require("buffer")
// fail with "Dynamic require of X is not supported" when loaded via import().
banner: {
js: `import { createRequire as __bundleCreateRequire } from 'node:module';\n` +
`import { fileURLToPath as __bundleFileURLToPath } from 'node:url';\n` +
`const require = __bundleCreateRequire(import.meta.url);\n` +
`const __filename = __bundleFileURLToPath(import.meta.url);\n` +
`const __dirname = __bundleFileURLToPath(new URL('.', import.meta.url));\n`,
},
// Silence warnings about __dirname/__filename in ESM (they're polyfilled above).
logLevel: 'warning',
})
.then((result) => {
const elapsed = Date.now() - t0;
const sizeKB = Math.round(fs.statSync(bundleOutPath).size / 1024);
console.log(
`[bundle-openclaw-gateway] Done in ${elapsed}ms (${sizeKB} KB)` +
(result.warnings.length ? `, ${result.warnings.length} warnings` : ''),
);

// Copy the compile-cache warmup script alongside the bundle so it's
// available in the packaged app (resources/cfmind/warmup-compile-cache.cjs).
const warmupSrc = path.join(rootDir, 'scripts', 'warmup-compile-cache.cjs');
const warmupDst = path.join(runtimeDir, 'warmup-compile-cache.cjs');
if (fs.existsSync(warmupSrc)) {
fs.copyFileSync(warmupSrc, warmupDst);
console.log(`[bundle-openclaw-gateway] Copied warmup-compile-cache.cjs to ${warmupDst}`);
}
})
.catch((err) => {
console.error('[bundle-openclaw-gateway] esbuild failed:', err.message || err);
process.exit(1);
});
3 changes: 2 additions & 1 deletion scripts/electron-builder-hooks.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ function ensureBundledOpenClawRuntime(context) {
if (existsSync(gatewayAsarPath)) {
let entries;
try {
entries = new Set(asar.listPackage(gatewayAsarPath));
// Normalize paths: on Windows, asar.listPackage may return backslash paths
entries = new Set(asar.listPackage(gatewayAsarPath).map(e => e.replace(/\\/g, '/')));
} catch (error) {
throw new Error(
'[electron-builder-hooks] Failed to read OpenClaw gateway.asar: '
Expand Down
Loading