Files
FreeCADWasmDev/tools/verify_web_offline_reproducibility.mjs
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

104 lines
4.3 KiB
JavaScript

import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const toolsRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
const projectRoot = path.resolve(toolsRoot, "..");
const packageRoot = path.join(projectRoot, "web", "offline-package");
const docsRoot = path.join(projectRoot, "docs");
const assembler = path.join(toolsRoot, "assemble_web_offline_package.mjs");
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "freecad-web-repro-"));
function walk(root) {
const files = [];
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
const absolute = path.join(root, entry.name);
if (entry.isDirectory()) files.push(...walk(absolute));
else if (entry.isFile()) files.push(absolute);
}
return files.sort();
}
function sha256(filePath) {
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
}
function snapshot(destination) {
for (const source of walk(packageRoot)) {
const relative = path.relative(packageRoot, source);
const target = path.join(destination, relative);
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.copyFileSync(source, target);
}
}
function normalizeJson(value, file) {
const copy = JSON.parse(JSON.stringify(value));
if (file === "manifest.json") {
copy.generatedAt = "<timestamp>";
copy.manifestFile.selfSha256 = "<self-hash>";
const versionEntry = copy.files.find((entry) => entry.path === "version.json");
if (versionEntry) versionEntry.sha256 = "<timestamp-content-hash>";
}
if (file === "version.json") copy.generatedAt = "<timestamp>";
return JSON.stringify(copy, null, 2) + "\n";
}
function normalizedContent(root, relative) {
const file = path.join(root, relative);
if (relative === "manifest.json" || relative === "version.json") return normalizeJson(JSON.parse(fs.readFileSync(file, "utf8")), relative);
return fs.readFileSync(file);
}
const first = path.join(tempRoot, "first");
const second = path.join(tempRoot, "second");
execFileSync(process.execPath, [assembler], { cwd: projectRoot, stdio: "ignore" });
snapshot(first);
const firstManifest = JSON.parse(fs.readFileSync(path.join(first, "manifest.json"), "utf8"));
execFileSync(process.execPath, [assembler], { cwd: projectRoot, stdio: "ignore" });
snapshot(second);
const firstFiles = new Set(walk(first).map((file) => path.relative(first, file).split(path.sep).join("/")));
const secondFiles = new Set(walk(second).map((file) => path.relative(second, file).split(path.sep).join("/")));
const differences = [];
for (const file of new Set([...firstFiles, ...secondFiles])) {
if (!firstFiles.has(file) || !secondFiles.has(file)) {
differences.push({ file, type: "file-set-difference" });
continue;
}
const left = normalizedContent(first, file);
const right = normalizedContent(second, file);
if (!Buffer.from(left).equals(Buffer.from(right))) differences.push({ file, type: "content-difference" });
}
const result = {
schema: "freecad-web-offline-package-repro/v1",
package: "FreeCAD/web/offline-package",
firstManifestSourceRevision: firstManifest.sourceRevision,
fixedInputs: [
"FreeCAD/web/dist",
"FreeCAD/docs/web-license-notices.md",
"FreeCAD/docs/web-license-inventory.json",
"FreeCAD/freecad-source revision",
],
allowedVariableFields: ["version.json.generatedAt", "manifest.json.generatedAt", "manifest.json.manifestFile.selfSha256"],
differences,
snapshotHashes: {
firstManifest: sha256(path.join(first, "manifest.json")),
secondManifest: sha256(path.join(second, "manifest.json")),
finalManifest: sha256(path.join(packageRoot, "manifest.json")),
},
summary: {
comparedFileCount: firstFiles.size,
differenceCount: differences.length,
result: differences.length === 0 ? "pass_reproducible_except_declared_timestamps" : "fail_reproducibility_difference",
},
};
fs.writeFileSync(path.join(docsRoot, "freecad-web-offline-package-repro.json"), `${JSON.stringify(result, null, 2)}\n`);
console.log(`compared ${result.summary.comparedFileCount} files; differences=${result.summary.differenceCount}`);
console.log(`result=${result.summary.result}`);
if (differences.length > 0) process.exitCode = 1;