80 lines
6.2 KiB
JavaScript
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))
|