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 workspaceRoot = path.resolve(projectRoot, ".."); const distRoot = path.join(projectRoot, "web", "dist"); const evidencePath = path.join(projectRoot, "docs", "web-license-evidence.json"); const outputPath = path.join(projectRoot, "docs", "web-license-artifact-map.json"); const evidence = JSON.parse(fs.readFileSync(evidencePath, "utf8")); const sourceRevision = execFileSync("git", ["-C", path.join(projectRoot, "freecad-source"), "rev-parse", "HEAD"], { encoding: "utf8", }).trim(); const dependencyRows = [...evidence.sourceDependencies, ...evidence.webDependencies]; const dependencyByProject = new Map(dependencyRows.map((row) => [row.project, row])); const artifactRules = [ { match: (relative) => relative === "index.html", sources: ["FreeCAD/web/index.html"], dependencies: ["FreeCAD Web UI"], unresolved: [], }, { match: (relative) => /^assets\/index-[^/]+\.js$/.test(relative), sources: [ "FreeCAD/web/src/main.ts", "FreeCAD/web/src/storage.ts", "FreeCAD/web/src/mesh-cache.ts", "FreeCAD/web/src/recovery.ts", "FreeCAD/web/src/worker-protocol.ts", ], dependencies: ["FreeCAD Web UI", "three", "lucide"], unresolved: [], }, { match: (relative) => /^assets\/kernel\.worker-[^/]+\.js$/.test(relative), sources: ["FreeCAD/web/src/kernel.worker.ts", "FreeCAD/web/src/worker-protocol.ts"], dependencies: ["FreeCAD Web UI"], unresolved: [], }, { match: (relative) => /^kernel\/FreeCADWebKernel\.(js|wasm)$/.test(relative), sources: [ "FreeCAD/freecad-source/src/WebAssembly/FreeCADWebBridge.cpp", "FreeCAD/freecad-source/src/WebAssembly/CMakeLists.txt", ], dependencies: ["FreeCAD Web UI", "Open CASCADE"], unresolved: [ { name: "Emscripten", version: "3.1.6", reason: "build tool generated JavaScript/WASM glue; no runtime package is shipped", evidence: ["FreeCAD/docs/freecad-web-bridge-build.log", "FreeCAD/freecad-source/build-web/bridge/build.ninja"], }, ], }, { match: (relative) => relative === "samples/freecad-sample.step", sources: ["FreeCAD/freecad-source/data/tests/Step/as1-ac-214_small.stp"], dependencies: ["FreeCAD Web UI"], unresolved: [], }, ]; function sha256(filePath) { return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex"); } function filesUnder(root) { const files = []; for (const entry of fs.readdirSync(root, { withFileTypes: true })) { const entryPath = path.join(root, entry.name); if (entry.isDirectory()) files.push(...filesUnder(entryPath)); else if (entry.isFile()) files.push(entryPath); } return files.sort(); } function dependencyRecord(project) { const row = dependencyByProject.get(project); if (!row) throw new Error(`missing license evidence for dependency: ${project}`); return { id: `${project}@${row.version}`, project: row.project, version: row.version, licenseLabel: row.licenseLabel, evidence: row.evidence, sha256: row.sha256, status: row.status, }; } if (!fs.existsSync(distRoot)) throw new Error(`artifact directory does not exist: ${distRoot}`); const artifacts = filesUnder(distRoot).map((absolutePath) => { const relative = path.relative(distRoot, absolutePath).split(path.sep).join("/"); const rule = artifactRules.find((candidate) => candidate.match(relative)); if (!rule) throw new Error(`unmapped release artifact: ${relative}`); for (const source of rule.sources) { if (!fs.existsSync(path.join(workspaceRoot, source))) throw new Error(`missing source for ${relative}: ${source}`); } for (const unresolved of rule.unresolved) { for (const item of unresolved.evidence) { if (!fs.existsSync(path.join(workspaceRoot, item))) throw new Error(`missing unresolved evidence for ${relative}: ${item}`); } } return { path: `FreeCAD/web/dist/${relative}`, size: fs.statSync(absolutePath).size, sha256: sha256(absolutePath), sources: rule.sources, dependencyRefs: rule.dependencies.map(dependencyRecord), unresolvedDependencies: rule.unresolved, buildEvidence: relative.startsWith("kernel/") ? ["FreeCAD/docs/freecad-web-bridge-build.log", "FreeCAD/freecad-source/build-web/bridge/build.ninja"] : ["FreeCAD/web/package.json", "FreeCAD/web/package-lock.json"], }; }); const result = { schema: "freecad-web-license-artifact-map/v1", sourceRevision, artifactRoot: "FreeCAD/web/dist", generatedBy: "FreeCAD/tools/generate_web_license_artifact_map.mjs", build: { ui: "npm run build (FreeCAD/web/package.json)", kernel: "cmake --build FreeCAD/freecad-source/build-web/bridge --target FreeCADWebKernel", evidence: [ "FreeCAD/web/package.json", "FreeCAD/web/package-lock.json", "FreeCAD/docs/freecad-web-bridge-build.log", "FreeCAD/freecad-source/build-web/bridge/build.ninja", ], }, licenseEvidence: "FreeCAD/docs/web-license-evidence.json", artifacts, summary: { artifactCount: artifacts.length, unmappedArtifactCount: 0, artifactsWithUnresolvedBuildTools: artifacts.filter((artifact) => artifact.unresolvedDependencies.length > 0).length, result: "pass_all_release_files_mapped_unresolved_build_tools_explicit", }, }; fs.writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`); console.log(`mapped ${artifacts.length} release artifacts`); console.log(`unresolved build-tool annotations: ${result.summary.artifactsWithUnresolvedBuildTools}`); console.log(`wrote ${outputPath}`);