113 lines
7.6 KiB
JavaScript
113 lines
7.6 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import { access, readFile, stat } from 'node:fs/promises'
|
|
import { execFileSync } from 'node:child_process'
|
|
import { resolve } from 'node:path'
|
|
|
|
const root = resolve(new URL('..', import.meta.url).pathname)
|
|
const readJson = async (relative) => JSON.parse(await readFile(resolve(root, relative), 'utf8'))
|
|
const fail = (message) => { throw new Error(`FreeCAD desktop oracle: ${message}`) }
|
|
const config = await readJson('config/freecad-desktop-oracle.json')
|
|
const toolchain = await readJson('config/freecad-toolchain.json')
|
|
const source = resolve(root, '.cache/freecad/FreeCAD')
|
|
const build = resolve(root, config.buildDirectory)
|
|
const executable = resolve(root, `${config.installDirectory}/bin/FreeCAD`)
|
|
const probeReport = resolve(root, '.cache/freecad/reference-desktop.json')
|
|
|
|
const sourceRevision = execFileSync('git', ['-C', source, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim()
|
|
if (sourceRevision !== toolchain.source.commit) fail(`source revision ${sourceRevision} does not match ${toolchain.source.commit}.`)
|
|
|
|
const cache = await readFile(resolve(build, 'CMakeCache.txt'), 'utf8').catch(() => fail('CMakeCache.txt is missing; configure the desktop profile first.'))
|
|
const cacheValue = (name) => {
|
|
const line = cache.split(/\r?\n/).find((entry) => entry.startsWith(`${name}:`))
|
|
return line?.slice(line.indexOf('=') + 1)
|
|
}
|
|
const expectedOn = {
|
|
AddonManager: 'BUILD_ADDONMGR', Assembly: 'BUILD_ASSEMBLY', BIM: 'BUILD_BIM', CAM: 'BUILD_CAM',
|
|
Draft: 'BUILD_DRAFT', Fem: 'BUILD_FEM', Help: 'BUILD_HELP', Idf: 'BUILD_IDF', Import: 'BUILD_IMPORT',
|
|
Inspection: 'BUILD_INSPECTION', Material: 'BUILD_MATERIAL', Measure: 'BUILD_MEASURE', Mesh: 'BUILD_MESH',
|
|
MeshPart: 'BUILD_MESH_PART', OpenSCAD: 'BUILD_OPENSCAD', Part: 'BUILD_PART', PartDesign: 'BUILD_PART_DESIGN',
|
|
Plot: 'BUILD_PLOT', Points: 'BUILD_POINTS', ReverseEngineering: 'BUILD_REVERSEENGINEERING', Robot: 'BUILD_ROBOT',
|
|
Show: 'BUILD_SHOW', Sketcher: 'BUILD_SKETCHER', Spreadsheet: 'BUILD_SPREADSHEET', Start: 'BUILD_START',
|
|
Surface: 'BUILD_SURFACE', TechDraw: 'BUILD_TECHDRAW', Test: 'BUILD_TEST', Tux: 'BUILD_TUX', Web: 'BUILD_WEB',
|
|
}
|
|
const expectedOff = {
|
|
Cloud: 'BUILD_CLOUD', JtReader: 'BUILD_JTREADER', Sandbox: 'BUILD_SANDBOX',
|
|
}
|
|
if (cacheValue('BUILD_GUI') !== 'ON') fail('BUILD_GUI is not ON.')
|
|
for (const [module, key] of Object.entries(expectedOn)) if (cacheValue(key) !== 'ON') fail(`${module} requires ${key}=ON; got ${cacheValue(key) ?? '<missing>'}.`)
|
|
for (const [module, key] of Object.entries(expectedOff)) if (cacheValue(key) !== 'OFF') fail(`${module} requires ${key}=OFF; got ${cacheValue(key) ?? '<missing>'}.`)
|
|
|
|
await access(executable).catch(() => fail(`installed executable is missing: ${executable}`))
|
|
const versionOutput = execFileSync(executable, ['--console', '--version'], {
|
|
cwd: root,
|
|
encoding: 'utf8',
|
|
env: {
|
|
...process.env,
|
|
PYTHONPATH: `${resolve(root, config.sysrootDirectory, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`,
|
|
LD_LIBRARY_PATH: `${resolve(root, config.sysrootDirectory, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}`,
|
|
},
|
|
}).trim()
|
|
if (!versionOutput.includes('FreeCAD 1.1.1')) fail(`version output does not report FreeCAD 1.1.1: ${versionOutput}`)
|
|
const bytes = await readFile(executable)
|
|
const artifact = {
|
|
path: `${config.installDirectory}/bin/FreeCAD`,
|
|
sizeBytes: (await stat(executable)).size,
|
|
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
reportedVersion: versionOutput.split(/\r?\n/)[0],
|
|
}
|
|
if (config.artifact?.sha256 && config.artifact.sha256 !== artifact.sha256) fail(`artifact checksum mismatch: expected ${config.artifact.sha256}, got ${artifact.sha256}.`)
|
|
|
|
const report = await readJson('.cache/freecad/reference-desktop.json').catch(() => null)
|
|
if (!report) fail('34-module desktop probe report is missing; run probe:freecad-reference with FREECAD_ORACLE_PROFILE=desktop.')
|
|
if (report.baselineId !== config.baselineId || report.freecadVersion !== '1.1.1' || report.guiUp !== true || report.moduleCount !== 34 || report.modules?.length !== 34) {
|
|
fail('desktop probe report is not a GUI-up 34-module FreeCAD 1.1.1 report.')
|
|
}
|
|
const invalid = report.modules.filter((module) => !['compiled-importable', 'compiled-import-failure', 'gui-only-unprobeable', 'not-built'].includes(module.runtimeStatus))
|
|
if (invalid.length) fail(`probe contains invalid module statuses: ${invalid.map((module) => module.name).join(', ')}`)
|
|
const expectedObjects = ['Part::Box', 'Part::Cylinder', 'Part::Sphere', 'Part::Cone', 'Part::Feature', 'PartDesign::Feature', 'Sketcher::SketchObject']
|
|
if (!Array.isArray(report.objects) || report.objects.length !== expectedObjects.length) fail('desktop probe must include the seven core object fixtures.')
|
|
for (const typeId of expectedObjects) {
|
|
const object = report.objects.find((candidate) => candidate.runtimeTypeId === typeId && candidate.typeId === typeId)
|
|
if (!object?.available || !Array.isArray(object.properties)) fail(`runtime object fixture ${typeId} is missing or unavailable.`)
|
|
}
|
|
const propertyFlags = report.objects.flatMap((object) => object.properties || []).map((property) => property.status || [])
|
|
if (!propertyFlags.some((status) => status.includes('Hidden')) || !propertyFlags.some((status) => status.includes('Output'))) {
|
|
fail('runtime property fixtures must include Hidden and Output status flags.')
|
|
}
|
|
const runtimeObjects = report.runtimeObjects
|
|
if (!runtimeObjects?.available || runtimeObjects.candidateSource !== 'Document.supportedTypes' || !Array.isArray(runtimeObjects.types) || runtimeObjects.candidateCount !== runtimeObjects.types.length) {
|
|
fail('complete Document.supportedTypes runtime object evidence is missing.')
|
|
}
|
|
if (runtimeObjects.availableCount + runtimeObjects.unavailableCount !== runtimeObjects.candidateCount) fail('runtime object summary counts are inconsistent.')
|
|
const runtimeTypeIds = new Set()
|
|
for (const candidate of runtimeObjects.types) {
|
|
if (!candidate.typeId || runtimeTypeIds.has(candidate.typeId)) fail(`runtime object TypeId '${candidate.typeId || '<missing>'}' is invalid or duplicated.`)
|
|
runtimeTypeIds.add(candidate.typeId)
|
|
if (candidate.available) {
|
|
if (candidate.probeStatus !== 'available' || typeof candidate.runtimeTypeId !== 'string' || !candidate.runtimeTypeId || !Array.isArray(candidate.properties)) fail(`${candidate.typeId} lacks runtime property metadata.`)
|
|
if (candidate.properties.some((property) => !property.name || !property.typeId || !Array.isArray(property.status) || !Object.hasOwn(property, 'default'))) fail(`${candidate.typeId} has an incomplete property record.`)
|
|
} else if (candidate.probeStatus !== 'unavailable' || !candidate.error) fail(`${candidate.typeId} lacks an explicit instantiation failure.`)
|
|
}
|
|
for (const typeId of expectedObjects) if (!runtimeTypeIds.has(typeId)) fail(`core TypeId ${typeId} is absent from Document.supportedTypes.`)
|
|
const runtimePropertyCount = runtimeObjects.types.reduce((total, candidate) => total + (candidate.properties?.length ?? 0), 0)
|
|
|
|
console.log(JSON.stringify({
|
|
status: 'freecad-desktop-oracle-pass',
|
|
baselineId: config.baselineId,
|
|
sourceRevision,
|
|
buildDirectory: config.buildDirectory,
|
|
installDirectory: config.installDirectory,
|
|
artifact,
|
|
probe: {
|
|
path: '.cache/freecad/reference-desktop.json',
|
|
moduleCount: report.moduleCount,
|
|
guiUp: report.guiUp,
|
|
moduleStatusSummary: report.moduleStatusSummary,
|
|
objectFixtureCount: report.objects.length,
|
|
registeredObjectTypeCount: runtimeObjects.candidateCount,
|
|
runtimeObjectCount: runtimeObjects.availableCount,
|
|
unavailableObjectCount: runtimeObjects.unavailableCount,
|
|
runtimePropertyCount,
|
|
},
|
|
}, null, 2))
|