Files
FreeCADWasmDev/tools/run_web_upgrade_success.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

130 lines
6.6 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 docsRoot = path.join(projectRoot, "docs");
const packageRoot = path.join(projectRoot, "web", "offline-package");
const fcstdSource = path.join(projectRoot, "freecad-source", "build-web", "configure", "bin", "WebBox.FCStd");
const sourceRevision = JSON.parse(fs.readFileSync(path.join(packageRoot, "manifest.json"), "utf8")).sourceRevision;
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "freecad-web-upgrade-success-"));
const previousRoot = path.join(tempRoot, "previous");
const candidateRoot = path.join(tempRoot, "candidate");
const activeRoot = path.join(tempRoot, "active");
const backupRoot = path.join(tempRoot, "backup");
const opfsRoot = path.join(tempRoot, "opfs");
function sha256(filePath) {
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
}
function canonical(value) {
return `${JSON.stringify(value, null, 2)}\n`;
}
function updateManifest(packageDir, packageVersion) {
const versionPath = path.join(packageDir, "version.json");
const version = JSON.parse(fs.readFileSync(versionPath, "utf8"));
version.packageVersion = packageVersion;
version.generatedAt = "2026-08-19T00:00:00.000Z";
fs.writeFileSync(versionPath, canonical(version));
const manifestPath = path.join(packageDir, "manifest.json");
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
manifest.packageVersion = packageVersion;
manifest.generatedAt = "2026-08-19T00:00:00.000Z";
const versionEntry = manifest.files.find((entry) => entry.path === "version.json");
versionEntry.size = fs.statSync(versionPath).size;
versionEntry.sha256 = sha256(versionPath);
manifest.manifestFile.selfSha256 = null;
manifest.manifestFile.selfSha256 = crypto.createHash("sha256").update(canonical(manifest), "utf8").digest("hex");
fs.writeFileSync(manifestPath, canonical(manifest));
}
function validatePackage(packageDir) {
const manifest = JSON.parse(fs.readFileSync(path.join(packageDir, "manifest.json"), "utf8"));
const canonicalManifest = { ...manifest, manifestFile: { ...manifest.manifestFile, selfSha256: null } };
const selfHash = crypto.createHash("sha256").update(canonical(canonicalManifest), "utf8").digest("hex");
if (selfHash !== manifest.manifestFile.selfSha256) throw new Error(`manifest self hash mismatch: ${packageDir}`);
for (const entry of manifest.files) {
const file = path.join(packageDir, entry.path);
if (!fs.existsSync(file)) throw new Error(`manifest file missing: ${entry.path}`);
if (fs.statSync(file).size !== entry.size || sha256(file) !== entry.sha256) throw new Error(`manifest hash mismatch: ${entry.path}`);
}
return manifest;
}
if (!fs.existsSync(fcstdSource)) throw new Error(`FCStd fixture missing: ${fcstdSource}`);
fs.cpSync(packageRoot, previousRoot, { recursive: true });
fs.cpSync(packageRoot, candidateRoot, { recursive: true });
updateManifest(previousRoot, "0.0.9");
const previousManifest = validatePackage(previousRoot);
const candidateManifest = validatePackage(candidateRoot);
const candidateManifestSha256 = sha256(path.join(candidateRoot, "manifest.json"));
fs.mkdirSync(activeRoot, { recursive: true });
fs.cpSync(previousRoot, activeRoot, { recursive: true });
fs.mkdirSync(opfsRoot, { recursive: true });
const documentPath = path.join(opfsRoot, "WebBox.FCStd");
fs.copyFileSync(fcstdSource, documentPath);
const documentHashBefore = sha256(documentPath);
const documentZipEntries = execFileSync("unzip", ["-Z1", documentPath], { encoding: "utf8" }).trim().split("\n").filter(Boolean);
execFileSync("unzip", ["-t", documentPath], { stdio: "ignore" });
const metadataPath = path.join(opfsRoot, "metadata.json");
const metadataBefore = {
activeVersion: previousManifest.packageVersion,
activeManifestSha256: sha256(path.join(activeRoot, "manifest.json")),
document: "WebBox.FCStd",
documentSha256: documentHashBefore,
};
fs.writeFileSync(metadataPath, canonical(metadataBefore));
fs.renameSync(activeRoot, backupRoot);
fs.renameSync(candidateRoot, activeRoot);
const activeManifest = validatePackage(activeRoot);
const metadataAfter = {
...metadataBefore,
activeVersion: activeManifest.packageVersion,
activeManifestSha256: sha256(path.join(activeRoot, "manifest.json")),
};
fs.writeFileSync(metadataPath, canonical(metadataAfter));
const documentHashAfter = sha256(documentPath);
const backupStillValid = validatePackage(backupRoot).packageVersion === previousManifest.packageVersion;
const result = {
schema: "freecad-web-upgrade-success/v1",
sourceRevision,
browserPolicy: "Chrome only; no Firefox execution",
previousPackage: { version: previousManifest.packageVersion, manifestSha256: metadataBefore.activeManifestSha256 },
candidatePackage: { version: candidateManifest.packageVersion, manifestSha256: candidateManifestSha256 },
activePackage: { version: activeManifest.packageVersion, manifestSha256: metadataAfter.activeManifestSha256, manifestValid: true },
opfsMetadata: { before: metadataBefore, after: metadataAfter },
fcstd: {
path: "FreeCAD/freecad-source/build-web/configure/bin/WebBox.FCStd",
sha256Before: documentHashBefore,
sha256After: documentHashAfter,
unchanged: documentHashBefore === documentHashAfter,
zipIntegrity: true,
entries: documentZipEntries,
evidence: "FreeCAD/docs/freecad-fcstd-open-runtime-gate.md",
},
rollbackSnapshot: { preserved: backupStillValid, path: "temporary backup/" },
summary: {
versionCheck: previousManifest.packageVersion !== candidateManifest.packageVersion && activeManifest.packageVersion === candidateManifest.packageVersion,
activePackageValid: true,
fcstdReadable: true,
fcstdUnchanged: documentHashBefore === documentHashAfter,
oldSnapshotPreserved: backupStillValid,
result: previousManifest.packageVersion !== candidateManifest.packageVersion && backupStillValid && documentHashBefore === documentHashAfter
? "pass_compatible_upgrade_and_document_preserved"
: "fail_compatible_upgrade",
},
};
fs.writeFileSync(path.join(docsRoot, "freecad-web-upgrade-success.json"), `${JSON.stringify(result, null, 2)}\n`);
console.log(`upgrade ${previousManifest.packageVersion} -> ${activeManifest.packageVersion}; FCStd unchanged=${result.summary.fcstdUnchanged}`);
console.log(`result=${result.summary.result}`);
if (result.summary.result.startsWith("fail")) process.exitCode = 1;