Build and verify the candidate-only FreeCAD naming bridge and OCCT worker path, including three-stage StringHasher restoration. Add Datum, ShapeBinder, attachment-mode, and PartDesign structure oracles plus offline SDK build plans and CI boundary checks.
127 lines
6.6 KiB
JavaScript
127 lines
6.6 KiB
JavaScript
import { access, open, readFile } from 'node:fs/promises'
|
|
import { createHash } from 'node:crypto'
|
|
import { createReadStream } from 'node:fs'
|
|
import { isAbsolute, resolve } from 'node:path'
|
|
|
|
export const REQUIRED_FREECAD_NAMING_LIBRARIES = ['FreeCADBase', 'FreeCADApp', 'Part', 'QtCore', 'Python']
|
|
export const REQUIRED_FREECAD_NAMING_LINK_DEPENDENCIES = ['QtConcurrent', 'QtNetwork', 'QtXml', 'QtBundledPcre2', 'QtBundledZLIB', 'yaml-cpp', 'ICUCommon', 'ICUI18N', 'ICUData', 'XercesC', 'BoostProgramOptions', 'BoostRegex', 'BoostThread', 'BoostDateTime', 'BoostAtomic', 'PythonMpdecimal', 'PythonExpat', 'PythonHaclSha2', 'PythonZlib', 'PythonBzip2', 'PythonSqlite3']
|
|
export const REQUIRED_FREECAD_NAMING_CALLBACKS = ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson']
|
|
export const REQUIRED_FREECAD_NAMING_DEFINITIONS = ['__linux__=1', 'QT_NO_KEYWORDS', 'HAVE_CONFIG_H', 'PYCXX_6_2_COMPATIBILITY']
|
|
|
|
export const exists = async (path) => access(path).then(() => true).catch(() => false)
|
|
|
|
export const resolveFrom = (base, path) => isAbsolute(path) ? path : resolve(base, path)
|
|
|
|
export const sha256File = async (path) => new Promise((resolveHash, reject) => {
|
|
const hash = createHash('sha256')
|
|
const stream = createReadStream(path)
|
|
stream.on('error', reject)
|
|
stream.on('data', (chunk) => hash.update(chunk))
|
|
stream.on('end', () => resolveHash(hash.digest('hex')))
|
|
})
|
|
|
|
const readAt = async (handle, length, position) => {
|
|
const buffer = Buffer.alloc(length)
|
|
const { bytesRead } = await handle.read(buffer, 0, length, position)
|
|
return buffer.subarray(0, bytesRead)
|
|
}
|
|
|
|
const isWasmObject = (bytes) => bytes.length >= 8
|
|
&& bytes.subarray(0, 4).equals(Buffer.from([0x00, 0x61, 0x73, 0x6d]))
|
|
&& bytes.subarray(4, 8).equals(Buffer.from([0x01, 0x00, 0x00, 0x00]))
|
|
const isElfObject = (bytes) => bytes.length >= 4 && bytes.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46]))
|
|
const isLlvmBitcode = (bytes) => bytes.length >= 4
|
|
&& (bytes.subarray(0, 4).equals(Buffer.from([0x42, 0x43, 0xc0, 0xde]))
|
|
|| bytes.subarray(0, 4).equals(Buffer.from([0xde, 0xc0, 0x17, 0x0b])))
|
|
|
|
export async function inspectWasmStaticArchive(path) {
|
|
const handle = await open(path, 'r')
|
|
try {
|
|
const stat = await handle.stat()
|
|
if (stat.size < 68) throw new Error('archive is empty or truncated')
|
|
const magic = await readAt(handle, 8, 0)
|
|
if (magic.toString('ascii') !== '!<arch>\n') throw new Error('file is not a regular static ar archive')
|
|
let offset = 8
|
|
let memberCount = 0
|
|
let wasmObjectCount = 0
|
|
let llvmBitcodeCount = 0
|
|
let hostObjectCount = 0
|
|
let unsupportedMemberCount = 0
|
|
while (offset + 60 <= stat.size) {
|
|
const header = await readAt(handle, 60, offset)
|
|
if (header.length !== 60 || header.subarray(58, 60).toString('ascii') !== '`\n') throw new Error(`invalid ar member header at byte ${offset}`)
|
|
const rawName = header.subarray(0, 16).toString('ascii').trim()
|
|
const sizeText = header.subarray(48, 58).toString('ascii').trim()
|
|
if (!/^\d+$/.test(sizeText)) throw new Error(`invalid ar member size at byte ${offset}`)
|
|
const memberSize = Number(sizeText)
|
|
const dataOffset = offset + 60
|
|
if (!Number.isSafeInteger(memberSize) || memberSize < 0 || dataOffset + memberSize > stat.size) throw new Error(`ar member exceeds archive size at byte ${offset}`)
|
|
const special = rawName === '/' || rawName === '//' || rawName === '/SYM64/'
|
|
if (!special) {
|
|
memberCount += 1
|
|
const extendedName = rawName.startsWith('#1/') ? Number(rawName.slice(3)) : 0
|
|
if (!Number.isSafeInteger(extendedName) || extendedName < 0 || extendedName > memberSize) throw new Error(`invalid BSD ar filename at byte ${offset}`)
|
|
const prefix = await readAt(handle, Math.min(8, memberSize - extendedName), dataOffset + extendedName)
|
|
if (isWasmObject(prefix)) wasmObjectCount += 1
|
|
else if (isLlvmBitcode(prefix)) llvmBitcodeCount += 1
|
|
else if (isElfObject(prefix)) hostObjectCount += 1
|
|
else unsupportedMemberCount += 1
|
|
}
|
|
offset = dataOffset + memberSize + (memberSize % 2)
|
|
}
|
|
if (memberCount === 0) throw new Error('archive has no object members')
|
|
if (hostObjectCount > 0) throw new Error(`archive contains ${hostObjectCount} host ELF object member(s)`)
|
|
if (unsupportedMemberCount > 0) throw new Error(`archive contains ${unsupportedMemberCount} unsupported member(s)`)
|
|
if (wasmObjectCount + llvmBitcodeCount === 0) throw new Error('archive has no wasm or LLVM bitcode object members')
|
|
return { target: 'wasm32-emscripten', bytes: stat.size, memberCount, wasmObjectCount, llvmBitcodeCount }
|
|
} finally {
|
|
await handle.close()
|
|
}
|
|
}
|
|
|
|
export async function loadSdkPlan(projectRoot) {
|
|
const path = resolve(projectRoot, 'config/freecad-naming-sdk-plan.json')
|
|
return JSON.parse(await readFile(path, 'utf8'))
|
|
}
|
|
|
|
export async function inspectPlannedLibrary(projectRoot, library) {
|
|
const path = resolveFrom(projectRoot, library.path)
|
|
if (!await exists(path)) return { name: library.name, path: library.path, status: 'missing' }
|
|
const archive = await inspectWasmStaticArchive(path)
|
|
const sha256 = await sha256File(path)
|
|
if (library.expectedSha256 && sha256 !== library.expectedSha256) throw new Error(`${library.name} hash mismatch: expected ${library.expectedSha256}, got ${sha256}`)
|
|
return { name: library.name, path: library.path, status: 'verified', sha256, ...archive }
|
|
}
|
|
|
|
export async function inspectIncludeDirectory(projectRoot, entry) {
|
|
const path = resolveFrom(projectRoot, entry.path)
|
|
const missingHeaders = []
|
|
for (const header of entry.requiredHeaders ?? []) if (!await exists(resolve(path, header))) missingHeaders.push(header)
|
|
return { name: entry.name, path: entry.path, status: missingHeaders.length === 0 ? 'verified' : 'missing', missingHeaders }
|
|
}
|
|
|
|
export async function inspectPlannedRuntimeAsset(projectRoot, asset) {
|
|
const root = resolveFrom(projectRoot, asset.path)
|
|
const files = []
|
|
const missingFiles = []
|
|
for (const relativePath of asset.requiredFiles ?? []) {
|
|
const path = resolve(root, relativePath)
|
|
if (!await exists(path)) {
|
|
missingFiles.push(relativePath)
|
|
continue
|
|
}
|
|
const sha256 = await sha256File(path)
|
|
const expectedSha256 = asset.expectedSha256?.[relativePath]
|
|
if (expectedSha256 && sha256 !== expectedSha256) throw new Error(`${asset.name}/${relativePath} hash mismatch: expected ${expectedSha256}, got ${sha256}`)
|
|
files.push({ path: relativePath, sha256 })
|
|
}
|
|
return {
|
|
name: asset.name,
|
|
path: asset.path,
|
|
preloadTo: asset.preloadTo,
|
|
status: missingFiles.length === 0 ? 'verified' : 'missing',
|
|
files,
|
|
missingFiles,
|
|
}
|
|
}
|