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

124 lines
4.9 KiB
JavaScript

import crypto from "node:crypto";
import fs from "node:fs";
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 webRoot = path.join(projectRoot, "web");
const distRoot = path.join(webRoot, "dist");
const packageRoot = path.join(webRoot, "offline-package");
const docsRoot = path.join(projectRoot, "docs");
const sourceRevision = execFileSync("git", ["-C", path.join(projectRoot, "freecad-source"), "rev-parse", "HEAD"], { encoding: "utf8" }).trim();
const generatedAt = new Date().toISOString();
function sha256(filePath) {
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
}
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 copyTree(sourceRoot, destinationRoot, sourcePrefix) {
for (const sourceFile of walk(sourceRoot)) {
const relative = path.relative(sourceRoot, sourceFile);
const destination = path.join(destinationRoot, relative);
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.copyFileSync(sourceFile, destination);
copied.push({
packagePath: relative.split(path.sep).join("/"),
source: `${sourcePrefix}/${relative.split(path.sep).join("/")}`,
role: sourcePrefix === "FreeCAD/web/dist" ? "runtime-artifact" : "license-notice",
});
}
}
if (!fs.existsSync(distRoot)) throw new Error(`verified dist directory does not exist: ${distRoot}`);
fs.rmSync(packageRoot, { recursive: true, force: true });
fs.mkdirSync(packageRoot, { recursive: true });
const copied = [];
copyTree(distRoot, packageRoot, "FreeCAD/web/dist");
for (const relative of ["web-license-notices.md", "web-license-inventory.json"]) {
const source = path.join(docsRoot, relative);
const destination = path.join(packageRoot, "licenses", relative);
if (!fs.existsSync(source)) throw new Error(`license input missing: ${source}`);
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.copyFileSync(source, destination);
copied.push({ packagePath: `licenses/${relative}`, source: `FreeCAD/docs/${relative}`, role: "license-notice" });
}
const version = {
schema: "freecad-web-offline-package-version/v1",
packageVersion: "0.1.0",
sourceRevision,
generatedAt,
runtime: "FreeCAD 1.2 Web",
};
fs.writeFileSync(path.join(packageRoot, "version.json"), `${JSON.stringify(version, null, 2)}\n`);
copied.push({ packagePath: "version.json", source: "generated:assemble_web_offline_package.mjs", role: "version-metadata" });
const files = copied.sort((left, right) => left.packagePath.localeCompare(right.packagePath)).map((entry) => {
const absolute = path.join(packageRoot, entry.packagePath);
return {
path: entry.packagePath,
size: fs.statSync(absolute).size,
sha256: sha256(absolute),
source: entry.source,
role: entry.role,
};
});
const manifest = {
schema: "freecad-web-offline-package-manifest/v1",
packageRoot: "FreeCAD/web/offline-package",
packageVersion: version.packageVersion,
sourceRevision,
generatedAt,
deploymentRoot: "/",
referencePolicy: "root-relative references resolve only inside this package; no network or node_modules dependency",
generatedBy: "FreeCAD/tools/assemble_web_offline_package.mjs",
inputs: [
"FreeCAD/web/dist",
"FreeCAD/docs/web-license-notices.md",
"FreeCAD/docs/web-license-inventory.json",
],
files,
manifestFile: {
path: "manifest.json",
hashAlgorithm: "sha256",
selfHashCanonicalization: "manifest JSON with manifestFile.selfSha256 set to null, UTF-8 JSON + newline",
selfSha256: null,
},
summary: {
fileCount: files.length + 1,
runtimeArtifactCount: files.filter((entry) => entry.role === "runtime-artifact").length,
licenseNoticeCount: files.filter((entry) => entry.role === "license-notice").length,
result: "assembled_verified_inputs",
},
};
function canonicalManifest(value) {
return `${JSON.stringify(value, null, 2)}\n`;
}
manifest.manifestFile.selfSha256 = sha256FromText(canonicalManifest(manifest));
function sha256FromText(value) {
return crypto.createHash("sha256").update(value, "utf8").digest("hex");
}
const manifestText = canonicalManifest(manifest);
fs.writeFileSync(path.join(packageRoot, "manifest.json"), manifestText);
fs.writeFileSync(path.join(docsRoot, "freecad-web-offline-package-manifest.json"), manifestText);
console.log(`assembled ${manifest.summary.fileCount} package files at ${packageRoot}`);
console.log(`runtime artifacts: ${manifest.summary.runtimeArtifactCount}; license files: ${manifest.summary.licenseNoticeCount}`);
console.log(`manifest selfSha256: ${manifest.manifestFile.selfSha256}`);