Files
Web_FreeCAD_Bitbybit/scripts/check-camotics-native.mjs

80 lines
6.2 KiB
JavaScript

import { createHash } from 'node:crypto'
import { execFileSync, spawnSync } from 'node:child_process'
import { readFile, stat } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const evidence = JSON.parse(await readFile(resolve(root, 'config/camotics-native-artifact.json'), 'utf8'))
const sha256 = (value) => createHash('sha256').update(value).digest('hex')
const run = (command, args, options = {}) => {
const result = spawnSync(command, args, { encoding: 'utf8', ...options })
if (result.error || result.status !== 0) throw new Error(`${command} failed: ${result.error?.message || result.stderr || result.stdout}`)
return result
}
if (evidence.schemaVersion !== 2 || evidence.status !== 'pass') throw new Error('CAMotics native evidence is not passing.')
const camoticsPath = resolve(root, evidence.source.path)
const camoticsRevision = execFileSync('git', ['-C', camoticsPath, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim()
if (camoticsRevision !== evidence.source.revision) throw new Error(`CAMotics source revision drift: ${camoticsRevision}`)
const cbangPath = resolve(root, evidence.cbang.path)
const cbangRevision = execFileSync('git', ['-C', cbangPath, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim()
if (cbangRevision !== evidence.cbang.revision) throw new Error(`C! source revision drift: ${cbangRevision}`)
if (execFileSync('git', ['-C', cbangPath, 'status', '--porcelain'], { encoding: 'utf8' }).trim() !== '') throw new Error('Pinned C! checkout is dirty.')
const cbangSConstruct = await readFile(resolve(root, 'CAMotics/cbang/SConstruct'), 'utf8')
if (!cbangSConstruct.includes(`version = '${evidence.cbang.version}'`)) throw new Error('C! version does not match native evidence.')
if (!evidence.build.options.includes('v8_compress_pointers=0') || evidence.build.withGui !== true || evidence.build.withTpl !== true || evidence.build.browserExecutable !== false || evidence.pipelineBoundary.workspaceNativeCliAvailable !== true) throw new Error('CAMotics native build boundary is invalid.')
for (const [path, expected] of Object.entries(evidence.artifacts)) {
const absolute = resolve(root, path)
const artifact = await readFile(absolute)
const metadata = await stat(absolute)
if (!metadata.isFile() || (metadata.mode & 0o111) === 0) throw new Error(`CAMotics artifact is not executable: ${path}`)
if (artifact.byteLength !== expected.bytes || sha256(artifact) !== expected.sha256) throw new Error(`CAMotics artifact size or digest mismatch: ${path}`)
if (artifact.subarray(0, 4).toString('ascii') !== '\x7fELF') throw new Error(`CAMotics artifact is not an ELF executable: ${path}`)
}
for (const [path, expected] of Object.entries(evidence.cliArtifacts ?? {})) {
const absolute = resolve(root, path)
const artifact = await readFile(absolute)
const metadata = await stat(absolute)
if (!metadata.isFile() || (metadata.mode & 0o111) === 0 || artifact.byteLength !== expected.bytes || sha256(artifact) !== expected.sha256) throw new Error(`CAMotics CLI artifact size or digest mismatch: ${path}`)
if (artifact.subarray(0, 4).toString('ascii') !== '\x7fELF') throw new Error(`CAMotics CLI artifact is not an ELF file: ${path}`)
}
const camoticsVersionProbe = run(resolve(root, 'CAMotics/camotics'), ['--version'])
const tplVersionProbe = run(resolve(root, 'CAMotics/tplang'), ['--version'])
const camoticsVersion = `${camoticsVersionProbe.stdout}${camoticsVersionProbe.stderr}`.trim()
const tplVersion = `${tplVersionProbe.stdout}${tplVersionProbe.stderr}`.trim()
if (camoticsVersion !== evidence.source.version || tplVersion !== evidence.source.version) throw new Error(`CAMotics version probe failed: ${camoticsVersion}/${tplVersion}`)
const tplTests = run(resolve(root, 'CAMotics/tests/testHarness'), ['--no-color'], { cwd: resolve(root, 'CAMotics/tests/tplTests') })
const passed = Number(tplTests.stdout.match(/^Passed\s+(\d+)/m)?.[1] ?? -1)
const failed = Number(tplTests.stdout.match(/^Failed\s+(\d+)/m)?.[1] ?? -1)
if (passed !== evidence.tplTests.passed || failed !== evidence.tplTests.failed) throw new Error(`TPL test result drift: ${passed}/${failed}`)
const tplExample = run(resolve(root, 'CAMotics/tplang'), [resolve(root, 'CAMotics/examples/box/box.tpl')])
const tplBytes = Buffer.from(tplExample.stdout)
if (tplBytes.byteLength !== evidence.tplExample.bytes || sha256(tplBytes) !== evidence.tplExample.sha256) throw new Error('TPL example output drift.')
const nativeLinks = run('ldd', [resolve(root, 'CAMotics/camotics')]).stdout
for (const required of ['libQt5Widgets', 'libQt5WebSockets', 'libQt5Network', 'libQt5Core', 'libnode']) if (!nativeLinks.includes(required)) throw new Error(`CAMotics GUI is missing runtime dependency ${required}.`)
if (evidence.guiSmoke.status !== 'pass' || evidence.guiSmoke.window.mapState !== 'IsViewable' || evidence.guiSmoke.window.width !== 1200 || evidence.guiSmoke.window.height !== 800 || evidence.guiSmoke.window.depth !== 24 || evidence.guiSmoke.processAliveBeforeTerminate !== true || evidence.guiSmoke.browserRuntime !== false) throw new Error('CAMotics GUI smoke boundary is invalid.')
if (evidence.pipelineBoundary.workspaceNativeGuiAvailable !== true || evidence.pipelineBoundary.workspaceNativeTplAvailable !== true || evidence.pipelineBoundary.workspaceNativeCliAvailable !== true || evidence.pipelineBoundary.browserNativeGuiAvailable !== false || evidence.pipelineBoundary.browserNativeTplAvailable !== false || evidence.pipelineBoundary.canonicalGcodeParser !== 'linuxcnc-wasm' || evidence.pipelineBoundary.camoticsParsesCanonicalPipelineGcode !== false) throw new Error('CAMotics/LinuxCNC authority boundary is invalid.')
for (const [path, smoke] of Object.entries(evidence.cliSmoke ?? {})) if (smoke.exitCode !== 0 || smoke.containsUsage !== true) throw new Error(`CAMotics CLI smoke failed: ${path}`)
console.log(JSON.stringify({
status: 'camotics-native-pass',
camoticsRevision,
cbangRevision,
versions: { camotics: camoticsVersion, tplang: tplVersion },
artifacts: evidence.artifacts,
cliArtifacts: evidence.cliArtifacts,
cliSmoke: evidence.cliSmoke,
tplTests: { passed, failed },
tplExample: evidence.tplExample,
guiSmoke: evidence.guiSmoke,
pipelineBoundary: evidence.pipelineBoundary,
}, null, 2))