Files
FreeCADWasmDev/web/tools/sync-assets.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

360 lines
17 KiB
JavaScript

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