import { access, readFile } from 'node:fs/promises' import { createHash } from 'node:crypto' import { resolve, isAbsolute } from 'node:path' const root = resolve(new URL('..', import.meta.url).pathname) const sdkRoot = process.env.FREECAD_WASM_SDK_DIR ? resolve(root, process.env.FREECAD_WASM_SDK_DIR) : '' const required = (value, name) => { if (typeof value !== 'string' || !value.trim()) throw new Error(`FreeCAD WASM SDK manifest requires ${name}.`) return value.trim() } const exists = async (path) => access(path).then(() => true).catch(() => false) const sha256 = async (path) => createHash('sha256').update(await readFile(path)).digest('hex') const fail = (message) => { throw new Error(`FreeCAD WASM naming SDK: ${message}`) } if (!sdkRoot) { console.log(JSON.stringify({ status: 'sdk-not-configured', availability: 'unavailable', systemExact: false, reason: 'FREECAD_WASM_SDK_DIR is not configured; the shipped Worker remains OCCT-only.' }, null, 2)) process.exit(0) } const manifestPath = resolve(sdkRoot, 'manifest.json') if (!await exists(manifestPath)) fail(`missing manifest: ${manifestPath}`) let manifest try { manifest = JSON.parse(await readFile(manifestPath, 'utf8')) } catch (error) { fail(`manifest is not valid JSON: ${error instanceof Error ? error.message : String(error)}`) } if (manifest.schemaVersion !== 1) fail(`unsupported manifest schema ${String(manifest.schemaVersion)}.`) if (required(manifest.freecadVersion, 'freecadVersion') !== '1.1.1') fail('freecadVersion must be 1.1.1.') if (required(manifest.sourceCommit, 'sourceCommit') !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('sourceCommit is not the locked FreeCAD commit.') if (required(manifest.emscriptenVersion, 'emscriptenVersion') !== '3.1.69') fail('emscriptenVersion must be 3.1.69.') if (required(manifest.qtTarget, 'qtTarget') !== 'wasm32-emscripten' || required(manifest.pythonTarget, 'pythonTarget') !== 'wasm32-emscripten') fail('Qt and Python must both be wasm32-emscripten targets.') const includeDirs = Array.isArray(manifest.includeDirs) ? manifest.includeDirs : fail('includeDirs must be an array.') const resolvedIncludeDirs = includeDirs.map((includeDir) => isAbsolute(includeDir) ? includeDir : resolve(sdkRoot, includeDir)) const libraries = Array.isArray(manifest.libraries) ? manifest.libraries : fail('libraries must be an array.') const libraryNames = new Set(libraries.map((library) => library?.name)) for (const name of ['FreeCADBase', 'FreeCADApp', 'Part', 'QtCore', 'Python']) if (!libraryNames.has(name)) fail(`libraries must include ${name}.`) const namingBridge = manifest.namingBridge && typeof manifest.namingBridge === 'object' ? manifest.namingBridge : fail('namingBridge is required.') const bridgePath = required(namingBridge.source, 'namingBridge.source') const bridge = isAbsolute(bridgePath) ? bridgePath : resolve(sdkRoot, bridgePath) if (!await exists(bridge)) fail(`missing naming bridge source: ${bridge}`) if (!Array.isArray(namingBridge.exports) || !['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson'].every((name) => namingBridge.exports.includes(name))) fail('namingBridge.exports must declare all three versioned ABI callbacks.') for (const path of resolvedIncludeDirs) { if (!await exists(path)) fail(`missing include directory: ${path}`) } for (const library of libraries) { if (!library || typeof library !== 'object') fail('libraries entries must be objects.') const name = required(library.name, 'libraries[].name') const pathValue = required(library.path, `libraries[${name}].path`) const path = isAbsolute(pathValue) ? pathValue : resolve(sdkRoot, pathValue) if (!path.endsWith('.a')) fail(`library ${name} must be a static .a archive.`) if (!await exists(path)) fail(`missing static library ${name}: ${path}`) if ((await readFile(path)).byteLength === 0) fail(`static library ${name} is empty.`) if (typeof library.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(library.sha256)) fail(`library ${name} requires a lowercase SHA-256.`) const actualHash = await sha256(path) if (actualHash !== library.sha256) fail(`library ${name} hash mismatch: expected ${library.sha256}, got ${actualHash}.`) } for (const header of ['App/StringHasher.h', 'App/MappedName.h', 'App/ElementMap.h']) { if (!(await Promise.all(resolvedIncludeDirs.map((includeDir) => exists(resolve(includeDir, header))))).some(Boolean)) fail(`missing locked FreeCAD private header in includeDirs: ${header}`) } const bridgeSha = await sha256(bridge) if (typeof namingBridge.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(namingBridge.sha256)) fail('namingBridge.sha256 requires a lowercase SHA-256.') if (namingBridge.sha256 !== bridgeSha) fail(`naming bridge hash mismatch: expected ${namingBridge.sha256}, got ${bridgeSha}.`) console.log(JSON.stringify({ status: 'sdk-ready', availability: 'available', systemExact: false, sdkRoot, freecadVersion: manifest.freecadVersion, sourceCommit: manifest.sourceCommit, emscriptenVersion: manifest.emscriptenVersion, includeDirs, libraries: libraries.map(({ name, path, sha256 }) => ({ name, path, sha256 })), namingBridge: { source: bridgePath, sha256: bridgeSha, exports: [...namingBridge.exports] } }, null, 2))