mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
214 lines
6.3 KiB
JavaScript
214 lines
6.3 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { readFileSync, statSync } from "node:fs";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const INSTALL_LIFECYCLE_SCRIPTS = new Set([
|
|
"pnpm:devPreinstall",
|
|
"preinstall",
|
|
"install",
|
|
"postinstall",
|
|
"preprepare",
|
|
"prepare",
|
|
"postprepare",
|
|
]);
|
|
|
|
// These audited root hooks call the files hashed in INSTALL_INPUT_FILES.
|
|
// Hook drift fails closed: an arbitrary lifecycle command can read any tracked
|
|
// source, so a semantic dependency fingerprint cannot safely infer its inputs.
|
|
const FILTERED_SCRIPT_CONTRACTS = new Map([
|
|
[
|
|
"package.json",
|
|
{
|
|
postinstall: "node scripts/postinstall-bundled-plugins.mjs",
|
|
preinstall: "node scripts/preinstall-package-manager-warning.mjs",
|
|
prepare: "node scripts/prepare-git-hooks.mjs",
|
|
},
|
|
],
|
|
]);
|
|
|
|
const INSTALL_INPUT_FILES = [
|
|
"pnpm-lock.yaml",
|
|
"pnpm-workspace.yaml",
|
|
".npmrc",
|
|
".pnpmfile.cjs",
|
|
"pnpmfile.cjs",
|
|
".github/actions/setup-node-env/dependency-fingerprint.mjs",
|
|
".github/actions/setup-node-env/sticky-importers.sh",
|
|
".github/actions/setup-node-env/verify-importers.mjs",
|
|
"scripts/postinstall-bundled-plugins.mjs",
|
|
"scripts/lib/package-dist-imports.mjs",
|
|
"scripts/preinstall-package-manager-warning.mjs",
|
|
"scripts/prepare-git-hooks.mjs",
|
|
];
|
|
|
|
function canonicalize(value) {
|
|
if (Array.isArray(value)) {
|
|
return value.map(canonicalize);
|
|
}
|
|
if (value === null || typeof value !== "object") {
|
|
return value;
|
|
}
|
|
return Object.fromEntries(
|
|
Object.entries(value)
|
|
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
.map(([key, child]) => [key, canonicalize(child)]),
|
|
);
|
|
}
|
|
|
|
function installLifecycleScripts(manifest) {
|
|
if (
|
|
!manifest.scripts ||
|
|
typeof manifest.scripts !== "object" ||
|
|
Array.isArray(manifest.scripts)
|
|
) {
|
|
return {};
|
|
}
|
|
return Object.fromEntries(
|
|
Object.entries(manifest.scripts).filter(([name]) => INSTALL_LIFECYCLE_SCRIPTS.has(name)),
|
|
);
|
|
}
|
|
|
|
function hasAuditedLifecycleScripts(manifest, relativePath) {
|
|
const installScripts = installLifecycleScripts(manifest);
|
|
if (Object.keys(installScripts).length === 0) {
|
|
return true;
|
|
}
|
|
return (
|
|
JSON.stringify(canonicalize(installScripts)) ===
|
|
JSON.stringify(canonicalize(FILTERED_SCRIPT_CONTRACTS.get(relativePath)))
|
|
);
|
|
}
|
|
|
|
function normalizeManifest(manifest) {
|
|
const normalized = { ...manifest };
|
|
if (
|
|
manifest.scripts &&
|
|
typeof manifest.scripts === "object" &&
|
|
!Array.isArray(manifest.scripts)
|
|
) {
|
|
const installScripts = installLifecycleScripts(manifest);
|
|
if (Object.keys(installScripts).length === 0) {
|
|
delete normalized.scripts;
|
|
} else {
|
|
normalized.scripts = installScripts;
|
|
}
|
|
}
|
|
return canonicalize(normalized);
|
|
}
|
|
|
|
function addRecord(hash, kind, relativePath, contents) {
|
|
hash.update(kind);
|
|
hash.update("\0");
|
|
hash.update(relativePath);
|
|
hash.update("\0");
|
|
hash.update(String(Buffer.byteLength(contents)));
|
|
hash.update("\0");
|
|
hash.update(contents);
|
|
hash.update("\0");
|
|
}
|
|
|
|
function trackedPackageManifests(workspace) {
|
|
const result = spawnSync(
|
|
"git",
|
|
["ls-files", "-z", "--", "package.json", ":(glob)**/package.json"],
|
|
{
|
|
cwd: workspace,
|
|
encoding: "utf8",
|
|
},
|
|
);
|
|
if (result.status !== 0) {
|
|
const detail = result.error?.message || result.stderr.trim() || `exit ${result.status}`;
|
|
throw new Error(`git ls-files failed: ${detail}`);
|
|
}
|
|
return result.stdout
|
|
.split("\0")
|
|
.filter((entry) => entry === "package.json" || entry.endsWith("/package.json"))
|
|
.sort();
|
|
}
|
|
|
|
export function computeDependencyFingerprint({ workspace, frozenLockfile }) {
|
|
const hash = createHash("sha256");
|
|
addRecord(hash, "contract", "frozen-lockfile", String(frozenLockfile));
|
|
|
|
const manifests = trackedPackageManifests(workspace);
|
|
if (manifests.length === 0) {
|
|
throw new Error(`no tracked package.json files found under ${workspace}`);
|
|
}
|
|
const parsedManifests = manifests.map((relativePath) => {
|
|
const source = readFileSync(path.join(workspace, relativePath), "utf8");
|
|
let manifest;
|
|
try {
|
|
manifest = JSON.parse(source);
|
|
} catch (error) {
|
|
throw new Error(`invalid JSON in ${relativePath}: ${error.message}`);
|
|
}
|
|
return { manifest, relativePath };
|
|
});
|
|
const unauditedLifecycleManifests = parsedManifests
|
|
.filter(({ manifest, relativePath }) => !hasAuditedLifecycleScripts(manifest, relativePath))
|
|
.map(({ relativePath }) => relativePath);
|
|
if (unauditedLifecycleManifests.length > 0) {
|
|
throw new Error(
|
|
`unaudited install lifecycle scripts in ${unauditedLifecycleManifests.join(", ")}; update FILTERED_SCRIPT_CONTRACTS and INSTALL_INPUT_FILES`,
|
|
);
|
|
}
|
|
for (const { manifest, relativePath } of parsedManifests) {
|
|
addRecord(hash, "manifest", relativePath, JSON.stringify(normalizeManifest(manifest)));
|
|
}
|
|
|
|
for (const relativePath of INSTALL_INPUT_FILES) {
|
|
const absolutePath = path.join(workspace, relativePath);
|
|
let kind = "missing-file";
|
|
let contents = "";
|
|
try {
|
|
if (statSync(absolutePath).isFile()) {
|
|
kind = "file";
|
|
contents = readFileSync(absolutePath);
|
|
} else {
|
|
throw new Error(`${relativePath} is not a regular file`);
|
|
}
|
|
} catch (error) {
|
|
if (error.code !== "ENOENT") {
|
|
throw error;
|
|
}
|
|
}
|
|
addRecord(hash, kind, relativePath, contents);
|
|
}
|
|
|
|
return `v2-${hash.digest("hex")}`;
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
let workspace = process.cwd();
|
|
let frozenLockfile;
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
if (arg === "--workspace") {
|
|
workspace = path.resolve(argv[++index]);
|
|
} else if (arg === "--frozen-lockfile") {
|
|
frozenLockfile = argv[++index];
|
|
} else {
|
|
throw new Error(`unknown argument: ${arg}`);
|
|
}
|
|
}
|
|
if (frozenLockfile !== "true" && frozenLockfile !== "false") {
|
|
throw new Error("--frozen-lockfile must be true or false");
|
|
}
|
|
return { frozenLockfile, workspace };
|
|
}
|
|
|
|
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
if (isMain) {
|
|
try {
|
|
console.log(computeDependencyFingerprint(parseArgs(process.argv.slice(2))));
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : String(error));
|
|
process.exitCode = 1;
|
|
}
|
|
}
|