mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-20 11:42:23 +02:00
116 lines
4.9 KiB
JavaScript
116 lines
4.9 KiB
JavaScript
#!/usr/bin/env node
|
|
// Builds or compares a SHA-256 manifest of every file under the packaged artifact root. With --write the current state becomes the manifest; without it, drift from the committed manifest fails the build. Catches non-deterministic build steps (zip timestamps, random ids in bundles) that survive even fully-pinned dependencies.
|
|
|
|
'use strict';
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
const h = require('./lib/app-harness');
|
|
|
|
function parseArgs(argv) {
|
|
const out = { app: null, write: false, manifest: null, only: null };
|
|
for (let i = 0; i < argv.length; i++) {
|
|
if (argv[i] === '--app') out.app = argv[++i];
|
|
else if (argv[i] === '--write') out.write = true;
|
|
else if (argv[i] === '--manifest') out.manifest = argv[++i];
|
|
else if (argv[i] === '--only') out.only = argv[++i];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// Files that legitimately differ run-to-run (timestamped logs, OS metadata).
|
|
// Hashing them would force a manifest churn every build for no signal.
|
|
const SKIP = [
|
|
/\.DS_Store$/,
|
|
/[\\/]Thumbs\.db$/,
|
|
/[\\/]desktop\.ini$/,
|
|
/\.log$/,
|
|
];
|
|
|
|
function walk(dir, root, out = []) {
|
|
let entries = [];
|
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return out; }
|
|
for (const e of entries) {
|
|
const full = path.join(dir, e.name);
|
|
if (e.isDirectory()) walk(full, root, out);
|
|
else if (e.isFile()) {
|
|
const rel = path.relative(root, full).replace(/\\/g, '/');
|
|
if (SKIP.some((rx) => rx.test(rel))) continue;
|
|
out.push({ rel, full });
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function sha256(file) {
|
|
const hash = crypto.createHash('sha256');
|
|
const buf = fs.readFileSync(file);
|
|
hash.update(buf);
|
|
return { sha: hash.digest('hex'), size: buf.length };
|
|
}
|
|
|
|
function defaultManifestPath() {
|
|
// The manifest lives in-repo under scripts/ci/ so CI compares to the
|
|
// committed snapshot, and we ship one per platform because Mac/Win/Linux
|
|
// produce different file sets.
|
|
return path.join(h.REPO_ROOT, 'scripts', 'ci', `dist-manifest.${process.platform}.json`);
|
|
}
|
|
|
|
function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
const appExe = h.packagedAppPath(args.app);
|
|
const root = process.platform === 'darwin'
|
|
? path.dirname(path.dirname(path.dirname(appExe)))
|
|
: path.dirname(appExe);
|
|
const manifestPath = args.manifest || defaultManifestPath();
|
|
|
|
process.stdout.write(`Artifact root: ${root}\n`);
|
|
process.stdout.write(`Manifest: ${manifestPath}\n`);
|
|
|
|
const files = walk(root, root);
|
|
const filtered = args.only ? files.filter((f) => new RegExp(args.only).test(f.rel)) : files;
|
|
process.stdout.write(`Hashing ${filtered.length} file(s)...\n`);
|
|
|
|
const current = {};
|
|
for (const f of filtered) {
|
|
const { sha, size } = sha256(f.full);
|
|
current[f.rel] = { sha, size };
|
|
}
|
|
|
|
if (args.write) {
|
|
fs.writeFileSync(manifestPath, JSON.stringify(current, null, 2));
|
|
process.stdout.write(`\nMANIFEST WRITE: ${Object.keys(current).length} entries -> ${manifestPath}\n`);
|
|
process.stdout.write('Commit this file so the next build can compare against it.\n');
|
|
process.exit(0);
|
|
}
|
|
|
|
let committed;
|
|
try { committed = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); }
|
|
catch { process.stderr.write(`\nMANIFEST FAIL: no committed manifest at ${manifestPath}. Run once with --write to seed it.\n`); process.exit(1); }
|
|
|
|
const added = [];
|
|
const removed = [];
|
|
const changed = [];
|
|
for (const k of Object.keys(current)) {
|
|
if (!committed[k]) added.push(k);
|
|
else if (committed[k].sha !== current[k].sha) changed.push({ k, was: committed[k], now: current[k] });
|
|
}
|
|
for (const k of Object.keys(committed)) {
|
|
if (!current[k]) removed.push(k);
|
|
}
|
|
|
|
if (added.length === 0 && removed.length === 0 && changed.length === 0) {
|
|
process.stdout.write('\nMANIFEST PASS: artifact is byte-identical to the committed manifest.\n');
|
|
process.exit(0);
|
|
}
|
|
|
|
process.stderr.write(`\nMANIFEST FAIL: artifact diverges from committed manifest.\n`);
|
|
if (added.length) { process.stderr.write(` added (${added.length}):\n`); for (const a of added.slice(0, 20)) process.stderr.write(` + ${a}\n`); if (added.length > 20) process.stderr.write(` ...+${added.length - 20} more\n`); }
|
|
if (removed.length) { process.stderr.write(` removed (${removed.length}):\n`); for (const r of removed.slice(0, 20)) process.stderr.write(` - ${r}\n`); if (removed.length > 20) process.stderr.write(` ...-${removed.length - 20} more\n`); }
|
|
if (changed.length) { process.stderr.write(` changed (${changed.length}):\n`); for (const c of changed.slice(0, 20)) process.stderr.write(` ~ ${c.k}\n was ${c.was.sha.slice(0, 12)}.. (${c.was.size}b), now ${c.now.sha.slice(0, 12)}.. (${c.now.size}b)\n`); if (changed.length > 20) process.stderr.write(` ...~${changed.length - 20} more\n`); }
|
|
process.stderr.write('\nIf the divergence is intentional, re-run with --write and commit the new manifest.\n');
|
|
process.exit(1);
|
|
}
|
|
|
|
main();
|