124 lines
4.9 KiB
JavaScript
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}`);
|