mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-16 21:01:34 +00:00
321 lines
10 KiB
JavaScript
Executable File
321 lines
10 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { delimiter, join, resolve } from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
const WORKSPACE_DIRS_ENV = "OPENCLAW_OCM_WORKSPACE_DEPENDENCY_DIRS";
|
|
const REAL_NPM_ENV = "OPENCLAW_OCM_REAL_NPM_BIN";
|
|
const ALLOW_UNRELEASED_CHANGELOG_ENV = "OPENCLAW_PREPACK_ALLOW_UNRELEASED_CHANGELOG";
|
|
const RUNTIME_BUILD_PROFILE_ENV = "OPENCLAW_OCM_RUNTIME_BUILD_PROFILE";
|
|
const supportedRuntimeBuildProfiles = new Set(["sourcePerformance"]);
|
|
const fullGitCommitPattern = /^[0-9a-f]{40}$/iu;
|
|
|
|
export function parseWorkspaceDependencyDirs(
|
|
raw = process.env[WORKSPACE_DIRS_ENV],
|
|
cwd = process.cwd(),
|
|
) {
|
|
return (raw ?? "")
|
|
.split(delimiter)
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean)
|
|
.map((entry) => resolve(cwd, entry));
|
|
}
|
|
|
|
function optionValue(args, name) {
|
|
const index = args.indexOf(name);
|
|
return index >= 0 ? args[index + 1] : undefined;
|
|
}
|
|
|
|
export function resolveWorkspaceInstallPlan(args, workspaceDirs, cwd = process.cwd()) {
|
|
if (args[0] !== "install" || workspaceDirs.length === 0) {
|
|
return null;
|
|
}
|
|
const prefixDir = optionValue(args, "--prefix");
|
|
const rootArchive = args.at(-1);
|
|
if (!prefixDir || !rootArchive?.endsWith(".tgz")) {
|
|
throw new Error("OCM workspace dependency install requires --prefix and a root .tgz archive");
|
|
}
|
|
return {
|
|
installArgs: args.slice(0, -1),
|
|
prefixDir: resolve(cwd, prefixDir),
|
|
rootArchive: resolve(cwd, rootArchive),
|
|
};
|
|
}
|
|
|
|
export function buildInstallManifest(rootArchive, workspacePackages) {
|
|
return {
|
|
private: true,
|
|
dependencies: {
|
|
openclaw: pathToFileURL(rootArchive).href,
|
|
...Object.fromEntries(
|
|
workspacePackages.map(({ name, tarball }) => [name, pathToFileURL(tarball).href]),
|
|
),
|
|
},
|
|
};
|
|
}
|
|
|
|
function runNpm(npm, args, options = {}) {
|
|
const result = spawnSync(npm, args, {
|
|
env: process.env,
|
|
...options,
|
|
});
|
|
if (result.error) {
|
|
throw result.error;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function resolveNpmEnvironment(args, env = process.env) {
|
|
if (args[0] !== "pack") {
|
|
return env;
|
|
}
|
|
return {
|
|
...env,
|
|
[ALLOW_UNRELEASED_CHANGELOG_ENV]: "1",
|
|
};
|
|
}
|
|
|
|
export function resolveRuntimePackPlan(args, env = process.env) {
|
|
if (args[0] !== "pack") {
|
|
return null;
|
|
}
|
|
const profile = env[RUNTIME_BUILD_PROFILE_ENV]?.trim();
|
|
if (!profile) {
|
|
return null;
|
|
}
|
|
if (!supportedRuntimeBuildProfiles.has(profile)) {
|
|
throw new Error(`invalid ${RUNTIME_BUILD_PROFILE_ENV}: ${profile}`);
|
|
}
|
|
return {
|
|
profile,
|
|
packArgs: args.includes("--ignore-scripts") ? args : [...args, "--ignore-scripts"],
|
|
};
|
|
}
|
|
|
|
export function resolveRuntimePackEnvironment(
|
|
env = process.env,
|
|
now = () => new Date(),
|
|
readGitCommit = () => {
|
|
const result = spawnSync("git", ["rev-parse", "HEAD"], {
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
});
|
|
return result.status === 0 ? result.stdout.trim() : null;
|
|
},
|
|
) {
|
|
const explicitTimestamp = env.OPENCLAW_BUILD_TIMESTAMP?.trim();
|
|
const explicitCommit = env.GIT_COMMIT?.trim() || env.GIT_SHA?.trim();
|
|
const checkedOutCommit = explicitCommit ? null : readGitCommit()?.trim();
|
|
const commit = explicitCommit || checkedOutCommit || env.GITHUB_SHA?.trim();
|
|
if (commit && !fullGitCommitPattern.test(commit)) {
|
|
throw new Error("runtime pack commit must be a full 40-character hexadecimal SHA");
|
|
}
|
|
return {
|
|
...env,
|
|
OPENCLAW_BUILD_TIMESTAMP: explicitTimestamp || now().toISOString(),
|
|
...(commit ? { GIT_COMMIT: commit.toLowerCase() } : {}),
|
|
};
|
|
}
|
|
|
|
function runTar(args) {
|
|
const result = spawnSync("tar", args, {
|
|
env: process.env,
|
|
stdio: "inherit",
|
|
});
|
|
if (result.error) {
|
|
throw result.error;
|
|
}
|
|
if (result.status !== 0) {
|
|
throw new Error(`tar failed with status ${result.status ?? 1}`);
|
|
}
|
|
}
|
|
|
|
function runChecked(command, args, options = {}) {
|
|
const result = runNpm(command, args, options);
|
|
if (result.status !== 0) {
|
|
throw new Error(`${command} failed with status ${result.status ?? 1}`);
|
|
}
|
|
}
|
|
|
|
function supportsPreparedRuntimePack(env) {
|
|
const script = `
|
|
const mod = await import("./scripts/openclaw-prepack.ts");
|
|
process.exit(typeof mod.preparePrepackArtifacts === "function" ? 0 : 1);
|
|
`;
|
|
const result = runNpm(
|
|
process.execPath,
|
|
["--import", "tsx", "--input-type=module", "--eval", script],
|
|
{
|
|
env,
|
|
stdio: "ignore",
|
|
},
|
|
);
|
|
return result.status === 0;
|
|
}
|
|
|
|
function prepareRuntimePack(profile, env) {
|
|
runChecked(process.execPath, ["scripts/build-all.mjs", profile], {
|
|
env,
|
|
stdio: "inherit",
|
|
});
|
|
runChecked(process.execPath, ["scripts/ui.js", "build"], {
|
|
env,
|
|
stdio: "inherit",
|
|
});
|
|
const script = `
|
|
const mod = await import("./scripts/openclaw-prepack.ts");
|
|
await mod.preparePrepackArtifacts();
|
|
`;
|
|
runChecked(process.execPath, ["--import", "tsx", "--input-type=module", "--eval", script], {
|
|
env,
|
|
stdio: "inherit",
|
|
});
|
|
}
|
|
|
|
function restoreRuntimePack(env) {
|
|
const script = `
|
|
const mod = await import("./scripts/package-changelog.mjs");
|
|
await mod.restorePackageChangelog();
|
|
`;
|
|
runChecked(process.execPath, ["--input-type=module", "--eval", script], {
|
|
env,
|
|
stdio: "inherit",
|
|
});
|
|
}
|
|
|
|
function packWorkspaceDependencies(npm, workspaceDirs, outputDir) {
|
|
return workspaceDirs.map((packageDir) => {
|
|
const packageJson = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
|
|
if (typeof packageJson.name !== "string" || packageJson.name.trim() === "") {
|
|
throw new Error(`workspace dependency has no package name: ${packageDir}`);
|
|
}
|
|
if (typeof packageJson.version !== "string" || packageJson.version.trim() === "") {
|
|
throw new Error(`workspace dependency has no package version: ${packageDir}`);
|
|
}
|
|
const before = new Set(readdirSync(outputDir));
|
|
const result = runNpm(npm, ["pack", packageDir, "--pack-destination", outputDir, "--silent"], {
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "inherit"],
|
|
});
|
|
if (result.status !== 0) {
|
|
throw new Error(`npm pack failed for ${packageJson.name} with status ${result.status ?? 1}`);
|
|
}
|
|
const tarballs = readdirSync(outputDir).filter(
|
|
(entry) => entry.endsWith(".tgz") && !before.has(entry),
|
|
);
|
|
if (tarballs.length !== 1) {
|
|
throw new Error(
|
|
`expected npm pack to create one archive for ${packageJson.name}, found ${tarballs.length}`,
|
|
);
|
|
}
|
|
return {
|
|
name: packageJson.name,
|
|
version: packageJson.version,
|
|
tarball: join(outputDir, tarballs[0]),
|
|
};
|
|
});
|
|
}
|
|
|
|
export function rewriteWorkspaceDependencyVersions(packageJson, workspacePackages) {
|
|
const workspaceVersions = new Map(workspacePackages.map(({ name, version }) => [name, version]));
|
|
let rewritten = 0;
|
|
for (const section of [
|
|
"dependencies",
|
|
"optionalDependencies",
|
|
"peerDependencies",
|
|
"devDependencies",
|
|
]) {
|
|
const dependencies = packageJson[section];
|
|
if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) {
|
|
continue;
|
|
}
|
|
for (const [name, spec] of Object.entries(dependencies)) {
|
|
if (typeof spec !== "string" || !spec.startsWith("workspace:")) {
|
|
continue;
|
|
}
|
|
const version = workspaceVersions.get(name);
|
|
if (!version) {
|
|
throw new Error(`root archive references unconfigured workspace dependency: ${name}`);
|
|
}
|
|
dependencies[name] = version;
|
|
rewritten += 1;
|
|
}
|
|
}
|
|
return rewritten;
|
|
}
|
|
|
|
function patchRootArchiveWorkspaceDependencies(rootArchive, workspacePackages, outputDir) {
|
|
const unpackDir = join(outputDir, "root-archive");
|
|
mkdirSync(unpackDir);
|
|
runTar(["-xzf", rootArchive, "-C", unpackDir]);
|
|
|
|
const packageJsonPath = join(unpackDir, "package", "package.json");
|
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
const rewritten = rewriteWorkspaceDependencyVersions(packageJson, workspacePackages);
|
|
if (rewritten === 0) {
|
|
return rootArchive;
|
|
}
|
|
|
|
writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
|
const patchedArchive = join(outputDir, "openclaw-root-patched.tgz");
|
|
runTar(["-czf", patchedArchive, "-C", unpackDir, "package"]);
|
|
return patchedArchive;
|
|
}
|
|
|
|
function main() {
|
|
const args = process.argv.slice(2);
|
|
const npm = process.env[REAL_NPM_ENV]?.trim() || "npm";
|
|
const workspaceDirs = parseWorkspaceDependencyDirs();
|
|
const npmEnv = resolveNpmEnvironment(args);
|
|
const runtimePackPlan = resolveRuntimePackPlan(args);
|
|
const runtimePackEnv = runtimePackPlan ? resolveRuntimePackEnvironment(npmEnv) : null;
|
|
if (runtimePackPlan && runtimePackEnv && supportsPreparedRuntimePack(runtimePackEnv)) {
|
|
// This adapter-only archive is installed into OCM and never published.
|
|
// Standard npm pack still runs the full package build.
|
|
try {
|
|
prepareRuntimePack(runtimePackPlan.profile, runtimePackEnv);
|
|
const result = runNpm(npm, runtimePackPlan.packArgs, {
|
|
env: runtimePackEnv,
|
|
stdio: "inherit",
|
|
});
|
|
return result.status ?? 1;
|
|
} finally {
|
|
restoreRuntimePack(runtimePackEnv);
|
|
}
|
|
}
|
|
const plan = resolveWorkspaceInstallPlan(args, workspaceDirs);
|
|
if (!plan) {
|
|
const result = runNpm(npm, args, {
|
|
env: npmEnv,
|
|
stdio: "inherit",
|
|
});
|
|
return result.status ?? 1;
|
|
}
|
|
|
|
const packDir = mkdtempSync(join(tmpdir(), "openclaw-ocm-workspace-deps-"));
|
|
try {
|
|
const workspacePackages = packWorkspaceDependencies(npm, workspaceDirs, packDir);
|
|
const rootArchive = patchRootArchiveWorkspaceDependencies(
|
|
plan.rootArchive,
|
|
workspacePackages,
|
|
packDir,
|
|
);
|
|
mkdirSync(plan.prefixDir, { recursive: true });
|
|
writeFileSync(
|
|
join(plan.prefixDir, "package.json"),
|
|
`${JSON.stringify(buildInstallManifest(rootArchive, workspacePackages), null, 2)}\n`,
|
|
);
|
|
const result = runNpm(npm, plan.installArgs, { stdio: "inherit" });
|
|
return result.status ?? 1;
|
|
} finally {
|
|
rmSync(packDir, { force: true, recursive: true });
|
|
}
|
|
}
|
|
|
|
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
process.exitCode = main();
|
|
}
|