feat: expand oracle and topology evidence gates
This commit is contained in:
2
.github/workflows/real-verification.yml
vendored
2
.github/workflows/real-verification.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
- run: ./npmw ci --ignore-scripts
|
||||
- run: ./npmw run ci:real -- --lane=oracle
|
||||
env:
|
||||
FREECAD_ORACLE_PROFILE: headless
|
||||
FREECAD_ORACLE_PROFILE: desktop
|
||||
|
||||
wasm:
|
||||
runs-on: [self-hosted, linux, cad]
|
||||
|
||||
@@ -13,7 +13,22 @@
|
||||
"fuse",
|
||||
"cut",
|
||||
"common",
|
||||
"pad"
|
||||
"rotate",
|
||||
"pad",
|
||||
"pocket",
|
||||
"loft",
|
||||
"pipe",
|
||||
"revolution",
|
||||
"groove",
|
||||
"fillet",
|
||||
"chamfer",
|
||||
"hole",
|
||||
"draft",
|
||||
"thickness",
|
||||
"linear-pattern",
|
||||
"polar-pattern",
|
||||
"mirrored",
|
||||
"multi-transform"
|
||||
],
|
||||
"transport": "step-text"
|
||||
},
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"fuse",
|
||||
"cut",
|
||||
"common",
|
||||
"rotate",
|
||||
"pad",
|
||||
"pocket",
|
||||
"loft",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,8 @@
|
||||
|
||||
`--lane=all` 会按 oracle、WASM、Chrome 顺序执行全部 lane。任一命令退出非零都会使整个 lane 失败;运行摘要写入 `.cache/ci/real-verification.json`,该文件不作为仓库基线证据提交。
|
||||
|
||||
oracle lane 默认会重新抓取锁定的 FreeCAD commit 并初始化子模块。仅在本机已经具备该 commit、且需要离线复现时,才可显式使用 `FREECAD_SOURCE_OFFLINE=1`;该模式会拒绝未锁定的 checkout,并跳过网络相关子模块更新,不会替代 CI 的真实 fetch。
|
||||
|
||||
## Runner 要求
|
||||
|
||||
工作流使用标签为 `self-hosted, linux, cad` 的 runner,因为 FreeCAD 原生编译和外围 CAM harness 需要较大的本地工具链。runner 必须提供:
|
||||
|
||||
@@ -1755,3 +1755,11 @@ PartDesign transform oracle 由 4 项扩为 10 项:补入 PolarPattern `4000`
|
||||
新增 `config/freecad-parameter-mutation-plan.json`、`config/freecad-parameter-mutation-report.json` 和 `scripts/generate-freecad-parameter-mutation-report.mjs`。矩阵把每个 35 个 feature family 分成 nominal 参数、非法边界、两个以上参数快照、编辑-重算-恢复事务、FCStd 往返五项;报告从现有 FreeCAD/Chrome 黄金证据抽取,不把 outcome-only 记录当参数快照。当前为 `1/35` family 完整、`35/35` nominal、`35/35` invalid boundary、`15/35` 参数变体、`6/35` 编辑恢复、`34/35` 往返;门禁输出 `freecad-parameter-mutation-fail-closed`,并已通过 `check:execution-plan` 接入默认 `verify`。
|
||||
|
||||
运行 `./npmw run generate:freecad-parameter-mutations` 可在新增原生黄金模型后更新报告;`./npmw run check:freecad-parameter-mutations` 会拒绝陈旧报告。只有每个 family 都有真实参数快照及编辑后重算/恢复证据,且阶段 builder、MappedNameRef/StringHasher 和同构来源边界同时闭合,才允许继续评估 `EX-REL-01`,不允许通过修改 claim 或能力矩阵字段绕过。
|
||||
|
||||
## 227. 2026-08-10 ORA 运行时全量探针、TSN 阶段证据与真实 oracle lane
|
||||
|
||||
桌面 FreeCAD 1.1.1 已通过真实 GUI 入口完成全量注册对象探针:`352` 个注册文档对象 TypeId,`348` 个可实例化,`4` 个明确记录原生不可用,累计 `5510` 个运行时属性元数据及默认值。`check:freecad-desktop-oracle` 与 `check:freecad-oracle-coverage` 均验证通过;源代码索引包含 `766` 个 TypeId 声明、`202` 个文档对象使用点,其中 `6` 个使用点尚无对应运行时注册对象,因此 exact 仍保持 fail-closed。
|
||||
|
||||
TSN 阶段门禁已接入默认执行计划。Pad 证据为 `25` 条跨 modified/generated 记录,Pocket 为 `42` 条记录,含普通两阶段和 two-sided 四阶段的 source/result/topology/relation/cleanup 校验;当前原生 provider 只捕获 `cut/fuse/pad` 三类操作,19 类目标操作仍缺 `16` 类逐阶段证据,且 `MappedNameRef/StringHasher` 私有 token 仍是明确阻塞项。
|
||||
|
||||
真实 oracle lane 已在固定桌面安装上完成 `27` 个命令,包含 ORA、Part/PartDesign/Sketcher、FCStd roundtrip 与 exact naming gates;WASM lane 已重跑 Chrome Pad/Pocket history harness。源代码网络抓取在本地因 GitHub 超时,验证使用显式 `FREECAD_SOURCE_OFFLINE=1` 的同一 pinned checkout;CI 默认路径仍执行真实 tag/submodule fetch,不会把离线缓存当作成功证据。
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"check:baseline": "node scripts/check-baseline.mjs",
|
||||
"check:freecad-source": "node scripts/verify-freecad-source.mjs",
|
||||
"check:bitbybit-history": "node scripts/check-bitbybit-history-surface.mjs",
|
||||
"check:execution-plan": "node scripts/check-freecad-execution-plan.mjs && node scripts/check-freecad-web-exact-parity-plan.mjs && node scripts/check-freecad-web-exact-promotion.mjs && node scripts/check-freecad-oracle-coverage.mjs && node scripts/check-freecad-golden-coverage.mjs && node scripts/generate-freecad-parameter-mutation-report.mjs --check",
|
||||
"check:execution-plan": "node scripts/check-freecad-execution-plan.mjs && node scripts/check-freecad-web-exact-parity-plan.mjs && node scripts/check-freecad-web-exact-promotion.mjs && node scripts/check-freecad-oracle-coverage.mjs && node scripts/check-freecad-golden-coverage.mjs && node scripts/generate-freecad-parameter-mutation-report.mjs --check && node scripts/check-freecad-tsn-stage-evidence.mjs",
|
||||
"check:freecad-web-exact-parity-plan": "node scripts/check-freecad-web-exact-parity-plan.mjs",
|
||||
"check:freecad-web-exact-promotion": "node scripts/check-freecad-web-exact-promotion.mjs",
|
||||
"generate:freecad-inventory": "node scripts/generate-freecad-source-inventory.mjs",
|
||||
@@ -101,6 +101,7 @@
|
||||
"check:chrome-native-pad-history": "node scripts/check-chrome-native-pad-history.mjs",
|
||||
"test:chrome-native-pocket-history": "node scripts/run-chrome-native-pocket-history.mjs",
|
||||
"check:chrome-native-pocket-history": "node scripts/check-chrome-native-pocket-history.mjs",
|
||||
"check:freecad-tsn-stage-evidence": "node scripts/check-freecad-tsn-stage-evidence.mjs",
|
||||
"test:chrome-native-revolution-history": "node scripts/run-chrome-native-revolution-history.mjs",
|
||||
"check:chrome-native-revolution-history": "node scripts/check-chrome-native-revolution-history.mjs",
|
||||
"test:chrome-native-groove-history": "node scripts/run-chrome-native-groove-history.mjs",
|
||||
|
||||
@@ -74,6 +74,22 @@ const propertyFlags = report.objects.flatMap((object) => object.properties || []
|
||||
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',
|
||||
@@ -88,5 +104,9 @@ console.log(JSON.stringify({
|
||||
guiUp: report.guiUp,
|
||||
moduleStatusSummary: report.moduleStatusSummary,
|
||||
objectFixtureCount: report.objects.length,
|
||||
registeredObjectTypeCount: runtimeObjects.candidateCount,
|
||||
runtimeObjectCount: runtimeObjects.availableCount,
|
||||
unavailableObjectCount: runtimeObjects.unavailableCount,
|
||||
runtimePropertyCount,
|
||||
},
|
||||
}, null, 2))
|
||||
|
||||
@@ -38,8 +38,24 @@ for (const [id, command] of inventoryCommands) {
|
||||
for (const id of referenceCommands.keys()) if (!inventoryCommands.has(id)) fail(`desktop GUI command ${id} is missing from the generated inventory.`)
|
||||
for (const command of guiInventory.sourceOnlyCommands) if (!command.reason || !Array.isArray(command.sourceModules)) fail(`source-only command ${command.id} lacks an explicit reason.`)
|
||||
|
||||
const runtimeObjectCount = reference.objects?.filter((object) => object.available && object.runtimeTypeId).length ?? 0
|
||||
const staticObjectCandidateCount = typeInventory.modules.reduce((total, module) => total + module.objects.length, 0)
|
||||
const runtimeObjects = reference.runtimeObjects
|
||||
if (!runtimeObjects?.available || runtimeObjects.candidateSource !== 'Document.supportedTypes' || !Array.isArray(runtimeObjects.types)) fail('desktop oracle is missing the complete Document.supportedTypes runtime inventory.')
|
||||
if (runtimeObjects.candidateCount !== runtimeObjects.types.length || runtimeObjects.availableCount + runtimeObjects.unavailableCount !== runtimeObjects.candidateCount) fail('runtime object counts are inconsistent.')
|
||||
const runtimeTypeIds = new Set()
|
||||
for (const object of runtimeObjects.types) {
|
||||
if (typeof object.typeId !== 'string' || !object.typeId || runtimeTypeIds.has(object.typeId)) fail(`runtime object TypeId '${object.typeId || '<unknown>'}' is duplicated or invalid.`)
|
||||
runtimeTypeIds.add(object.typeId)
|
||||
if (object.available && (typeof object.runtimeTypeId !== 'string' || !object.runtimeTypeId || !Array.isArray(object.properties))) fail(`runtime object ${object.typeId} lacks TypeId/property metadata.`)
|
||||
if (!object.available && (!object.error || object.probeStatus !== 'unavailable')) fail(`runtime object ${object.typeId} lacks an explicit unavailable status.`)
|
||||
}
|
||||
for (const coreTypeId of ['Part::Box', 'Part::Cylinder', 'Part::Sphere', 'Part::Cone', 'Part::Feature', 'PartDesign::Feature', 'Sketcher::SketchObject']) {
|
||||
if (!runtimeTypeIds.has(coreTypeId)) fail(`core runtime object ${coreTypeId} is absent from Document.supportedTypes.`)
|
||||
}
|
||||
const runtimeObjectCount = runtimeObjects.availableCount
|
||||
const sourceUsageTypeIds = new Set((typeInventory.documentObjectUsages ?? []).map((record) => record.typeId))
|
||||
if (sourceUsageTypeIds.size === 0 || !Array.isArray(typeInventory.typeIdDeclarations)) fail('source TypeId declaration and addObject usage indexes are missing.')
|
||||
const sourceUsageMissingRuntime = [...sourceUsageTypeIds].filter((typeId) => !runtimeTypeIds.has(typeId)).sort()
|
||||
const staticObjectCandidateCount = sourceUsageTypeIds.size
|
||||
const moduleStatusCounts = Object.fromEntries([...new Set(reference.modules.map((module) => module.runtimeStatus))].sort().map((status) => [status, reference.modules.filter((module) => module.runtimeStatus === status).length]))
|
||||
console.log(JSON.stringify({
|
||||
status: 'freecad-oracle-coverage-pass',
|
||||
@@ -49,8 +65,15 @@ console.log(JSON.stringify({
|
||||
guiCommands: guiInventory.commandCount,
|
||||
guiCommandsRuntimeAligned: true,
|
||||
sourceOnlyCommands: guiInventory.sourceOnlyCommands.length,
|
||||
registeredObjectTypeCount: runtimeObjects.candidateCount,
|
||||
runtimeObjectCount,
|
||||
unavailableObjectCount: runtimeObjects.unavailableCount,
|
||||
runtimePropertyCount: runtimeObjects.types.reduce((total, object) => total + (object.properties?.length ?? 0), 0),
|
||||
sourceDeclaredTypeIdCount: typeInventory.typeIdDeclarations.length,
|
||||
sourceDocumentObjectUsageCount: staticObjectCandidateCount,
|
||||
sourceUsageMissingRuntimeCount: sourceUsageMissingRuntime.length,
|
||||
sourceUsageMissingRuntime: sourceUsageMissingRuntime,
|
||||
staticObjectCandidateCount,
|
||||
exactPromotionReady: false,
|
||||
remaining: ['not-built modules require an explicit build/proxy decision', 'static TypeId/property candidates require per-object runtime probes'],
|
||||
remaining: ['not-built modules require an explicit build/proxy decision', 'source-declared TypeId/property candidates outside the registered document-object set require a native API inventory'],
|
||||
}, null, 2))
|
||||
|
||||
38
scripts/check-freecad-tsn-stage-evidence.mjs
Normal file
38
scripts/check-freecad-tsn-stage-evidence.mjs
Normal file
@@ -0,0 +1,38 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
|
||||
const fail = (message) => { throw new Error(`FreeCAD TSN stage evidence: ${message}`) }
|
||||
const [history, pad, pocket] = await Promise.all([
|
||||
load('config/chrome-native-history-verification.json'),
|
||||
load('config/chrome-native-pad-history-verification.json'),
|
||||
load('config/chrome-native-pocket-history-verification.json'),
|
||||
])
|
||||
const requiredOperations = ['fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe', 'revolution', 'groove', 'fillet', 'chamfer', 'hole', 'draft', 'thickness', 'linear-pattern', 'polar-pattern', 'mirrored', 'multi-transform']
|
||||
if (history.status !== 'pass' || history.nativeCapabilities?.availability !== 'available' || history.nativeCapabilities.transport !== 'step-text') fail('the shared native history provider is not available.')
|
||||
if (JSON.stringify(history.nativeCapabilities.operations) !== JSON.stringify(requiredOperations)) fail('the native history operation registry drifted from the locked 19-operation contract.')
|
||||
if (pad.status !== 'pass' || pad.execution?.status !== 'completed' || pad.execution.recordCount <= 0 || pad.execution.crossKindRecords <= 0) fail('Pad has no completed cross-kind stage history.')
|
||||
if (pad.execution.profileKinds?.sort().join(',') !== 'edge,face,vertex' || pad.execution.summary?.isValid !== true || pad.execution.summary?.solids !== 1 || pad.workerDisposed !== true) fail('Pad stage topology or worker ownership evidence is incomplete.')
|
||||
if (pocket.status !== 'pass' || pocket.history?.stages?.length !== 2 || pocket.twoSidedHistory?.stages?.length !== 4) fail('Pocket staged history evidence is incomplete.')
|
||||
const stageRecords = [...(pocket.history.stages ?? []), ...(pocket.twoSidedHistory.stages ?? [])]
|
||||
for (const stage of stageRecords) {
|
||||
if (!stage.stageId || !stage.operation || !Array.isArray(stage.inputObjectIds) || stage.inputObjectIds.length === 0 || !stage.resultObjectId || stage.topologyEntries <= 0 || stage.recordCount <= 0) fail(`stage ${stage.stageId || '<unknown>'} lacks source/result topology evidence.`)
|
||||
}
|
||||
if (pocket.history.relations?.modified <= 0 || pocket.history.relations?.deleted <= 0 || pocket.twoSidedHistory.structuralValid !== true || pocket.twoSidedHistory.solids !== 1) fail('Pocket relation or two-sided structural evidence is incomplete.')
|
||||
if (pocket.afterRelease?.shapeCount !== 0 || pocket.afterRelease?.kernelReferenceCount !== 0 || pad.workerDisposed !== true) fail('TSN harnesses did not release native resources.')
|
||||
|
||||
const capturedOperations = [...new Set(stageRecords.map((stage) => stage.operation))].sort()
|
||||
const missingOperations = requiredOperations.filter((operation) => !capturedOperations.includes(operation))
|
||||
console.log(JSON.stringify({
|
||||
status: 'freecad-tsn-stage-evidence-pass',
|
||||
exactPromotionReady: false,
|
||||
provider: history.nativeCapabilities.providerId,
|
||||
requiredBuilderOperations: requiredOperations.length,
|
||||
capturedStageOperations: capturedOperations,
|
||||
capturedBuilderOperationCount: capturedOperations.length,
|
||||
missingBuilderOperations: missingOperations,
|
||||
pad: { records: pad.execution.recordCount, crossKindRecords: pad.execution.crossKindRecords, relations: pad.execution.relations },
|
||||
pocket: { records: pocket.history.recordCount, stages: pocket.history.stages.length, twoSidedStages: pocket.twoSidedHistory.stages.length, relations: pocket.history.relations },
|
||||
blockers: [`${missingOperations.length} of ${requiredOperations.length} builder operations still need explicit native stage captures`, 'FreeCAD private MappedNameRef/StringHasher token evidence remains unavailable for the browser provider'],
|
||||
}, null, 2))
|
||||
@@ -15,10 +15,28 @@ for (const module of inventory.modules) {
|
||||
for (const object of module.objects) {
|
||||
if (typeof object.sourceFile !== 'string' || !object.sourceFile.startsWith('src/Mod/')) fail(`${module.name} has an invalid sourceFile.`)
|
||||
for (const field of ['classes', 'typeIdSymbols', 'propertyRegistrations', 'propertyMembers', 'pythonProperties']) if (!Array.isArray(object[field])) fail(`${module.name}/${object.sourceFile}.${field} must be an array.`)
|
||||
for (const value of [...object.classes, ...object.typeIdSymbols, ...object.propertyRegistrations]) if (typeof value !== 'string' || !value) fail(`${module.name}/${object.sourceFile} contains an invalid symbol.`)
|
||||
for (const field of ['typeIdDeclarations', 'documentObjectUsages']) if (object[field] !== undefined && !Array.isArray(object[field])) fail(`${module.name}/${object.sourceFile}.${field} must be an array when present.`)
|
||||
const declaredTypeIds = object.typeIdDeclarations ?? []
|
||||
const documentObjectUsages = object.documentObjectUsages ?? []
|
||||
for (const value of [...object.classes, ...object.typeIdSymbols, ...object.propertyRegistrations, ...declaredTypeIds, ...documentObjectUsages]) if (typeof value !== 'string' || !value) fail(`${module.name}/${object.sourceFile} contains an invalid symbol.`)
|
||||
for (const value of [...object.propertyMembers, ...object.pythonProperties]) if (!value || typeof value.name !== 'string' || !value.name) fail(`${module.name}/${object.sourceFile} contains an invalid property.`)
|
||||
}
|
||||
}
|
||||
const validateTypeIdIndex = (records, field, expectedCount) => {
|
||||
if (!Array.isArray(records) || records.length !== expectedCount) fail(`${field} count does not match totals.`)
|
||||
const typeIds = new Set()
|
||||
for (const record of records) {
|
||||
if (!record?.typeId || typeIds.has(record.typeId) || !/^[A-Za-z_][A-Za-z0-9_]*::[A-Za-z_][A-Za-z0-9_]*$/.test(record.typeId)) fail(`${field} contains an invalid or duplicate TypeId.`)
|
||||
typeIds.add(record.typeId)
|
||||
if (!Array.isArray(record.sourceModules) || record.sourceModules.length === 0 || record.sourceModules.some((name) => !names.has(name))) fail(`${field}/${record.typeId} has invalid source modules.`)
|
||||
if (!Array.isArray(record.sourceFiles) || record.sourceFiles.length === 0 || record.sourceFiles.some((path) => !path.startsWith('src/Mod/'))) fail(`${field}/${record.typeId} has invalid source files.`)
|
||||
}
|
||||
}
|
||||
validateTypeIdIndex(inventory.typeIdDeclarations, 'typeIdDeclarations', inventory.totals?.typeIdDeclarationCount)
|
||||
validateTypeIdIndex(inventory.documentObjectUsages, 'documentObjectUsages', inventory.totals?.documentObjectUsageCount)
|
||||
const declarationEvidenceCount = inventory.modules.reduce((total, module) => total + module.objects.reduce((sum, object) => sum + (object.typeIdDeclarations?.length ?? 0), 0), 0)
|
||||
const usageEvidenceCount = inventory.modules.reduce((total, module) => total + module.objects.reduce((sum, object) => sum + (object.documentObjectUsages?.length ?? 0), 0), 0)
|
||||
if (declarationEvidenceCount !== inventory.totals.typeIdDeclarationEvidenceCount || usageEvidenceCount !== inventory.totals.documentObjectUsageEvidenceCount) fail('TypeId source evidence totals are inconsistent.')
|
||||
const sourceRoot = process.env.FREECAD_SOURCE_DIR ? resolve(process.env.FREECAD_SOURCE_DIR) : resolve(root, inventory.baseline.sourcePath || '.cache/freecad/FreeCAD')
|
||||
let sourceAvailable = false
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { mkdir, stat } from 'node:fs/promises'
|
||||
import { mkdir, rename, stat } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
|
||||
@@ -9,24 +9,64 @@ const directoryArgument = process.argv.slice(2).find((argument) => argument.star
|
||||
const target = resolve(root, directoryArgument?.slice('--directory='.length) || toolchain.sourceCheckout.defaultDirectory)
|
||||
const git = (...args) => execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'], timeout: 300000 }).trim()
|
||||
const exists = await stat(target).then(() => true, () => false)
|
||||
const offline = process.env.FREECAD_SOURCE_OFFLINE === '1'
|
||||
|
||||
if (!exists) {
|
||||
if (offline) throw new Error(`FREECAD_SOURCE_OFFLINE=1 requires an existing pinned checkout: ${target}`)
|
||||
await mkdir(dirname(target), { recursive: true })
|
||||
git('clone', '--filter=blob:none', '--no-checkout', toolchain.source.repository, target)
|
||||
} else {
|
||||
const remote = git('-C', target, 'remote', 'get-url', 'origin')
|
||||
if (remote !== toolchain.source.repository) throw new Error(`Existing checkout has unexpected origin: ${remote}`)
|
||||
const dirty = git('-C', target, 'status', '--porcelain')
|
||||
if (dirty) throw new Error(`Existing FreeCAD checkout is dirty: ${target}`)
|
||||
const submodulePaths = git('-C', target, 'config', '--file', '.gitmodules', '--get-regexp', 'path')
|
||||
.split(/\r?\n/)
|
||||
.filter(Boolean)
|
||||
.map((line) => line.slice(line.indexOf(' ') + 1).trim())
|
||||
const parentDirty = git('-C', target, 'status', '--porcelain')
|
||||
const unexpectedDirty = parentDirty.split(/\r?\n/).filter((line) => line && !submodulePaths.some((path) => line.endsWith(` ${path}`) || line.endsWith(`\t${path}`)))
|
||||
if (unexpectedDirty.length > 0) throw new Error(`Existing FreeCAD checkout is dirty outside submodules: ${target}`)
|
||||
|
||||
// A previous interrupted clone can leave a non-empty directory without a
|
||||
// gitlink checkout. Move only those exact submodule targets into an ignored,
|
||||
// recoverable cache before asking Git to initialize them again.
|
||||
if (!offline) {
|
||||
const staleRoot = resolve(root, '.cache/freecad/stale-submodules', String(Date.now()))
|
||||
let moved = 0
|
||||
for (const submodulePath of submodulePaths) {
|
||||
const submodule = resolve(target, submodulePath)
|
||||
const present = await stat(submodule).then(() => true, () => false)
|
||||
if (!present) continue
|
||||
const hasGitMetadata = await stat(resolve(submodule, '.git')).then(() => true, () => false)
|
||||
const childDirty = hasGitMetadata
|
||||
? git('-C', submodule, 'status', '--porcelain').length > 0
|
||||
: true
|
||||
if (!hasGitMetadata || childDirty) {
|
||||
const destination = resolve(staleRoot, submodulePath)
|
||||
await mkdir(dirname(destination), { recursive: true })
|
||||
await rename(submodule, destination)
|
||||
moved += 1
|
||||
console.error(`Moved stale FreeCAD submodule ${submodulePath} to ${destination}`)
|
||||
}
|
||||
}
|
||||
if (moved > 0) await mkdir(staleRoot, { recursive: true })
|
||||
} else {
|
||||
console.error('FREECAD_SOURCE_OFFLINE=1: preserving the pinned source checkout and skipping submodule repair.')
|
||||
}
|
||||
}
|
||||
|
||||
git('-C', target, 'fetch', '--depth=1', 'origin', `refs/tags/${toolchain.source.tag}`)
|
||||
const fetched = git('-C', target, 'rev-parse', 'FETCH_HEAD')
|
||||
if (fetched !== toolchain.source.commit) throw new Error(`Fetched FreeCAD tag resolved to ${fetched}, expected ${toolchain.source.commit}.`)
|
||||
git('-C', target, 'checkout', '--detach', toolchain.source.commit)
|
||||
const currentRevision = git('-C', target, 'rev-parse', 'HEAD')
|
||||
if (offline && currentRevision === toolchain.source.commit) {
|
||||
console.error(`FREECAD_SOURCE_OFFLINE=1: using pinned FreeCAD source ${currentRevision}.`)
|
||||
} else {
|
||||
git('-C', target, 'fetch', '--depth=1', 'origin', `refs/tags/${toolchain.source.tag}`)
|
||||
const fetched = git('-C', target, 'rev-parse', 'FETCH_HEAD')
|
||||
if (fetched !== toolchain.source.commit) throw new Error(`Fetched FreeCAD tag resolved to ${fetched}, expected ${toolchain.source.commit}.`)
|
||||
git('-C', target, 'checkout', '--detach', toolchain.source.commit)
|
||||
}
|
||||
// The locked source graph includes GSL, OndselSolver, AddonManager and the
|
||||
// test support checkout. Initialize every gitlink so a clean oracle build does
|
||||
// not silently fall back to a partial module set.
|
||||
git('-C', target, 'submodule', 'sync', '--recursive')
|
||||
git('-C', target, 'submodule', 'update', '--init', '--recursive')
|
||||
if (offline) console.error('FREECAD_SOURCE_OFFLINE=1: skipping network-dependent submodule update.')
|
||||
else git('-C', target, 'submodule', 'update', '--init', '--recursive')
|
||||
console.log(`FreeCAD source ready: ${target} @ ${toolchain.source.commit}`)
|
||||
|
||||
@@ -8,6 +8,20 @@ import FreeCAD as App
|
||||
|
||||
|
||||
def property_metadata(obj):
|
||||
def json_value(value):
|
||||
if value is None or isinstance(value, (bool, int, float, str)):
|
||||
return value
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [json_value(item) for item in value]
|
||||
if isinstance(value, dict):
|
||||
return {str(key): json_value(item) for key, item in value.items()}
|
||||
# FreeCAD quantities and placements keep a stable textual form while
|
||||
# the report remains valid JSON for every registered property type.
|
||||
try:
|
||||
return str(value)
|
||||
except Exception:
|
||||
return "<unserializable>"
|
||||
|
||||
properties = []
|
||||
for name in obj.PropertiesList:
|
||||
try:
|
||||
@@ -22,23 +36,61 @@ def property_metadata(obj):
|
||||
status = list(obj.getPropertyStatus(name))
|
||||
except Exception:
|
||||
status = []
|
||||
properties.append({"name": name, "typeId": type_id, "group": group, "status": status})
|
||||
try:
|
||||
default = json_value(getattr(obj, name))
|
||||
except Exception:
|
||||
default = "<unreadable>"
|
||||
properties.append({"name": name, "typeId": type_id, "group": group, "status": status, "default": default})
|
||||
return properties
|
||||
|
||||
|
||||
def probe_object(document, type_id):
|
||||
object_name = "Reference" + type_id.replace(":", "_")
|
||||
try:
|
||||
obj = document.addObject(type_id, "Reference" + type_id.replace(":", "_"))
|
||||
obj = document.addObject(type_id, object_name)
|
||||
except Exception as error:
|
||||
return {"typeId": type_id, "available": False, "error": str(error)}
|
||||
return {"typeId": type_id, "available": False, "probeStatus": "unavailable", "error": str(error)}
|
||||
return {
|
||||
"typeId": type_id,
|
||||
"available": True,
|
||||
"probeStatus": "available",
|
||||
"name": obj.Name,
|
||||
"runtimeTypeId": obj.TypeId,
|
||||
"properties": property_metadata(obj),
|
||||
}
|
||||
|
||||
|
||||
def probe_registered_objects(document):
|
||||
try:
|
||||
registered = sorted(set(str(type_id) for type_id in document.supportedTypes()))
|
||||
except Exception as error:
|
||||
return {"available": False, "types": [], "error": str(error)}
|
||||
|
||||
records = []
|
||||
for index, type_id in enumerate(registered):
|
||||
record = probe_object(document, type_id)
|
||||
record["candidateIndex"] = index
|
||||
record["candidateSource"] = "Document.supportedTypes"
|
||||
records.append(record)
|
||||
# A few GUI-only TechDraw providers dereference their view during
|
||||
# removeObject(). Keep those instances alive until FreeCAD exits so a
|
||||
# probe cannot turn a valid metadata observation into a process crash.
|
||||
if not bool(getattr(App, "GuiUp", False)) and record.get("available") and record.get("name"):
|
||||
try:
|
||||
document.removeObject(record["name"])
|
||||
except Exception as error:
|
||||
record["cleanupError"] = str(error)
|
||||
|
||||
return {
|
||||
"available": True,
|
||||
"candidateSource": "Document.supportedTypes",
|
||||
"candidateCount": len(records),
|
||||
"availableCount": sum(1 for record in records if record.get("available")),
|
||||
"unavailableCount": sum(1 for record in records if not record.get("available")),
|
||||
"types": records,
|
||||
}
|
||||
|
||||
|
||||
ALL_REFERENCE_MODULES = [
|
||||
"AddonManager", "Assembly", "BIM", "CAM", "Cloud", "Draft", "Fem", "Help",
|
||||
"Idf", "Import", "Inspection", "JtReader", "Material", "Measure", "Mesh",
|
||||
@@ -180,6 +232,9 @@ def probe_gui_commands(document, selected_object):
|
||||
|
||||
document = App.newDocument("ReferenceProbe")
|
||||
box_object = document.addObject("Part::Box", "CommandProbeBox")
|
||||
modules = [probe_module(name) for name in ALL_REFERENCE_MODULES]
|
||||
gui_commands = probe_gui_commands(document, box_object)
|
||||
runtime_objects = probe_registered_objects(document)
|
||||
version = App.Version()
|
||||
result = {
|
||||
"schemaVersion": 1,
|
||||
@@ -191,12 +246,13 @@ result = {
|
||||
"guiUp": bool(getattr(App, "GuiUp", False)),
|
||||
"buildPurpose": "desktop-reference-oracle" if bool(getattr(App, "GuiUp", False)) else "headless-reference-oracle",
|
||||
"moduleCount": len(ALL_REFERENCE_MODULES),
|
||||
"modules": [probe_module(name) for name in ALL_REFERENCE_MODULES],
|
||||
"modules": modules,
|
||||
"objects": [probe_object(document, type_id) for type_id in [
|
||||
"Part::Box", "Part::Cylinder", "Part::Sphere", "Part::Cone",
|
||||
"Part::Feature", "PartDesign::Feature", "Sketcher::SketchObject",
|
||||
]],
|
||||
"guiCommands": probe_gui_commands(document, box_object),
|
||||
"runtimeObjects": runtime_objects,
|
||||
"guiCommands": gui_commands,
|
||||
}
|
||||
result["moduleStatusSummary"] = {
|
||||
status: sum(1 for module in result["modules"] if module["runtimeStatus"] == status)
|
||||
|
||||
@@ -26,6 +26,8 @@ const propertyRegistrationPattern = /\bADD_PROPERTY(?:_TYPE)?\s*\(\s*([A-Za-z_][
|
||||
const propertyMemberPattern = /\bApp::(Property[A-Za-z0-9_]+)\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:;|,)/g
|
||||
const pythonPropertyPattern = /\.addProperty\(\s*([^,\n]+),\s*["']([^"']+)["']/g
|
||||
const typeIdPattern = /\b([A-Za-z_][A-Za-z0-9_]*)::(?:getClassTypeId|getTypeId)\s*\(/g
|
||||
const typeDeclarationPattern = /\b(?:PROPERTY_SOURCE(?:_[A-Z_]+)?|TYPESYSTEM_SOURCE(?:_[A-Z_]+)?|EXTENSION_PROPERTY_SOURCE(?:_[A-Z_]+)?)\s*\(\s*([A-Za-z_][A-Za-z0-9_]*::[A-Za-z_][A-Za-z0-9_]*)/g
|
||||
const documentObjectUsagePattern = /\baddObject\s*\(\s*["']([A-Za-z_][A-Za-z0-9_]*::[A-Za-z_][A-Za-z0-9_]*)["']/g
|
||||
const unique = (values) => [...new Set(values)].sort()
|
||||
|
||||
const moduleEntries = (await readdir(sourceMod, { withFileTypes: true })).filter((entry) => entry.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))
|
||||
@@ -37,6 +39,8 @@ for (const entry of moduleEntries) {
|
||||
let propertyRegistrationCount = 0
|
||||
let propertyMemberCount = 0
|
||||
let pythonPropertyCount = 0
|
||||
let typeIdDeclarationCount = 0
|
||||
let documentObjectUsageCount = 0
|
||||
for (const file of files) {
|
||||
const text = await readFile(file, 'utf8')
|
||||
const classes = unique([...text.matchAll(classPattern)].map((match) => match[1]))
|
||||
@@ -44,18 +48,25 @@ for (const entry of moduleEntries) {
|
||||
const members = unique([...text.matchAll(propertyMemberPattern)].map((match) => ({ type: match[1], name: match[2] })))
|
||||
const pythonProperties = [...text.matchAll(pythonPropertyPattern)].map((match) => ({ typeExpression: match[1].trim(), name: match[2] }))
|
||||
const typeIds = unique([...text.matchAll(typeIdPattern)].map((match) => match[1]))
|
||||
const typeIdDeclarations = unique([...text.matchAll(typeDeclarationPattern)].map((match) => match[1]))
|
||||
const documentObjectUsages = unique([...text.matchAll(documentObjectUsagePattern)].map((match) => match[1]))
|
||||
propertyRegistrationCount += registrations.length
|
||||
propertyMemberCount += members.length
|
||||
pythonPropertyCount += pythonProperties.length
|
||||
if (classes.length || registrations.length || members.length || pythonProperties.length || typeIds.length) {
|
||||
objects.push({
|
||||
typeIdDeclarationCount += typeIdDeclarations.length
|
||||
documentObjectUsageCount += documentObjectUsages.length
|
||||
if (classes.length || registrations.length || members.length || pythonProperties.length || typeIds.length || typeIdDeclarations.length || documentObjectUsages.length) {
|
||||
const object = {
|
||||
sourceFile: relative(sourceRoot, file),
|
||||
classes,
|
||||
typeIdSymbols: typeIds,
|
||||
propertyRegistrations: registrations,
|
||||
propertyMembers: members,
|
||||
pythonProperties,
|
||||
})
|
||||
}
|
||||
if (typeIdDeclarations.length > 0) object.typeIdDeclarations = typeIdDeclarations
|
||||
if (documentObjectUsages.length > 0) object.documentObjectUsages = documentObjectUsages
|
||||
objects.push(object)
|
||||
}
|
||||
}
|
||||
modules.push({
|
||||
@@ -66,22 +77,45 @@ for (const entry of moduleEntries) {
|
||||
propertyRegistrationCount,
|
||||
propertyMemberCount,
|
||||
pythonPropertyCount,
|
||||
typeIdDeclarationCount,
|
||||
documentObjectUsageCount,
|
||||
objects,
|
||||
runtimeStatus: 'requires-freecad-reference-probe',
|
||||
})
|
||||
}
|
||||
|
||||
const indexTypeIds = (field) => {
|
||||
const index = new Map()
|
||||
for (const module of modules) for (const object of module.objects) for (const typeId of object[field] ?? []) {
|
||||
const record = index.get(typeId) || { typeId, sourceModules: new Set(), sourceFiles: new Set() }
|
||||
record.sourceModules.add(module.name)
|
||||
record.sourceFiles.add(object.sourceFile)
|
||||
index.set(typeId, record)
|
||||
}
|
||||
return [...index.values()]
|
||||
.map((record) => ({ typeId: record.typeId, sourceModules: [...record.sourceModules].sort(), sourceFiles: [...record.sourceFiles].sort() }))
|
||||
.sort((left, right) => left.typeId.localeCompare(right.typeId))
|
||||
}
|
||||
const typeIdDeclarations = indexTypeIds('typeIdDeclarations')
|
||||
const documentObjectUsages = indexTypeIds('documentObjectUsages')
|
||||
|
||||
const inventory = {
|
||||
schemaVersion: 1,
|
||||
baseline: { freecadVersion: '1.1.1', commit: baselineCommit, sourcePath: '.cache/freecad/FreeCAD' },
|
||||
generatedBy: 'scripts/generate-freecad-type-property-inventory.mjs',
|
||||
moduleCount: modules.length,
|
||||
typeIdDeclarations,
|
||||
documentObjectUsages,
|
||||
totals: {
|
||||
sourceFileCount: modules.reduce((sum, module) => sum + module.sourceFileCount, 0),
|
||||
objectSourceCount: modules.reduce((sum, module) => sum + module.objectSourceCount, 0),
|
||||
propertyRegistrationCount: modules.reduce((sum, module) => sum + module.propertyRegistrationCount, 0),
|
||||
propertyMemberCount: modules.reduce((sum, module) => sum + module.propertyMemberCount, 0),
|
||||
pythonPropertyCount: modules.reduce((sum, module) => sum + module.pythonPropertyCount, 0),
|
||||
typeIdDeclarationCount: typeIdDeclarations.length,
|
||||
typeIdDeclarationEvidenceCount: modules.reduce((sum, module) => sum + module.typeIdDeclarationCount, 0),
|
||||
documentObjectUsageCount: documentObjectUsages.length,
|
||||
documentObjectUsageEvidenceCount: modules.reduce((sum, module) => sum + module.documentObjectUsageCount, 0),
|
||||
},
|
||||
modules,
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ 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 candidates = process.env.FREECAD_CMD ? [process.env.FREECAD_CMD] : [localOracle, ...fallbackOracle]
|
||||
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 })
|
||||
@@ -32,7 +33,10 @@ const runtimeEnv = profile === 'desktop'
|
||||
MPLBACKEND: 'Agg',
|
||||
}
|
||||
: process.env
|
||||
const execution = spawnSync(command, commandArgs, {
|
||||
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,
|
||||
@@ -53,5 +57,44 @@ for (const module of result.modules) {
|
||||
throw new Error(`Reference probe returned an invalid module status for ${module.name || '<unknown>'}.`)
|
||||
}
|
||||
}
|
||||
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 || '<unknown>'}.`)
|
||||
}
|
||||
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, result }, null, 2))
|
||||
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))
|
||||
|
||||
@@ -40,6 +40,7 @@ const lanes = {
|
||||
'configure:freecad-native',
|
||||
'build:freecad-native',
|
||||
'probe:freecad-reference',
|
||||
'check:freecad-desktop-oracle',
|
||||
'test:golden:freecad',
|
||||
'test:golden:freecad:families',
|
||||
'test:golden:freecad:failures',
|
||||
@@ -61,6 +62,7 @@ const lanes = {
|
||||
'check:freecad-composite-history-elementmap',
|
||||
'check:freecad-exact-history-elementmap-gate',
|
||||
'check:freecad-native-naming-evidence',
|
||||
'test:freecad-fcstd-native',
|
||||
],
|
||||
wasm: [
|
||||
...wasmBuilds,
|
||||
@@ -71,10 +73,14 @@ const lanes = {
|
||||
'check:planegcs-artifact',
|
||||
'test:chrome-planegcs',
|
||||
'test:chrome-native-history',
|
||||
'test:chrome-native-pad-history',
|
||||
'test:chrome-native-pocket-history',
|
||||
'test:chrome-camotics-wasm',
|
||||
'test:chrome-cam-native-simulation',
|
||||
'check:chrome-planegcs',
|
||||
'check:chrome-native-history',
|
||||
'check:chrome-native-pad-history',
|
||||
'check:chrome-native-pocket-history',
|
||||
'check:chrome-camotics-wasm',
|
||||
'check:chrome-cam-native-simulation',
|
||||
],
|
||||
@@ -83,13 +89,13 @@ const lanes = {
|
||||
const selectedLanes = requestedLane === 'all' ? ['oracle', 'wasm', 'chrome'] : [requestedLane]
|
||||
const results = []
|
||||
|
||||
function runScript(name) {
|
||||
function runScript(name, laneEnvironment = {}) {
|
||||
if (!scripts[name]) throw new Error(`Required npm script is missing: ${name}`)
|
||||
const startedAt = Date.now()
|
||||
console.log(`\n[real-verification] ${name}`)
|
||||
const result = spawnSync(npmw, ['run', name], {
|
||||
cwd: root,
|
||||
env: { ...process.env, CI_REAL_VERIFICATION: '1' },
|
||||
env: { ...process.env, CI_REAL_VERIFICATION: '1', ...laneEnvironment },
|
||||
stdio: 'inherit',
|
||||
})
|
||||
const record = { script: name, status: result.status ?? 1, durationMs: Date.now() - startedAt }
|
||||
@@ -100,7 +106,14 @@ function runScript(name) {
|
||||
try {
|
||||
for (const lane of selectedLanes) {
|
||||
console.log(`\n[real-verification] lane=${lane}`)
|
||||
for (const name of lanes[lane]) runScript(name)
|
||||
const laneEnvironment = lane === 'oracle'
|
||||
? {
|
||||
FREECAD_ORACLE_PROFILE: 'desktop',
|
||||
FREECAD_CMD: resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd'),
|
||||
FREECAD_REFERENCE_CMD: resolve(root, '.cache/freecad/install-desktop/bin/FreeCAD'),
|
||||
}
|
||||
: {}
|
||||
for (const name of lanes[lane]) runScript(name, laneEnvironment)
|
||||
}
|
||||
} finally {
|
||||
const outputDir = resolve(root, '.cache', 'ci')
|
||||
|
||||
Reference in New Issue
Block a user