360 lines
17 KiB
JavaScript
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,
|
|
},
|
|
}));
|