import fs from "node:fs"; import crypto from "node:crypto"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { pthreadSidecar, artifactRevision, writeManifestIfChanged, missingArtifactPair, copyArtifactAtomically } from "./artifact-assets.mjs"; const scriptStarted = performance.now(); // Keep a persistent source signature so unchanged multi-megabyte WASM files // are neither copied nor hashed on every incremental batch. let syncState = { schema: "freecad-web-assets-sync/v1", entries: {} }; let syncStateDirty = false; const syncMetrics = { copyAttempts: 0, copied: 0, skipped: 0, missing: 0, copySeconds: 0, hashComputed: 0, hashCacheHits: 0, hashSeconds: 0, }; const missingSources = []; const webRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const sourceRoot = path.resolve(webRoot, "../freecad-source"); const kernelRoot = path.join(webRoot, "public", "kernel"); const moduleRoot = path.join(webRoot, "public", "modules"); const sampleRoot = path.join(webRoot, "public", "samples"); const syncStatePath = path.resolve(webRoot, "../docs/current/web-assets-sync-cache.json"); try { const cached = JSON.parse(fs.readFileSync(syncStatePath, "utf8")); if (cached && cached.schema === "freecad-web-assets-sync/v1" && cached.entries) syncState = cached; } catch { // The first sync has no cache; all present sources are copied once. } const sourceSignature = (file) => { const stat = fs.statSync(file); return `${stat.size}:${stat.mtimeMs}:${stat.ino ?? 0}`; }; const copyFile = fs.copyFileSync.bind(fs); fs.copyFileSync = (source, destination) => { syncMetrics.copyAttempts += 1; if (!fs.existsSync(source)) { syncMetrics.missing += 1; missingSources.push(path.resolve(source)); return; } const sourcePath = path.resolve(source); const destinationPath = path.resolve(destination); const incomplete = missingArtifactPair(sourcePath); if (incomplete.length) { syncMetrics.missing += 1; missingSources.push(...incomplete.filter(file => !missingSources.includes(file))); return; } const sidecar = pthreadSidecar(sourcePath, destinationPath); if (sidecar) fs.copyFileSync(...sidecar); const key = `copy:${sourcePath}->${destinationPath}`; const signature = sourceSignature(sourcePath); const cached = syncState.entries[key]; try { const destinationStat = fs.statSync(destinationPath); if (cached?.signature === signature && destinationStat.size === Number(cached.destinationSize)) { syncMetrics.skipped += 1; return; } } catch { // Destination is absent or inaccessible and must be copied. } const copyStarted = performance.now(); copyArtifactAtomically(sourcePath, destinationPath, copyFile); syncMetrics.copySeconds += (performance.now() - copyStarted) / 1000; syncMetrics.copied += 1; syncState.entries[key] = { signature, destinationSize: fs.statSync(destinationPath).size }; syncStateDirty = true; }; const cloudKernelRoot = path.join(sourceRoot, "build-web", "cloud-configure-r2", "bin"); const importKernelRoot = path.join(sourceRoot, "build-web", "configure", "bin"); const assemblyKernelRoot = path.join(sourceRoot, "build-web", "assembly-configure", "bin"); const camKernelRoot = path.join(sourceRoot, "build-web", "cam-configure-r1", "bin"); const femKernelRoot = path.join(sourceRoot, "build-web", "fem-wasm", "bin"); const techdrawNativeKernelRoots = [ path.join(sourceRoot, "build-web", "techdraw-native-configure", "bin"), path.join(sourceRoot, "build-web", "techdraw-native-full-pthread", "bin"), path.join(sourceRoot, "build-web", "techdraw-native-occt-pthread", "bin"), ]; const techdrawNativeKernelRoot = techdrawNativeKernelRoots.find((root) => fs.existsSync(path.join(root, "FreeCADWebTechDrawRuntimeProbe.js")) && fs.existsSync(path.join(root, "FreeCADWebTechDrawRuntimeProbe.wasm")) ) ?? techdrawNativeKernelRoots[0]; fs.mkdirSync(kernelRoot, { recursive: true }); fs.mkdirSync(moduleRoot, { recursive: true }); fs.mkdirSync(sampleRoot, { recursive: true }); for (const file of ["FreeCADWebKernel.js", "FreeCADWebKernel.wasm"]) { fs.copyFileSync(path.join(sourceRoot, "build-web", "bridge", file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebCloudKernelProbe.js", "FreeCADWebCloudKernelProbe.wasm"]) { fs.copyFileSync(path.join(cloudKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebImportRuntimeProbe.js", "FreeCADWebImportRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebCAMProbe.js", "FreeCADWebCAMProbe.wasm"]) { fs.copyFileSync(path.join(camKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebFemGenericFamilyProbe.js", "FreeCADWebFemGenericFamilyProbe.wasm"]) { fs.copyFileSync(path.join(femKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebDraftRuntimeProbe.js", "FreeCADWebDraftRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebBimRuntimeProbe.js", "FreeCADWebBimRuntimeProbe.wasm", "FreeCADWebBimRuntimeProbe.data"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebHelpRuntimeProbe.js", "FreeCADWebHelpRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebAssemblyProbe.js", "FreeCADWebAssemblyProbe.wasm", "FreeCADWebAssemblyKernelProbe.js", "FreeCADWebAssemblyKernelProbe.wasm"]) { fs.copyFileSync(path.join(assemblyKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebInspectionRuntimeProbe.js", "FreeCADWebInspectionRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebJtReaderRuntimeProbe.js", "FreeCADWebJtReaderRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebMaterialKernelProbe.js", "FreeCADWebMaterialKernelProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebMeasureKernelProbe.js", "FreeCADWebMeasureKernelProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebMeshRuntimeProbe.js", "FreeCADWebMeshRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebMeshPartRuntimeProbe.js", "FreeCADWebMeshPartRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebOpenSCADRuntimeProbe.js", "FreeCADWebOpenSCADRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebPartBoxProbe.js", "FreeCADWebPartBoxProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of fs.readdirSync(importKernelRoot).filter((name) => /^FreeCADWeb[A-Za-z0-9]+DesktopCommandProbe\.(?:js|wasm|worker\.js)$/.test(name))) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebPartIoProbe.js", "FreeCADWebPartIoProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebDocumentProbe.js", "FreeCADWebDocumentProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebWorkbenchCommandProbe.js", "FreeCADWebWorkbenchCommandProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebPartDesignRuntimeProbe.js", "FreeCADWebPartDesignRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebPointsRuntimeProbe.js", "FreeCADWebPointsRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebSandboxRuntimeProbe.js", "FreeCADWebSandboxRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebSketcherProbe.js", "FreeCADWebSketcherProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebPlotRuntimeProbe.js", "FreeCADWebPlotRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebReverseEngineeringRuntimeProbe.js", "FreeCADWebReverseEngineeringRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebRobotRuntimeProbe.js", "FreeCADWebRobotRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebShowRuntimeProbe.js", "FreeCADWebShowRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebSurfaceRuntimeProbe.js", "FreeCADWebSurfaceRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebStartRuntimeProbe.js", "FreeCADWebStartRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebStartInventoryProbe.js", "FreeCADWebStartInventoryProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebTemplatePyModRuntimeProbe.js", "FreeCADWebTemplatePyModRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebTuxRuntimeProbe.js", "FreeCADWebTuxRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebWebRuntimeProbe.js", "FreeCADWebWebRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebTestRuntimeProbe.js", "FreeCADWebTestRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebPythonProbe.js", "FreeCADWebPythonProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebTechDrawRuntimeProbe.js", "FreeCADWebTechDrawRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(techdrawNativeKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebSpreadsheetRuntimeProbe.js", "FreeCADWebSpreadsheetRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebDataPropertyFamilyProbe.js", "FreeCADWebDataPropertyFamilyProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebDocumentGraphFamilyProbe.js", "FreeCADWebDocumentGraphFamilyProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } for (const file of ["FreeCADWebJtReaderRuntimeProbe.js", "FreeCADWebJtReaderRuntimeProbe.wasm"]) { fs.copyFileSync(path.join(importKernelRoot, file), path.join(kernelRoot, file)); } fs.copyFileSync( path.join(sourceRoot, "data", "tests", "Step", "as1-ac-214_small.stp"), path.join(sampleRoot, "freecad-sample.step"), ); const digest = (file) => { const filePath = path.resolve(file); const key = `digest:${filePath}`; let signature; try { signature = sourceSignature(filePath); } catch { return null; } const cached = syncState.entries[key]; if (cached?.signature === signature && typeof cached.hash === "string") { syncMetrics.hashCacheHits += 1; return cached.hash; } const hashStarted = performance.now(); const hash = crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex"); syncMetrics.hashSeconds += (performance.now() - hashStarted) / 1000; syncMetrics.hashComputed += 1; syncState.entries[key] = { signature, hash }; syncStateDirty = true; return hash; }; const buildProfileForProbe = (stem) => { const roots = { FreeCADWebCAMProbe: path.join(sourceRoot, "build-web", "cam-configure-r1"), FreeCADWebFemGenericFamilyProbe: path.join(sourceRoot, "build-web", "fem-wasm"), }; const root = roots[stem]; if (!root) return null; try { const cache = fs.readFileSync(path.join(root, "CMakeCache.txt"), "utf8"); const match = cache.match(/^FREECAD_WEB_FAST_LINK:BOOL=(ON|OFF)$/m); return match?.[1] === "ON" ? "fast-dev" : "release"; } catch { return null; } }; const probeForWorker = (worker) => { const sourcePath = path.join(webRoot, "src", worker); if (!fs.existsSync(sourcePath)) return null; const source = fs.readFileSync(sourcePath, "utf8"); const match = source.match(/FreeCADWeb[A-Za-z0-9]+Probe\.js/); return match ? match[0].slice(0, -3) : null; }; const artifactForProbe = (stem) => { if (!stem) return null; const runtime = `${stem}.js`; const wasm = `${stem}.wasm`; const data = `${stem}.data`; const runtimePath = path.join(kernelRoot, runtime); const wasmPath = path.join(kernelRoot, wasm); const dataPath = path.join(kernelRoot, data); const pthreadWorker = `${stem}.worker.js`; const pthreadPath = path.join(kernelRoot, pthreadWorker); if (!fs.existsSync(runtimePath) || !fs.existsSync(wasmPath)) return null; const buildProfile = buildProfileForProbe(stem); const hashes = { runtime: digest(runtimePath), wasm: digest(wasmPath), ...(fs.existsSync(dataPath) ? { data: digest(dataPath) } : {}), ...(fs.existsSync(pthreadPath) ? { pthreadWorker: digest(pthreadPath) } : {}), }; return { revision: artifactRevision(hashes), ...(buildProfile ? { buildProfile } : {}), runtimeUrl: `/kernel/${runtime}`, wasmUrl: `/kernel/${wasm}`, ...(fs.existsSync(dataPath) ? { dataUrl: `/kernel/${data}` } : {}), ...(fs.existsSync(pthreadPath) ? { pthreadWorkerUrl: `/kernel/${pthreadWorker}` } : {}), hashes, }; }; const registrySource = fs.existsSync(path.join(webRoot, "src", "module-registry.generated.ts")) ? fs.readFileSync(path.join(webRoot, "src", "module-registry.generated.ts"), "utf8") : ""; const modules = {}; for (const match of registrySource.matchAll(/\{ module: "([^"]+)", workers: (\[[^\]]*\])/g)) { const [, module, workersText] = match; const workers = [...workersText.matchAll(/"([^"]+\.worker\.ts)"/g)].map((item) => item[1]); const workerArtifacts = {}; for (const worker of workers) { const artifact = artifactForProbe(probeForWorker(worker)); if (artifact) workerArtifacts[worker] = artifact; } const revisions = Object.values(workerArtifacts).map((item) => item.revision).sort(); if (revisions.length) { modules[module] = { revision: crypto.createHash("sha256").update(revisions.join("|")).digest("hex").slice(0, 16), assetBaseUrl: "/kernel/", workers: workerArtifacts, }; } } const moduleManifest = { schema: "freecad-web-module-manifest/v1", generatedAt: new Date().toISOString(), policy: { dynamicLoad: true, mergeRequired: false }, modules, }; const manifestStarted = performance.now(); syncMetrics.manifestChanged = writeManifestIfChanged(path.join(moduleRoot, "manifest.json"), moduleManifest); syncMetrics.manifestSeconds = (performance.now() - manifestStarted) / 1000; if (syncStateDirty) { fs.mkdirSync(path.dirname(syncStatePath), { recursive: true }); const temporary = `${syncStatePath}.tmp`; fs.writeFileSync(temporary, `${JSON.stringify(syncState, null, 2)}\n`); fs.renameSync(temporary, syncStatePath); } const timingPath = path.resolve(webRoot, "../docs/current/web-assets-sync-timing.json"); const timing = { schema: "freecad-web-assets-sync-timing/v1", generatedAt: new Date().toISOString(), modules: Object.keys(modules).length, buildProfile: buildProfileForProbe("FreeCADWebCAMProbe") ?? "unknown", missingSources, ...syncMetrics, totalSeconds: (performance.now() - scriptStarted) / 1000, }; fs.writeFileSync(timingPath, `${JSON.stringify(timing, null, 2)}\n`); console.log(JSON.stringify({ modules: timing.modules, manifest: "public/modules/manifest.json", timing: { totalSeconds: timing.totalSeconds, copied: timing.copied, skipped: timing.skipped, missing: timing.missing, hashComputed: timing.hashComputed, hashCacheHits: timing.hashCacheHits, copySeconds: timing.copySeconds, hashSeconds: timing.hashSeconds, manifestSeconds: timing.manifestSeconds, manifestChanged: timing.manifestChanged, }, }));