import { spawnSync } from 'node:child_process' import { writeFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') const profile = process.env.FREECAD_ORACLE_PROFILE || 'headless' if (!['headless', 'desktop'].includes(profile)) throw new Error(`Unknown FreeCAD oracle profile: ${profile}`) const localOracle = resolve(root, profile === 'desktop' ? '.cache/freecad/install-desktop/bin/FreeCAD' : '.cache/freecad/install-native/bin/FreeCADCmd') const fallbackOracle = profile === 'desktop' ? ['FreeCAD', 'freecad'] : ['FreeCADCmd', 'freecadcmd'] const referenceCommand = process.env.FREECAD_REFERENCE_CMD || process.env.FREECAD_CMD const candidates = referenceCommand ? [referenceCommand] : [localOracle, ...fallbackOracle] let command = null for (const candidate of candidates) { const probe = spawnSync(candidate, profile === 'desktop' ? ['--console', '--version'] : ['--version'], { encoding: 'utf8', timeout: 15000 }) if (!probe.error || probe.error.code !== 'ENOENT') { command = candidate; break } } if (!command) throw new Error('FreeCADCmd 1.1.1 is unavailable. Set FREECAD_CMD to the locked oracle executable.') // Desktop mode must stay in the normal GUI run mode so the command-line Python // file is processed by the Qt application; --console switches to a blocking // stdin loop and prevents a deterministic probe exit. const sysroot = resolve(root, '.cache/freecad/sysroot') const commandArgs = profile === 'desktop' ? ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), resolve(root, 'scripts/freecad-reference-probe.py')] : [resolve(root, 'scripts/freecad-reference-probe.py')] const runtimeEnv = profile === 'desktop' ? { ...process.env, PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`, LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}`, MATPLOTLIBRC: resolve(sysroot, 'usr/share/matplotlib/mpl-data/matplotlibrc'), MPLBACKEND: 'Agg', } : process.env const desktopNeedsVirtualDisplay = profile === 'desktop' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY const launchCommand = desktopNeedsVirtualDisplay ? 'xvfb-run' : command const launchArgs = desktopNeedsVirtualDisplay ? ['-a', command, ...commandArgs] : commandArgs const execution = spawnSync(launchCommand, launchArgs, { cwd: root, encoding: 'utf8', timeout: 120000, maxBuffer: 20 * 1024 * 1024, env: runtimeEnv, }) const output = `${execution.stdout || ''}\n${execution.stderr || ''}` const resultLine = output.split(/\r?\n/).find((line) => line.startsWith('FREECAD_REFERENCE_RESULT=')) if (execution.error || execution.status !== 0 || !resultLine) { throw new Error(`FreeCAD reference probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`) } const result = JSON.parse(resultLine.slice('FREECAD_REFERENCE_RESULT='.length)) if (result.freecadVersion !== '1.1.1') throw new Error(`Expected FreeCAD 1.1.1, received ${result.freecadVersion}`) const moduleStatuses = new Set(['not-built', 'compiled-importable', 'compiled-import-failure', 'gui-only-unprobeable']) if (!Array.isArray(result.modules) || result.modules.length !== 34 || result.moduleCount !== 34) throw new Error('Reference probe must report the 34 configured FreeCAD modules.') for (const module of result.modules) { if (typeof module.name !== 'string' || typeof module.compiled !== 'boolean' || typeof module.importable !== 'boolean' || typeof module.guiRequired !== 'boolean' || typeof module.guiAvailable !== 'boolean' || !moduleStatuses.has(module.runtimeStatus)) { throw new Error(`Reference probe returned an invalid module status for ${module.name || ''}.`) } } const runtimeObjects = result.runtimeObjects if (!runtimeObjects?.available || runtimeObjects.candidateSource !== 'Document.supportedTypes' || !Array.isArray(runtimeObjects.types)) { throw new Error('Reference probe must include the complete Document.supportedTypes runtime inventory.') } if (runtimeObjects.candidateCount !== runtimeObjects.types.length || runtimeObjects.availableCount + runtimeObjects.unavailableCount !== runtimeObjects.candidateCount) { throw new Error('Reference probe returned inconsistent runtime object counts.') } const runtimeTypeIds = new Set() for (const candidate of runtimeObjects.types) { if (typeof candidate.typeId !== 'string' || !candidate.typeId || runtimeTypeIds.has(candidate.typeId) || !['available', 'unavailable'].includes(candidate.probeStatus)) { throw new Error(`Reference probe returned an invalid runtime object candidate: ${candidate.typeId || ''}.`) } runtimeTypeIds.add(candidate.typeId) if (candidate.available) { if (candidate.probeStatus !== 'available' || typeof candidate.runtimeTypeId !== 'string' || !candidate.runtimeTypeId || !Array.isArray(candidate.properties)) { throw new Error(`Reference probe returned incomplete metadata for ${candidate.typeId}.`) } for (const property of candidate.properties) { if (typeof property.name !== 'string' || typeof property.typeId !== 'string' || !Array.isArray(property.status) || !Object.hasOwn(property, 'default')) { throw new Error(`Reference probe returned invalid property metadata for ${candidate.typeId}.`) } } } else if (candidate.probeStatus !== 'unavailable' || typeof candidate.error !== 'string' || !candidate.error) { throw new Error(`Reference probe did not explain why ${candidate.typeId} is unavailable.`) } } await writeFile(resolve(root, `.cache/freecad/reference-${profile}.json`), `${JSON.stringify(result, null, 2)}\n`) console.log(JSON.stringify({ command: launchCommand === command ? command : `${launchCommand} ${command}`, profile, freecadVersion: result.freecadVersion, moduleStatusSummary: result.moduleStatusSummary, guiCommandCount: result.guiCommands?.commands?.length ?? 0, runtimeObjectSummary: { candidateCount: runtimeObjects.candidateCount, availableCount: runtimeObjects.availableCount, unavailableCount: runtimeObjects.unavailableCount, propertyCount: runtimeObjects.types.reduce((total, candidate) => total + (candidate.properties?.length ?? 0), 0), }, report: `.cache/freecad/reference-${profile}.json`, }, null, 2))