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') !== '!\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, } }