UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

327 lines (317 loc) • 12.1 kB
// Package-local repair for npm installs that omitted fs-safe's precompiled addon. import { spawnSync } from "node:child_process"; import { accessSync, constants, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync, } from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "../windows-cmd-helpers.mjs"; const PROBE_TIMEOUT_MS = 10_000; const INSTALL_TIMEOUT_MS = 60_000; const scriptPath = fileURLToPath(import.meta.url); const NPM_CLI_REPORT = "OPENCLAW_NPM_CLI:"; // Inline preloading gives delayed manager descendants no temporary file to outlive. // It reports the selected CLI before npm initializes or can run package scripts. const NPM_CLI_PRELOAD = `data:text/javascript;base64,${Buffer.from(` import fs from "node:fs"; import path from "node:path"; try { const cli = fs.realpathSync(process.argv[1]); if (path.basename(cli) === "npm-cli.js" && JSON.parse(fs.readFileSync(path.join(path.dirname(cli), "../package.json"), "utf8")).name === "npm") { fs.writeFileSync(1, ${JSON.stringify(NPM_CLI_REPORT)} + JSON.stringify(cli) + "\\n"); process.exit(0); } } catch {} `).toString("base64")}`; async function probePrebuild(packageRoot) { const requireFromPackage = createRequire(path.join(packageRoot, "package.json")); const manifestPath = requireFromPackage.resolve("@openclaw/fs-safe/package.json"); const entryDirectory = path.dirname(requireFromPackage.resolve("@openclaw/fs-safe")); const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); const dependencies = manifest.optionalDependencies ?? {}; const { configureFsSafeNative, getFsSafeNativeConfig } = await import( pathToFileURL(requireFromPackage.resolve("@openclaw/fs-safe/config")).href ); if (getFsSafeNativeConfig().mode === "off") { return { status: "disabled" }; } configureFsSafeNative({ mode: "require" }); const { readCloneFileMetadata } = await import( pathToFileURL(requireFromPackage.resolve("@openclaw/fs-safe/copy")).href ); try { // An empty metadata query enters N-API without touching any filesystem path. await readCloneFileMetadata([]); return { status: "ready" }; } catch (error) { // Let fs-safe select the platform/libc. Only Node's exact missing-package // error permits repair; a broken addon or a failed filesystem call does not. const firstLine = error?.cause?.message?.split("\n", 1)[0]; const name = Object.keys(dependencies).find( (candidate) => /^@openclaw\/fs-safe-[a-z0-9-]+$/u.test(candidate) && firstLine === `Cannot find module '${candidate}'`, ); if ( error?.code !== "helper-unavailable" || error?.cause?.code !== "MODULE_NOT_FOUND" || path.dirname(error.cause.requireStack?.[0] ?? "") !== entryDirectory || !name ) { return { status: "unavailable", reason: "the native binding could not be loaded or used" }; } const version = dependencies[name]; if ( typeof version !== "string" || !/^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/u.test(version) ) { return { status: "unavailable", reason: "the native dependency is not an exact version" }; } const requireFromFsSafe = createRequire(manifestPath); try { requireFromFsSafe.resolve(`${name}/package.json`); return { status: "unavailable", reason: "the installed native package is incomplete" }; } catch (resolveError) { if (resolveError?.code !== "MODULE_NOT_FOUND") { throw resolveError; } } return { status: "missing", name, version, root: path.dirname(manifestPath) }; } } function runProbe(packageRoot, env) { const result = spawnSync(process.execPath, [scriptPath, "--probe", packageRoot], { encoding: "utf8", env, timeout: PROBE_TIMEOUT_MS, windowsHide: true, maxBuffer: 64 * 1024, }); if (result.status !== 0 || result.error) { throw new Error("the native binding check failed"); } return JSON.parse(result.stdout); } function inspectNpmCli(launcher, env, cwd) { const batch = process.platform === "win32" && /\.(?:cmd|bat)$/iu.test(launcher); const command = batch ? resolveWindowsCmdExePath(env) : launcher; const args = batch ? ["/d", "/s", "/c", buildCmdExeCommandLine(launcher, ["--version"])] : ["--version"]; // Managers retain their own startup/pin selection. Only discovery uses the // shim; the download below always runs as a directly owned runtime process. const result = spawnSync(command, args, { cwd, env: { ...env, NODE_OPTIONS: [env.NODE_OPTIONS, `--import=${NPM_CLI_PRELOAD}`].filter(Boolean).join(" "), BUN_OPTIONS: [env.BUN_OPTIONS, `--preload=${NPM_CLI_PRELOAD}`].filter(Boolean).join(" "), }, encoding: "utf8", timeout: PROBE_TIMEOUT_MS, windowsHide: true, maxBuffer: 64 * 1024, }); if (result.status !== 0 || result.error) { throw new Error( result.error?.code === "ETIMEDOUT" ? "npm launcher discovery timed out" : "the selected npm launcher could not be inspected", ); } const reports = result.stdout.split(/\r?\n/u).filter((line) => line.startsWith(NPM_CLI_REPORT)); const cli = reports.length === 1 ? JSON.parse(reports[0].slice(NPM_CLI_REPORT.length)) : null; if ( typeof cli !== "string" || !path.isAbsolute(cli) || path.basename(cli) !== "npm-cli.js" || !statSync(cli).isFile() ) { throw new Error("the selected npm launcher did not report its npm-cli.js"); } return realpathSync(cli); } function resolveNpmCli(env, cwd) { const npmExecPath = env.npm_execpath; if (npmExecPath && path.basename(npmExecPath).toLowerCase() === "npm-cli.js") { return npmExecPath; } const pathValue = Object.entries(env).find(([key]) => key.toLowerCase() === "path")?.[1] ?? ""; const pathExt = Object.entries(env).find(([key]) => key.toLowerCase() === "pathext")?.[1]; const extensions = process.platform === "win32" ? (pathExt ?? ".COM;.EXE;.BAT;.CMD").split(";").map((extension) => extension.toLowerCase()) : [""]; for (const entry of pathValue.split(path.delimiter).filter(Boolean)) { for (const extension of extensions) { const launcher = path.resolve(cwd, entry.replace(/^"(.*)"$/u, "$1"), `npm${extension}`); try { if (!statSync(launcher).isFile()) { continue; } if (process.platform !== "win32") { accessSync(launcher, constants.X_OK); } } catch { continue; } // Direct package layouts need no discovery. Other shims must reveal their // selected npm; never skip an earlier PATH owner to try a different one. const cli = process.platform === "win32" && extension === ".cmd" ? path.join(path.dirname(launcher), "node_modules", "npm", "bin", "npm-cli.js") : realpathSync(launcher); if (path.basename(cli) === "npm-cli.js" && existsSync(cli) && statSync(cli).isFile()) { return cli; } return inspectNpmCli(launcher, env, cwd); } } throw new Error("npm is unavailable for the native dependency download"); } function runNpmInstall(npmCli, ownerRoot, stageRoot, spec, env) { const args = [ "install", spec, "--prefix", stageRoot, "--global=false", "--location=project", "--workspaces=false", "--include=optional", "--ignore-scripts", "--package-lock=false", "--save=false", "--no-audit", "--no-fund", "--loglevel=error", "--fetch-retries=0", ]; // A global lifecycle exports its CLI-only allow-scripts policy. npm rejects // that policy in a local project even with --ignore-scripts; this repair runs none. const childEnv = Object.fromEntries( Object.entries(env).filter(([key]) => key.toLowerCase() !== "npm_config_allow_scripts"), ); const result = spawnSync(process.execPath, [npmCli, ...args], { cwd: ownerRoot, env: childEnv, encoding: "utf8", timeout: INSTALL_TIMEOUT_MS, windowsHide: true, maxBuffer: 64 * 1024, }); if (result.status !== 0 || result.error) { throw new Error(`npm could not download ${spec}`); } } function ensurePackageDirectory(directory) { try { mkdirSync(directory); } catch (error) { if (error?.code !== "EEXIST") { throw error; } } if (!lstatSync(directory).isDirectory()) { throw new Error("the native dependency directory is not a real directory"); } } /** Restores only the missing, exact native dependency inside its installed owner. */ export function restoreFsSafePrebuild(packageRoot, env = process.env, log = console) { let stageRoot; let installedRoot; try { if (!existsSync(path.join(packageRoot, "package.json"))) { return; } const manifest = JSON.parse(readFileSync(path.join(packageRoot, "package.json"), "utf8")); if (!manifest.dependencies?.["@openclaw/fs-safe"]) { return; } const probe = runProbe(packageRoot, env); if (probe.status === "ready" || probe.status === "disabled") { return; } if (probe.status !== "missing") { throw new Error(probe.reason); } const ownerRoot = realpathSync(packageRoot); const fsSafeRoot = realpathSync(probe.root); const relativeRoot = path.relative(ownerRoot, fsSafeRoot); if ( !relativeRoot || relativeRoot.startsWith(`..${path.sep}`) || relativeRoot === ".." || path.isAbsolute(relativeRoot) ) { throw new Error("the native dependency belongs to another package-manager installation"); } const npmCli = resolveNpmCli(env, ownerRoot); stageRoot = mkdtempSync(path.join(ownerRoot, ".openclaw-prebuild-")); writeFileSync(path.join(stageRoot, "package.json"), '{"private":true}\n'); runNpmInstall(npmCli, ownerRoot, stageRoot, `${probe.name}@${probe.version}`, env); const stagedPackage = path.join(stageRoot, "node_modules", probe.name); const installed = JSON.parse(readFileSync(path.join(stagedPackage, "package.json"), "utf8")); if (installed.name !== probe.name || installed.version !== probe.version) { throw new Error("npm did not install the matching native package"); } const destination = path.join(fsSafeRoot, "node_modules", probe.name); ensurePackageDirectory(path.join(fsSafeRoot, "node_modules")); ensurePackageDirectory(path.dirname(destination)); // An exclusive directory reservation preserves any pre-existing package. mkdirSync(destination); installedRoot = destination; for (const entry of readdirSync(stagedPackage)) { cpSync(path.join(stagedPackage, entry), path.join(destination, entry), { recursive: true, force: false, errorOnExist: true, }); } if (runProbe(packageRoot, env).status !== "ready") { throw new Error("the restored native binding could not be loaded or used"); } installedRoot = undefined; log.log?.(`[postinstall] restored ${probe.name}@${probe.version}`); } catch (error) { log.warn?.( `[postinstall] fs-safe native support is unavailable: ${error instanceof Error ? error.message : String(error)}. Installation will continue; retry the OpenClaw installation to restore native support.`, ); } finally { for (const ownedRoot of [installedRoot, stageRoot]) { if (!ownedRoot) { continue; } try { rmSync(ownedRoot, { recursive: true, force: true }); } catch { log.warn?.("[postinstall] could not clean up the fs-safe native repair directory"); } } } } if ( process.argv[2] === "--probe" && process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url ) { try { process.stdout.write(JSON.stringify(await probePrebuild(process.argv[3]))); } catch { process.exitCode = 1; } }