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}`);