103 lines
9.2 KiB
JavaScript
103 lines
9.2 KiB
JavaScript
import { readFile } from 'node:fs/promises'
|
|
import { resolve, isAbsolute } from 'node:path'
|
|
import {
|
|
REQUIRED_FREECAD_NAMING_CALLBACKS,
|
|
REQUIRED_FREECAD_NAMING_DEFINITIONS,
|
|
REQUIRED_FREECAD_NAMING_LIBRARIES,
|
|
REQUIRED_FREECAD_NAMING_LINK_DEPENDENCIES,
|
|
exists,
|
|
inspectWasmStaticArchive,
|
|
sha256File,
|
|
} from './freecad-naming-sdk-lib.mjs'
|
|
|
|
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 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 already-published production Worker must be checked through its artifact and production reports.' }, 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.')
|
|
if (manifest.productionPublication !== false || manifest.boundary?.freecadNamingBuildStatus !== 'production-linked' || manifest.boundary?.exTsn02 !== 'completed' || manifest.boundary?.systemExact !== false) fail('manifest must remain a non-publishing SDK manifest while reflecting the completed production linkage and non-exact system boundary.')
|
|
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))
|
|
if (libraryNames.size !== libraries.length) fail('library names must be unique.')
|
|
for (const name of REQUIRED_FREECAD_NAMING_LIBRARIES) if (!libraryNames.has(name)) fail(`libraries must include ${name}.`)
|
|
for (const name of REQUIRED_FREECAD_NAMING_LINK_DEPENDENCIES) 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}`)
|
|
const hostAdapterPath = required(namingBridge.hostAdapter, 'namingBridge.hostAdapter')
|
|
const hostAdapter = isAbsolute(hostAdapterPath) ? hostAdapterPath : resolve(sdkRoot, hostAdapterPath)
|
|
if (!await exists(hostAdapter)) fail(`missing naming host adapter: ${hostAdapter}`)
|
|
if (!Array.isArray(namingBridge.exports) || !REQUIRED_FREECAD_NAMING_CALLBACKS.every((name) => namingBridge.exports.includes(name))) fail('namingBridge.exports must declare all three versioned ABI callbacks.')
|
|
const compileOptions = manifest.compileOptions && typeof manifest.compileOptions === 'object' ? manifest.compileOptions : fail('compileOptions is required.')
|
|
if (compileOptions.cxxStandard !== 'c++20' || compileOptions.pthread !== true || JSON.stringify(compileOptions.definitions) !== JSON.stringify(REQUIRED_FREECAD_NAMING_DEFINITIONS)) fail('compileOptions must lock C++20, pthread and the FreeCAD wasm compatibility definitions.')
|
|
const forceIncludePath = required(compileOptions.forceInclude, 'compileOptions.forceInclude')
|
|
const forceInclude = isAbsolute(forceIncludePath) ? forceIncludePath : resolve(sdkRoot, forceIncludePath)
|
|
if (!await exists(forceInclude)) fail(`missing force-include header: ${forceInclude}`)
|
|
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 (typeof library.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(library.sha256)) fail(`library ${name} requires a lowercase SHA-256.`)
|
|
let archive
|
|
try { archive = await inspectWasmStaticArchive(path) } catch (error) { fail(`library ${name} is not a wasm static archive: ${error instanceof Error ? error.message : String(error)}.`) }
|
|
if (archive.target !== 'wasm32-emscripten') fail(`library ${name} has unexpected target ${archive.target}.`)
|
|
const actualHash = await sha256File(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 sha256File(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}.`)
|
|
const hostAdapterSha = await sha256File(hostAdapter)
|
|
if (typeof namingBridge.hostAdapterSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(namingBridge.hostAdapterSha256) || namingBridge.hostAdapterSha256 !== hostAdapterSha) fail(`naming host adapter hash mismatch: expected ${String(namingBridge.hostAdapterSha256)}, got ${hostAdapterSha}.`)
|
|
const forceIncludeSha = await sha256File(forceInclude)
|
|
if (typeof compileOptions.forceIncludeSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(compileOptions.forceIncludeSha256) || compileOptions.forceIncludeSha256 !== forceIncludeSha) fail(`force-include header hash mismatch: expected ${String(compileOptions.forceIncludeSha256)}, got ${forceIncludeSha}.`)
|
|
const runtimeAssets = Array.isArray(manifest.runtimeAssets) ? manifest.runtimeAssets : fail('runtimeAssets must be an array.')
|
|
if (!runtimeAssets.some((asset) => asset?.name === 'PythonWasmStdlib')) fail('runtimeAssets must include PythonWasmStdlib.')
|
|
for (const asset of runtimeAssets) {
|
|
if (!asset || typeof asset !== 'object') fail('runtimeAssets entries must be objects.')
|
|
const name = required(asset.name, 'runtimeAssets[].name')
|
|
const pathValue = required(asset.path, `runtimeAssets[${name}].path`)
|
|
required(asset.preloadTo, `runtimeAssets[${name}].preloadTo`)
|
|
const assetRoot = isAbsolute(pathValue) ? pathValue : resolve(sdkRoot, pathValue)
|
|
if (!await exists(assetRoot)) fail(`missing runtime asset ${name}: ${assetRoot}`)
|
|
if (!Array.isArray(asset.files) || asset.files.length === 0) fail(`runtime asset ${name} requires hashed files.`)
|
|
for (const file of asset.files) {
|
|
const relativePath = required(file?.path, `runtimeAssets[${name}].files[].path`)
|
|
const filePath = resolve(assetRoot, relativePath)
|
|
if (!await exists(filePath)) fail(`missing runtime asset file ${name}/${relativePath}.`)
|
|
if (typeof file.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(file.sha256)) fail(`runtime asset file ${name}/${relativePath} requires a lowercase SHA-256.`)
|
|
const actualHash = await sha256File(filePath)
|
|
if (actualHash !== file.sha256) fail(`runtime asset file ${name}/${relativePath} hash mismatch: expected ${file.sha256}, got ${actualHash}.`)
|
|
}
|
|
}
|
|
console.log(JSON.stringify({ status: 'sdk-ready', availability: 'candidate-complete', systemExact: false, productionPublication: 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, hostAdapter: hostAdapterPath, hostAdapterSha256: hostAdapterSha, exports: [...namingBridge.exports] }, compileOptions, runtimeAssets, boundary: manifest.boundary }, null, 2))
|