diff --git a/openclaw.mjs b/openclaw.mjs index b280daed5c9..1ceda9c2246 100755 --- a/openclaw.mjs +++ b/openclaw.mjs @@ -1,9 +1,11 @@ #!/usr/bin/env node import { spawnSync } from "node:child_process"; -import { existsSync, readFileSync } from "node:fs"; +import { existsSync, readFileSync, statSync } from "node:fs"; import { access } from "node:fs/promises"; import module from "node:module"; +import os from "node:os"; +import path from "node:path"; import { fileURLToPath } from "node:url"; const MIN_NODE_MAJOR = 22; @@ -46,6 +48,41 @@ const isSourceCheckoutLauncher = () => const isNodeCompileCacheDisabled = () => process.env.NODE_DISABLE_COMPILE_CACHE !== undefined; const isNodeCompileCacheRequested = () => process.env.NODE_COMPILE_CACHE !== undefined && !isNodeCompileCacheDisabled(); +const sanitizeCompileCachePathSegment = (value) => { + const normalized = value.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, ""); + return normalized.length > 0 ? normalized : "unknown"; +}; +const readPackageVersion = () => { + try { + const parsed = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8")); + if (typeof parsed?.version === "string" && parsed.version.trim().length > 0) { + return parsed.version; + } + } catch { + // Fall through to an install-metadata-only cache key. + } + return "unknown"; +}; +const resolvePackagedCompileCacheDirectory = () => { + const packageJsonUrl = new URL("./package.json", import.meta.url); + const version = sanitizeCompileCachePathSegment(readPackageVersion()); + let installMarker = "no-package-json"; + try { + const stat = statSync(packageJsonUrl); + installMarker = `${Math.trunc(stat.mtimeMs)}-${stat.size}`; + } catch { + // Package archives should always have package.json, but keep startup best-effort. + } + const baseDirectory = isNodeCompileCacheRequested() + ? process.env.NODE_COMPILE_CACHE + : path.join(os.tmpdir(), "node-compile-cache"); + return path.join( + baseDirectory, + "openclaw", + version, + sanitizeCompileCachePathSegment(installMarker), + ); +}; const respawnWithoutCompileCacheIfNeeded = () => { if (!isSourceCheckoutLauncher()) { @@ -79,10 +116,46 @@ const respawnWithoutCompileCacheIfNeeded = () => { respawnWithoutCompileCacheIfNeeded(); +const respawnWithPackagedCompileCacheIfNeeded = () => { + if (isSourceCheckoutLauncher() || isNodeCompileCacheDisabled()) { + return false; + } + if (process.env.OPENCLAW_PACKAGED_COMPILE_CACHE_RESPAWNED === "1") { + return false; + } + const currentDirectory = module.getCompileCacheDir?.(); + if (!currentDirectory) { + return false; + } + const desiredDirectory = resolvePackagedCompileCacheDirectory(); + if (path.resolve(currentDirectory) === path.resolve(desiredDirectory)) { + return false; + } + const env = { + ...process.env, + NODE_COMPILE_CACHE: desiredDirectory, + OPENCLAW_PACKAGED_COMPILE_CACHE_RESPAWNED: "1", + }; + const result = spawnSync( + process.execPath, + [...process.execArgv, fileURLToPath(import.meta.url), ...process.argv.slice(2)], + { + stdio: "inherit", + env, + }, + ); + if (result.error) { + throw result.error; + } + process.exit(result.status ?? 1); +}; + +respawnWithPackagedCompileCacheIfNeeded(); + // https://nodejs.org/api/module.html#module-compile-cache if (module.enableCompileCache && !isNodeCompileCacheDisabled() && !isSourceCheckoutLauncher()) { try { - module.enableCompileCache(); + module.enableCompileCache(resolvePackagedCompileCacheDirectory()); } catch { // Ignore errors } diff --git a/scripts/e2e/parallels-npm-update-smoke.sh b/scripts/e2e/parallels-npm-update-smoke.sh index 503120c2efe..222662e30f3 100755 --- a/scripts/e2e/parallels-npm-update-smoke.sh +++ b/scripts/e2e/parallels-npm-update-smoke.sh @@ -1078,7 +1078,7 @@ gateway_listener_ready() { gateway_log_ready() { latest="\$(/bin/ls -t /tmp/openclaw/openclaw-*.log 2>/dev/null | /usr/bin/head -n 1 || true)" [ -n "\$latest" ] || return 1 - /usr/bin/tail -n 160 "\$latest" | /usr/bin/grep -q 'ready (' + /usr/bin/tail -n 160 "\$latest" | /usr/bin/grep -Eq 'ready( \(|[[:space:]]*\$)' } gateway_smoke_ready() { gateway_listener_ready && gateway_log_ready @@ -1682,7 +1682,7 @@ gateway_listener_ready() { gateway_log_ready() { latest="\$(/bin/ls -t /tmp/openclaw/openclaw-*.log 2>/dev/null | /usr/bin/head -n 1 || true)" [ -n "\$latest" ] || return 1 - /usr/bin/tail -n 160 "\$latest" | /usr/bin/grep -q 'ready (' + /usr/bin/tail -n 160 "\$latest" | /usr/bin/grep -Eq 'ready( \(|[[:space:]]*\$)' } gateway_smoke_ready() { gateway_listener_ready && gateway_log_ready diff --git a/test/openclaw-launcher.e2e.test.ts b/test/openclaw-launcher.e2e.test.ts index 3282ef5a70b..1700cece923 100644 --- a/test/openclaw-launcher.e2e.test.ts +++ b/test/openclaw-launcher.e2e.test.ts @@ -177,6 +177,32 @@ describe("openclaw launcher", () => { expect(result.stdout).toBe("cache:enabled;respawn:0"); }); + it("scopes packaged launcher compile cache inside configured cache roots", async () => { + const fixtureRoot = await makeLauncherFixture(fixtureRoots); + await fs.writeFile(path.join(fixtureRoot, "package.json"), '{"version":"2026.4.27-beta.1"}\n'); + await fs.writeFile( + path.join(fixtureRoot, "dist", "entry.js"), + [ + 'import module from "node:module";', + 'process.stdout.write(module.getCompileCacheDir?.() ?? "cache:disabled");', + ].join("\n"), + "utf8", + ); + + const result = spawnSync(process.execPath, [path.join(fixtureRoot, "openclaw.mjs")], { + cwd: fixtureRoot, + env: launcherEnv({ + NODE_COMPILE_CACHE: path.join(fixtureRoot, ".node-compile-cache"), + }), + encoding: "utf8", + }); + + expect(result.status).toBe(0); + expect(result.stdout).toContain( + path.join(".node-compile-cache", "openclaw", "2026.4.27-beta.1"), + ); + }); + it("enables compile cache for packaged launchers", async () => { const fixtureRoot = await makeLauncherFixture(fixtureRoots); await addCompileCacheProbe(fixtureRoot);