143 lines
7.3 KiB
JavaScript
143 lines
7.3 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import { execFileSync, spawnSync } from 'node:child_process'
|
|
import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join, resolve } from 'node:path'
|
|
|
|
const root = resolve(new URL('..', import.meta.url).pathname)
|
|
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
|
|
}
|
|
const artifactInfo = async (path) => {
|
|
const bytes = await readFile(resolve(root, path))
|
|
const metadata = await stat(resolve(root, path))
|
|
return { bytes: bytes.byteLength, sha256: sha256(bytes), mode: metadata.mode.toString(8) }
|
|
}
|
|
|
|
const camoticsRevision = execFileSync('git', ['-C', resolve(root, 'CAMotics'), 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim()
|
|
const cbangRevision = execFileSync('git', ['-C', resolve(root, '.cache/camotics-native/cbang'), 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim()
|
|
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 !== tplVersion) throw new Error(`CAMotics/TPL version mismatch: ${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 < 0 || failed < 0) throw new Error('Could not parse CAMotics TPL test output.')
|
|
|
|
const tplExample = run(resolve(root, 'CAMotics/tplang'), [resolve(root, 'CAMotics/examples/box/box.tpl')])
|
|
const tplExampleBytes = Buffer.from(tplExample.stdout)
|
|
const guiDir = await mkdtemp(join(tmpdir(), 'camotics-native-gui-'))
|
|
let guiSmoke
|
|
try {
|
|
const gui = spawnSync('xvfb-run', ['-a', '-s', '-screen 0 1280x800x24 +extension GLX', 'bash', '-lc', `
|
|
set -u
|
|
smoke_dir="$CAMOTICS_SMOKE_DIR"
|
|
export XDG_CONFIG_HOME="$smoke_dir/config" XDG_CACHE_HOME="$smoke_dir/cache" XDG_DATA_HOME="$smoke_dir/data" LIBGL_ALWAYS_SOFTWARE=1
|
|
mkdir -p "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" "$XDG_DATA_HOME"
|
|
"$CAMOTICS_BINARY" >"$smoke_dir/stdout" 2>"$smoke_dir/stderr" &
|
|
pid=$!
|
|
found=0
|
|
for _ in $(seq 1 100); do
|
|
if xdotool search --onlyvisible --name "^CAMotics$" >/dev/null 2>&1; then found=1; break; fi
|
|
sleep 0.1
|
|
done
|
|
if [ "$found" -eq 1 ]; then
|
|
wid=$(xdotool search --onlyvisible --name "^CAMotics$" | head -n1)
|
|
xwininfo -id "$wid" >"$smoke_dir/xwininfo"
|
|
fi
|
|
alive=0
|
|
kill -0 "$pid" 2>/dev/null && alive=1
|
|
kill -TERM "$pid" 2>/dev/null || true
|
|
wait "$pid" 2>/dev/null || true
|
|
printf '%s\n' "$found" >"$smoke_dir/found"
|
|
printf '%s\n' "$alive" >"$smoke_dir/alive"
|
|
`], {
|
|
encoding: 'utf8',
|
|
env: { ...process.env, CAMOTICS_SMOKE_DIR: guiDir, CAMOTICS_BINARY: resolve(root, 'CAMotics/camotics') },
|
|
timeout: 30_000,
|
|
})
|
|
if (gui.error || gui.status !== 0) throw new Error(`Xvfb GUI smoke failed: ${gui.error?.message || gui.stderr || gui.stdout}`)
|
|
const found = (await readFile(join(guiDir, 'found'), 'utf8')).trim() === '1'
|
|
const alive = (await readFile(join(guiDir, 'alive'), 'utf8')).trim() === '1'
|
|
const startupStdout = await readFile(join(guiDir, 'stdout'), 'utf8')
|
|
const startupStderr = await readFile(join(guiDir, 'stderr'), 'utf8')
|
|
const xwininfo = found ? await readFile(join(guiDir, 'xwininfo'), 'utf8') : ''
|
|
const numberFrom = (label) => Number(xwininfo.match(new RegExp(`^ ${label}:\\s+(\\d+)`, 'm'))?.[1] ?? -1)
|
|
const mapState = xwininfo.match(/^ Map State:\s+(.+)$/m)?.[1]?.trim() ?? ''
|
|
guiSmoke = {
|
|
status: found && alive && mapState === 'IsViewable' && !/Fatal error|FATAL/i.test(startupStderr) ? 'pass' : 'fail',
|
|
processAliveBeforeTerminate: alive,
|
|
browserRuntime: false,
|
|
window: { title: xwininfo.match(/^xwininfo: Window id: \S+ "([^"]+)"$/m)?.[1] ?? '', width: numberFrom('Width'), height: numberFrom('Height'), depth: numberFrom('Depth'), mapState },
|
|
startupBannerSha256: sha256(Buffer.from(startupStdout)),
|
|
startupStderrSha256: sha256(Buffer.from(startupStderr)),
|
|
}
|
|
} finally {
|
|
await rm(guiDir, { recursive: true, force: true })
|
|
}
|
|
if (guiSmoke.status !== 'pass') throw new Error(`CAMotics GUI smoke did not pass: ${JSON.stringify(guiSmoke)}`)
|
|
|
|
const artifacts = {}
|
|
for (const path of ['CAMotics/camotics', 'CAMotics/tplang']) artifacts[path] = await artifactInfo(path)
|
|
const cliArtifacts = {}
|
|
for (const path of ['CAMotics/camsim', 'CAMotics/gcodetool', 'CAMotics/planner', 'CAMotics/build/camotics.so']) cliArtifacts[path] = await artifactInfo(path)
|
|
const cliSmoke = {}
|
|
for (const path of ['CAMotics/camsim', 'CAMotics/gcodetool', 'CAMotics/planner']) {
|
|
const probe = run(resolve(root, path), ['--help'])
|
|
cliSmoke[path] = { exitCode: probe.status, containsUsage: `${probe.stdout}${probe.stderr}`.includes('Usage:') }
|
|
}
|
|
const config = {
|
|
schemaVersion: 2,
|
|
status: 'pass',
|
|
source: {
|
|
path: 'CAMotics',
|
|
repository: 'https://github.com/CauldronDevelopmentLLC/CAMotics.git',
|
|
revision: camoticsRevision,
|
|
version: camoticsVersion,
|
|
},
|
|
cbang: {
|
|
path: '.cache/camotics-native/cbang',
|
|
repository: 'https://github.com/CauldronDevelopmentLLC/cbang.git',
|
|
revision: cbangRevision,
|
|
version: '1.7.2',
|
|
v8PointerCompression: false,
|
|
},
|
|
build: {
|
|
mode: 'release-native-qt5-tpl-cli',
|
|
compiler: execFileSync('g++', ['--version'], { encoding: 'utf8' }).split('\n')[0],
|
|
sconsVersion: execFileSync('scons', ['--version'], { encoding: 'utf8' }).split('\n')[0],
|
|
nodeV8PackageVersion: execFileSync('dpkg-query', ['-W', '-f=${Version}', 'libnode-dev'], { encoding: 'utf8' }).trim(),
|
|
qtVersion: execFileSync('qmake', ['-query', 'QT_VERSION'], { encoding: 'utf8' }).trim(),
|
|
options: ['strict=0', 'cxxstd=c++17', 'v8_compress_pointers=0'],
|
|
withGui: true,
|
|
withTpl: true,
|
|
browserExecutable: false,
|
|
},
|
|
artifacts,
|
|
cliArtifacts,
|
|
cliSmoke,
|
|
tplTests: { command: 'cd CAMotics/tests/tplTests && ../testHarness --no-color', passed, failed, outputSha256: sha256(Buffer.from(tplTests.stdout)) },
|
|
tplExample: { input: 'CAMotics/examples/box/box.tpl', bytes: tplExampleBytes.byteLength, lines: tplExample.stdout.trimEnd().split('\n').length, sha256: sha256(tplExampleBytes) },
|
|
guiSmoke,
|
|
pipelineBoundary: {
|
|
workspaceNativeGuiAvailable: true,
|
|
workspaceNativeTplAvailable: true,
|
|
workspaceNativeCliAvailable: true,
|
|
browserNativeGuiAvailable: false,
|
|
browserNativeTplAvailable: false,
|
|
nativeRole: 'host-native-qt5-tpl-cli-sidecar',
|
|
generationAuthority: 'camotics-source-stage',
|
|
canonicalGcodeParser: 'linuxcnc-wasm',
|
|
camoticsParsesCanonicalPipelineGcode: false,
|
|
order: ['cad', 'opencamlib-wasm', 'camotics-source-stage', 'gcode', 'linuxcnc-wasm-parse-execute'],
|
|
},
|
|
}
|
|
await writeFile(resolve(root, 'config/camotics-native-artifact.json'), `${JSON.stringify(config, null, 2)}\n`)
|
|
console.log(JSON.stringify(config, null, 2))
|