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) ?? ''}.`) for (const [module, key] of Object.entries(expectedOff)) if (cacheValue(key) !== 'OFF') fail(`${module} requires ${key}=OFF; got ${cacheValue(key) ?? ''}.`) 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 || ''}' 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))