import { spawnSync } from 'node:child_process' import { createHash } from 'node:crypto' import { readFile, 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 shardCount = Number(process.env.FREECAD_GUI_COMMAND_SHARD_COUNT || 100) if (!Number.isInteger(shardCount) || shardCount < 2 || shardCount > 256) throw new Error('FREECAD_GUI_COMMAND_SHARD_COUNT must be an integer from 2 through 256.') const mergeOnly = process.argv.includes('--merge-only') const shardArgument = process.argv.find((argument) => argument.startsWith('--shard=')) const requestedShard = shardArgument ? Number(shardArgument.slice('--shard='.length)) : null if (mergeOnly && shardArgument) throw new Error('--merge-only and --shard cannot be used together.') if (shardArgument && (!Number.isInteger(requestedShard) || requestedShard < 0 || requestedShard >= shardCount)) throw new Error(`--shard must be an integer from 0 through ${shardCount - 1}.`) if (!mergeOnly) { const shardIndexes = requestedShard == null ? Array.from({ length: shardCount }, (_, index) => index) : [requestedShard] for (const index of shardIndexes) { console.log(`[freecad-gui-command-shards] execute ${index + 1}/${shardCount}`) const execution = spawnSync(process.execPath, ['scripts/run-freecad-reference-probe.mjs'], { cwd: root, env: { ...process.env, FREECAD_ORACLE_PROFILE: 'desktop', FREECAD_REFERENCE_SCOPE: 'gui-commands', FREECAD_GUI_COMMAND_SHARD: `${index}/${shardCount}`, }, stdio: 'inherit', }) if (execution.status !== 0) throw new Error(`FreeCAD GUI command shard ${index}/${shardCount} failed with exit ${execution.status}.`) } } if (requestedShard != null) { console.log(JSON.stringify({ status: 'freecad-gui-command-shard-pass', shard: requestedShard, shardCount }, null, 2)) process.exit(0) } const shardReports = [] for (let index = 0; index < shardCount; index += 1) { const relativePath = `.cache/freecad/reference-desktop-gui-commands-${index}-of-${shardCount}.json` const bytes = await readFile(resolve(root, relativePath)) shardReports.push({ relativePath, bytes, report: JSON.parse(bytes) }) } const first = shardReports[0].report const universeCount = first.guiCommands?.commandUniverseCount const universeSha256 = first.guiCommands?.commandUniverseSha256 const commands = [] let expectedStart = 0 for (const [index, { report }] of shardReports.entries()) { const gui = report.guiCommands if (report.baselineId !== 'freecad-1.1.1' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.probeScope !== 'gui-commands') throw new Error(`GUI command shard ${index} has the wrong baseline.`) if (gui?.shard?.index !== index || gui.shard.count !== shardCount || gui.shard.start !== expectedStart || gui.shard.end <= gui.shard.start) throw new Error(`GUI command shard ${index} has a non-contiguous range.`) if (gui.commandUniverseCount !== universeCount || gui.commandUniverseSha256 !== universeSha256) throw new Error(`GUI command shard ${index} has a different command universe.`) if (gui.stateCommandCount !== gui.commands?.length || gui.stateCommandCount !== gui.shard.end - gui.shard.start) throw new Error(`GUI command shard ${index} has an incomplete state range.`) if (JSON.stringify(report.modules) !== JSON.stringify(first.modules) || JSON.stringify(gui.workbenches) !== JSON.stringify(first.guiCommands.workbenches)) throw new Error(`GUI command shard ${index} changed module or workbench registration evidence.`) commands.push(...gui.commands) expectedStart = gui.shard.end } if (expectedStart !== universeCount || commands.length !== universeCount || new Set(commands.map((command) => command.id)).size !== universeCount) throw new Error('Merged GUI command shards do not cover the command universe exactly once.') const mergedUniverseSha256 = createHash('sha256').update(commands.map((command) => command.id).join('\n')).digest('hex') if (mergedUniverseSha256 !== universeSha256) throw new Error('Merged GUI command order does not match the declared command universe hash.') const merged = { ...first, guiCommands: { ...first.guiCommands, stateCommandCount: commands.length, shard: null, commands, mergedShards: shardReports.map(({ relativePath, bytes, report }) => ({ path: relativePath, sha256: createHash('sha256').update(bytes).digest('hex'), start: report.guiCommands.shard.start, end: report.guiCommands.shard.end, })), }, } const output = resolve(root, '.cache/freecad/reference-desktop-gui-commands.json') await writeFile(output, `${JSON.stringify(merged, null, 2)}\n`) console.log(JSON.stringify({ status: 'freecad-gui-command-shards-merged', shardCount, commandCount: commands.length, commandUniverseSha256: universeSha256, output }, null, 2))