feat: add candidate FreeCAD naming SDK and attachment oracles
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.
This commit is contained in:
10
scripts/boost-emscripten-compiler-wrapper.sh
Executable file
10
scripts/boost-emscripten-compiler-wrapper.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${1:-}" == "-r" && "${2:-}" == "-o" && -n "${3:-}" ]]; then
|
||||
output="$3"
|
||||
shift 3
|
||||
exec emar rcs "${output}" "$@"
|
||||
fi
|
||||
|
||||
exec emcc "$@"
|
||||
49
scripts/build-boost-wasm.sh
Executable file
49
scripts/build-boost-wasm.sh
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
BOOST_ROOT="${BOOST_WASM_CACHE_DIR:-${ROOT_DIR}/.cache/toolchains/boost}"
|
||||
DOWNLOAD_DIR="${BOOST_ROOT}/downloads"
|
||||
SOURCE_DIR="${BOOST_ROOT}/src"
|
||||
INSTALL_DIR="${BOOST_ROOT}/install-wasm"
|
||||
ARCHIVE="${DOWNLOAD_DIR}/boost_1_83_0.tar.bz2"
|
||||
DOWNLOAD_URL="${BOOST_DOWNLOAD_URL:-https://mirrors.aliyun.com/blfs/conglomeration/boost/boost_1_83_0.tar.bz2}"
|
||||
EXPECTED_SHA256="6478edfe2f3305127cffe8caf73ea0176c53769f4bf1585be237eb30798c3b8e"
|
||||
JOBS="${JOBS:-$(nproc)}"
|
||||
|
||||
if [[ "$(emcc --version | sed -n '1s/.* \([0-9][0-9.]*\) .*/\1/p')" != "3.1.69" ]]; then
|
||||
echo "Boost wasm SDK must use Emscripten 3.1.69." >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "${DOWNLOAD_DIR}" "${SOURCE_DIR}" "${INSTALL_DIR}"
|
||||
if [[ ! -f "${ARCHIVE}" ]]; then
|
||||
curl --fail --location --retry 3 --connect-timeout 15 --output "${ARCHIVE}.part" "${DOWNLOAD_URL}"
|
||||
mv "${ARCHIVE}.part" "${ARCHIVE}"
|
||||
fi
|
||||
[[ "$(sha256sum "${ARCHIVE}" | awk '{print $1}')" == "${EXPECTED_SHA256}" ]] || { echo "Boost 1.83.0 archive SHA-256 mismatch." >&2; exit 1; }
|
||||
if [[ ! -x "${SOURCE_DIR}/bootstrap.sh" ]]; then
|
||||
tar -xjf "${ARCHIVE}" -C "${SOURCE_DIR}" --strip-components=1
|
||||
fi
|
||||
if [[ ! -x "${SOURCE_DIR}/b2" ]]; then
|
||||
(cd "${SOURCE_DIR}" && ./bootstrap.sh --with-libraries=program_options,regex,thread,date_time)
|
||||
fi
|
||||
cp -f "${ROOT_DIR}/scripts/boost-emscripten-compiler-wrapper.sh" "${SOURCE_DIR}/boost-emscripten-compiler-wrapper.sh"
|
||||
chmod +x "${SOURCE_DIR}/boost-emscripten-compiler-wrapper.sh"
|
||||
(cd "${SOURCE_DIR}" && ./b2 --user-config="${ROOT_DIR}/config/boost-emscripten-user-config.jam" \
|
||||
toolset=emscripten-3.1.69 link=static threading=multi variant=release \
|
||||
--with-program_options --with-regex --with-thread --with-date_time \
|
||||
--prefix="${INSTALL_DIR}" cxxflags=-pthread linkflags=-pthread install -j"${JOBS}")
|
||||
for name in program_options regex thread date_time atomic; do
|
||||
cp -f "${INSTALL_DIR}/lib/libboost_${name}.bc" "${INSTALL_DIR}/lib/libboost_${name}.a"
|
||||
done
|
||||
|
||||
cd "${ROOT_DIR}"
|
||||
node --input-type=module - "${INSTALL_DIR}" <<'NODE'
|
||||
import { inspectWasmStaticArchive } from './scripts/freecad-naming-sdk-lib.mjs'
|
||||
const install = process.argv[2]
|
||||
const archives = {}
|
||||
for (const name of ['program_options', 'regex', 'thread', 'date_time', 'atomic']) {
|
||||
archives[name] = await inspectWasmStaticArchive(`${install}/lib/libboost_${name}.a`)
|
||||
}
|
||||
console.log(JSON.stringify({ status: 'boost-wasm-pass', version: '1.83.0', pthread: true, archives }, null, 2))
|
||||
NODE
|
||||
85
scripts/build-cpython-wasm.sh
Normal file
85
scripts/build-cpython-wasm.sh
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PYTHON_ROOT="${CPYTHON_WASM_CACHE_DIR:-${ROOT_DIR}/.cache/toolchains/python}"
|
||||
DOWNLOAD_DIR="${PYTHON_ROOT}/downloads"
|
||||
SOURCE_DIR="${PYTHON_ROOT}/src"
|
||||
BUILD_DIR="${PYTHON_ROOT}/build-wasm"
|
||||
INSTALL_DIR="${PYTHON_ROOT}/install-wasm"
|
||||
EM_CACHE_DIR="${ROOT_DIR}/.cache/toolchains/emscripten-freecad-cache"
|
||||
ARCHIVE="${DOWNLOAD_DIR}/Python-3.13.5.tar.xz"
|
||||
DOWNLOAD_URL="${CPYTHON_DOWNLOAD_URL:-https://repo.huaweicloud.com/python/3.13.5/Python-3.13.5.tar.xz}"
|
||||
EXPECTED_MD5="dbaa8833aa736eddbb18a6a6ae0c10fa"
|
||||
EXPECTED_SHA256="93e583f243454e6e9e4588ca2c2662206ad961659863277afcdb96801647d640"
|
||||
SOURCE_DATE_EPOCH_VALUE="1735689600"
|
||||
JOBS="${JOBS:-$(nproc)}"
|
||||
|
||||
if [[ "$(emcc --version | sed -n '1s/.* \([0-9][0-9.]*\) .*/\1/p')" != "3.1.69" ]]; then
|
||||
echo "CPython wasm SDK must use Emscripten 3.1.69." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$(python3.13 --version | awk '{print $2}')" != "3.13.5" ]]; then
|
||||
echo "CPython wasm cross-build requires host Python 3.13.5." >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "${DOWNLOAD_DIR}" "${SOURCE_DIR}" "${BUILD_DIR}" "${INSTALL_DIR}/include/python3.13" "${INSTALL_DIR}/lib"
|
||||
if [[ ! -f "${ARCHIVE}" ]]; then
|
||||
curl --fail --location --retry 3 --connect-timeout 15 --output "${ARCHIVE}.part" "${DOWNLOAD_URL}"
|
||||
mv "${ARCHIVE}.part" "${ARCHIVE}"
|
||||
fi
|
||||
[[ "$(md5sum "${ARCHIVE}" | awk '{print $1}')" == "${EXPECTED_MD5}" ]] || { echo "CPython archive MD5 mismatch." >&2; exit 1; }
|
||||
[[ "$(sha256sum "${ARCHIVE}" | awk '{print $1}')" == "${EXPECTED_SHA256}" ]] || { echo "CPython archive SHA-256 mismatch." >&2; exit 1; }
|
||||
if [[ ! -f "${SOURCE_DIR}/configure" ]]; then
|
||||
tar -xJf "${ARCHIVE}" -C "${SOURCE_DIR}" --strip-components=1
|
||||
fi
|
||||
|
||||
(
|
||||
cd "${BUILD_DIR}"
|
||||
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH_VALUE}" \
|
||||
CONFIG_SITE="${SOURCE_DIR}/Tools/wasm/config.site-wasm32-emscripten" \
|
||||
EM_FROZEN_CACHE=0 EM_CACHE="${EM_CACHE_DIR}" emconfigure "${SOURCE_DIR}/configure" -C \
|
||||
--host=wasm32-unknown-emscripten \
|
||||
--build="$("${SOURCE_DIR}/config.guess")" \
|
||||
--with-emscripten-target=node \
|
||||
--enable-wasm-pthreads \
|
||||
--disable-wasm-dynamic-linking \
|
||||
--with-build-python=/usr/bin/python3.13 \
|
||||
--with-ensurepip=no \
|
||||
--disable-test-modules \
|
||||
--without-mimalloc
|
||||
)
|
||||
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH_VALUE}" EM_FROZEN_CACHE=0 EM_CACHE="${EM_CACHE_DIR}" \
|
||||
emmake make -C "${BUILD_DIR}" -j"${JOBS}" libpython3.13.a pybuilddir.txt
|
||||
PYTHON_SYSCONFIG_DATA="${BUILD_DIR}/$(tr -d '\n' < "${BUILD_DIR}/pybuilddir.txt")/_sysconfigdata__emscripten_wasm32-emscripten.py"
|
||||
[[ -s "${PYTHON_SYSCONFIG_DATA}" ]] || { echo "CPython wasm sysconfig data is missing." >&2; exit 1; }
|
||||
touch --date="@${SOURCE_DATE_EPOCH_VALUE}" "${PYTHON_SYSCONFIG_DATA}"
|
||||
rm -f "${BUILD_DIR}/usr/local/lib/python313.zip"
|
||||
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH_VALUE}" EM_FROZEN_CACHE=1 EM_CACHE="${EM_CACHE_DIR}" \
|
||||
emmake make -C "${BUILD_DIR}" wasm_stdlib
|
||||
cp -a "${SOURCE_DIR}/Include/." "${INSTALL_DIR}/include/python3.13/"
|
||||
cp -f "${BUILD_DIR}/pyconfig.h" "${INSTALL_DIR}/include/python3.13/pyconfig.h"
|
||||
cp -f "${BUILD_DIR}/libpython3.13.a" "${INSTALL_DIR}/lib/libpython3.13.a"
|
||||
mkdir -p "${INSTALL_DIR}/lib/python-deps" "${INSTALL_DIR}/share/python-wasm-stdlib"
|
||||
cp -f "${BUILD_DIR}/Modules/_decimal/libmpdec/libmpdec.a" "${INSTALL_DIR}/lib/python-deps/libmpdec.a"
|
||||
cp -f "${BUILD_DIR}/Modules/expat/libexpat.a" "${INSTALL_DIR}/lib/python-deps/libexpat.a"
|
||||
cp -f "${BUILD_DIR}/Modules/_hacl/libHacl_Hash_SHA2.a" "${INSTALL_DIR}/lib/python-deps/libHacl_Hash_SHA2.a"
|
||||
cp -f "${EM_CACHE_DIR}/sysroot/lib/wasm32-emscripten/libz.a" "${INSTALL_DIR}/lib/python-deps/libz.a"
|
||||
cp -f "${EM_CACHE_DIR}/sysroot/lib/wasm32-emscripten/libbz2.a" "${INSTALL_DIR}/lib/python-deps/libbz2.a"
|
||||
cp -f "${EM_CACHE_DIR}/sysroot/lib/wasm32-emscripten/libsqlite3-mt.a" "${INSTALL_DIR}/lib/python-deps/libsqlite3-mt.a"
|
||||
cp -a "${BUILD_DIR}/usr/local/." "${INSTALL_DIR}/share/python-wasm-stdlib/"
|
||||
|
||||
cd "${ROOT_DIR}"
|
||||
node --input-type=module - "${INSTALL_DIR}" <<'NODE'
|
||||
import { inspectWasmStaticArchive } from './scripts/freecad-naming-sdk-lib.mjs'
|
||||
const install = process.argv[2]
|
||||
const dependencies = ['libmpdec.a', 'libexpat.a', 'libHacl_Hash_SHA2.a', 'libz.a', 'libbz2.a', 'libsqlite3-mt.a']
|
||||
console.log(JSON.stringify({
|
||||
status: 'cpython-wasm-pass',
|
||||
version: '3.13.5',
|
||||
pthread: true,
|
||||
archive: await inspectWasmStaticArchive(`${install}/lib/libpython3.13.a`),
|
||||
dependencies: Object.fromEntries(await Promise.all(dependencies.map(async (name) => [name, await inspectWasmStaticArchive(`${install}/lib/python-deps/${name}`)]))),
|
||||
wasmStdlib: `${install}/share/python-wasm-stdlib/lib/python313.zip`,
|
||||
}, null, 2))
|
||||
NODE
|
||||
88
scripts/build-freecad-naming-bridge-candidate.sh
Executable file
88
scripts/build-freecad-naming-bridge-candidate.sh
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SDK_ROOT="${ROOT_DIR}/.cache/toolchains/freecad-naming-sdk"
|
||||
SOURCE_ROOT="${ROOT_DIR}/.cache/toolchains/freecad-naming-sdk/source-occt8"
|
||||
BUILD_ROOT="${ROOT_DIR}/.cache/toolchains/freecad-naming-sdk/build-wasm"
|
||||
OCCT_BUILD="${ROOT_DIR}/.cache/bitbybit/occt-history-build-offline"
|
||||
EM_CACHE_DIR="${ROOT_DIR}/.cache/toolchains/emscripten-freecad-cache"
|
||||
DIST_DIR="${ROOT_DIR}/native/freecad-naming-bridge/dist"
|
||||
BRIDGE_SOURCE="${ROOT_DIR}/native/freecad-naming-bridge/freecad_naming_bridge.cpp"
|
||||
QT="${ROOT_DIR}/.cache/toolchains/qt6/install-wasm-freecad"
|
||||
PYTHON="${ROOT_DIR}/.cache/toolchains/python/install-wasm"
|
||||
ICU="${ROOT_DIR}/.cache/toolchains/icu/install-wasm"
|
||||
XERCES="${ROOT_DIR}/.cache/toolchains/xerces-c/install-wasm"
|
||||
BOOST="${ROOT_DIR}/.cache/toolchains/boost/install-wasm"
|
||||
YAML="${ROOT_DIR}/.cache/toolchains/yaml-cpp/install-wasm"
|
||||
|
||||
cd "${ROOT_DIR}"
|
||||
node scripts/check-freecad-naming-sdk-readiness.mjs >/dev/null
|
||||
for path in \
|
||||
"${SDK_ROOT}/lib/libFreeCADBase.a" \
|
||||
"${SDK_ROOT}/lib/libFreeCADApp.a" \
|
||||
"${SDK_ROOT}/lib/libPart.a" \
|
||||
"${PYTHON}/lib/python-deps/libmpdec.a" \
|
||||
"${PYTHON}/lib/python-deps/libexpat.a" \
|
||||
"${PYTHON}/lib/python-deps/libHacl_Hash_SHA2.a" \
|
||||
"${PYTHON}/lib/python-deps/libz.a" \
|
||||
"${PYTHON}/lib/python-deps/libbz2.a" \
|
||||
"${PYTHON}/lib/python-deps/libsqlite3-mt.a" \
|
||||
"${PYTHON}/share/python-wasm-stdlib/lib/python313.zip" \
|
||||
"${BRIDGE_SOURCE}"; do
|
||||
[[ -s "${path}" ]] || { echo "Missing isolated bridge input: ${path}" >&2; exit 1; }
|
||||
done
|
||||
mkdir -p "${DIST_DIR}"
|
||||
|
||||
mapfile -t OCCT_LIBRARIES < <(find "${OCCT_BUILD}/lin32/clang/lib" -maxdepth 1 -type f -name '*.a' | sort)
|
||||
EM_FROZEN_CACHE=0 EM_CACHE="${EM_CACHE_DIR}" em++ "${BRIDGE_SOURCE}" \
|
||||
-I"${SOURCE_ROOT}/src" \
|
||||
-I"${BUILD_ROOT}/src" \
|
||||
-I"${SOURCE_ROOT}/src/3rdParty/PyCXX" \
|
||||
-I"${OCCT_BUILD}/include/opencascade" \
|
||||
-I"${ROOT_DIR}/.cache/occt/occt/src/Deprecated/NCollectionAliases" \
|
||||
-I"${QT}/include" -I"${QT}/include/QtCore" \
|
||||
-I"${PYTHON}/include/python3.13" \
|
||||
-I"${XERCES}/include" -I"${ICU}/include" -I"${YAML}/include" \
|
||||
-I"${ROOT_DIR}/.cache/offline-sysroot/include" \
|
||||
-include "${ROOT_DIR}/config/freecad-wasm-sdk-compat.h" \
|
||||
-D__linux__=1 -DQT_NO_KEYWORDS -DHAVE_CONFIG_H -DPYCXX_6_2_COMPATIBILITY \
|
||||
-std=c++20 -O1 -fexceptions -pthread --bind \
|
||||
-Wl,--start-group \
|
||||
"${SDK_ROOT}/lib/libPart.a" \
|
||||
"${SDK_ROOT}/lib/libFreeCADApp.a" \
|
||||
"${SDK_ROOT}/lib/libFreeCADBase.a" \
|
||||
"${OCCT_LIBRARIES[@]}" \
|
||||
"${PYTHON}/lib/libpython3.13.a" \
|
||||
"${PYTHON}/lib/python-deps/libmpdec.a" \
|
||||
"${PYTHON}/lib/python-deps/libexpat.a" \
|
||||
"${PYTHON}/lib/python-deps/libHacl_Hash_SHA2.a" \
|
||||
"${PYTHON}/lib/python-deps/libz.a" \
|
||||
"${PYTHON}/lib/python-deps/libbz2.a" \
|
||||
"${PYTHON}/lib/python-deps/libsqlite3-mt.a" \
|
||||
"${XERCES}/lib/libxerces-c.a" \
|
||||
"${YAML}/lib/libyaml-cpp.a" \
|
||||
"${BOOST}/lib/libboost_program_options.a" \
|
||||
"${BOOST}/lib/libboost_regex.a" \
|
||||
"${BOOST}/lib/libboost_thread.a" \
|
||||
"${BOOST}/lib/libboost_date_time.a" \
|
||||
"${BOOST}/lib/libboost_atomic.a" \
|
||||
"${ICU}/lib/libicui18n.a" "${ICU}/lib/libicuuc.a" "${ICU}/lib/libicudata.a" \
|
||||
"${QT}/lib/libQt6Concurrent.a" "${QT}/lib/libQt6Network.a" "${QT}/lib/libQt6Xml.a" \
|
||||
"${QT}/lib/libQt6Core.a" "${QT}/lib/libQt6BundledPcre2.a" "${QT}/lib/libQt6BundledZLIB.a" \
|
||||
-Wl,--end-group \
|
||||
-sMODULARIZE=1 -sEXPORT_ES6=1 -sENVIRONMENT=node \
|
||||
-sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=268435456 -sMAXIMUM_MEMORY=2147483648 \
|
||||
-sNO_EXIT_RUNTIME=1 -sPTHREAD_POOL_SIZE=2 \
|
||||
--preload-file "${PYTHON}/share/python-wasm-stdlib@/usr/local" \
|
||||
--pre-js "${ROOT_DIR}/native/freecad-naming-bridge/pre.js" \
|
||||
-o "${DIST_DIR}/freecad-private-naming-bridge.js"
|
||||
|
||||
FREECAD_NAMING_BRIDGE_REPORT="${SDK_ROOT}/isolated-bridge-report.json" \
|
||||
npx tsx native/freecad-naming-bridge/smoke-test.ts
|
||||
sha256sum \
|
||||
"${DIST_DIR}/freecad-private-naming-bridge.js" \
|
||||
"${DIST_DIR}/freecad-private-naming-bridge.wasm" \
|
||||
"${DIST_DIR}/freecad-private-naming-bridge.data"
|
||||
[[ ! -e "${ROOT_DIR}/public/native/freecad-naming-bridge" ]] || { echo "Isolated bridge must not be published under public/." >&2; exit 1; }
|
||||
echo "Isolated bridge evidence: ${SDK_ROOT}/isolated-bridge-report.json"
|
||||
85
scripts/build-freecad-naming-sdk-candidate.sh
Executable file
85
scripts/build-freecad-naming-sdk-candidate.sh
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SDK_ROOT="${FREECAD_WASM_SDK_OUTPUT_DIR:-${ROOT_DIR}/.cache/toolchains/freecad-naming-sdk}"
|
||||
BUILD_DIR="${ROOT_DIR}/.cache/toolchains/freecad-naming-sdk/build-wasm"
|
||||
REPORT_PATH="${SDK_ROOT}/candidate-build-report.json"
|
||||
JOBS="${JOBS:-$(nproc)}"
|
||||
|
||||
cd "${ROOT_DIR}"
|
||||
node scripts/check-freecad-wasm-sdk-build-plan.mjs >/dev/null
|
||||
if [[ "${FREECAD_WASM_REUSE_OVERLAY:-0}" != "1" ]]; then
|
||||
bash scripts/prepare-freecad-occt8-overlay.sh
|
||||
fi
|
||||
if [[ "${FREECAD_WASM_REUSE_CONFIGURE:-0}" != "1" ]]; then
|
||||
node scripts/configure-freecad-naming-sdk-candidate.mjs
|
||||
fi
|
||||
|
||||
cmake --build "${BUILD_DIR}" --target Part --parallel "${JOBS}"
|
||||
mkdir -p "${SDK_ROOT}/lib"
|
||||
cp -f "${BUILD_DIR}/lib/libFreeCADBase.so" "${SDK_ROOT}/lib/libFreeCADBase.a"
|
||||
cp -f "${BUILD_DIR}/lib/libFreeCADApp.so" "${SDK_ROOT}/lib/libFreeCADApp.a"
|
||||
cp -f "${BUILD_DIR}/Mod/Part/Part.so" "${SDK_ROOT}/lib/libPart.a"
|
||||
|
||||
node --input-type=module - "${SDK_ROOT}" "${REPORT_PATH}" <<'NODE'
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
import { execFile } from 'node:child_process'
|
||||
import { promisify } from 'node:util'
|
||||
import { relative, resolve } from 'node:path'
|
||||
import { inspectWasmStaticArchive, sha256File } from './scripts/freecad-naming-sdk-lib.mjs'
|
||||
|
||||
const run = promisify(execFile)
|
||||
const root = process.cwd()
|
||||
const sdkRoot = resolve(process.argv[2])
|
||||
const reportPath = resolve(process.argv[3])
|
||||
const specs = [
|
||||
{ name: 'FreeCADBase', file: 'libFreeCADBase.a', requiredMembers: [] },
|
||||
{
|
||||
name: 'FreeCADApp',
|
||||
file: 'libFreeCADApp.a',
|
||||
requiredMembers: ['ElementMap.cpp.o', 'MappedElement.cpp.o', 'MappedName.cpp.o', 'ElementNamingUtils.cpp.o', 'StringHasher.cpp.o'],
|
||||
},
|
||||
{ name: 'Part', file: 'libPart.a', requiredMembers: ['TopoShape.cpp.o', 'TopoShapeExpansion.cpp.o', 'TopoShapeMapper.cpp.o'] },
|
||||
]
|
||||
const archives = []
|
||||
for (const spec of specs) {
|
||||
const path = resolve(sdkRoot, 'lib', spec.file)
|
||||
const inspected = await inspectWasmStaticArchive(path)
|
||||
const members = (await run('emar', ['t', path], { maxBuffer: 16 * 1024 * 1024 })).stdout.trim().split('\n').filter(Boolean)
|
||||
const missingMembers = spec.requiredMembers.filter((member) => !members.includes(member))
|
||||
if (missingMembers.length > 0) throw new Error(`${spec.name} archive is missing required members: ${missingMembers.join(', ')}`)
|
||||
archives.push({
|
||||
name: spec.name,
|
||||
path: relative(root, path),
|
||||
sha256: await sha256File(path),
|
||||
...inspected,
|
||||
requiredMembers: spec.requiredMembers,
|
||||
})
|
||||
}
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
status: 'candidate-archives-built',
|
||||
availability: 'candidate-archives-only',
|
||||
target: 'wasm32-emscripten',
|
||||
freecadVersion: '1.1.1',
|
||||
sourceCommit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d',
|
||||
emscriptenVersion: '3.1.69',
|
||||
archives,
|
||||
productionPublication: false,
|
||||
productionManifestGenerated: false,
|
||||
boundary: {
|
||||
freecadNamingBuildStatus: 'contract-only',
|
||||
exTsn02: 'in_progress',
|
||||
systemExact: false,
|
||||
workerLinked: false,
|
||||
availability: 'unavailable',
|
||||
callbacks: [],
|
||||
},
|
||||
}
|
||||
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
|
||||
console.log(JSON.stringify({ ...report, report: relative(root, reportPath) }, null, 2))
|
||||
NODE
|
||||
|
||||
git -C "${ROOT_DIR}/.cache/freecad/FreeCAD" diff --quiet
|
||||
git -C "${ROOT_DIR}/.cache/freecad/FreeCAD" diff --cached --quiet
|
||||
@@ -6,18 +6,41 @@ FREECAD_SOURCE_DIR="${FREECAD_SOURCE_DIR:-${ROOT_DIR}/.cache/freecad/FreeCAD}"
|
||||
QT6_WASM_DIR="${QT6_WASM_DIR:-${ROOT_DIR}/.cache/toolchains/qt6/install-wasm}"
|
||||
OFFLINE_INCLUDE_DIR="${OFFLINE_INCLUDE_DIR:-${ROOT_DIR}/.cache/offline-sysroot/include}"
|
||||
DIST_DIR="${ROOT_DIR}/native/freecad-naming-probe/dist"
|
||||
OBJECT_DIR="${DIST_DIR}/objects"
|
||||
SOURCE_ARCHIVE="${DIST_DIR}/libFreeCADPrivateNamingProbe.a"
|
||||
SHIM_DIR="${ROOT_DIR}/native/freecad-naming-probe/shims"
|
||||
FREECAD_COMMIT="0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
INDEXED_NAME_SHA256="73e0e60a9d6ee06851252e2071ebdd58698f8f99733903232529b3a194164435"
|
||||
MAPPED_NAME_SHA256="90173aba5f331ac9453589833f7e5fc63c5c80a35f1600dbc71211606b1c9dbc"
|
||||
STRING_HASHER_SHA256="fd32f35c9c2a0c21a60fa634ec37ec594d6f331924c5bba7fdbbcb351c3b612c"
|
||||
HANDLE_SHA256="8f02d27a8b672e9a56764f85e394265b4fcf023aba097f040b03dcb38bc393d2"
|
||||
MAPPED_ELEMENT_SHA256="f5448d9de693e319460d5e3ef52c63a54e0948963e4b92786aac38c77bdcd7a5"
|
||||
ELEMENT_NAMING_UTILS_SHA256="fc3223857ca6d2990d2b48f437f75841cdf23e1e79fa18ef18296f40acd3cdc3"
|
||||
ELEMENT_MAP_SHA256="e1ceaedb624688ddffa6a834c1b32539e8ba5bbdc0ec31401ea087e6a9c3eb52"
|
||||
|
||||
if [[ ! -d "${FREECAD_SOURCE_DIR}/.git" ]] || [[ "$(git -C "${FREECAD_SOURCE_DIR}" rev-parse HEAD)" != "${FREECAD_COMMIT}" ]]; then
|
||||
echo "FreeCAD private naming probe requires locked commit ${FREECAD_COMMIT}." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! git -C "${FREECAD_SOURCE_DIR}" diff --quiet HEAD -- \
|
||||
src/App/IndexedName.cpp src/App/IndexedName.h \
|
||||
src/App/MappedName.cpp src/App/MappedName.h \
|
||||
src/App/MappedElement.cpp src/App/MappedElement.h \
|
||||
src/App/ElementNamingUtils.cpp src/App/ElementNamingUtils.h \
|
||||
src/App/ElementMap.cpp src/App/ElementMap.h \
|
||||
src/App/StringHasher.cpp src/App/StringHasher.h \
|
||||
src/Base/Handle.cpp src/Base/Handle.h; then
|
||||
echo "FreeCAD private naming probe requires unmodified locked source inputs." >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s %s\n' \
|
||||
"${INDEXED_NAME_SHA256}" "${FREECAD_SOURCE_DIR}/src/App/IndexedName.cpp" \
|
||||
"${MAPPED_NAME_SHA256}" "${FREECAD_SOURCE_DIR}/src/App/MappedName.cpp" | sha256sum -c -
|
||||
"${MAPPED_NAME_SHA256}" "${FREECAD_SOURCE_DIR}/src/App/MappedName.cpp" \
|
||||
"${STRING_HASHER_SHA256}" "${FREECAD_SOURCE_DIR}/src/App/StringHasher.cpp" \
|
||||
"${HANDLE_SHA256}" "${FREECAD_SOURCE_DIR}/src/Base/Handle.cpp" \
|
||||
"${MAPPED_ELEMENT_SHA256}" "${FREECAD_SOURCE_DIR}/src/App/MappedElement.cpp" \
|
||||
"${ELEMENT_NAMING_UTILS_SHA256}" "${FREECAD_SOURCE_DIR}/src/App/ElementNamingUtils.cpp" \
|
||||
"${ELEMENT_MAP_SHA256}" "${FREECAD_SOURCE_DIR}/src/App/ElementMap.cpp" | sha256sum -c -
|
||||
if [[ "$(emcc --version | sed -n '1s/.* \([0-9][0-9.]*\) .*/\1/p')" != "3.1.69" ]]; then
|
||||
echo "FreeCAD private naming probe requires Emscripten 3.1.69." >&2
|
||||
exit 1
|
||||
@@ -37,19 +60,38 @@ if [[ ! -s "${OFFLINE_INCLUDE_DIR}/boost/signals2/signal.hpp" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${DIST_DIR}"
|
||||
em++ \
|
||||
"${ROOT_DIR}/native/freecad-naming-probe/probe.cpp" \
|
||||
"${FREECAD_SOURCE_DIR}/src/App/IndexedName.cpp" \
|
||||
"${FREECAD_SOURCE_DIR}/src/App/MappedName.cpp" \
|
||||
mkdir -p "${DIST_DIR}" "${OBJECT_DIR}"
|
||||
COMPILE_FLAGS=(
|
||||
-I"${SHIM_DIR}" \
|
||||
-include "${SHIM_DIR}/App/StringHasher.h" \
|
||||
-include "${SHIM_DIR}/ProbeAppHost.h" \
|
||||
-I"${FREECAD_SOURCE_DIR}/src" \
|
||||
-I"${QT6_WASM_DIR}/include" \
|
||||
-I"${QT6_WASM_DIR}/include/QtCore" \
|
||||
-I"${OFFLINE_INCLUDE_DIR}" \
|
||||
-std=c++17 -Oz -fexceptions --bind \
|
||||
-std=c++17 -Oz -fexceptions
|
||||
)
|
||||
SOURCE_OBJECTS=()
|
||||
for source in \
|
||||
"App/IndexedName.cpp" \
|
||||
"App/MappedName.cpp" \
|
||||
"App/StringHasher.cpp" \
|
||||
"App/MappedElement.cpp" \
|
||||
"App/ElementNamingUtils.cpp" \
|
||||
"App/ElementMap.cpp" \
|
||||
"Base/Handle.cpp"; do
|
||||
object="${OBJECT_DIR}/$(basename "${source}" .cpp).o"
|
||||
em++ "${COMPILE_FLAGS[@]}" -c "${FREECAD_SOURCE_DIR}/src/${source}" -o "${object}"
|
||||
SOURCE_OBJECTS+=("${object}")
|
||||
done
|
||||
ARCHIVE_TMP="${SOURCE_ARCHIVE}.$$.tmp"
|
||||
emar rcs "${ARCHIVE_TMP}" "${SOURCE_OBJECTS[@]}"
|
||||
mv "${ARCHIVE_TMP}" "${SOURCE_ARCHIVE}"
|
||||
|
||||
em++ \
|
||||
"${ROOT_DIR}/native/freecad-naming-probe/probe.cpp" \
|
||||
"${COMPILE_FLAGS[@]}" --bind \
|
||||
-Wl,--start-group \
|
||||
"${SOURCE_ARCHIVE}" \
|
||||
"${QT6_WASM_DIR}/lib/libQt6Core.a" \
|
||||
"${QT6_WASM_DIR}/lib/libQt6BundledPcre2.a" \
|
||||
"${QT6_WASM_DIR}/lib/libQt6BundledZLIB.a" \
|
||||
@@ -58,4 +100,4 @@ em++ \
|
||||
-sALLOW_MEMORY_GROWTH=1 -sNO_EXIT_RUNTIME=1 \
|
||||
-o "${DIST_DIR}/freecad-private-naming-source-probe.js"
|
||||
|
||||
sha256sum "${DIST_DIR}/freecad-private-naming-source-probe.js" "${DIST_DIR}/freecad-private-naming-source-probe.wasm"
|
||||
sha256sum "${SOURCE_ARCHIVE}" "${DIST_DIR}/freecad-private-naming-source-probe.js" "${DIST_DIR}/freecad-private-naming-source-probe.wasm"
|
||||
|
||||
57
scripts/build-icu-wasm.sh
Normal file
57
scripts/build-icu-wasm.sh
Normal file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
ICU_ROOT="${ICU_WASM_CACHE_DIR:-${ROOT_DIR}/.cache/toolchains/icu}"
|
||||
ICU_DOWNLOAD_DIR="${ICU_ROOT}/downloads"
|
||||
ICU_SOURCE_DIR="${ICU_ROOT}/src"
|
||||
ICU_INSTALL_DIR="${ICU_ROOT}/install-wasm"
|
||||
EM_CACHE_DIR="${ROOT_DIR}/.cache/toolchains/emscripten-freecad-cache"
|
||||
ARCHIVE="${ICU_DOWNLOAD_DIR}/icu4c-68_2-src.tgz"
|
||||
DOWNLOAD_URL="${ICU_DOWNLOAD_URL:-https://mirrors.aliyun.com/blfs/conglomeration/icu/icu4c-68_2-src.tgz}"
|
||||
EXPECTED_MD5="c21cbdfe31a1e325afe765a16f907d20"
|
||||
EXPECTED_SHA256="c79193dee3907a2199b8296a93b52c5cb74332c26f3d167269487680d479d625"
|
||||
PORT_URL="https://github.com/unicode-org/icu/releases/download/release-68-2/icu4c-68_2-src.zip"
|
||||
|
||||
if [[ "$(emcc --version | sed -n '1s/.* \([0-9][0-9.]*\) .*/\1/p')" != "3.1.69" ]]; then
|
||||
echo "ICU wasm SDK must use Emscripten 3.1.69." >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "${ICU_DOWNLOAD_DIR}" "${ICU_SOURCE_DIR}" "${ICU_INSTALL_DIR}/include" "${ICU_INSTALL_DIR}/lib"
|
||||
if [[ ! -f "${ARCHIVE}" ]]; then
|
||||
curl --fail --location --retry 3 --connect-timeout 15 --output "${ARCHIVE}.part" "${DOWNLOAD_URL}"
|
||||
mv "${ARCHIVE}.part" "${ARCHIVE}"
|
||||
fi
|
||||
if [[ "$(md5sum "${ARCHIVE}" | awk '{print $1}')" != "${EXPECTED_MD5}" ]]; then
|
||||
echo "ICU 68.2 archive MD5 mismatch." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$(sha256sum "${ARCHIVE}" | awk '{print $1}')" != "${EXPECTED_SHA256}" ]]; then
|
||||
echo "ICU 68.2 archive SHA-256 mismatch." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "${ICU_SOURCE_DIR}/source/common/unicode/utypes.h" ]]; then
|
||||
tar -xzf "${ARCHIVE}" -C "${ICU_SOURCE_DIR}" --strip-components=1
|
||||
fi
|
||||
|
||||
mkdir -p "${EM_CACHE_DIR}/ports/icu/icu"
|
||||
cp -a "${ICU_SOURCE_DIR}/." "${EM_CACHE_DIR}/ports/icu/icu/"
|
||||
printf '%s\n' "${PORT_URL}" > "${EM_CACHE_DIR}/ports/icu/.emscripten_url"
|
||||
EM_FROZEN_CACHE=0 EM_CACHE="${EM_CACHE_DIR}" em++ -xc++ /dev/null -pthread -sUSE_ICU=1 -o "${ICU_ROOT}/icu-port-smoke.js"
|
||||
|
||||
cp -a "${EM_CACHE_DIR}/sysroot/include/unicode" "${ICU_INSTALL_DIR}/include/"
|
||||
cp -f "${EM_CACHE_DIR}/sysroot/lib/wasm32-emscripten/libicu_common-mt.a" "${ICU_INSTALL_DIR}/lib/libicuuc.a"
|
||||
cp -f "${EM_CACHE_DIR}/sysroot/lib/wasm32-emscripten/libicu_i18n-mt.a" "${ICU_INSTALL_DIR}/lib/libicui18n.a"
|
||||
cp -f "${EM_CACHE_DIR}/sysroot/lib/wasm32-emscripten/libicu_stubdata-mt.a" "${ICU_INSTALL_DIR}/lib/libicudata.a"
|
||||
cp -f "${EM_CACHE_DIR}/sysroot/lib/wasm32-emscripten/libicu_io-mt.a" "${ICU_INSTALL_DIR}/lib/libicuio.a"
|
||||
|
||||
cd "${ROOT_DIR}"
|
||||
node --input-type=module - "${ICU_INSTALL_DIR}" <<'NODE'
|
||||
import { inspectWasmStaticArchive } from './scripts/freecad-naming-sdk-lib.mjs'
|
||||
const install = process.argv[2]
|
||||
const archives = {}
|
||||
for (const name of ['icuuc', 'icui18n', 'icudata', 'icuio']) {
|
||||
archives[name] = await inspectWasmStaticArchive(`${install}/lib/lib${name}.a`)
|
||||
}
|
||||
console.log(JSON.stringify({ status: 'icu-wasm-pass', version: '68.2', pthread: true, archives }, null, 2))
|
||||
NODE
|
||||
75
scripts/build-qt6-freecad-wasm.sh
Normal file
75
scripts/build-qt6-freecad-wasm.sh
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
QT_ROOT="${QT6_WASM_CACHE_DIR:-${ROOT_DIR}/.cache/toolchains/qt6}"
|
||||
QT_SOURCE_DIR="${QT_ROOT}/src"
|
||||
QT_BUILD_DIR="${QT_ROOT}/build-wasm-freecad"
|
||||
QT_INSTALL_DIR="${QT_ROOT}/install-wasm-freecad"
|
||||
EMSDK_SHIM_DIR="${QT_ROOT}/emsdk-shim"
|
||||
JOBS="${JOBS:-$(nproc)}"
|
||||
|
||||
if [[ ! -x "${QT_SOURCE_DIR}/configure" ]]; then
|
||||
echo "Qt 6.8.2 source is missing; run build:qt6-wasm-core or restore the offline Qt source first." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v emcc >/dev/null || ! command -v em++ >/dev/null; then
|
||||
echo "Emscripten emcc/em++ are required." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$(emcc --version | sed -n '1s/.* \([0-9][0-9.]*\) .*/\1/p')" != "3.1.69" ]]; then
|
||||
echo "FreeCAD Qt wasm SDK must use Emscripten 3.1.69." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${QT_BUILD_DIR}" "${QT_INSTALL_DIR}"
|
||||
if [[ ! -f "${QT_BUILD_DIR}/build.ninja" ]]; then
|
||||
(
|
||||
cd "${QT_BUILD_DIR}"
|
||||
EMSDK="${EMSDK_SHIM_DIR}" "${QT_SOURCE_DIR}/configure" \
|
||||
-platform wasm-emscripten \
|
||||
-qt-host-path /usr \
|
||||
-prefix /qt \
|
||||
-extprefix "${QT_INSTALL_DIR}" \
|
||||
-static -release -optimize-size \
|
||||
-feature-thread \
|
||||
-no-gui -no-widgets -no-dbus \
|
||||
-nomake examples -nomake tests -nomake benchmarks -nomake manual-tests \
|
||||
-no-pch -no-sbom \
|
||||
-- -G Ninja \
|
||||
-DCMAKE_TOOLCHAIN_FILE=/usr/share/emscripten/cmake/Modules/Platform/Emscripten.cmake \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DQT_BUILD_TOOLS_WHEN_CROSSCOMPILING=OFF \
|
||||
-DQT_FEATURE_wasm_simd128=OFF \
|
||||
-DQT_FEATURE_wasm_exceptions=OFF \
|
||||
-DQt6HostInfo_DIR=/usr/lib/x86_64-linux-gnu/cmake/Qt6HostInfo
|
||||
)
|
||||
fi
|
||||
if ! rg -q '^QT_FEATURE_thread:INTERNAL=ON$' "${QT_BUILD_DIR}/CMakeCache.txt"; then
|
||||
echo "Existing FreeCAD Qt wasm build does not enable thread support: ${QT_BUILD_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cmake --build "${QT_BUILD_DIR}" --target Core Concurrent Network Xml QTlsBackendCertOnlyPlugin QTlsBackendCertOnlyPlugin_init --parallel "${JOBS}"
|
||||
cmake --install "${QT_BUILD_DIR}" --component Devel
|
||||
for module in corelib concurrent network xml; do cmake --install "${QT_BUILD_DIR}/src/${module}"; done
|
||||
cmake --install "${QT_BUILD_DIR}/src/plugins/tls/certonly"
|
||||
mkdir -p "${QT_INSTALL_DIR}/mkspecs/common"
|
||||
cmake -E copy_directory "${QT_SOURCE_DIR}/mkspecs/wasm-emscripten" "${QT_INSTALL_DIR}/mkspecs/wasm-emscripten"
|
||||
cmake -E copy_directory "${QT_SOURCE_DIR}/mkspecs/common/wasm" "${QT_INSTALL_DIR}/mkspecs/common/wasm"
|
||||
for helper in QtFeatureCommon.cmake "${QT_SOURCE_DIR}"/cmake/QtPublic*.cmake; do
|
||||
cp -f "${QT_SOURCE_DIR}/cmake/${helper##*/}" "${QT_INSTALL_DIR}/lib/cmake/Qt6/${helper##*/}"
|
||||
done
|
||||
for library in Core Concurrent Network Xml BundledPcre2 BundledZLIB; do
|
||||
cp -f "${QT_BUILD_DIR}/lib/libQt6${library}.a" "${QT_INSTALL_DIR}/lib/libQt6${library}.a"
|
||||
done
|
||||
|
||||
node --input-type=module - "${ROOT_DIR}" "${QT_INSTALL_DIR}" <<'NODE'
|
||||
import { inspectWasmStaticArchive } from './scripts/freecad-naming-sdk-lib.mjs'
|
||||
const install = process.argv[3]
|
||||
const result = {}
|
||||
for (const name of ['Core', 'Concurrent', 'Network', 'Xml', 'BundledPcre2', 'BundledZLIB']) {
|
||||
result[name] = await inspectWasmStaticArchive(`${install}/lib/libQt6${name}.a`)
|
||||
}
|
||||
console.log(JSON.stringify({ status: 'qt6-freecad-wasm-pass', threadSupport: true, mkspec: `${install}/mkspecs/wasm-emscripten`, modules: result }, null, 2))
|
||||
NODE
|
||||
67
scripts/build-xerces-c-wasm.sh
Executable file
67
scripts/build-xerces-c-wasm.sh
Executable file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
XERCES_ROOT="${XERCES_C_WASM_CACHE_DIR:-${ROOT_DIR}/.cache/toolchains/xerces-c}"
|
||||
DOWNLOAD_DIR="${XERCES_ROOT}/downloads"
|
||||
SOURCE_DIR="${XERCES_ROOT}/src"
|
||||
BUILD_DIR="${XERCES_ROOT}/build-wasm"
|
||||
INSTALL_DIR="${XERCES_ROOT}/install-wasm"
|
||||
ICU_ROOT="${ICU_WASM_CACHE_DIR:-${ROOT_DIR}/.cache/toolchains/icu}"
|
||||
ARCHIVE="${DOWNLOAD_DIR}/xerces-c-3.2.4.tar.xz"
|
||||
DOWNLOAD_URL="${XERCES_C_DOWNLOAD_URL:-https://archive.apache.org/dist/xerces/c/3/sources/xerces-c-3.2.4.tar.xz}"
|
||||
EXPECTED_SHA256="075bc57940da0f9be6dd183c550c8ce0b9833e4550dc382048377a1a5e3b2bd9"
|
||||
JOBS="${JOBS:-$(nproc)}"
|
||||
|
||||
if [[ "$(emcc --version | sed -n '1s/.* \([0-9][0-9.]*\) .*/\1/p')" != "3.1.69" ]]; then
|
||||
echo "Xerces-C wasm SDK must use Emscripten 3.1.69." >&2
|
||||
exit 1
|
||||
fi
|
||||
for archive in libicuuc.a libicudata.a; do
|
||||
if [[ ! -f "${ICU_ROOT}/install-wasm/lib/${archive}" ]]; then
|
||||
echo "Xerces-C wasm SDK requires the staged ICU archive: ${archive}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
mkdir -p "${DOWNLOAD_DIR}" "${SOURCE_DIR}" "${BUILD_DIR}" "${INSTALL_DIR}/lib"
|
||||
if [[ ! -f "${ARCHIVE}" ]]; then
|
||||
curl --fail --location --retry 3 --connect-timeout 15 --output "${ARCHIVE}.part" "${DOWNLOAD_URL}"
|
||||
mv "${ARCHIVE}.part" "${ARCHIVE}"
|
||||
fi
|
||||
if [[ "$(sha256sum "${ARCHIVE}" | awk '{print $1}')" != "${EXPECTED_SHA256}" ]]; then
|
||||
echo "Xerces-C 3.2.4 archive SHA-256 mismatch." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "${SOURCE_DIR}/CMakeLists.txt" ]]; then
|
||||
tar -xJf "${ARCHIVE}" -C "${SOURCE_DIR}" --strip-components=1
|
||||
fi
|
||||
|
||||
emcmake cmake --fresh -S "${SOURCE_DIR}" -B "${BUILD_DIR}" -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX="${INSTALL_DIR}" \
|
||||
-DBUILD_SHARED_LIBS=OFF \
|
||||
-Dnetwork=OFF \
|
||||
-Dtranscoder=icu \
|
||||
-Dmsgloader=inmemory \
|
||||
-Dthreads=ON \
|
||||
-Dsse2=OFF \
|
||||
-DICU_ROOT="${ICU_ROOT}/install-wasm" \
|
||||
-DICU_INCLUDE_DIR="${ICU_ROOT}/install-wasm/include" \
|
||||
-DICU_UC_LIBRARY_RELEASE="${ICU_ROOT}/install-wasm/lib/libicuuc.a" \
|
||||
-DICU_DATA_LIBRARY_RELEASE="${ICU_ROOT}/install-wasm/lib/libicudata.a"
|
||||
cmake --build "${BUILD_DIR}" --target xerces-c --parallel "${JOBS}"
|
||||
# Upstream assigns samples and the core library to the same runtime component.
|
||||
# Install development files separately and stage the already-built archive directly.
|
||||
cmake --install "${BUILD_DIR}" --component development
|
||||
cp -f "${BUILD_DIR}/src/libxerces-c.a" "${INSTALL_DIR}/lib/libxerces-c.a"
|
||||
|
||||
cd "${ROOT_DIR}"
|
||||
node --input-type=module - "${INSTALL_DIR}" <<'NODE'
|
||||
import { inspectWasmStaticArchive } from './scripts/freecad-naming-sdk-lib.mjs'
|
||||
const install = process.argv[2]
|
||||
console.log(JSON.stringify({
|
||||
status: 'xerces-c-wasm-pass',
|
||||
version: '3.2.4',
|
||||
archive: await inspectWasmStaticArchive(`${install}/lib/libxerces-c.a`),
|
||||
}, null, 2))
|
||||
NODE
|
||||
48
scripts/build-yaml-cpp-wasm.sh
Normal file
48
scripts/build-yaml-cpp-wasm.sh
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
YAML_ROOT="${YAML_CPP_WASM_CACHE_DIR:-${ROOT_DIR}/.cache/toolchains/yaml-cpp}"
|
||||
YAML_SOURCE_DIR="${YAML_ROOT}/src"
|
||||
YAML_BUILD_DIR="${YAML_ROOT}/build-wasm"
|
||||
YAML_INSTALL_DIR="${YAML_ROOT}/install-wasm"
|
||||
JOBS="${JOBS:-$(nproc)}"
|
||||
LOCKED_COMMIT="f7320141120f720aecc4c32be25586e7da9eb978"
|
||||
|
||||
if [[ ! -f "${YAML_SOURCE_DIR}/CMakeLists.txt" ]]; then
|
||||
echo "yaml-cpp 0.8.0 source is missing: ${YAML_SOURCE_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$(git -C "${YAML_SOURCE_DIR}" rev-parse HEAD)" != "${LOCKED_COMMIT}" ]]; then
|
||||
echo "yaml-cpp source must be locked to ${LOCKED_COMMIT}." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n "$(git -C "${YAML_SOURCE_DIR}" status --porcelain)" ]]; then
|
||||
echo "yaml-cpp source checkout must be clean." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$(emcc --version | sed -n '1s/.* \([0-9][0-9.]*\) .*/\1/p')" != "3.1.69" ]]; then
|
||||
echo "yaml-cpp wasm SDK must use Emscripten 3.1.69." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
emcmake cmake --fresh -S "${YAML_SOURCE_DIR}" -B "${YAML_BUILD_DIR}" -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX="${YAML_INSTALL_DIR}" \
|
||||
-DYAML_BUILD_SHARED_LIBS=OFF \
|
||||
-DYAML_CPP_BUILD_TESTS=OFF \
|
||||
-DYAML_CPP_BUILD_TOOLS=OFF \
|
||||
-DYAML_CPP_INSTALL=ON \
|
||||
-DYAML_CPP_FORMAT_SOURCE=OFF
|
||||
cmake --build "${YAML_BUILD_DIR}" --parallel "${JOBS}"
|
||||
cmake --install "${YAML_BUILD_DIR}"
|
||||
|
||||
cd "${ROOT_DIR}"
|
||||
node --input-type=module - "${YAML_INSTALL_DIR}" <<'NODE'
|
||||
import { inspectWasmStaticArchive } from './scripts/freecad-naming-sdk-lib.mjs'
|
||||
const install = process.argv[2]
|
||||
console.log(JSON.stringify({
|
||||
status: 'yaml-cpp-wasm-pass',
|
||||
archive: await inspectWasmStaticArchive(`${install}/lib/libyaml-cpp.a`),
|
||||
}, null, 2))
|
||||
NODE
|
||||
19
scripts/check-chrome-freecad-naming-worker-candidate.mjs
Normal file
19
scripts/check-chrome-freecad-naming-worker-candidate.mjs
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile, stat } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const report = JSON.parse(await readFile(resolve(root, '.cache/toolchains/freecad-naming-sdk/chrome-candidate-worker-report.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`Chrome FreeCAD naming candidate Worker: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.browserId !== 'chrome' || report.crossOriginIsolated !== true) fail('live Chrome report is missing or failed.')
|
||||
if (JSON.stringify(report.callbacks) !== JSON.stringify(['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson'])) fail('callback set is incomplete.')
|
||||
if (report.chainedStage?.mappedNames !== 1 || report.chainedStage?.stringHasherEntries < 1 || report.thirdStage?.mappedNames !== 1 || report.thirdStage?.stringHasherEntries <= report.chainedStage.stringHasherEntries || report.invalidHistoryRejected !== true) fail('three-stage native evidence closure or fail-closed result is incomplete.')
|
||||
if (report.candidateOnly !== true || report.productionPublication !== false || report.productionWorkerLinked !== false) fail('candidate report crossed the production publication boundary.')
|
||||
for (const artifact of report.artifacts ?? []) {
|
||||
const path = resolve(root, '.cache/candidates/freecad-naming-worker', artifact.name)
|
||||
const [content, size] = await Promise.all([readFile(path), stat(path).then(({ size }) => size)])
|
||||
const sha256 = createHash('sha256').update(content).digest('hex')
|
||||
if (artifact.bytes !== size || artifact.sha256 !== sha256) fail(`stale artifact evidence for ${artifact.name}.`)
|
||||
}
|
||||
if (report.artifacts?.length !== 3) fail('artifact evidence must include JS, WASM and DATA.')
|
||||
console.log(JSON.stringify({ status: 'chrome-freecad-naming-candidate-worker-check-pass', generatedAt: report.generatedAt, callbacks: report.callbacks, chainedStage: report.chainedStage, thirdStage: report.thirdStage, candidateOnly: true, productionPublication: false, productionWorkerLinked: false }, null, 2))
|
||||
66
scripts/check-freecad-attachment-mode-oracle.mjs
Normal file
66
scripts/check-freecad-attachment-mode-oracle.mjs
Normal file
@@ -0,0 +1,66 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const oracle = JSON.parse(await readFile(resolve(root, 'config/freecad-attachment-mode-oracle.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD attachment mode oracle: ${message}`) }
|
||||
const near = (actual, expected) => typeof actual === 'number' && Math.abs(actual - expected) <= oracle.tolerance
|
||||
const same = (actual, expected) => JSON.stringify(actual) === JSON.stringify(expected)
|
||||
|
||||
const modes = [
|
||||
'Deactivated', 'Translate', 'ObjectXY', 'ObjectXZ', 'ObjectYZ', 'FlatFace', 'TangentPlane', 'NormalToEdge',
|
||||
'FrenetNB', 'FrenetTN', 'FrenetTB', 'Concentric', 'SectionOfRevolution', 'ThreePointsPlane', 'ThreePointsNormal', 'Folding',
|
||||
'ObjectX', 'ObjectY', 'ObjectZ', 'AxisOfCurvature', 'Directrix1', 'Directrix2', 'Asymptote1', 'Asymptote2', 'Tangent', 'Normal',
|
||||
'Binormal', 'TangentU', 'TangentV', 'TwoPointLine', 'IntersectionLine', 'ProximityLine', 'ObjectOrigin', 'Focus1', 'Focus2',
|
||||
'OnEdge', 'CenterOfCurvature', 'CenterOfMass', 'IntersectionPoint', 'Vertex', 'ProximityPoint1', 'ProximityPoint2',
|
||||
'AxisOfInertia1', 'AxisOfInertia2', 'AxisOfInertia3', 'InertialCS', 'FaceNormal', 'OZX', 'OZY', 'OXY', 'OXZ', 'OYZ', 'OYX',
|
||||
'ParallelPlane', 'MidPoint',
|
||||
]
|
||||
const implemented = {
|
||||
plane: ['Translate', 'ObjectXY', 'ObjectXZ', 'ObjectYZ', 'FlatFace', 'TangentPlane', 'NormalToEdge', 'FrenetNB', 'FrenetTN', 'FrenetTB', 'Concentric', 'SectionOfRevolution', 'ThreePointsPlane', 'ThreePointsNormal', 'Folding', 'InertialCS', 'OZX', 'OZY', 'OXY', 'OXZ', 'OYZ', 'OYX', 'ParallelPlane'],
|
||||
line: ['ObjectX', 'ObjectY', 'ObjectZ', 'AxisOfCurvature', 'Directrix1', 'Directrix2', 'Asymptote1', 'Asymptote2', 'Tangent', 'Normal', 'Binormal', 'TwoPointLine', 'IntersectionLine', 'ProximityLine', 'AxisOfInertia1', 'AxisOfInertia2', 'AxisOfInertia3', 'FaceNormal'],
|
||||
point: ['ObjectOrigin', 'Focus1', 'Focus2', 'OnEdge', 'CenterOfCurvature', 'CenterOfMass', 'Vertex', 'ProximityPoint1', 'ProximityPoint2'],
|
||||
}
|
||||
implemented.sketch = implemented.plane
|
||||
const engineTypes = { plane: 'Attacher::AttachEnginePlane', line: 'Attacher::AttachEngineLine', point: 'Attacher::AttachEnginePoint', sketch: 'Attacher::AttachEnginePlane' }
|
||||
const objectTypes = { plane: 'PartDesign::Plane', line: 'PartDesign::Line', point: 'PartDesign::Point', sketch: 'Sketcher::SketchObject' }
|
||||
|
||||
if (oracle.schemaVersion !== 1 || oracle.baselineId !== 'freecad-1.1.1-attachment-mode-oracle' || oracle.freecadVersion !== '1.1.1' || oracle.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || oracle.status !== 'pass' || oracle.tolerance !== 1e-7) fail('baseline is invalid.')
|
||||
if (oracle.registry?.modeCount !== 55 || !same(oracle.registry.modes, modes) || oracle.registry.implementedUnionCount !== 50 || !same(oracle.registry.unimplementedModes, ['Deactivated', 'TangentU', 'TangentV', 'IntersectionPoint', 'MidPoint'])) fail('persistent mode registry or implemented union changed.')
|
||||
|
||||
for (const name of Object.keys(engineTypes)) {
|
||||
const engine = oracle.engines?.[name]
|
||||
if (engine?.typeId !== objectTypes[name] || engine.attacherType !== engineTypes[name] || !same(engine.completeModeList, modes) || !same(engine.implementedModes, implemented[name])) fail(`${name} engine identity or implemented modes changed.`)
|
||||
for (const mode of implemented[name]) {
|
||||
const info = engine.modeInfo?.[mode]
|
||||
if (info?.modeIndex !== modes.indexOf(mode) || !Array.isArray(info.referenceCombinations) || info.referenceCombinations.length === 0) fail(`${name}.${mode} mode metadata is incomplete.`)
|
||||
}
|
||||
}
|
||||
if (!same(oracle.engines.plane.modeInfo.FlatFace.referenceCombinations, [['Plane']]) || !same(oracle.engines.line.modeInfo.TwoPointLine.referenceCombinations, [['Vertex', 'Vertex'], ['Line']]) || !same(oracle.engines.point.modeInfo.Vertex.referenceCombinations, [['Vertex'], ['Line']])) fail('representative reference combination contract changed.')
|
||||
|
||||
const expectedSuccess = {
|
||||
plane: { typeId: 'PartDesign::Plane', mode: 'FlatFace', refs: ['Plane'], support: ['Face1'] },
|
||||
line: { typeId: 'PartDesign::Line', mode: 'TwoPointLine', refs: ['Vertex', 'Vertex'], support: ['Vertex1', 'Vertex2'] },
|
||||
point: { typeId: 'PartDesign::Point', mode: 'Vertex', refs: ['Vertex'], support: ['Vertex1'] },
|
||||
sketch: { typeId: 'Sketcher::SketchObject', mode: 'FlatFace', refs: ['Plane'], support: ['Face1'] },
|
||||
}
|
||||
for (const phase of ['initial', 'edited', 'roundtrip']) {
|
||||
for (const [name, expected] of Object.entries(expectedSuccess)) {
|
||||
const result = oracle.success?.[phase]?.[name]
|
||||
if (result?.typeId !== expected.typeId || result.mapMode !== expected.mode || result.positionBySupport !== true || !same(result.state, ['Up-to-date']) || result.status !== 'Valid' || result.suggestion?.message !== 'OK' || !result.suggestion.allApplicableModes.includes(expected.mode) || !same(result.suggestion.referenceTypes, expected.refs) || !same(result.support?.[0]?.subElements, expected.support) || result.support?.[0]?.object !== 'SourceBox') fail(`${phase} ${name} success evidence is invalid.`)
|
||||
}
|
||||
}
|
||||
if (!near(oracle.success.initial.point.placement.position?.[2], 6) || !near(oracle.success.edited.point.placement.position?.[2], 9) || !near(oracle.success.roundtrip.point.placement.position?.[2], 9)) fail('source mutation did not move the attached point through edit and round-trip.')
|
||||
|
||||
const failureContract = {
|
||||
InvalidPlane: ['PartDesign::Plane', 'Vertex', 'Attachment mode Vertex is not implemented.'],
|
||||
InvalidLine: ['PartDesign::Line', 'FlatFace', 'Attachment mode FlatFace is not implemented.'],
|
||||
InvalidPoint: ['PartDesign::Point', 'FlatFace', 'Attachment mode FlatFace is not implemented.'],
|
||||
}
|
||||
for (const [name, [typeId, mode, status]] of Object.entries(failureContract)) {
|
||||
const initial = oracle.failures?.initial?.[name]
|
||||
const roundtrip = oracle.failures?.roundtrip?.[name]
|
||||
if (initial?.typeId !== typeId || initial.mapMode !== mode || !same(initial.state, ['Touched', 'Invalid']) || initial.status !== status || roundtrip?.mapMode !== mode || !same(roundtrip.state, ['Touched', 'Invalid']) || roundtrip.status !== status) fail(`${name} unsupported-mode failure did not survive FCStd round-trip.`)
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({ status: 'freecad-attachment-mode-oracle-pass', baselineId: oracle.baselineId, persistentModes: oracle.registry.modeCount, implementedModes: { plane: implemented.plane.length, line: implemented.line.length, point: implemented.point.length, sketch: implemented.sketch.length }, representativeSuccesses: Object.keys(expectedSuccess), unsupportedFailures: Object.keys(failureContract), pointMutationZ: [oracle.success.initial.point.placement.position[2], oracle.success.edited.point.placement.position[2]], fcstdRoundtrip: true }, null, 2))
|
||||
19
scripts/check-freecad-naming-bridge-candidate.mjs
Normal file
19
scripts/check-freecad-naming-bridge-candidate.mjs
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile, stat } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const report = JSON.parse(await readFile(resolve(root, '.cache/toolchains/freecad-naming-sdk/isolated-bridge-report.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD naming isolated bridge candidate: ${message}`) }
|
||||
if (report.schemaVersion !== 1 || report.status !== 'freecad-private-naming-isolated-bridge-pass') fail('execution report is missing or failed.')
|
||||
if (report.firstStage?.mappedNames !== 1 || report.firstStage?.stringHasherEntries !== 0 || report.chainedStage?.mappedNames !== 1 || report.chainedStage?.stringHasherEntries < 1 || report.thirdStage?.mappedNames !== 1 || report.thirdStage?.stringHasherEntries <= report.chainedStage.stringHasherEntries) fail('three-stage StringHasher restoration evidence is incomplete.')
|
||||
if (report.tamperedHasherRejected !== true || report.inconsistentTablesRejected !== true) fail('strict StringHasher fail-closed evidence is incomplete.')
|
||||
if (report.productionPublication !== false || report.productionWorkerLinked !== false) fail('candidate report crossed the production publication boundary.')
|
||||
for (const artifact of report.artifacts ?? []) {
|
||||
const path = resolve(root, 'native/freecad-naming-bridge/dist', artifact.name)
|
||||
const [content, size] = await Promise.all([readFile(path), stat(path).then(({ size }) => size)])
|
||||
const sha256 = createHash('sha256').update(content).digest('hex')
|
||||
if (artifact.bytes !== size || artifact.sha256 !== sha256) fail(`stale artifact evidence for ${artifact.name}.`)
|
||||
}
|
||||
if (report.artifacts?.length !== 3) fail('artifact evidence must include JS, WASM and DATA.')
|
||||
console.log(JSON.stringify({ status: 'freecad-naming-isolated-bridge-candidate-check-pass', stringHasherStages: [report.firstStage.stringHasherEntries, report.chainedStage.stringHasherEntries, report.thirdStage.stringHasherEntries], tamperedHasherRejected: true, inconsistentTablesRejected: true, candidateOnly: true, productionPublication: false, productionWorkerLinked: false }, null, 2))
|
||||
22
scripts/check-freecad-naming-next-tasks.mjs
Normal file
22
scripts/check-freecad-naming-next-tasks.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const packageJson = JSON.parse(await readFile(resolve(root, 'package.json'), 'utf8'))
|
||||
const plan = JSON.parse(await readFile(resolve(root, 'config/freecad-naming-next-tasks.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD naming next tasks: ${message}`) }
|
||||
const expectedIds = ['SDK-01', 'SDK-02A', 'SDK-02B', 'SDK-04', 'SDK-03', 'SDK-05', 'PAR-01', 'QA-01']
|
||||
if (plan.schemaVersion !== 1 || plan.scope !== 'freecad-private-naming-and-parameter-followup') fail('unsupported schema or scope.')
|
||||
if (JSON.stringify(plan.orderedTasks?.map(({ id }) => id)) !== JSON.stringify(expectedIds)) fail('ordered task list is incomplete or reordered.')
|
||||
const taskById = new Map(plan.orderedTasks.map((task) => [task.id, task]))
|
||||
for (const task of plan.orderedTasks) {
|
||||
if (!['pending', 'in_progress', 'completed'].includes(task.status)) fail(`${task.id} has an invalid status.`)
|
||||
if (!Array.isArray(task.deliverables) || !task.deliverables.length || !Array.isArray(task.acceptance) || !task.acceptance.length || !Array.isArray(task.evidence) || !task.evidence.length) fail(`${task.id} is missing its executable contract.`)
|
||||
for (const dependency of task.dependencies ?? []) if (!taskById.has(dependency)) fail(`${task.id} depends on unknown task ${dependency}.`)
|
||||
for (const evidence of task.evidence) if (!packageJson.scripts?.[evidence]) fail(`${task.id} references missing npm script ${evidence}.`)
|
||||
}
|
||||
if (taskById.get('SDK-01').status !== 'completed' || taskById.get('SDK-02A').status !== 'completed' || taskById.get('SDK-02B').status !== 'completed' || taskById.get('SDK-04').status !== 'completed' || taskById.get('SDK-03').status !== 'completed' || taskById.get('SDK-05').status !== 'completed' || taskById.get('PAR-01').status !== 'in_progress') fail('current SDK/parameter task status is stale.')
|
||||
if (JSON.stringify(taskById.get('SDK-03').dependencies) !== JSON.stringify(['SDK-02B', 'SDK-04'])) fail('SDK-03 dependencies must retain both the archive set and isolated bridge.')
|
||||
if (taskById.get('SDK-04').dependencies?.length !== 1 || taskById.get('SDK-04').dependencies[0] !== 'SDK-02B') fail('SDK-04 must start directly from the completed archive set.')
|
||||
if (plan.boundary?.freecadNamingBuildStatus !== 'contract-only' || plan.boundary?.exTsn02 !== 'in_progress' || plan.boundary?.systemExact !== false || plan.boundary?.productionWorker?.availability !== 'unavailable' || plan.boundary?.productionWorker?.callbacks?.length !== 0) fail('production boundary changed before the candidate Worker gate.')
|
||||
console.log(JSON.stringify({ status: 'freecad-naming-next-tasks-pass', tasks: plan.orderedTasks.length, completed: plan.orderedTasks.filter(({ status }) => status === 'completed').length, inProgress: plan.orderedTasks.filter(({ status }) => status === 'in_progress').map(({ id }) => id), boundary: plan.boundary }, null, 2))
|
||||
103
scripts/check-freecad-naming-sdk-readiness.mjs
Normal file
103
scripts/check-freecad-naming-sdk-readiness.mjs
Normal file
@@ -0,0 +1,103 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { execFile } from 'node:child_process'
|
||||
import { promisify } from 'node:util'
|
||||
import { resolve } from 'node:path'
|
||||
import {
|
||||
REQUIRED_FREECAD_NAMING_CALLBACKS,
|
||||
REQUIRED_FREECAD_NAMING_DEFINITIONS,
|
||||
REQUIRED_FREECAD_NAMING_LIBRARIES,
|
||||
exists,
|
||||
inspectIncludeDirectory,
|
||||
inspectPlannedLibrary,
|
||||
inspectPlannedRuntimeAsset,
|
||||
loadSdkPlan,
|
||||
resolveFrom,
|
||||
sha256File,
|
||||
} from './freecad-naming-sdk-lib.mjs'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const execFileAsync = promisify(execFile)
|
||||
const plan = await loadSdkPlan(root)
|
||||
const fail = (message) => { throw new Error(`FreeCAD WASM naming SDK readiness: ${message}`) }
|
||||
if (plan.schemaVersion !== 1 || plan.scope !== 'pre-production-sdk-readiness') fail('unsupported plan schema or scope.')
|
||||
if (plan.baseline?.freecadVersion !== '1.1.1' || plan.baseline?.sourceCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || plan.baseline?.emscriptenVersion !== '3.1.69' || plan.baseline?.target !== 'wasm32-emscripten') fail('baseline is not locked to the required FreeCAD/Emscripten wasm target.')
|
||||
if (plan.productionPublication !== false) fail('candidate SDK readiness cannot publish a production Worker.')
|
||||
if (plan.boundary?.freecadNamingBuildStatus !== 'contract-only' || plan.boundary?.exTsn02 !== 'in_progress' || plan.boundary?.systemExact !== false) fail('plan boundary must remain contract-only/in_progress/non-exact.')
|
||||
if (JSON.stringify(plan.libraries?.map(({ name }) => name)) !== JSON.stringify(REQUIRED_FREECAD_NAMING_LIBRARIES)) fail('library list or order does not match the production SDK contract.')
|
||||
if (!REQUIRED_FREECAD_NAMING_CALLBACKS.every((callback) => plan.namingBridge?.requiredExports?.includes(callback))) fail('naming bridge plan does not require all production callbacks.')
|
||||
if (plan.compileOptions?.cxxStandard !== 'c++20' || plan.compileOptions?.pthread !== true || JSON.stringify(plan.compileOptions?.definitions) !== JSON.stringify(REQUIRED_FREECAD_NAMING_DEFINITIONS)) fail('compile options must lock C++20, pthread and the FreeCAD wasm compatibility definitions.')
|
||||
|
||||
const libraries = []
|
||||
for (const library of plan.libraries) libraries.push(await inspectPlannedLibrary(root, library))
|
||||
const linkDependencies = []
|
||||
for (const library of plan.linkDependencies ?? []) linkDependencies.push(await inspectPlannedLibrary(root, library))
|
||||
const includeDirs = []
|
||||
for (const includeDir of plan.includeDirs ?? []) includeDirs.push(await inspectIncludeDirectory(root, includeDir))
|
||||
const runtimeAssets = []
|
||||
for (const asset of plan.runtimeAssets ?? []) runtimeAssets.push(await inspectPlannedRuntimeAsset(root, asset))
|
||||
const freeCadInclude = plan.includeDirs.find(({ name }) => name === 'FreeCAD')
|
||||
let sourceCommit = null
|
||||
if (freeCadInclude && await exists(resolveFrom(root, freeCadInclude.path))) {
|
||||
try {
|
||||
sourceCommit = (await execFileAsync('git', ['-C', resolveFrom(root, freeCadInclude.path), 'rev-parse', 'HEAD'])).stdout.trim()
|
||||
} catch (error) {
|
||||
fail(`cannot inspect FreeCAD source commit: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
if (sourceCommit !== plan.baseline.sourceCommit) fail(`FreeCAD source commit mismatch: expected ${plan.baseline.sourceCommit}, got ${sourceCommit}.`)
|
||||
}
|
||||
const bridgePath = resolveFrom(root, plan.namingBridge.path)
|
||||
const hostAdapterPath = resolveFrom(root, plan.namingBridge.hostAdapter)
|
||||
const forceIncludePath = resolveFrom(root, plan.compileOptions.forceInclude)
|
||||
const bridgePresent = await exists(bridgePath)
|
||||
const hostAdapterPresent = await exists(hostAdapterPath)
|
||||
const forceIncludePresent = await exists(forceIncludePath)
|
||||
let bridgeExports = []
|
||||
if (bridgePresent) {
|
||||
const source = await readFile(bridgePath, 'utf8')
|
||||
bridgeExports = REQUIRED_FREECAD_NAMING_CALLBACKS.filter((callback) => source.includes(callback))
|
||||
if (bridgeExports.length !== REQUIRED_FREECAD_NAMING_CALLBACKS.length) fail('present naming bridge source omits required callback names.')
|
||||
}
|
||||
if (hostAdapterPresent) {
|
||||
const source = await readFile(hostAdapterPath, 'utf8')
|
||||
if (!source.includes('Module.preRun') || !source.includes('FREECAD_USER_HOME')) fail('host adapter does not initialize the FreeCAD virtual user environment.')
|
||||
}
|
||||
const missingLibraries = libraries.filter(({ status }) => status !== 'verified').map(({ name }) => name)
|
||||
const missingLinkDependencies = linkDependencies.filter(({ status }) => status !== 'verified').map(({ name }) => name)
|
||||
const missingIncludes = includeDirs.filter(({ status }) => status !== 'verified').map(({ name }) => name)
|
||||
const missingRuntimeAssets = runtimeAssets.filter(({ status }) => status !== 'verified').map(({ name }) => name)
|
||||
const blockers = [
|
||||
...missingLibraries.map((name) => `${name} wasm static library`),
|
||||
...missingLinkDependencies.map((name) => `${name} wasm link dependency`),
|
||||
...missingIncludes.map((name) => `${name} headers`),
|
||||
...missingRuntimeAssets.map((name) => `${name} runtime asset`),
|
||||
...(!bridgePresent ? ['FreeCAD private naming Worker bridge'] : []),
|
||||
...(!hostAdapterPresent ? ['FreeCAD naming host adapter'] : []),
|
||||
...(!forceIncludePresent ? ['FreeCAD wasm force-include header'] : []),
|
||||
]
|
||||
console.log(JSON.stringify({
|
||||
status: blockers.length === 0 ? 'sdk-readiness-complete' : 'sdk-readiness-incomplete',
|
||||
availability: blockers.length === 0 ? 'candidate-complete' : 'unavailable',
|
||||
baseline: plan.baseline,
|
||||
libraries,
|
||||
linkDependencies,
|
||||
includeDirs,
|
||||
runtimeAssets,
|
||||
sourceCommit,
|
||||
namingBridge: {
|
||||
path: plan.namingBridge.path,
|
||||
status: bridgePresent ? 'verified' : 'missing',
|
||||
sha256: bridgePresent ? await sha256File(bridgePath) : null,
|
||||
hostAdapter: plan.namingBridge.hostAdapter,
|
||||
hostAdapterSha256: hostAdapterPresent ? await sha256File(hostAdapterPath) : null,
|
||||
exports: bridgeExports,
|
||||
},
|
||||
compileOptions: {
|
||||
...plan.compileOptions,
|
||||
forceIncludeSha256: forceIncludePresent ? await sha256File(forceIncludePath) : null,
|
||||
},
|
||||
blockers,
|
||||
readyLibraries: libraries.length - missingLibraries.length,
|
||||
requiredLibraries: libraries.length,
|
||||
publishToWorker: false,
|
||||
boundary: plan.boundary,
|
||||
}, null, 2))
|
||||
@@ -1,6 +1,14 @@
|
||||
import { access, readFile } from 'node:fs/promises'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve, isAbsolute } from 'node:path'
|
||||
import {
|
||||
REQUIRED_FREECAD_NAMING_CALLBACKS,
|
||||
REQUIRED_FREECAD_NAMING_DEFINITIONS,
|
||||
REQUIRED_FREECAD_NAMING_LIBRARIES,
|
||||
REQUIRED_FREECAD_NAMING_LINK_DEPENDENCIES,
|
||||
exists,
|
||||
inspectWasmStaticArchive,
|
||||
sha256File,
|
||||
} from './freecad-naming-sdk-lib.mjs'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const sdkRoot = process.env.FREECAD_WASM_SDK_DIR ? resolve(root, process.env.FREECAD_WASM_SDK_DIR) : ''
|
||||
@@ -8,8 +16,6 @@ const required = (value, name) => {
|
||||
if (typeof value !== 'string' || !value.trim()) throw new Error(`FreeCAD WASM SDK manifest requires ${name}.`)
|
||||
return value.trim()
|
||||
}
|
||||
const exists = async (path) => access(path).then(() => true).catch(() => false)
|
||||
const sha256 = async (path) => createHash('sha256').update(await readFile(path)).digest('hex')
|
||||
const fail = (message) => { throw new Error(`FreeCAD WASM naming SDK: ${message}`) }
|
||||
|
||||
if (!sdkRoot) {
|
||||
@@ -26,16 +32,27 @@ if (required(manifest.freecadVersion, 'freecadVersion') !== '1.1.1') fail('freec
|
||||
if (required(manifest.sourceCommit, 'sourceCommit') !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('sourceCommit is not the locked FreeCAD commit.')
|
||||
if (required(manifest.emscriptenVersion, 'emscriptenVersion') !== '3.1.69') fail('emscriptenVersion must be 3.1.69.')
|
||||
if (required(manifest.qtTarget, 'qtTarget') !== 'wasm32-emscripten' || required(manifest.pythonTarget, 'pythonTarget') !== 'wasm32-emscripten') fail('Qt and Python must both be wasm32-emscripten targets.')
|
||||
if (manifest.productionPublication !== false || manifest.boundary?.freecadNamingBuildStatus !== 'contract-only' || manifest.boundary?.exTsn02 !== 'in_progress' || manifest.boundary?.systemExact !== false) fail('manifest must preserve the candidate-only contract-only/in_progress/non-exact boundary.')
|
||||
const includeDirs = Array.isArray(manifest.includeDirs) ? manifest.includeDirs : fail('includeDirs must be an array.')
|
||||
const resolvedIncludeDirs = includeDirs.map((includeDir) => isAbsolute(includeDir) ? includeDir : resolve(sdkRoot, includeDir))
|
||||
const libraries = Array.isArray(manifest.libraries) ? manifest.libraries : fail('libraries must be an array.')
|
||||
const libraryNames = new Set(libraries.map((library) => library?.name))
|
||||
for (const name of ['FreeCADBase', 'FreeCADApp', 'Part', 'QtCore', 'Python']) if (!libraryNames.has(name)) fail(`libraries must include ${name}.`)
|
||||
if (libraryNames.size !== libraries.length) fail('library names must be unique.')
|
||||
for (const name of REQUIRED_FREECAD_NAMING_LIBRARIES) if (!libraryNames.has(name)) fail(`libraries must include ${name}.`)
|
||||
for (const name of REQUIRED_FREECAD_NAMING_LINK_DEPENDENCIES) if (!libraryNames.has(name)) fail(`libraries must include ${name}.`)
|
||||
const namingBridge = manifest.namingBridge && typeof manifest.namingBridge === 'object' ? manifest.namingBridge : fail('namingBridge is required.')
|
||||
const bridgePath = required(namingBridge.source, 'namingBridge.source')
|
||||
const bridge = isAbsolute(bridgePath) ? bridgePath : resolve(sdkRoot, bridgePath)
|
||||
if (!await exists(bridge)) fail(`missing naming bridge source: ${bridge}`)
|
||||
if (!Array.isArray(namingBridge.exports) || !['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson'].every((name) => namingBridge.exports.includes(name))) fail('namingBridge.exports must declare all three versioned ABI callbacks.')
|
||||
const hostAdapterPath = required(namingBridge.hostAdapter, 'namingBridge.hostAdapter')
|
||||
const hostAdapter = isAbsolute(hostAdapterPath) ? hostAdapterPath : resolve(sdkRoot, hostAdapterPath)
|
||||
if (!await exists(hostAdapter)) fail(`missing naming host adapter: ${hostAdapter}`)
|
||||
if (!Array.isArray(namingBridge.exports) || !REQUIRED_FREECAD_NAMING_CALLBACKS.every((name) => namingBridge.exports.includes(name))) fail('namingBridge.exports must declare all three versioned ABI callbacks.')
|
||||
const compileOptions = manifest.compileOptions && typeof manifest.compileOptions === 'object' ? manifest.compileOptions : fail('compileOptions is required.')
|
||||
if (compileOptions.cxxStandard !== 'c++20' || compileOptions.pthread !== true || JSON.stringify(compileOptions.definitions) !== JSON.stringify(REQUIRED_FREECAD_NAMING_DEFINITIONS)) fail('compileOptions must lock C++20, pthread and the FreeCAD wasm compatibility definitions.')
|
||||
const forceIncludePath = required(compileOptions.forceInclude, 'compileOptions.forceInclude')
|
||||
const forceInclude = isAbsolute(forceIncludePath) ? forceIncludePath : resolve(sdkRoot, forceIncludePath)
|
||||
if (!await exists(forceInclude)) fail(`missing force-include header: ${forceInclude}`)
|
||||
for (const path of resolvedIncludeDirs) {
|
||||
if (!await exists(path)) fail(`missing include directory: ${path}`)
|
||||
}
|
||||
@@ -46,15 +63,40 @@ for (const library of libraries) {
|
||||
const path = isAbsolute(pathValue) ? pathValue : resolve(sdkRoot, pathValue)
|
||||
if (!path.endsWith('.a')) fail(`library ${name} must be a static .a archive.`)
|
||||
if (!await exists(path)) fail(`missing static library ${name}: ${path}`)
|
||||
if ((await readFile(path)).byteLength === 0) fail(`static library ${name} is empty.`)
|
||||
if (typeof library.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(library.sha256)) fail(`library ${name} requires a lowercase SHA-256.`)
|
||||
const actualHash = await sha256(path)
|
||||
let archive
|
||||
try { archive = await inspectWasmStaticArchive(path) } catch (error) { fail(`library ${name} is not a wasm static archive: ${error instanceof Error ? error.message : String(error)}.`) }
|
||||
if (archive.target !== 'wasm32-emscripten') fail(`library ${name} has unexpected target ${archive.target}.`)
|
||||
const actualHash = await sha256File(path)
|
||||
if (actualHash !== library.sha256) fail(`library ${name} hash mismatch: expected ${library.sha256}, got ${actualHash}.`)
|
||||
}
|
||||
for (const header of ['App/StringHasher.h', 'App/MappedName.h', 'App/ElementMap.h']) {
|
||||
if (!(await Promise.all(resolvedIncludeDirs.map((includeDir) => exists(resolve(includeDir, header))))).some(Boolean)) fail(`missing locked FreeCAD private header in includeDirs: ${header}`)
|
||||
}
|
||||
const bridgeSha = await sha256(bridge)
|
||||
const bridgeSha = await sha256File(bridge)
|
||||
if (typeof namingBridge.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(namingBridge.sha256)) fail('namingBridge.sha256 requires a lowercase SHA-256.')
|
||||
if (namingBridge.sha256 !== bridgeSha) fail(`naming bridge hash mismatch: expected ${namingBridge.sha256}, got ${bridgeSha}.`)
|
||||
console.log(JSON.stringify({ status: 'sdk-ready', availability: 'available', systemExact: false, sdkRoot, freecadVersion: manifest.freecadVersion, sourceCommit: manifest.sourceCommit, emscriptenVersion: manifest.emscriptenVersion, includeDirs, libraries: libraries.map(({ name, path, sha256 }) => ({ name, path, sha256 })), namingBridge: { source: bridgePath, sha256: bridgeSha, exports: [...namingBridge.exports] } }, null, 2))
|
||||
const hostAdapterSha = await sha256File(hostAdapter)
|
||||
if (typeof namingBridge.hostAdapterSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(namingBridge.hostAdapterSha256) || namingBridge.hostAdapterSha256 !== hostAdapterSha) fail(`naming host adapter hash mismatch: expected ${String(namingBridge.hostAdapterSha256)}, got ${hostAdapterSha}.`)
|
||||
const forceIncludeSha = await sha256File(forceInclude)
|
||||
if (typeof compileOptions.forceIncludeSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(compileOptions.forceIncludeSha256) || compileOptions.forceIncludeSha256 !== forceIncludeSha) fail(`force-include header hash mismatch: expected ${String(compileOptions.forceIncludeSha256)}, got ${forceIncludeSha}.`)
|
||||
const runtimeAssets = Array.isArray(manifest.runtimeAssets) ? manifest.runtimeAssets : fail('runtimeAssets must be an array.')
|
||||
if (!runtimeAssets.some((asset) => asset?.name === 'PythonWasmStdlib')) fail('runtimeAssets must include PythonWasmStdlib.')
|
||||
for (const asset of runtimeAssets) {
|
||||
if (!asset || typeof asset !== 'object') fail('runtimeAssets entries must be objects.')
|
||||
const name = required(asset.name, 'runtimeAssets[].name')
|
||||
const pathValue = required(asset.path, `runtimeAssets[${name}].path`)
|
||||
required(asset.preloadTo, `runtimeAssets[${name}].preloadTo`)
|
||||
const assetRoot = isAbsolute(pathValue) ? pathValue : resolve(sdkRoot, pathValue)
|
||||
if (!await exists(assetRoot)) fail(`missing runtime asset ${name}: ${assetRoot}`)
|
||||
if (!Array.isArray(asset.files) || asset.files.length === 0) fail(`runtime asset ${name} requires hashed files.`)
|
||||
for (const file of asset.files) {
|
||||
const relativePath = required(file?.path, `runtimeAssets[${name}].files[].path`)
|
||||
const filePath = resolve(assetRoot, relativePath)
|
||||
if (!await exists(filePath)) fail(`missing runtime asset file ${name}/${relativePath}.`)
|
||||
if (typeof file.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(file.sha256)) fail(`runtime asset file ${name}/${relativePath} requires a lowercase SHA-256.`)
|
||||
const actualHash = await sha256File(filePath)
|
||||
if (actualHash !== file.sha256) fail(`runtime asset file ${name}/${relativePath} hash mismatch: expected ${file.sha256}, got ${actualHash}.`)
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify({ status: 'sdk-ready', availability: 'candidate-complete', systemExact: false, productionPublication: false, sdkRoot, freecadVersion: manifest.freecadVersion, sourceCommit: manifest.sourceCommit, emscriptenVersion: manifest.emscriptenVersion, includeDirs, libraries: libraries.map(({ name, path, sha256 }) => ({ name, path, sha256 })), namingBridge: { source: bridgePath, sha256: bridgeSha, hostAdapter: hostAdapterPath, hostAdapterSha256: hostAdapterSha, exports: [...namingBridge.exports] }, compileOptions, runtimeAssets, boundary: manifest.boundary }, null, 2))
|
||||
|
||||
52
scripts/check-freecad-partdesign-structure-oracle.mjs
Normal file
52
scripts/check-freecad-partdesign-structure-oracle.mjs
Normal file
@@ -0,0 +1,52 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const oracle = JSON.parse(await readFile(resolve(root, 'config/freecad-partdesign-structure-oracle.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD PartDesign structure oracle: ${message}`) }
|
||||
const near = (actual, expected) => typeof actual === 'number' && Math.abs(actual - expected) <= oracle.tolerance
|
||||
const expectedCommit = '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d'
|
||||
if (oracle.schemaVersion !== 1 || oracle.baselineId !== 'freecad-1.1.1-partdesign-structure-oracle' || oracle.freecadVersion !== '1.1.1' || oracle.gitCommit !== expectedCommit || oracle.status !== 'pass' || oracle.tolerance !== 1e-7) fail('baseline is invalid.')
|
||||
|
||||
const expectedInitialGroups = {
|
||||
SourceBody: ['SourceBox'],
|
||||
DatumBody: ['DatumPlane', 'DatumLine', 'DatumPoint'],
|
||||
BinderBody: ['ShapeBinder'],
|
||||
SubBinderBody: ['SubShapeBinder'],
|
||||
}
|
||||
const expectedInitialTips = { SourceBody: 'SourceBox', DatumBody: null, BinderBody: null, SubBinderBody: null }
|
||||
for (const [body, group] of Object.entries(expectedInitialGroups)) {
|
||||
if (JSON.stringify(oracle.initial?.bodyGroups?.[body]) !== JSON.stringify(group) || oracle.initial?.bodyTips?.[body] !== expectedInitialTips[body]) fail(`initial ${body} grouping or Tip state is invalid.`)
|
||||
}
|
||||
if (!near(oracle.initial.sourceVolume, 480) || !near(oracle.initial.shapeBinderVolume, 480) || !near(oracle.initial.subShapeBinderLength, 28)) fail('initial source or binder shape evidence is invalid.')
|
||||
if (oracle.initial.datumPlane?.mapMode !== 'FlatFace' || JSON.stringify(oracle.initial.datumPlane.support) !== JSON.stringify([{ object: 'XY_Plane', subElements: [] }]) || !near(oracle.initial.datumPlane.attachmentOffset.position?.[2], 5) || !near(oracle.initial.datumPlane.attachmentOffset.angleDegrees, 30) || !near(oracle.initial.datumPlane.placement.position?.[2], 5) || !near(oracle.initial.datumPlane.placement.angleDegrees, 30) || JSON.stringify(oracle.initial.datumPlane.state) !== JSON.stringify(['Up-to-date'])) fail('DatumPlane support, offset or placement evidence is invalid.')
|
||||
if (oracle.initial.datumLine?.mapMode !== 'ObjectX' || JSON.stringify(oracle.initial.datumLine.state) !== JSON.stringify(['Up-to-date']) || oracle.initial.datumPoint?.mapMode !== 'ObjectOrigin' || JSON.stringify(oracle.initial.datumPoint.attachmentOffset.position) !== JSON.stringify([1, 2, 3]) || JSON.stringify(oracle.initial.datumPoint.state) !== JSON.stringify(['Up-to-date'])) fail('DatumLine or DatumPoint evidence is invalid.')
|
||||
if (JSON.stringify(oracle.initial.shapeBinder?.support) !== JSON.stringify([{ object: 'SourceBox', subElements: [] }]) || oracle.initial.shapeBinder.traceSupport !== true || JSON.stringify(oracle.initial.shapeBinder.state) !== JSON.stringify(['Up-to-date'])) fail('ShapeBinder support evidence is invalid.')
|
||||
const expectedSubSupport = [{ object: 'SourceBox', subElements: ['Edge1', 'Edge2', 'Edge3', 'Edge4'] }]
|
||||
if (JSON.stringify(oracle.initial.subShapeBinder?.support) !== JSON.stringify(expectedSubSupport) || JSON.stringify(oracle.initial.subShapeBinder.state) !== JSON.stringify(['Up-to-date'])) fail('SubShapeBinder support evidence is invalid.')
|
||||
|
||||
if (!near(oracle.edited?.sourceVolume, 672) || !near(oracle.edited?.shapeBinderVolume, 672) || !near(oracle.edited?.subShapeBinderLength, 28) || !near(oracle.edited?.datumPlaneAttachmentOffset?.position?.[2], 7) || !near(oracle.edited?.datumPlaneAttachmentOffset?.angleDegrees, 45) || !near(oracle.edited?.datumPlanePlacement?.position?.[2], 7) || !near(oracle.edited?.datumPlanePlacement?.angleDegrees, 45) || Object.entries(expectedInitialTips).some(([body, tip]) => oracle.edited?.bodyTips?.[body] !== tip)) fail('edited recompute evidence is invalid.')
|
||||
if (oracle.invalidSupport?.typeId !== 'PartDesign::Plane' || JSON.stringify(oracle.invalidSupport.support) !== JSON.stringify([{ object: 'SourceBox', subElements: ['Face999'] }]) || JSON.stringify(oracle.invalidSupport.state) !== JSON.stringify(['Touched', 'Invalid']) || oracle.invalidSupport.status !== 'AttachEngine3D: subshape not found SourceBox.Face999') fail('invalid attachment support evidence is invalid.')
|
||||
|
||||
const expectedRoundtripGroups = { ...expectedInitialGroups, DatumBody: [...expectedInitialGroups.DatumBody, 'InvalidDatumPlane'] }
|
||||
for (const [body, group] of Object.entries(expectedRoundtripGroups)) {
|
||||
if (JSON.stringify(oracle.roundtrip?.bodyGroups?.[body]) !== JSON.stringify(group) || oracle.roundtrip?.bodyTips?.[body] !== expectedInitialTips[body]) fail(`FCStd round-trip ${body} grouping or Tip state is invalid.`)
|
||||
}
|
||||
if (!near(oracle.roundtrip.sourceVolume, 672) || !near(oracle.roundtrip.shapeBinderVolume, 672) || !near(oracle.roundtrip.subShapeBinderLength, 28)) fail('FCStd round-trip shape evidence is invalid.')
|
||||
if (JSON.stringify(oracle.roundtrip.shapeBinderSupport) !== JSON.stringify([{ object: 'SourceBox', subElements: [] }]) || JSON.stringify(oracle.roundtrip.subShapeBinderSupport) !== JSON.stringify(expectedSubSupport) || JSON.stringify(oracle.roundtrip.datumPlaneSupport) !== JSON.stringify([{ object: 'XY_Plane', subElements: [] }]) || oracle.roundtrip.datumPlaneMapMode !== 'FlatFace' || !near(oracle.roundtrip.datumPlaneAttachmentOffset.position?.[2], 7) || !near(oracle.roundtrip.datumPlaneAttachmentOffset.angleDegrees, 45) || JSON.stringify(oracle.roundtrip.invalidDatumState) !== JSON.stringify(['Touched', 'Invalid']) || oracle.roundtrip.invalidDatumStatus !== oracle.invalidSupport.status) fail('FCStd round-trip reference or failure evidence is invalid.')
|
||||
|
||||
const externalSupport = [{ object: 'ExternalBox', subElements: ['Face1'] }]
|
||||
const crossInitial = oracle.crossDocument?.initial
|
||||
if (crossInitial?.shapeBinderExternalLink?.accepted !== false || crossInitial.shapeBinderExternalLink.errorType !== 'ValueError' || crossInitial.shapeBinderExternalLink.error !== 'PropertyLinkSubList does not support external object' || crossInitial.shapeBinderSupportType !== 'App::PropertyLinkSubListGlobal' || crossInitial.subShapeBinderSupportType !== 'App::PropertyXLinkSubList') fail('cross-document binder property boundary is invalid.')
|
||||
if (!near(crossInitial.sourceVolume, 120) || !near(crossInitial.sourceFaceArea, 30) || crossInitial.shapeBinderShapeNull !== true || !near(crossInitial.subShapeBinderArea, 30) || JSON.stringify(crossInitial.shapeBinderSupport) !== '[]' || JSON.stringify(crossInitial.subShapeBinderSupport) !== JSON.stringify(externalSupport) || crossInitial.subShapeBinderSourceDocument !== 'PartDesignExternalSource' || crossInitial.shapeBodyTip !== null || crossInitial.subBodyTip !== null) fail('cross-document initial shape, support or Tip evidence is invalid.')
|
||||
|
||||
const crossRoundtrip = oracle.crossDocument?.roundtrip
|
||||
if (!near(crossRoundtrip?.sourceVolume, 120) || !near(crossRoundtrip?.sourceFaceArea, 30) || crossRoundtrip?.shapeBinderShapeNull !== true || !near(crossRoundtrip?.subShapeBinderArea, 30) || JSON.stringify(crossRoundtrip?.shapeBinderSupport) !== '[]' || JSON.stringify(crossRoundtrip?.subShapeBinderSupport) !== JSON.stringify(externalSupport) || crossRoundtrip?.subShapeBinderSourceDocument !== 'PartDesignExternalSource' || crossRoundtrip?.shapeBodyTip !== null || crossRoundtrip?.subBodyTip !== null) fail('cross-document FCStd round-trip evidence is invalid.')
|
||||
|
||||
const crossEdited = oracle.crossDocument?.edited
|
||||
if (!near(crossEdited?.sourceVolume, 192) || !near(crossEdited?.sourceFaceArea, 48) || crossEdited?.shapeBinderShapeNull !== true || !near(crossEdited?.subShapeBinderArea, 48) || JSON.stringify(crossEdited?.shapeBinderState) !== JSON.stringify(['Up-to-date']) || JSON.stringify(crossEdited?.subShapeBinderState) !== JSON.stringify(['Up-to-date'])) fail('cross-document source mutation evidence is invalid.')
|
||||
|
||||
const crossDeleted = oracle.crossDocument?.deleted
|
||||
if (JSON.stringify(crossDeleted?.shapeBinderSupport) !== '[]' || JSON.stringify(crossDeleted?.subShapeBinderSupport) !== '[]' || crossDeleted?.shapeBinderShapeNull !== true || crossDeleted?.subShapeBinderShapeNull !== false || !near(crossDeleted?.subShapeBinderCachedArea, 48) || JSON.stringify(crossDeleted?.shapeBinderState) !== JSON.stringify(['Up-to-date']) || JSON.stringify(crossDeleted?.subShapeBinderState) !== JSON.stringify(['Up-to-date']) || crossDeleted?.shapeBinderStatus !== 'Valid' || crossDeleted?.subShapeBinderStatus !== 'Valid') fail('cross-document source deletion evidence is invalid.')
|
||||
|
||||
console.log(JSON.stringify({ status: 'freecad-partdesign-structure-oracle-pass', baselineId: oracle.baselineId, bodies: Object.keys(oracle.initial.bodyGroups).length, datumTypes: ['PartDesign::Plane', 'PartDesign::Line', 'PartDesign::Point'], binderTypes: ['PartDesign::ShapeBinder', 'PartDesign::SubShapeBinder'], mutation: { sourceVolume: [oracle.initial.sourceVolume, oracle.edited.sourceVolume], shapeBinderVolume: [oracle.initial.shapeBinderVolume, oracle.edited.shapeBinderVolume], datumPlaneZ: [oracle.initial.datumPlane.attachmentOffset.position[2], oracle.edited.datumPlaneAttachmentOffset.position[2]] }, crossDocument: { shapeBinder: 'external-link-rejected', subShapeBinderArea: [crossInitial.subShapeBinderArea, crossEdited.subShapeBinderArea], deletion: 'support-cleared-shape-cache-retained' }, invalidSupport: oracle.invalidSupport.status, fcstdRoundtrip: true }, null, 2))
|
||||
@@ -3,10 +3,15 @@ import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const load = async (path) => JSON.parse(await readFile(resolve(root, path), 'utf8'))
|
||||
const [abi, matrix, plan] = await Promise.all([
|
||||
const [abi, matrix, plan, readiness, sdkPlan, nextTasks, buildScript, smokeTest] = await Promise.all([
|
||||
load('config/freecad-sketcher-partdesign-abi-contract.json'),
|
||||
load('config/compatibility-matrix.json'),
|
||||
load('config/freecad-web-exact-parity-plan.json'),
|
||||
load('config/freecad-private-naming-source-readiness.json'),
|
||||
load('config/freecad-naming-sdk-plan.json'),
|
||||
load('config/freecad-naming-next-tasks.json'),
|
||||
readFile(resolve(root, 'scripts/build-freecad-naming-source-probe.sh'), 'utf8'),
|
||||
readFile(resolve(root, 'native/freecad-naming-probe/smoke-test.mjs'), 'utf8'),
|
||||
])
|
||||
const fail = (message) => { throw new Error(`FreeCAD private naming boundary: ${message}`) }
|
||||
|
||||
@@ -16,16 +21,63 @@ if (!task) fail('EX-TSN-02 is missing from the exact parity plan.')
|
||||
if (abi.privateNamingAbi?.shippedWorkerImplementation !== 'not-linked') fail('the shipped ABI contract must remain not-linked until the production artifact exports verified callbacks.')
|
||||
if (abi.claim?.exactFreeCadParity !== false) fail('the supported facade contract cannot claim exact FreeCAD parity.')
|
||||
if (matrix.nativeOcctHistory?.freecadNamingBuild?.status !== 'contract-only') fail('the FreeCAD naming build must remain contract-only before a locked SDK and callback probe exist.')
|
||||
if (matrix.nativeOcctHistory?.freecadNamingBuild?.prerequisiteStatus !== 'real-isolated-freecad-private-naming-bridge-pass') fail('the prerequisite status must identify the real isolated FreeCAD naming bridge.')
|
||||
if (matrix.nativeOcctHistory?.freecadNamingBuild?.sdkReadinessStatus !== 'candidate-complete') fail('the SDK readiness status must identify the complete candidate SDK without implying production linkage.')
|
||||
if (task.status !== 'in_progress') fail(`EX-TSN-02 must remain in_progress, received ${String(task.status)}.`)
|
||||
if (matrix.systemExactEvaluation?.exact !== false) fail('systemExact must remain false.')
|
||||
const blockers = matrix.systemExactEvaluation?.blockers ?? []
|
||||
if (!blockers.includes('shipped Worker lacks the FreeCAD-linked private naming ABI implementation')) fail('systemExact blockers must retain the missing FreeCAD-linked Worker implementation.')
|
||||
if (!task.exactBlockedBy?.includes('The shipped Worker does not link the FreeCAD private naming implementation for every builder')) fail('EX-TSN-02 must retain its production Worker linkage blocker.')
|
||||
|
||||
if (readiness.scope !== 'standalone-wasm-source-prerequisite') fail('source readiness must remain a standalone prerequisite, not a production implementation.')
|
||||
if (sdkPlan.scope !== 'pre-production-sdk-readiness' || sdkPlan.productionPublication !== false) fail('SDK readiness must remain pre-production and must not publish a Worker.')
|
||||
if (sdkPlan.boundary?.freecadNamingBuildStatus !== 'contract-only' || sdkPlan.boundary?.exTsn02 !== 'in_progress' || sdkPlan.boundary?.systemExact !== false) fail('SDK readiness boundary disagrees with the authoritative status files.')
|
||||
if (nextTasks.boundary?.freecadNamingBuildStatus !== 'contract-only' || nextTasks.boundary?.exTsn02 !== 'in_progress' || nextTasks.boundary?.systemExact !== false || nextTasks.boundary?.productionWorker?.availability !== 'unavailable' || nextTasks.boundary?.productionWorker?.callbacks?.length !== 0) fail('next-task plan disagrees with the production boundary.')
|
||||
if (matrix.nativeOcctHistory?.freecadNamingBuild?.sdkReadinessCommand !== './npmw run check:freecad-naming-sdk-readiness') fail('compatibility matrix must expose the SDK readiness command.')
|
||||
if (readiness.production?.workerLinked !== false) fail('the readiness manifest cannot claim that the production Worker is linked.')
|
||||
if (readiness.production?.exportedCallbacks?.length !== 0) fail('the prerequisite probe cannot declare production callback exports.')
|
||||
if (readiness.boundary?.freecadNamingBuildStatus !== 'contract-only' || readiness.boundary?.exTsn02 !== 'in_progress' || readiness.boundary?.systemExact !== false) fail('the readiness manifest boundary disagrees with the authoritative status files.')
|
||||
if (readiness.candidateAbi?.scope !== 'isolated-non-production') fail('the candidate ABI must remain isolated and non-production.')
|
||||
if (readiness.isolatedSourceArchive?.target !== 'wasm32-emscripten' || readiness.isolatedSourceArchive?.expectedObjectMembers !== 7 || readiness.isolatedSourceArchive?.hostAdapterBound !== true || readiness.isolatedSourceArchive?.productionEligible !== false) fail('the source archive must remain a seven-object, host-adapter-bound wasm prerequisite.')
|
||||
if (readiness.candidateAbi?.strictWebValidator !== 'pass') fail('the candidate ABI must pass the strict Web validator.')
|
||||
if (readiness.candidateAbi?.occtBuilderContext !== false || readiness.candidateAbi?.publishToWorker !== false) fail('the candidate ABI cannot claim OCCT builder context or Worker publication.')
|
||||
for (const resource of ['MappedNameRef', 'StringHasher', 'ElementMap2']) {
|
||||
if (!readiness.candidateAbi?.nativeResources?.includes(resource)) fail(`the candidate ABI must validate native ${resource} evidence.`)
|
||||
}
|
||||
for (const callback of readiness.candidateAbi?.exports ?? []) {
|
||||
if (!callback.startsWith('freecadNamingCandidate')) fail(`candidate export ${callback} could be mistaken for a production callback.`)
|
||||
}
|
||||
for (const callback of readiness.production?.requiredCallbacks ?? []) {
|
||||
if (!smokeTest.includes(callback)) fail(`the source smoke test must reject production callback ${callback}.`)
|
||||
}
|
||||
for (const source of readiness.linkedOriginalSources ?? []) {
|
||||
if (!buildScript.includes(source.path.split('/').at(-1)) || !buildScript.includes(source.sha256)) fail(`the source probe build does not pin ${source.path}.`)
|
||||
}
|
||||
for (const expected of ['src/App/StringHasher.cpp', 'src/App/MappedElement.cpp', 'src/App/ElementNamingUtils.cpp', 'src/App/ElementMap.cpp', 'src/Base/Handle.cpp']) {
|
||||
if (!readiness.linkedOriginalSources?.some((source) => source.path === expected)) fail(`the source readiness manifest must include ${expected}.`)
|
||||
}
|
||||
for (const expected of ['FreeCADBase static library', 'FreeCADApp static library', 'Part static library', 'Python static library', 'real Application and Document integration', 'FreeCAD private naming Worker bridge']) {
|
||||
if (!readiness.notLinkedProductionComponents?.includes(expected)) fail(`the source readiness manifest must retain ${expected} as not linked.`)
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
status: 'boundary-pass',
|
||||
shippedWorkerImplementation: 'not-linked',
|
||||
task: { id: task.id, status: task.status },
|
||||
systemExact: matrix.systemExactEvaluation.exact,
|
||||
sdkStatus: matrix.nativeOcctHistory.freecadNamingBuild.status,
|
||||
prerequisiteStatus: matrix.nativeOcctHistory.freecadNamingBuild.prerequisiteStatus,
|
||||
sdkReadinessStatus: matrix.nativeOcctHistory.freecadNamingBuild.sdkReadinessStatus,
|
||||
linkedOriginalSources: readiness.linkedOriginalSources.map((source) => source.path),
|
||||
candidateAbi: {
|
||||
scope: readiness.candidateAbi.scope,
|
||||
strictWebValidator: readiness.candidateAbi.strictWebValidator,
|
||||
publishToWorker: readiness.candidateAbi.publishToWorker,
|
||||
},
|
||||
sdkReadiness: {
|
||||
scope: sdkPlan.scope,
|
||||
productionPublication: sdkPlan.productionPublication,
|
||||
requiredLibraries: sdkPlan.libraries.map(({ name }) => name),
|
||||
},
|
||||
workerLinked: readiness.production.workerLinked,
|
||||
}, null, 2))
|
||||
|
||||
48
scripts/check-freecad-wasm-sdk-build-plan.mjs
Normal file
48
scripts/check-freecad-wasm-sdk-build-plan.mjs
Normal file
@@ -0,0 +1,48 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import { execFile } from 'node:child_process'
|
||||
import { promisify } from 'node:util'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const plan = JSON.parse(await readFile(resolve(root, 'config/freecad-wasm-sdk-build-plan.json'), 'utf8'))
|
||||
const fail = (message) => { throw new Error(`FreeCAD wasm SDK build plan: ${message}`) }
|
||||
const lockedCommit = '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d'
|
||||
if (plan.schemaVersion !== 1 || plan.scope !== 'candidate-only-freecad-wasm-sdk-build') fail('unsupported schema or scope.')
|
||||
if (plan.baseline?.freecadVersion !== '1.1.1' || plan.baseline?.sourceCommit !== lockedCommit || plan.baseline?.emscriptenVersion !== '3.1.69' || plan.baseline?.target !== 'wasm32-emscripten') fail('baseline is not locked to FreeCAD 1.1.1 and Emscripten 3.1.69 wasm32.')
|
||||
if (plan.configuration?.productionPublication !== false) fail('candidate SDK build cannot publish production artifacts.')
|
||||
if (plan.boundary?.freecadNamingBuildStatus !== 'contract-only' || plan.boundary?.exTsn02 !== 'in_progress' || plan.boundary?.systemExact !== false || plan.boundary?.workerLinked !== false || plan.boundary?.callbacks?.length !== 0) fail('production boundary must remain contract-only, unlinked, and callback-free.')
|
||||
if (JSON.stringify(plan.requiredArchives) !== JSON.stringify(['FreeCADBase', 'FreeCADApp', 'Part', 'Python'])) fail('required archive list is incomplete or reordered.')
|
||||
const expectedDependencies = [
|
||||
['Qt6', '6.8.2', 'npm run build:qt6-freecad-wasm'],
|
||||
['yaml-cpp', '0.8.0', 'npm run build:yaml-cpp-wasm'],
|
||||
['ICU', '68.2', 'npm run build:icu-wasm'],
|
||||
['CPython', '3.13.5', 'npm run build:cpython-wasm'],
|
||||
['Xerces-C', '3.2.4', 'npm run build:xerces-c-wasm'],
|
||||
['Boost', '1.83.0', 'npm run build:boost-wasm'],
|
||||
]
|
||||
if (JSON.stringify(plan.dependencyBuilds?.map(({ name, version, command }) => [name, version, command])) !== JSON.stringify(expectedDependencies)) fail('dependency build order, versions, or commands changed.')
|
||||
for (const dependency of plan.dependencyBuilds) {
|
||||
if (!Array.isArray(dependency.targetArchives) || dependency.targetArchives.length === 0) fail(`${dependency.name} has no target archive contract.`)
|
||||
}
|
||||
if (plan.dependencyBuilds.find(({ name }) => name === 'yaml-cpp')?.sourceCommit !== 'f7320141120f720aecc4c32be25586e7da9eb978') fail('yaml-cpp source commit is not locked.')
|
||||
if (plan.dependencyBuilds.find(({ name }) => name === 'ICU')?.sourceSha256 !== 'c79193dee3907a2199b8296a93b52c5cb74332c26f3d167269487680d479d625') fail('ICU source hash is not locked.')
|
||||
if (plan.dependencyBuilds.find(({ name }) => name === 'CPython')?.sourceSha256 !== '93e583f243454e6e9e4588ca2c2662206ad961659863277afcdb96801647d640') fail('CPython source hash is not locked.')
|
||||
if (plan.dependencyBuilds.find(({ name }) => name === 'Xerces-C')?.sourceSha256 !== '075bc57940da0f9be6dd183c550c8ce0b9833e4550dc382048377a1a5e3b2bd9') fail('Xerces-C source hash is not locked.')
|
||||
if (plan.dependencyBuilds.find(({ name }) => name === 'Boost')?.sourceSha256 !== '6478edfe2f3305127cffe8caf73ea0176c53769f4bf1585be237eb30798c3b8e') fail('Boost source hash is not locked.')
|
||||
const sourceDir = resolve(root, plan.source.path)
|
||||
const execFileAsync = promisify(execFile)
|
||||
let sourceCommit = null
|
||||
try {
|
||||
sourceCommit = (await execFileAsync('git', ['-C', sourceDir, 'rev-parse', 'HEAD'])).stdout.trim()
|
||||
} catch (error) {
|
||||
fail(`cannot inspect source checkout: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
if (sourceCommit !== lockedCommit) fail(`source checkout is ${sourceCommit}, expected ${lockedCommit}.`)
|
||||
let dirty = ''
|
||||
try { dirty = (await execFileAsync('git', ['-C', sourceDir, 'status', '--porcelain'])).stdout.trim() } catch (error) { fail(`cannot inspect source status: ${error instanceof Error ? error.message : String(error)}`) }
|
||||
if (plan.source.requiredClean && dirty) fail('source checkout has uncommitted changes.')
|
||||
const emcc = process.env.EMCC || 'emcc'
|
||||
let emccVersion = ''
|
||||
try { emccVersion = (await execFileAsync(emcc, ['--version'])).stdout.split('\n')[0].trim() } catch (error) { fail(`cannot inspect Emscripten: ${error instanceof Error ? error.message : String(error)}`) }
|
||||
if (!emccVersion.includes('3.1.69')) fail(`Emscripten version is not 3.1.69: ${emccVersion}`)
|
||||
console.log(JSON.stringify({ status: 'freecad-wasm-sdk-build-plan-pass', sourceCommit, sourceClean: dirty.length === 0, emcc: emccVersion, productionPublication: false, boundary: plan.boundary }, null, 2))
|
||||
@@ -5,7 +5,7 @@ const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
|
||||
const [
|
||||
fcstdFuzz, geometryFuzz, sketchFuzz, successFixtures, failureFixtures, supplementalSuccessFixtures, supplementalFailureFixtures,
|
||||
sketchOracle, partdesignBase, partdesignLoft, partdesignDressup, partdesignTransform, partdesignFailures, partdesignRevolutionGroove, partBuilders,
|
||||
sketchOracle, partdesignBase, partdesignLoft, partdesignDressup, partdesignTransform, partdesignStructure, attachmentModes, partdesignFailures, partdesignRevolutionGroove, partBuilders,
|
||||
fcstdRoundTrip, app, browserMatrix, performance, fault, opfsMigration, security, qa08, addon, script,
|
||||
] = await Promise.all([
|
||||
load('config/qa04-fcstd-fuzz-verification.json'),
|
||||
@@ -20,6 +20,8 @@ const [
|
||||
load('config/freecad-partdesign-loft-oracle.json'),
|
||||
load('config/freecad-partdesign-dressup-oracle.json'),
|
||||
load('config/freecad-partdesign-transform-oracle.json'),
|
||||
load('config/freecad-partdesign-structure-oracle.json'),
|
||||
load('config/freecad-attachment-mode-oracle.json'),
|
||||
load('config/freecad-partdesign-failure-oracle.json'),
|
||||
load('config/freecad-partdesign-revolution-groove-oracle.json'),
|
||||
load('config/freecad-part-builders-oracle.json'),
|
||||
@@ -44,6 +46,8 @@ if (successFixtures.scenarios?.length !== 100 || failureFixtures.failures?.lengt
|
||||
if (sketchOracle.status !== 'pass' || sketchOracle.summary?.constraintTypes !== 19 || sketchOracle.summary.successPassed !== 19 || sketchOracle.summary.failurePassed !== 19) throw new Error('QA-02 Sketcher oracle evidence is incomplete.')
|
||||
const partdesignCases = [partdesignBase, partdesignLoft, partdesignDressup, partdesignTransform].reduce((total, report) => total + (report.summary?.passed ?? 0), 0)
|
||||
if (partdesignCases !== 23 || [partdesignBase, partdesignLoft, partdesignDressup, partdesignTransform].some((report) => report.status !== 'pass' || report.freecadVersion !== '1.1.1')) throw new Error('QA-02 PartDesign oracle evidence is incomplete.')
|
||||
if (partdesignStructure.status !== 'pass' || partdesignStructure.freecadVersion !== '1.1.1' || Object.keys(partdesignStructure.initial?.bodyGroups ?? {}).length !== 4 || partdesignStructure.crossDocument?.initial?.shapeBinderExternalLink?.accepted !== false || partdesignStructure.crossDocument?.initial?.subShapeBinderSupportType !== 'App::PropertyXLinkSubList' || partdesignStructure.crossDocument?.edited?.subShapeBinderArea !== 48 || partdesignStructure.crossDocument?.deleted?.subShapeBinderCachedArea !== 48) throw new Error('QA-02 PartDesign structure and cross-document evidence is incomplete.')
|
||||
if (attachmentModes.status !== 'pass' || attachmentModes.freecadVersion !== '1.1.1' || attachmentModes.registry?.modeCount !== 55 || attachmentModes.registry?.implementedUnionCount !== 50 || attachmentModes.engines?.plane?.implementedModes?.length !== 23 || attachmentModes.engines?.line?.implementedModes?.length !== 18 || attachmentModes.engines?.point?.implementedModes?.length !== 9 || attachmentModes.success?.roundtrip?.point?.placement?.position?.[2] !== 9 || Object.keys(attachmentModes.failures?.roundtrip ?? {}).length !== 3) throw new Error('QA-02 Attachment mode registry, execution or round-trip evidence is incomplete.')
|
||||
if (partdesignFailures.status !== 'pass' || partdesignFailures.freecadVersion !== '1.1.1' || partdesignFailures.summary?.cases !== 17 || partdesignFailures.summary.passed !== 17 || partdesignFailures.summary.rejected !== 13 || partdesignFailures.summary.acceptedEmpty !== 4 || partdesignFailures.summary.accepted !== 0) throw new Error('QA-02 PartDesign failure oracle evidence is incomplete.')
|
||||
if (partdesignRevolutionGroove.status !== 'pass' || partdesignRevolutionGroove.freecadVersion !== '1.1.1' || partdesignRevolutionGroove.summary?.cases !== 2 || partdesignRevolutionGroove.summary.passed !== 2 || partdesignRevolutionGroove.cases?.some((fixture) => fixture.shapeNull !== false || fixture.shapeValid !== true || fixture.solids !== 1)) throw new Error('QA-02 PartDesign Revolution/Groove oracle evidence is incomplete.')
|
||||
if (partBuilders.status !== 'pass' || partBuilders.freecadVersion !== '1.1.1' || partBuilders.summary?.successCases !== 6 || partBuilders.summary.successPassed !== 6 || partBuilders.summary.failureCases !== 6 || partBuilders.summary.failurePassed !== 6 || partBuilders.summary.rejected !== 6 || partBuilders.summary.acceptedEmpty !== 0) throw new Error('QA-02 Part builders oracle evidence is incomplete.')
|
||||
@@ -68,7 +72,7 @@ console.log(JSON.stringify({
|
||||
tasks: 8,
|
||||
unitSuites: testFiles.length,
|
||||
unitTests: testCases,
|
||||
freecad: { successFixtures: 105, failureFixtures: 56, primarySuccessFixtures: 100, primaryFailureFixtures: 51, supplementalSuccessFixtures: 5, supplementalFailureFixtures: 5, sketchConstraints: 19, partdesignCases, partdesignFailureCases: partdesignFailures.summary.cases, partdesignRejectedFailures: partdesignFailures.summary.rejected, partdesignAcceptedEmpty: partdesignFailures.summary.acceptedEmpty, partdesignRevolutionGrooveCases: partdesignRevolutionGroove.summary.cases, partBuilderSuccessCases: partBuilders.summary.successCases, partBuilderFailureCases: partBuilders.summary.failureCases, fcstdRoundTrips: fcstdRoundTrip.scenarioCount },
|
||||
freecad: { successFixtures: 105, failureFixtures: 56, primarySuccessFixtures: 100, primaryFailureFixtures: 51, supplementalSuccessFixtures: 5, supplementalFailureFixtures: 5, sketchConstraints: 19, partdesignCases, partdesignStructureBodies: Object.keys(partdesignStructure.initial.bodyGroups).length, attachmentModes: attachmentModes.registry.modeCount, attachmentImplementedUnion: attachmentModes.registry.implementedUnionCount, partdesignFailureCases: partdesignFailures.summary.cases, partdesignRejectedFailures: partdesignFailures.summary.rejected, partdesignAcceptedEmpty: partdesignFailures.summary.acceptedEmpty, partdesignRevolutionGrooveCases: partdesignRevolutionGroove.summary.cases, partBuilderSuccessCases: partBuilders.summary.successCases, partBuilderFailureCases: partBuilders.summary.failureCases, fcstdRoundTrips: fcstdRoundTrip.scenarioCount },
|
||||
fuzz: { fcstd: fcstdFuzz.cases, geometry: geometryFuzz.cases, solverModels: sketchFuzz.models },
|
||||
chrome: { workflows: app.workflows.length, screenshots: Object.keys(app.screenshots).length, accessibilityNodes: app.screenReader.nodes },
|
||||
browsers: Object.fromEntries(browserMatrix.browsers.map((entry) => [entry.engine, { status: entry.status, persistence: entry.persistence, geometrySource: entry.canvas.geometrySource }])),
|
||||
|
||||
105
scripts/configure-freecad-naming-sdk-candidate.mjs
Normal file
105
scripts/configure-freecad-naming-sdk-candidate.mjs
Normal file
@@ -0,0 +1,105 @@
|
||||
import { mkdir, writeFile } from 'node:fs/promises'
|
||||
import { execFile } from 'node:child_process'
|
||||
import { promisify } from 'node:util'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const run = promisify(execFile)
|
||||
const sourceDir = resolve(root, '.cache/toolchains/freecad-naming-sdk/source-occt8')
|
||||
const buildDir = resolve(root, '.cache/toolchains/freecad-naming-sdk/build-wasm')
|
||||
const installDir = resolve(root, '.cache/toolchains/freecad-naming-sdk/install-wasm')
|
||||
const qtPrefix = resolve(root, '.cache/toolchains/qt6/install-wasm-freecad')
|
||||
const yamlPrefix = resolve(root, '.cache/toolchains/yaml-cpp/install-wasm')
|
||||
const icuPrefix = resolve(root, '.cache/toolchains/icu/install-wasm')
|
||||
const pythonPrefix = resolve(root, '.cache/toolchains/python/install-wasm')
|
||||
const xercesPrefix = resolve(root, '.cache/toolchains/xerces-c/install-wasm')
|
||||
const occtBuild = resolve(root, '.cache/bitbybit/occt-history-build-offline')
|
||||
const occtSource = resolve(root, '.cache/occt/occt')
|
||||
const offlineSysroot = resolve(root, '.cache/offline-sysroot')
|
||||
const boostPrefix = resolve(root, '.cache/toolchains/boost/install-wasm')
|
||||
const reportPath = resolve(root, '.cache/toolchains/freecad-naming-sdk/configure-report.json')
|
||||
const command = [
|
||||
'emcmake', 'cmake', '--fresh', '-S', sourceDir, '-B', buildDir, '-G', 'Ninja',
|
||||
'-DCMAKE_BUILD_TYPE=Release',
|
||||
`-DCMAKE_INSTALL_PREFIX=${installDir}`,
|
||||
`-DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=${resolve(root, 'config/freecad-wasm-sdk-bootstrap.cmake')}`,
|
||||
'-DBUILD_GUI=OFF', '-DBUILD_PART=ON', '-DBUILD_MATERIAL=ON', '-DBUILD_BIM=OFF', '-DBUILD_FLAT_MESH=OFF',
|
||||
'-DBUILD_PART_DESIGN=OFF', '-DBUILD_SKETCHER=OFF',
|
||||
'-DBUILD_MESH=OFF', '-DBUILD_MESH_PART=OFF', '-DBUILD_FEM=OFF', '-DBUILD_CAM=OFF', '-DBUILD_ASSEMBLY=OFF',
|
||||
'-DBUILD_DRAFT=OFF', '-DBUILD_DRAWING=OFF', '-DBUILD_HELP=OFF', '-DBUILD_IDF=OFF', '-DBUILD_IMPORT=OFF',
|
||||
'-DBUILD_INSPECTION=OFF', '-DBUILD_OPENSCAD=OFF', '-DBUILD_PLOT=OFF', '-DBUILD_POINTS=OFF',
|
||||
'-DBUILD_REVERSEENGINEERING=OFF', '-DBUILD_ROBOT=OFF', '-DBUILD_SHOW=OFF', '-DBUILD_SPREADSHEET=OFF',
|
||||
'-DBUILD_START=OFF', '-DBUILD_SURFACE=OFF', '-DBUILD_TECHDRAW=OFF', '-DBUILD_TEST=OFF', '-DBUILD_TUX=OFF',
|
||||
'-DBUILD_WEB=OFF', '-DBUILD_ADDONMGR=OFF', '-DBUILD_MEASURE=OFF', '-DBUILD_CLOUD=OFF', '-DBUILD_JTREADER=OFF',
|
||||
'-DBUILD_VR=OFF', '-DBUILD_TEMPLATE=OFF', '-DBUILD_SANDBOX=OFF', '-DENABLE_DEVELOPER_TESTS=OFF',
|
||||
'-DFREECAD_USE_FREETYPE=OFF', '-DFREECAD_USE_EXTERNAL_FMT=ON', '-DFREECAD_CHECK_PIVY=OFF',
|
||||
'-DINSTALL_TO_SITEPACKAGES=OFF', '-DFREECAD_USE_CCACHE=OFF', '-DBUILD_DYNAMIC_LINK_PYTHON=OFF',
|
||||
`-DFREECAD_WASM_OFFLINE_SYSROOT=${offlineSysroot}`,
|
||||
`-DCMAKE_PREFIX_PATH=${qtPrefix};${yamlPrefix};${icuPrefix};${xercesPrefix}`,
|
||||
`-DQt6_DIR=${resolve(qtPrefix, 'lib/cmake/Qt6')}`,
|
||||
`-DQt6Core_DIR=${resolve(qtPrefix, 'lib/cmake/Qt6Core')}`,
|
||||
`-DQt6Concurrent_DIR=${resolve(qtPrefix, 'lib/cmake/Qt6Concurrent')}`,
|
||||
`-DQt6Network_DIR=${resolve(qtPrefix, 'lib/cmake/Qt6Network')}`,
|
||||
`-DQt6Xml_DIR=${resolve(qtPrefix, 'lib/cmake/Qt6Xml')}`,
|
||||
`-DQt6ZlibPrivate_DIR=${resolve(qtPrefix, 'lib/cmake/Qt6ZlibPrivate')}`,
|
||||
`-DQt6BundledZLIB_DIR=${resolve(qtPrefix, 'lib/cmake/Qt6BundledZLIB')}`,
|
||||
`-DQt6BundledPcre2_DIR=${resolve(qtPrefix, 'lib/cmake/Qt6BundledPcre2')}`,
|
||||
`-DZLIB_INCLUDE_DIR=${resolve(qtPrefix, 'include/QtZlib')}`,
|
||||
`-DZLIB_LIBRARY=${resolve(qtPrefix, 'lib/libQt6BundledZLIB.a')}`,
|
||||
`-Dyaml-cpp_DIR=${resolve(yamlPrefix, 'lib/cmake/yaml-cpp')}`,
|
||||
`-DICU_ROOT=${icuPrefix}`,
|
||||
`-DICU_INCLUDE_DIR=${resolve(icuPrefix, 'include')}`,
|
||||
`-DICU_UC_LIBRARY_RELEASE=${resolve(icuPrefix, 'lib/libicuuc.a')}`,
|
||||
`-DICU_I18N_LIBRARY_RELEASE=${resolve(icuPrefix, 'lib/libicui18n.a')}`,
|
||||
`-DXercesC_INCLUDE_DIR=${resolve(xercesPrefix, 'include')}`,
|
||||
`-DXercesC_LIBRARY=${resolve(xercesPrefix, 'lib/libxerces-c.a')}`,
|
||||
'-DPython3_EXECUTABLE=/usr/bin/python3.13',
|
||||
`-DPython3_INCLUDE_DIR=${resolve(pythonPrefix, 'include/python3.13')}`,
|
||||
`-DPython3_INCLUDE_DIRS=${resolve(pythonPrefix, 'include/python3.13')}`,
|
||||
`-DPython3_LIBRARY=${resolve(pythonPrefix, 'lib/libpython3.13.a')}`,
|
||||
'-DOCCT_CMAKE_FALLBACK=ON',
|
||||
`-DOCC_INCLUDE_DIR=${resolve(occtBuild, 'include/opencascade')}`,
|
||||
`-DOCC_LIBRARY=${resolve(occtBuild, 'lin32/clang/lib/libTKernel.a')}`,
|
||||
`-DFREECAD_WASM_OCCT_COMPAT_INCLUDE=${resolve(occtSource, 'src/Deprecated/NCollectionAliases')}`,
|
||||
`-DEIGEN3_INCLUDE_DIR=${resolve(offlineSysroot, 'include/eigen3')}`,
|
||||
`-DBOOST_WASM_ROOT=${boostPrefix}`,
|
||||
`-DBoost_DIR=${resolve(root, 'config/cmake/boost')}`,
|
||||
'-DBoost_USE_STATIC_LIBS=ON',
|
||||
'-DQT_HOST_PATH=/usr',
|
||||
'-DQT_HOST_PATH_CMAKE_DIR=/usr/lib/x86_64-linux-gnu/cmake',
|
||||
'-DFETCHCONTENT_FULLY_DISCONNECTED=ON',
|
||||
`-DCMAKE_TOOLCHAIN_FILE=${resolve(root, 'config/emscripten-freecad-candidate-toolchain.cmake')}`,
|
||||
'-DCMAKE_AR=emar', '-DCMAKE_RANLIB=emranlib',
|
||||
'-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER', '-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY', '-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY',
|
||||
]
|
||||
const startedAt = new Date().toISOString()
|
||||
let status = 'configured'
|
||||
let exitCode = 0
|
||||
let output = ''
|
||||
try {
|
||||
const result = await run(command[0], command.slice(1), { cwd: root, maxBuffer: 16 * 1024 * 1024 })
|
||||
output = `${result.stdout}${result.stderr}`
|
||||
} catch (error) {
|
||||
status = 'blocked'
|
||||
exitCode = error.status ?? 1
|
||||
output = `${error.stdout ?? ''}${error.stderr ?? ''}`
|
||||
}
|
||||
const missing = []
|
||||
if (status === 'blocked') {
|
||||
if (output.includes('Could not find a valid Qt installation') || output.includes('Could NOT find Qt6')) missing.push('Qt6 wasm package configuration')
|
||||
if (output.includes('Could NOT find Python3')) missing.push('Emscripten Python3 development target')
|
||||
if (output.includes('Could NOT find ICU') || output.includes('Failed to find all ICU components')) missing.push('ICU wasm static libraries')
|
||||
if (output.includes('Could NOT find XercesC') || output.includes('Failed to find XercesC')) missing.push('Xerces-C wasm static library')
|
||||
if (output.includes('Could NOT find ZLIB')) missing.push('ZLIB wasm static library')
|
||||
if (output.includes('Could NOT find OCC') || output.includes('OpenCASCADE not found')) missing.push('OpenCASCADE wasm SDK')
|
||||
if (output.includes('Could NOT find Eigen3') || output.includes('Eigen3 not found')) missing.push('Eigen3 headers')
|
||||
if (output.includes('Could NOT find Boost') || output.includes('provided by "Boost"')) missing.push('Boost wasm static libraries')
|
||||
if (output.includes('Could NOT find yaml-cpp') || output.includes('provided by "yaml-cpp"')) missing.push('yaml-cpp wasm package')
|
||||
if (output.includes('Unknown CMake command "qt_add_translation"')) missing.push('Qt translation cross-build adapter')
|
||||
if (output.includes('Qt6::Core') && output.includes('mkspecs/wasm-emscripten') && output.includes('non-existent path')) missing.push('Qt6 wasm mkspec installation')
|
||||
if (missing.length === 0) missing.push('unclassified CMake configure failure')
|
||||
}
|
||||
await mkdir(resolve(root, '.cache/toolchains/freecad-naming-sdk'), { recursive: true })
|
||||
await writeFile(reportPath, `${JSON.stringify({ schemaVersion: 1, status, exitCode, startedAt, command, missing, buildDir, outputTail: output.slice(-12000), productionPublication: false, boundary: { freecadNamingBuildStatus: 'contract-only', exTsn02: 'in_progress', systemExact: false, workerLinked: false, callbacks: [] } }, null, 2)}\n`)
|
||||
console.log(JSON.stringify({ status, exitCode, report: reportPath, missing, productionPublication: false }, null, 2))
|
||||
if (status !== 'configured') process.exit(exitCode)
|
||||
182
scripts/freecad-attachment-mode-oracle.py
Normal file
182
scripts/freecad-attachment-mode-oracle.py
Normal file
@@ -0,0 +1,182 @@
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
def version_text():
|
||||
return ".".join(str(value) for value in App.Version()[:3])
|
||||
|
||||
|
||||
def placement_json(placement):
|
||||
axis = placement.Rotation.Axis
|
||||
base = placement.Base
|
||||
return {
|
||||
"position": [float(base.x), float(base.y), float(base.z)],
|
||||
"axis": [float(axis.x), float(axis.y), float(axis.z)],
|
||||
"angleDegrees": float(placement.Rotation.Angle * 180.0 / 3.141592653589793),
|
||||
}
|
||||
|
||||
|
||||
def support_json(value):
|
||||
result = []
|
||||
for obj, sub_elements in value:
|
||||
subs = [sub_elements] if isinstance(sub_elements, str) else list(sub_elements)
|
||||
result.append({
|
||||
"document": obj.Document.Name,
|
||||
"object": obj.Name,
|
||||
"subElements": [str(sub) for sub in subs if str(sub)],
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def engine_json(obj):
|
||||
attacher = obj.Attacher
|
||||
implemented = list(attacher.ImplementedModes)
|
||||
return {
|
||||
"typeId": obj.TypeId,
|
||||
"attacherType": attacher.AttacherType,
|
||||
"completeModeList": list(attacher.CompleteModeList),
|
||||
"implementedModes": implemented,
|
||||
"modeInfo": {
|
||||
mode: {
|
||||
"modeIndex": int(attacher.getModeInfo(mode)["ModeIndex"]),
|
||||
"referenceCombinations": attacher.getModeInfo(mode)["ReferenceCombinations"],
|
||||
}
|
||||
for mode in implemented
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def object_state(obj):
|
||||
suggestion = obj.Attacher.suggestModes()
|
||||
return {
|
||||
"typeId": obj.TypeId,
|
||||
"mapMode": str(obj.MapMode),
|
||||
"support": support_json(obj.AttachmentSupport),
|
||||
"placement": placement_json(obj.Placement),
|
||||
"state": [str(value) for value in obj.State],
|
||||
"status": str(obj.getStatusString()),
|
||||
"positionBySupport": bool(obj.positionBySupport()),
|
||||
"suggestion": {
|
||||
"message": suggestion["message"],
|
||||
"bestFitMode": suggestion["bestFitMode"],
|
||||
"allApplicableModes": suggestion["allApplicableModes"],
|
||||
"referenceTypes": suggestion["references_Types"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
document = App.newDocument("AttachmentModeOracle")
|
||||
source_body = document.addObject("PartDesign::Body", "SourceBody")
|
||||
source_box = source_body.newObject("PartDesign::AdditiveBox", "SourceBox")
|
||||
source_box.Length = 4
|
||||
source_box.Width = 5
|
||||
source_box.Height = 6
|
||||
document.recompute()
|
||||
|
||||
plane_body = document.addObject("PartDesign::Body", "PlaneBody")
|
||||
datum_plane = plane_body.newObject("PartDesign::Plane", "DatumPlane")
|
||||
datum_plane.AttachmentSupport = [(source_box, "Face1")]
|
||||
datum_plane.MapMode = "FlatFace"
|
||||
|
||||
line_body = document.addObject("PartDesign::Body", "LineBody")
|
||||
datum_line = line_body.newObject("PartDesign::Line", "DatumLine")
|
||||
datum_line.AttachmentSupport = [(source_box, ("Vertex1", "Vertex2"))]
|
||||
datum_line.MapMode = "TwoPointLine"
|
||||
|
||||
point_body = document.addObject("PartDesign::Body", "PointBody")
|
||||
datum_point = point_body.newObject("PartDesign::Point", "DatumPoint")
|
||||
datum_point.AttachmentSupport = [(source_box, "Vertex1")]
|
||||
datum_point.MapMode = "Vertex"
|
||||
|
||||
sketch_body = document.addObject("PartDesign::Body", "SketchBody")
|
||||
sketch = sketch_body.newObject("Sketcher::SketchObject", "Sketch")
|
||||
sketch.AttachmentSupport = [(source_box, "Face1")]
|
||||
sketch.MapMode = "FlatFace"
|
||||
|
||||
invalid_plane_body = document.addObject("PartDesign::Body", "InvalidPlaneBody")
|
||||
invalid_plane = invalid_plane_body.newObject("PartDesign::Plane", "InvalidPlane")
|
||||
invalid_plane.AttachmentSupport = [(source_box, "Vertex1")]
|
||||
invalid_plane.MapMode = "Vertex"
|
||||
|
||||
invalid_line_body = document.addObject("PartDesign::Body", "InvalidLineBody")
|
||||
invalid_line = invalid_line_body.newObject("PartDesign::Line", "InvalidLine")
|
||||
invalid_line.AttachmentSupport = [(source_box, "Face1")]
|
||||
invalid_line.MapMode = "FlatFace"
|
||||
|
||||
invalid_point_body = document.addObject("PartDesign::Body", "InvalidPointBody")
|
||||
invalid_point = invalid_point_body.newObject("PartDesign::Point", "InvalidPoint")
|
||||
invalid_point.AttachmentSupport = [(source_box, "Face1")]
|
||||
invalid_point.MapMode = "FlatFace"
|
||||
|
||||
document.recompute()
|
||||
engine_objects = {
|
||||
"plane": datum_plane,
|
||||
"line": datum_line,
|
||||
"point": datum_point,
|
||||
"sketch": sketch,
|
||||
}
|
||||
engines = {name: engine_json(obj) for name, obj in engine_objects.items()}
|
||||
success_initial = {name: object_state(obj) for name, obj in engine_objects.items()}
|
||||
failures = {
|
||||
obj.Name: {
|
||||
"typeId": obj.TypeId,
|
||||
"mapMode": str(obj.MapMode),
|
||||
"support": support_json(obj.AttachmentSupport),
|
||||
"state": [str(value) for value in obj.State],
|
||||
"status": str(obj.getStatusString()),
|
||||
}
|
||||
for obj in [invalid_plane, invalid_line, invalid_point]
|
||||
}
|
||||
|
||||
source_box.Height = 9
|
||||
document.recompute()
|
||||
success_edited = {name: object_state(obj) for name, obj in engine_objects.items()}
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="freecad-attachment-mode-") as temp_dir:
|
||||
path = os.path.join(temp_dir, "AttachmentModeOracle.FCStd")
|
||||
document.saveAs(path)
|
||||
App.closeDocument(document.Name)
|
||||
reopened = App.openDocument(path)
|
||||
reopened.recompute()
|
||||
roundtrip = {
|
||||
name: object_state(reopened.getObject(object_name))
|
||||
for name, object_name in {
|
||||
"plane": "DatumPlane",
|
||||
"line": "DatumLine",
|
||||
"point": "DatumPoint",
|
||||
"sketch": "Sketch",
|
||||
}.items()
|
||||
}
|
||||
roundtrip_failures = {
|
||||
object_name: {
|
||||
"mapMode": str(reopened.getObject(object_name).MapMode),
|
||||
"state": [str(value) for value in reopened.getObject(object_name).State],
|
||||
"status": str(reopened.getObject(object_name).getStatusString()),
|
||||
}
|
||||
for object_name in ["InvalidPlane", "InvalidLine", "InvalidPoint"]
|
||||
}
|
||||
App.closeDocument(reopened.Name)
|
||||
|
||||
all_modes = engines["plane"]["completeModeList"]
|
||||
implemented_union = sorted({mode for engine in engines.values() for mode in engine["implementedModes"]})
|
||||
report = {
|
||||
"schemaVersion": 1,
|
||||
"baselineId": "freecad-1.1.1-attachment-mode-oracle",
|
||||
"freecadVersion": version_text(),
|
||||
"gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d",
|
||||
"status": "pass",
|
||||
"tolerance": 1e-7,
|
||||
"registry": {
|
||||
"modeCount": len(all_modes),
|
||||
"modes": all_modes,
|
||||
"implementedUnionCount": len(implemented_union),
|
||||
"unimplementedModes": [mode for mode in all_modes if mode not in implemented_union],
|
||||
},
|
||||
"engines": engines,
|
||||
"success": {"initial": success_initial, "edited": success_edited, "roundtrip": roundtrip},
|
||||
"failures": {"initial": failures, "roundtrip": roundtrip_failures},
|
||||
}
|
||||
print("FREECAD_ATTACHMENT_MODE_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))
|
||||
126
scripts/freecad-naming-sdk-lib.mjs
Normal file
126
scripts/freecad-naming-sdk-lib.mjs
Normal file
@@ -0,0 +1,126 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
240
scripts/freecad-partdesign-structure-oracle.py
Normal file
240
scripts/freecad-partdesign-structure-oracle.py
Normal file
@@ -0,0 +1,240 @@
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
def version_text():
|
||||
return ".".join(str(value) for value in App.Version()[:3])
|
||||
|
||||
|
||||
def names(objects):
|
||||
return [obj.Name for obj in objects]
|
||||
|
||||
|
||||
def link_sub_values(value):
|
||||
output = []
|
||||
for obj, sub_elements in value:
|
||||
if isinstance(sub_elements, str):
|
||||
subs = [sub_elements] if sub_elements else []
|
||||
else:
|
||||
subs = [str(sub) for sub in sub_elements if str(sub)]
|
||||
output.append({"object": obj.Name, "subElements": subs})
|
||||
return output
|
||||
|
||||
|
||||
def placement_json(placement):
|
||||
axis = placement.Rotation.Axis
|
||||
base = placement.Base
|
||||
return {
|
||||
"position": [float(base.x), float(base.y), float(base.z)],
|
||||
"axis": [float(axis.x), float(axis.y), float(axis.z)],
|
||||
"angleDegrees": float(placement.Rotation.Angle * 180.0 / 3.141592653589793),
|
||||
}
|
||||
|
||||
|
||||
document = App.newDocument("PartDesignStructureOracle")
|
||||
source_body = document.addObject("PartDesign::Body", "SourceBody")
|
||||
source_box = source_body.newObject("PartDesign::AdditiveBox", "SourceBox")
|
||||
source_box.Length = 10
|
||||
source_box.Width = 8
|
||||
source_box.Height = 6
|
||||
|
||||
datum_body = document.addObject("PartDesign::Body", "DatumBody")
|
||||
datum_plane = datum_body.newObject("PartDesign::Plane", "DatumPlane")
|
||||
datum_plane.AttachmentSupport = [(document.XY_Plane, "")]
|
||||
datum_plane.MapMode = "FlatFace"
|
||||
datum_plane.AttachmentOffset = App.Placement(App.Vector(0, 0, 5), App.Rotation(App.Vector(0, 0, 1), 30))
|
||||
datum_line = datum_body.newObject("PartDesign::Line", "DatumLine")
|
||||
datum_line.AttachmentSupport = [(document.XY_Plane, "")]
|
||||
datum_line.MapMode = "ObjectX"
|
||||
datum_point = datum_body.newObject("PartDesign::Point", "DatumPoint")
|
||||
datum_point.AttachmentSupport = [(document.XY_Plane, "")]
|
||||
datum_point.MapMode = "ObjectOrigin"
|
||||
datum_point.AttachmentOffset = App.Placement(App.Vector(1, 2, 3), App.Rotation())
|
||||
|
||||
binder_body = document.addObject("PartDesign::Body", "BinderBody")
|
||||
shape_binder = binder_body.newObject("PartDesign::ShapeBinder", "ShapeBinder")
|
||||
shape_binder.Support = [(source_box, "")]
|
||||
shape_binder.TraceSupport = True
|
||||
|
||||
sub_binder_body = document.addObject("PartDesign::Body", "SubBinderBody")
|
||||
sub_binder = sub_binder_body.newObject("PartDesign::SubShapeBinder", "SubShapeBinder")
|
||||
sub_binder.Support = [(source_box, ("Edge1", "Edge2", "Edge3", "Edge4"))]
|
||||
|
||||
document.recompute()
|
||||
initial = {
|
||||
"sourceVolume": float(source_box.Shape.Volume),
|
||||
"shapeBinderVolume": float(shape_binder.Shape.Volume),
|
||||
"subShapeBinderLength": float(sub_binder.Shape.Length),
|
||||
"bodyTips": {body.Name: body.Tip.Name if body.Tip else None for body in [source_body, datum_body, binder_body, sub_binder_body]},
|
||||
"bodyGroups": {body.Name: names(body.Group) for body in [source_body, datum_body, binder_body, sub_binder_body]},
|
||||
"datumPlane": {"mapMode": datum_plane.MapMode, "support": link_sub_values(datum_plane.AttachmentSupport), "attachmentOffset": placement_json(datum_plane.AttachmentOffset), "placement": placement_json(datum_plane.Placement), "state": [str(value) for value in datum_plane.State]},
|
||||
"datumLine": {"mapMode": datum_line.MapMode, "support": link_sub_values(datum_line.AttachmentSupport), "state": [str(value) for value in datum_line.State]},
|
||||
"datumPoint": {"mapMode": datum_point.MapMode, "support": link_sub_values(datum_point.AttachmentSupport), "attachmentOffset": placement_json(datum_point.AttachmentOffset), "placement": placement_json(datum_point.Placement), "state": [str(value) for value in datum_point.State]},
|
||||
"shapeBinder": {"support": link_sub_values(shape_binder.Support), "traceSupport": bool(shape_binder.TraceSupport), "state": [str(value) for value in shape_binder.State]},
|
||||
"subShapeBinder": {"support": link_sub_values(sub_binder.Support), "state": [str(value) for value in sub_binder.State]},
|
||||
}
|
||||
|
||||
source_box.Length = 14
|
||||
datum_plane.AttachmentOffset = App.Placement(App.Vector(0, 0, 7), App.Rotation(App.Vector(0, 0, 1), 45))
|
||||
document.recompute()
|
||||
edited = {
|
||||
"sourceVolume": float(source_box.Shape.Volume),
|
||||
"shapeBinderVolume": float(shape_binder.Shape.Volume),
|
||||
"subShapeBinderLength": float(sub_binder.Shape.Length),
|
||||
"datumPlaneAttachmentOffset": placement_json(datum_plane.AttachmentOffset),
|
||||
"datumPlanePlacement": placement_json(datum_plane.Placement),
|
||||
"bodyTips": {body.Name: body.Tip.Name if body.Tip else None for body in [source_body, datum_body, binder_body, sub_binder_body]},
|
||||
}
|
||||
|
||||
invalid_plane = datum_body.newObject("PartDesign::Plane", "InvalidDatumPlane")
|
||||
invalid_plane.AttachmentSupport = [(source_box, "Face999")]
|
||||
invalid_plane.MapMode = "FlatFace"
|
||||
document.recompute()
|
||||
invalid_support = {
|
||||
"typeId": invalid_plane.TypeId,
|
||||
"support": link_sub_values(invalid_plane.AttachmentSupport),
|
||||
"state": [str(value) for value in invalid_plane.State],
|
||||
"status": str(invalid_plane.getStatusString()),
|
||||
}
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="freecad-partdesign-structure-") as temp_dir:
|
||||
path = os.path.join(temp_dir, "PartDesignStructureOracle.FCStd")
|
||||
document.recompute()
|
||||
document.saveAs(path)
|
||||
App.closeDocument(document.Name)
|
||||
reopened_document = App.openDocument(path)
|
||||
reopened_document.recompute()
|
||||
reopened_source = reopened_document.getObject("SourceBox")
|
||||
reopened_plane = reopened_document.getObject("DatumPlane")
|
||||
reopened_shape_binder = reopened_document.getObject("ShapeBinder")
|
||||
reopened_sub_binder = reopened_document.getObject("SubShapeBinder")
|
||||
reopened_invalid = reopened_document.getObject("InvalidDatumPlane")
|
||||
reopened_bodies = [reopened_document.getObject(name) for name in ["SourceBody", "DatumBody", "BinderBody", "SubBinderBody"]]
|
||||
roundtrip = {
|
||||
"sourceVolume": float(reopened_source.Shape.Volume),
|
||||
"shapeBinderVolume": float(reopened_shape_binder.Shape.Volume),
|
||||
"subShapeBinderLength": float(reopened_sub_binder.Shape.Length),
|
||||
"shapeBinderSupport": link_sub_values(reopened_shape_binder.Support),
|
||||
"subShapeBinderSupport": link_sub_values(reopened_sub_binder.Support),
|
||||
"datumPlaneSupport": link_sub_values(reopened_plane.AttachmentSupport),
|
||||
"datumPlaneMapMode": reopened_plane.MapMode,
|
||||
"datumPlaneAttachmentOffset": placement_json(reopened_plane.AttachmentOffset),
|
||||
"bodyTips": {body.Name: body.Tip.Name if body.Tip else None for body in reopened_bodies},
|
||||
"bodyGroups": {body.Name: names(body.Group) for body in reopened_bodies},
|
||||
"invalidDatumState": [str(value) for value in reopened_invalid.State],
|
||||
"invalidDatumStatus": str(reopened_invalid.getStatusString()),
|
||||
}
|
||||
App.closeDocument(reopened_document.Name)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="freecad-partdesign-cross-document-") as temp_dir:
|
||||
source_path = os.path.join(temp_dir, "PartDesignExternalSource.FCStd")
|
||||
consumer_path = os.path.join(temp_dir, "PartDesignExternalConsumer.FCStd")
|
||||
external_source = App.newDocument("PartDesignExternalSource")
|
||||
external_source_body = external_source.addObject("PartDesign::Body", "ExternalSourceBody")
|
||||
external_box = external_source_body.newObject("PartDesign::AdditiveBox", "ExternalBox")
|
||||
external_box.Length = 4
|
||||
external_box.Width = 5
|
||||
external_box.Height = 6
|
||||
external_source.recompute()
|
||||
external_source.saveAs(source_path)
|
||||
external_consumer = App.newDocument("PartDesignExternalConsumer")
|
||||
external_consumer.saveAs(consumer_path)
|
||||
external_shape_body = external_consumer.addObject("PartDesign::Body", "ExternalShapeBody")
|
||||
external_shape_binder = external_shape_body.newObject("PartDesign::ShapeBinder", "ExternalShapeBinder")
|
||||
try:
|
||||
external_shape_binder.Support = [(external_box, "")]
|
||||
shape_binder_external_link = {"accepted": True, "errorType": None, "error": None}
|
||||
except Exception as error:
|
||||
shape_binder_external_link = {"accepted": False, "errorType": type(error).__name__, "error": str(error)}
|
||||
external_sub_body = external_consumer.addObject("PartDesign::Body", "ExternalSubBody")
|
||||
external_sub_binder = external_sub_body.newObject("PartDesign::SubShapeBinder", "ExternalSubShapeBinder")
|
||||
external_sub_binder.Support = [(external_box, ("Face1",))]
|
||||
external_consumer.recompute()
|
||||
cross_document_initial = {
|
||||
"sourceVolume": float(external_box.Shape.Volume),
|
||||
"shapeBinderExternalLink": shape_binder_external_link,
|
||||
"shapeBinderSupportType": external_shape_binder.getTypeIdOfProperty("Support"),
|
||||
"subShapeBinderSupportType": external_sub_binder.getTypeIdOfProperty("Support"),
|
||||
"shapeBinderShapeNull": bool(external_shape_binder.Shape.isNull()),
|
||||
"subShapeBinderArea": float(external_sub_binder.Shape.Area),
|
||||
"sourceFaceArea": float(external_box.Shape.Face1.Area),
|
||||
"shapeBinderSupport": link_sub_values(external_shape_binder.Support),
|
||||
"subShapeBinderSupport": link_sub_values(external_sub_binder.Support),
|
||||
"subShapeBinderSourceDocument": external_sub_binder.Support[0][0].Document.Name,
|
||||
"shapeBodyTip": external_shape_body.Tip.Name if external_shape_body.Tip else None,
|
||||
"subBodyTip": external_sub_body.Tip.Name if external_sub_body.Tip else None,
|
||||
}
|
||||
external_source.save()
|
||||
external_consumer.save()
|
||||
App.closeDocument(external_consumer.Name)
|
||||
App.closeDocument(external_source.Name)
|
||||
reopened_source = App.openDocument(source_path)
|
||||
reopened_consumer = App.openDocument(consumer_path)
|
||||
reopened_source.recompute()
|
||||
reopened_consumer.recompute()
|
||||
reopened_box = reopened_source.getObject("ExternalBox")
|
||||
reopened_shape_body = reopened_consumer.getObject("ExternalShapeBody")
|
||||
reopened_sub_body = reopened_consumer.getObject("ExternalSubBody")
|
||||
reopened_shape_binder = reopened_consumer.getObject("ExternalShapeBinder")
|
||||
reopened_sub_binder = reopened_consumer.getObject("ExternalSubShapeBinder")
|
||||
cross_document_roundtrip = {
|
||||
"sourceVolume": float(reopened_box.Shape.Volume),
|
||||
"shapeBinderShapeNull": bool(reopened_shape_binder.Shape.isNull()),
|
||||
"subShapeBinderArea": float(reopened_sub_binder.Shape.Area),
|
||||
"sourceFaceArea": float(reopened_box.Shape.Face1.Area),
|
||||
"shapeBinderSupport": link_sub_values(reopened_shape_binder.Support),
|
||||
"subShapeBinderSupport": link_sub_values(reopened_sub_binder.Support),
|
||||
"subShapeBinderSourceDocument": reopened_sub_binder.Support[0][0].Document.Name,
|
||||
"shapeBodyTip": reopened_shape_body.Tip.Name if reopened_shape_body.Tip else None,
|
||||
"subBodyTip": reopened_sub_body.Tip.Name if reopened_sub_body.Tip else None,
|
||||
}
|
||||
reopened_box.Width = 8
|
||||
reopened_source.recompute()
|
||||
reopened_consumer.recompute()
|
||||
cross_document_edited = {
|
||||
"sourceVolume": float(reopened_box.Shape.Volume),
|
||||
"shapeBinderShapeNull": bool(reopened_shape_binder.Shape.isNull()),
|
||||
"subShapeBinderArea": float(reopened_sub_binder.Shape.Area),
|
||||
"sourceFaceArea": float(reopened_box.Shape.Face1.Area),
|
||||
"shapeBinderState": [str(value) for value in reopened_shape_binder.State],
|
||||
"subShapeBinderState": [str(value) for value in reopened_sub_binder.State],
|
||||
}
|
||||
reopened_source.removeObject("ExternalBox")
|
||||
reopened_source.recompute()
|
||||
reopened_consumer.recompute()
|
||||
cross_document_deleted = {
|
||||
"shapeBinderSupport": link_sub_values(reopened_shape_binder.Support),
|
||||
"subShapeBinderSupport": link_sub_values(reopened_sub_binder.Support),
|
||||
"shapeBinderShapeNull": bool(reopened_shape_binder.Shape.isNull()),
|
||||
"subShapeBinderShapeNull": bool(reopened_sub_binder.Shape.isNull()),
|
||||
"subShapeBinderCachedArea": float(reopened_sub_binder.Shape.Area),
|
||||
"shapeBinderState": [str(value) for value in reopened_shape_binder.State],
|
||||
"subShapeBinderState": [str(value) for value in reopened_sub_binder.State],
|
||||
"shapeBinderStatus": str(reopened_shape_binder.getStatusString()),
|
||||
"subShapeBinderStatus": str(reopened_sub_binder.getStatusString()),
|
||||
}
|
||||
App.closeDocument(reopened_consumer.Name)
|
||||
App.closeDocument(reopened_source.Name)
|
||||
|
||||
report = {
|
||||
"schemaVersion": 1,
|
||||
"baselineId": "freecad-1.1.1-partdesign-structure-oracle",
|
||||
"freecadVersion": version_text(),
|
||||
"gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d",
|
||||
"status": "pass",
|
||||
"tolerance": 1e-7,
|
||||
"initial": initial,
|
||||
"edited": edited,
|
||||
"invalidSupport": invalid_support,
|
||||
"roundtrip": roundtrip,
|
||||
"crossDocument": {
|
||||
"initial": cross_document_initial,
|
||||
"roundtrip": cross_document_roundtrip,
|
||||
"edited": cross_document_edited,
|
||||
"deleted": cross_document_deleted,
|
||||
},
|
||||
}
|
||||
print("FREECAD_PARTDESIGN_STRUCTURE_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))
|
||||
92
scripts/generate-freecad-naming-sdk-manifest.mjs
Normal file
92
scripts/generate-freecad-naming-sdk-manifest.mjs
Normal file
@@ -0,0 +1,92 @@
|
||||
import { mkdir, writeFile } from 'node:fs/promises'
|
||||
import { resolve, relative } from 'node:path'
|
||||
import {
|
||||
REQUIRED_FREECAD_NAMING_CALLBACKS,
|
||||
REQUIRED_FREECAD_NAMING_DEFINITIONS,
|
||||
inspectIncludeDirectory,
|
||||
inspectPlannedLibrary,
|
||||
inspectPlannedRuntimeAsset,
|
||||
loadSdkPlan,
|
||||
resolveFrom,
|
||||
exists,
|
||||
sha256File,
|
||||
} from './freecad-naming-sdk-lib.mjs'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const outputRoot = process.env.FREECAD_WASM_SDK_OUTPUT_DIR
|
||||
? resolve(root, process.env.FREECAD_WASM_SDK_OUTPUT_DIR)
|
||||
: resolve(root, '.cache/toolchains/freecad-naming-sdk')
|
||||
const plan = await loadSdkPlan(root)
|
||||
const fail = (message) => { throw new Error(`FreeCAD WASM SDK manifest generation: ${message}`) }
|
||||
|
||||
const libraries = []
|
||||
for (const library of [...(plan.libraries ?? []), ...(plan.linkDependencies ?? [])]) {
|
||||
const inspected = await inspectPlannedLibrary(root, library)
|
||||
if (inspected.status !== 'verified') fail(`missing ${library.name} wasm static library: ${resolveFrom(root, library.path)}`)
|
||||
libraries.push({
|
||||
name: library.name,
|
||||
path: relative(outputRoot, resolveFrom(root, library.path)),
|
||||
sha256: inspected.sha256,
|
||||
})
|
||||
}
|
||||
const includeDirs = []
|
||||
for (const includeDir of plan.includeDirs ?? []) {
|
||||
const inspected = await inspectIncludeDirectory(root, includeDir)
|
||||
if (inspected.status !== 'verified') fail(`include directory ${includeDir.name} is incomplete: ${inspected.missingHeaders.join(', ')}`)
|
||||
includeDirs.push(relative(outputRoot, resolveFrom(root, includeDir.path)))
|
||||
}
|
||||
const bridge = resolveFrom(root, plan.namingBridge.path)
|
||||
if (!await exists(bridge)) fail(`missing naming bridge source: ${bridge}`)
|
||||
const hostAdapter = resolveFrom(root, plan.namingBridge.hostAdapter)
|
||||
if (!await exists(hostAdapter)) fail(`missing naming host adapter: ${hostAdapter}`)
|
||||
const forceInclude = resolveFrom(root, plan.compileOptions.forceInclude)
|
||||
if (!await exists(forceInclude)) fail(`missing force-include header: ${forceInclude}`)
|
||||
if (plan.compileOptions.cxxStandard !== 'c++20' || plan.compileOptions.pthread !== true || JSON.stringify(plan.compileOptions.definitions) !== JSON.stringify(REQUIRED_FREECAD_NAMING_DEFINITIONS)) fail('compile options do not match the locked SDK contract')
|
||||
const bridgeSha256 = await sha256File(bridge)
|
||||
const runtimeAssets = []
|
||||
for (const asset of plan.runtimeAssets ?? []) {
|
||||
const inspected = await inspectPlannedRuntimeAsset(root, asset)
|
||||
if (inspected.status !== 'verified') fail(`runtime asset ${asset.name} is incomplete: ${inspected.missingFiles.join(', ')}`)
|
||||
runtimeAssets.push({
|
||||
name: asset.name,
|
||||
path: relative(outputRoot, resolveFrom(root, asset.path)),
|
||||
preloadTo: asset.preloadTo,
|
||||
files: inspected.files,
|
||||
})
|
||||
}
|
||||
const manifest = {
|
||||
schemaVersion: 1,
|
||||
freecadVersion: plan.baseline.freecadVersion,
|
||||
sourceCommit: plan.baseline.sourceCommit,
|
||||
emscriptenVersion: plan.baseline.emscriptenVersion,
|
||||
qtTarget: plan.baseline.target,
|
||||
pythonTarget: plan.baseline.target,
|
||||
includeDirs,
|
||||
libraries,
|
||||
namingBridge: {
|
||||
source: relative(outputRoot, bridge),
|
||||
sha256: bridgeSha256,
|
||||
hostAdapter: relative(outputRoot, hostAdapter),
|
||||
hostAdapterSha256: await sha256File(hostAdapter),
|
||||
exports: [...REQUIRED_FREECAD_NAMING_CALLBACKS],
|
||||
},
|
||||
compileOptions: {
|
||||
cxxStandard: plan.compileOptions.cxxStandard,
|
||||
pthread: plan.compileOptions.pthread,
|
||||
forceInclude: relative(outputRoot, forceInclude),
|
||||
forceIncludeSha256: await sha256File(forceInclude),
|
||||
definitions: [...plan.compileOptions.definitions],
|
||||
},
|
||||
runtimeAssets,
|
||||
productionPublication: false,
|
||||
boundary: { ...plan.boundary },
|
||||
}
|
||||
await mkdir(outputRoot, { recursive: true })
|
||||
const output = resolve(outputRoot, 'manifest.json')
|
||||
await writeFile(output, `${JSON.stringify(manifest, null, 2)}\n`)
|
||||
console.log(JSON.stringify({
|
||||
status: 'freecad-naming-sdk-manifest-generated',
|
||||
output,
|
||||
productionPublished: false,
|
||||
systemExact: false,
|
||||
}, null, 2))
|
||||
@@ -607,7 +607,7 @@ async function smoke() {
|
||||
FREECAD_SOURCE_OFFLINE: '1',
|
||||
OCCT_SOURCE_DIR: resolve(root, '.cache/occt/occt'),
|
||||
}
|
||||
for (const script of ['check:runtime', 'check:freecad-source', 'check:freecad-private-naming-boundary', 'check:occt-history-artifact', 'test:occt-history', 'test:planegcs', 'build']) {
|
||||
for (const script of ['check:runtime', 'check:freecad-source', 'build:freecad-naming-source-probe', 'test:freecad-naming-source-probe', 'check:freecad-naming-sdk-readiness', 'check:freecad-wasm-sdk-build-plan', 'check:freecad-naming-next-tasks', 'check:freecad-private-naming-boundary', 'check:occt-history-artifact', 'test:occt-history', 'test:planegcs', 'build']) {
|
||||
print(`[offline:smoke] npm run ${script}`)
|
||||
await run(resolve(root, 'npmw'), ['run', script], { env: environment })
|
||||
}
|
||||
|
||||
166
scripts/prepare-freecad-occt8-overlay.sh
Executable file
166
scripts/prepare-freecad-occt8-overlay.sh
Executable file
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
FREECAD_SOURCE_DIR="${ROOT_DIR}/.cache/freecad/FreeCAD"
|
||||
OVERLAY_DIR="${ROOT_DIR}/.cache/toolchains/freecad-naming-sdk/source-occt8"
|
||||
OCCT_MIGRATION="${ROOT_DIR}/.cache/occt/occt/adm/scripts/migration_800/migrate_raise_to_throw.py"
|
||||
OCCT_TYPEDEF_MIGRATION="${ROOT_DIR}/.cache/occt/occt/adm/scripts/migration_800/replace_typedefs.py"
|
||||
OCCT_TYPEDEF_MAP="${ROOT_DIR}/.cache/occt/occt/adm/scripts/migration_800/collected_typedefs.json"
|
||||
OCCT_TYPEDEF_RESULTS="adm/scripts/migration_800/replacement_results.json"
|
||||
LOCKED_COMMIT="0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
|
||||
|
||||
[[ -f "${FREECAD_SOURCE_DIR}/CMakeLists.txt" ]] || { echo "FreeCAD source is missing: ${FREECAD_SOURCE_DIR}" >&2; exit 1; }
|
||||
[[ -f "${OCCT_MIGRATION}" ]] || { echo "OCCT 8 migration script is missing: ${OCCT_MIGRATION}" >&2; exit 1; }
|
||||
[[ -f "${OCCT_TYPEDEF_MIGRATION}" && -f "${OCCT_TYPEDEF_MAP}" ]] || { echo "OCCT 8 typedef migration inputs are missing" >&2; exit 1; }
|
||||
git -C "${ROOT_DIR}/.cache/occt/occt" diff --quiet -- "${OCCT_TYPEDEF_RESULTS}" || { echo "OCCT migration result file is already modified" >&2; exit 1; }
|
||||
cleanup_migration_result() {
|
||||
git -C "${ROOT_DIR}/.cache/occt/occt" restore -- "${OCCT_TYPEDEF_RESULTS}"
|
||||
}
|
||||
trap cleanup_migration_result EXIT
|
||||
[[ "$(git -C "${FREECAD_SOURCE_DIR}" rev-parse HEAD)" == "${LOCKED_COMMIT}" ]] || { echo "FreeCAD source commit is not locked" >&2; exit 1; }
|
||||
git -C "${FREECAD_SOURCE_DIR}" diff --quiet
|
||||
git -C "${FREECAD_SOURCE_DIR}" diff --cached --quiet
|
||||
|
||||
if [[ -e "${OVERLAY_DIR}" && ! -f "${OVERLAY_DIR}/.git" ]]; then
|
||||
echo "Refusing to replace non-worktree overlay: ${OVERLAY_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -f "${OVERLAY_DIR}/.git" ]]; then
|
||||
git -C "${FREECAD_SOURCE_DIR}" worktree remove --force "${OVERLAY_DIR}"
|
||||
fi
|
||||
git -C "${FREECAD_SOURCE_DIR}" worktree prune
|
||||
git -C "${FREECAD_SOURCE_DIR}" worktree add --detach "${OVERLAY_DIR}" "${LOCKED_COMMIT}"
|
||||
|
||||
# The OCCT 8 script locates its own source root, so import it as a module and
|
||||
# apply the documented Raise-to-throw phase only to FreeCAD's source overlay.
|
||||
PYTHONDONTWRITEBYTECODE=1 python3 - "${OCCT_MIGRATION}" "${OVERLAY_DIR}/src" <<'PY'
|
||||
import importlib.util
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
script = pathlib.Path(sys.argv[1])
|
||||
source = pathlib.Path(sys.argv[2])
|
||||
spec = importlib.util.spec_from_file_location("occt8_raise_migration", script)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
files = module.find_files_with_raise(source)
|
||||
for path in files:
|
||||
module.process_file(path)
|
||||
print(f"Applied OCCT 8 Raise-to-throw migration to {len(files)} FreeCAD source files")
|
||||
PY
|
||||
PYTHONDONTWRITEBYTECODE=1 python3 "${OCCT_TYPEDEF_MIGRATION}" \
|
||||
"${OVERLAY_DIR}/src" --input "${OCCT_TYPEDEF_MAP}" --jobs "${JOBS:-4}"
|
||||
# replace_typedefs.py always writes a diagnostic beside the pinned tool. Its
|
||||
# content is not an SDK input, so restore it immediately after the overlay run.
|
||||
cleanup_migration_result
|
||||
|
||||
# OCCT 8 moved iterator typedefs into their collection headers. The official
|
||||
# typedef migration replaces usages but intentionally leaves include-only
|
||||
# references untouched, so map the removed OCCT 7 header names explicitly.
|
||||
while IFS='|' read -r removed_header collection_header; do
|
||||
mapfile -d '' include_files < <(
|
||||
rg -l -0 -F "#include <${removed_header}>" "${OVERLAY_DIR}/src" || true
|
||||
)
|
||||
if (( ${#include_files[@]} > 0 )); then
|
||||
sed -i "s|#include <${removed_header}>|#include <${collection_header}>|g" "${include_files[@]}"
|
||||
fi
|
||||
done <<'EOF'
|
||||
BRepCheck_ListIteratorOfListOfStatus.hxx|BRepCheck_ListOfStatus.hxx
|
||||
TColStd_ListIteratorOfListOfTransient.hxx|TColStd_ListOfTransient.hxx
|
||||
TColStd_MapIteratorOfMapOfTransient.hxx|TColStd_MapOfTransient.hxx
|
||||
TopTools_DataMapIteratorOfDataMapOfIntegerListOfShape.hxx|TopTools_DataMapOfIntegerListOfShape.hxx
|
||||
TopTools_DataMapIteratorOfDataMapOfShapeShape.hxx|TopTools_DataMapOfShapeShape.hxx
|
||||
TopTools_ListIteratorOfListOfShape.hxx|TopTools_ListOfShape.hxx
|
||||
EOF
|
||||
|
||||
# Standard_Failure no longer participates in OCCT RTTI. OCCT 8 documents
|
||||
# ExceptionType() as the source-compatible exception class-name API.
|
||||
while IFS='|' read -r legacy_expression replacement_expression; do
|
||||
mapfile -d '' exception_files < <(
|
||||
rg -l -0 -F "${legacy_expression}" "${OVERLAY_DIR}/src" || true
|
||||
)
|
||||
if (( ${#exception_files[@]} > 0 )); then
|
||||
sed -i "s|${legacy_expression}|${replacement_expression}|g" "${exception_files[@]}"
|
||||
fi
|
||||
done <<'EOF'
|
||||
e.DynamicType()->get_type_name()|e.ExceptionType()
|
||||
e.DynamicType()->Name()|e.ExceptionType()
|
||||
ex.DynamicType()->Name()|ex.ExceptionType()
|
||||
EOF
|
||||
|
||||
# BRepAdaptor_Curve exposes these inherited parameter accessors directly in
|
||||
# OCCT 8; the removed BRepLProp_CurveTool only forwarded these calls here.
|
||||
while IFS='|' read -r legacy_expression replacement_expression; do
|
||||
mapfile -d '' curve_tool_files < <(
|
||||
rg -l -0 -F "${legacy_expression}" "${OVERLAY_DIR}/src" || true
|
||||
)
|
||||
if (( ${#curve_tool_files[@]} > 0 )); then
|
||||
sed -i "s|${legacy_expression}|${replacement_expression}|g" "${curve_tool_files[@]}"
|
||||
fi
|
||||
done <<'EOF'
|
||||
BRepLProp_CurveTool::FirstParameter(adapt)|adapt.FirstParameter()
|
||||
BRepLProp_CurveTool::LastParameter(adapt)|adapt.LastParameter()
|
||||
EOF
|
||||
mapfile -d '' curve_tool_include_files < <(
|
||||
rg -l -0 '^[[:space:]]*#[[:space:]]*include[[:space:]]*<BRepLProp_CurveTool\.hxx>' \
|
||||
"${OVERLAY_DIR}/src" || true
|
||||
)
|
||||
if (( ${#curve_tool_include_files[@]} > 0 )); then
|
||||
sed -i -E '\|^[[:space:]]*#[[:space:]]*include[[:space:]]*<BRepLProp_CurveTool\.hxx>|d' \
|
||||
"${curve_tool_include_files[@]}"
|
||||
fi
|
||||
|
||||
# OCCT 8 consolidates Geom2dLProp into GeomLProp while preserving the 2D
|
||||
# specialization's constructor and methods under the unified class name.
|
||||
mapfile -d '' geom2d_lprop_files < <(
|
||||
rg -l -0 -F 'Geom2dLProp_CLProps2d' "${OVERLAY_DIR}/src" || true
|
||||
)
|
||||
if (( ${#geom2d_lprop_files[@]} > 0 )); then
|
||||
sed -i \
|
||||
-e 's|#include <Geom2dLProp_CLProps2d.hxx>|#include <GeomLProp_CLProps.hxx>|g' \
|
||||
-e 's|\bGeom2dLProp_CLProps2d\b|GeomLProp_CLProps2d|g' \
|
||||
"${geom2d_lprop_files[@]}"
|
||||
fi
|
||||
|
||||
# OCCT 8 adds an initializer-list constructor to NCollection_List, making
|
||||
# these calls ambiguous with FreeCAD's std::vector<TopoShape> overload. The
|
||||
# latter was the only viable overload with the pinned FreeCAD/OCCT 7 sources.
|
||||
TOPO_SHAPE_EXPANSION="${OVERLAY_DIR}/src/Mod/Part/App/TopoShapeExpansion.cpp"
|
||||
sed -i \
|
||||
-e 's|mapper.populate(MappingStatus::Modified, e, {e1, e2, e3, e4});|mapper.populate(MappingStatus::Modified, e, std::vector<TopoShape>{e1, e2, e3, e4});|' \
|
||||
-e 's|mapper.populate(MappingStatus::Generated, v, {TopExp::FirstVertex(e1)});|mapper.populate(MappingStatus::Generated, v, std::vector<TopoShape>{TopExp::FirstVertex(e1)});|' \
|
||||
-e 's|mapper.populate(MappingStatus::Generated, v, {TopExp::LastVertex(e4)});|mapper.populate(MappingStatus::Generated, v, std::vector<TopoShape>{TopExp::LastVertex(e4)});|' \
|
||||
"${TOPO_SHAPE_EXPANSION}"
|
||||
|
||||
# Standard_Failure inherits std::exception in OCCT 8 and what() returns the
|
||||
# same message as the deprecated GetMessageString(). Remove the three now
|
||||
# unreachable duplicate fallbacks that immediately follow an identical
|
||||
# std::exception handler.
|
||||
TOPO_SHAPE_PY="${OVERLAY_DIR}/src/Mod/Part/App/TopoShapePyImp.cpp"
|
||||
perl -0pi -e '
|
||||
s|( catch \(const std::exception& e\) \{\n PyErr_SetString\(PartExceptionOCCError, e\.what\(\)\);\n return nullptr;\n \})\n catch \(Standard_Failure& e\) \{\n(?:\n)? PyErr_SetString\(PartExceptionOCCError, e\.GetMessageString\(\)\);\n return nullptr;\n \}|$1|g
|
||||
' "${TOPO_SHAPE_PY}"
|
||||
|
||||
if git -C "${OVERLAY_DIR}" status --porcelain | awk '{print $2}' | grep -Ev '^src/' >/dev/null; then
|
||||
echo "OCCT 8 migration changed files outside the FreeCAD src directory" >&2
|
||||
exit 1
|
||||
fi
|
||||
remaining="$( (rg -l 'Standard_[A-Za-z0-9_]+::Raise\s*\(' "${OVERLAY_DIR}/src" -g '*.{h,hpp,hxx,c,cpp,cxx,lxx,pxx}' || true) | wc -l)"
|
||||
[[ "${remaining}" == "0" ]] || { echo "OCCT 8 migration left ${remaining} source files with legacy Raise calls" >&2; exit 1; }
|
||||
removed_iterator_headers="$( (rg -l '#include <(BRepCheck_ListIteratorOfListOfStatus|TColStd_ListIteratorOfListOfTransient|TColStd_MapIteratorOfMapOfTransient|TopTools_DataMapIteratorOfDataMapOfIntegerListOfShape|TopTools_DataMapIteratorOfDataMapOfShapeShape|TopTools_ListIteratorOfListOfShape)\.hxx>' "${OVERLAY_DIR}/src" || true) | wc -l)"
|
||||
[[ "${removed_iterator_headers}" == "0" ]] || { echo "OCCT 8 migration left ${removed_iterator_headers} files with removed iterator headers" >&2; exit 1; }
|
||||
legacy_exception_rtti="$( (rg -l '\b(e|ex)\.DynamicType\(\)->(Name|get_type_name)\(\)' "${OVERLAY_DIR}/src" || true) | wc -l)"
|
||||
[[ "${legacy_exception_rtti}" == "0" ]] || { echo "OCCT 8 migration left ${legacy_exception_rtti} files with legacy exception RTTI" >&2; exit 1; }
|
||||
legacy_curve_tool="$( (rg -l '\bBRepLProp_CurveTool\b' "${OVERLAY_DIR}/src" || true) | wc -l)"
|
||||
[[ "${legacy_curve_tool}" == "0" ]] || { echo "OCCT 8 migration left ${legacy_curve_tool} files using removed BRepLProp_CurveTool" >&2; exit 1; }
|
||||
legacy_geom2d_lprop="$( (rg -l '\bGeom2dLProp_CLProps2d\b' "${OVERLAY_DIR}/src" || true) | wc -l)"
|
||||
[[ "${legacy_geom2d_lprop}" == "0" ]] || { echo "OCCT 8 migration left ${legacy_geom2d_lprop} files using removed Geom2dLProp_CLProps2d" >&2; exit 1; }
|
||||
ambiguous_shape_mapper_calls="$( (rg -n 'mapper\.populate\(MappingStatus::(Modified|Generated), (e|v), \{' "${TOPO_SHAPE_EXPANSION}" || true) | wc -l)"
|
||||
[[ "${ambiguous_shape_mapper_calls}" == "0" ]] || { echo "OCCT 8 migration left ${ambiguous_shape_mapper_calls} ambiguous ShapeMapper calls" >&2; exit 1; }
|
||||
unreachable_occt_exception_fallbacks="$( (rg -U -n 'catch \(const std::exception& e\) \{\n PyErr_SetString\(PartExceptionOCCError, e\.what\(\)\);\n return nullptr;\n \}\n catch \(Standard_Failure& e\)' "${TOPO_SHAPE_PY}" || true) | wc -l)"
|
||||
[[ "${unreachable_occt_exception_fallbacks}" == "0" ]] || { echo "OCCT 8 migration left ${unreachable_occt_exception_fallbacks} unreachable Standard_Failure handlers" >&2; exit 1; }
|
||||
|
||||
git -C "${FREECAD_SOURCE_DIR}" diff --quiet
|
||||
git -C "${FREECAD_SOURCE_DIR}" diff --cached --quiet
|
||||
echo "FreeCAD OCCT 8 candidate overlay ready: ${OVERLAY_DIR}"
|
||||
106
scripts/run-chrome-freecad-naming-worker-candidate.mjs
Normal file
106
scripts/run-chrome-freecad-naming-worker-candidate.mjs
Normal file
@@ -0,0 +1,106 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { createReadStream, existsSync } from 'node:fs'
|
||||
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
|
||||
import { createServer } from 'node:http'
|
||||
import { extname, join, normalize, resolve } from 'node:path'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { createChromeProfile, removeChromeProfile } from './chrome-profile.mjs'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const candidateRoot = resolve(root, '.cache/candidates/freecad-naming-worker')
|
||||
const reportPath = resolve(root, '.cache/toolchains/freecad-naming-sdk/chrome-candidate-worker-report.json')
|
||||
const chrome = process.env.CHROME_BIN || '/home/mes123456/.local/bin/google-chrome'
|
||||
const artifactNames = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm', 'bitbybit-occt-history.data']
|
||||
for (const name of artifactNames) if (!existsSync(resolve(candidateRoot, name))) throw new Error(`Missing candidate Worker artifact: ${name}`)
|
||||
if (!existsSync(chrome)) throw new Error(`Chrome executable is unavailable: ${chrome}`)
|
||||
|
||||
const artifacts = await Promise.all(artifactNames.map(async (name) => {
|
||||
const path = resolve(candidateRoot, name)
|
||||
const [bytes, content] = await Promise.all([stat(path).then(({ size }) => size), readFile(path)])
|
||||
return { name, bytes, sha256: createHash('sha256').update(content).digest('hex') }
|
||||
}))
|
||||
let resolveReport
|
||||
let rejectReport
|
||||
const reportPromise = new Promise((resolveValue, rejectValue) => { resolveReport = resolveValue; rejectReport = rejectValue })
|
||||
const html = `<!doctype html><meta charset="utf-8"><title>FreeCAD naming candidate Worker</title><script type="module">
|
||||
const send = async (report) => fetch('/__report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
||||
try {
|
||||
if (!crossOriginIsolated || typeof SharedArrayBuffer !== 'function') throw new Error('Chrome candidate harness requires cross-origin isolation and SharedArrayBuffer.')
|
||||
const createCandidate = (await import('/candidate/bitbybit-occt-history.js')).default
|
||||
const candidate = await createCandidate({ locateFile: (path) => new URL('/candidate/' + path, location.href).href })
|
||||
const callbacks = ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson']
|
||||
if (!callbacks.every((name) => typeof candidate[name] === 'function')) throw new Error('Candidate module omits a FreeCAD naming callback.')
|
||||
const descriptor = JSON.parse(candidate.freecadNamingCapabilitiesJson())
|
||||
if (candidate.freecadNamingAbiVersion() !== 1 || descriptor.freecadVersion !== '1.1.1' || descriptor.sourceCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') throw new Error('Candidate naming ABI descriptor is not locked.')
|
||||
const object = candidate.makeBox(10, 10, 10)
|
||||
const tool = candidate.makeBox(5, 5, 5)
|
||||
const objectStep = candidate.shapeToStep(object)
|
||||
const toolStep = candidate.shapeToStep(tool)
|
||||
const history = candidate.booleanHistoryFromStep(objectStep, toolStep, 'cut')
|
||||
const record = history.records.find((entry) => entry.relation !== 'deleted' && Number.isSafeInteger(entry.sourceIndex) && (Number.isSafeInteger(entry.resultIndex) || entry.resultIndexes?.some(Number.isSafeInteger)))
|
||||
if (!record) throw new Error('Chrome OCCT cut returned no usable history record.')
|
||||
const resultIndex = Number.isSafeInteger(record.resultIndex) ? record.resultIndex : record.resultIndexes.find(Number.isSafeInteger)
|
||||
const firstRequest = { schemaVersion: 1, requestId: 'chrome-candidate-1', documentId: 'chrome-candidate-document', documentVersion: 1, operationId: 'chrome-cut-1', operation: 'cut', stageId: 'chrome:stage:1', resultObjectId: 'chrome:result:1', resultObjectTag: 99, inputs: [{ inputId: 'object', objectId: 'source-object', role: 'object', step: objectStep, objectTag: 42 }, { inputId: 'tool', objectId: 'source-tool', role: 'tool', step: toolStep, objectTag: 43 }], stages: [{ stageId: 'chrome:stage:1', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }], resultStep: history.resultStep, history: { ...history, records: [{ ...record, resultIndex, resultIndexes: undefined }] } }
|
||||
const first = JSON.parse(candidate.freecadNamingEvidenceJson(JSON.stringify(firstRequest)))
|
||||
if (first.status !== 'native-evidence' || first.mappedNames?.length !== 1 || first.elementMap2?.maps?.length !== 1 || !Array.isArray(first.stringHasher?.entries)) throw new Error('Chrome first-stage naming evidence is incomplete.')
|
||||
const secondRequest = { ...firstRequest, requestId: 'chrome-candidate-2', documentVersion: 2, operationId: 'chrome-cut-2', stageId: 'chrome:stage:2', resultObjectId: 'chrome:result:2', resultObjectTag: 100, inputs: [{ inputId: 'object', objectId: 'chrome:result:1', role: 'object', stageId: 'chrome:stage:1', step: history.resultStep, objectTag: 99, namingEvidence: first }], stages: [{ stageId: 'chrome:stage:2', operation: 'cut', inputIds: ['object'], ordinal: 0 }], history: { ...history, records: [{ relation: 'modified', source: 'object', kind: record.resultKind || record.kind, sourceIndex: resultIndex, resultIndex: resultIndex + 1 }] } }
|
||||
const second = JSON.parse(candidate.freecadNamingEvidenceJson(JSON.stringify(secondRequest)))
|
||||
const reference = second.mappedNames?.[0]?.reference
|
||||
const hasherIds = new Set(second.stringHasher?.entries?.map((entry) => entry.id))
|
||||
const tokens = second.elementMap2?.maps?.flatMap((map) => map.sections.flatMap((section) => section.names.flatMap((name) => name.tokens))) ?? []
|
||||
if (second.status !== 'native-evidence' || !reference?.name?.startsWith('#') || !Number.isSafeInteger(reference.prefixStringId) || !reference.stringIds?.includes(reference.prefixStringId) || !hasherIds.has(reference.prefixStringId) || !tokens.some((token) => token.marker === '$' && token.name === reference.name)) throw new Error('Chrome chained MappedNameRef/StringHasher/ElementMap2 closure is invalid.')
|
||||
const thirdRequest = { ...secondRequest, requestId: 'chrome-candidate-3', documentVersion: 3, operationId: 'chrome-cut-3', stageId: 'chrome:stage:3', resultObjectId: 'chrome:result:3', resultObjectTag: 101, inputs: [{ inputId: 'object', objectId: 'chrome:result:2', role: 'object', stageId: 'chrome:stage:2', step: history.resultStep, objectTag: 100, namingEvidence: second }], stages: [{ stageId: 'chrome:stage:3', operation: 'cut', inputIds: ['object'], ordinal: 0 }], history: { ...history, records: [{ relation: 'modified', source: 'object', kind: record.resultKind || record.kind, sourceIndex: resultIndex + 1, resultIndex: resultIndex + 2 }] } }
|
||||
const third = JSON.parse(candidate.freecadNamingEvidenceJson(JSON.stringify(thirdRequest)))
|
||||
const thirdIds = new Set(third.stringHasher?.entries?.map((entry) => entry.id))
|
||||
const thirdReference = third.mappedNames?.[0]?.reference
|
||||
if (third.status !== 'native-evidence' || !thirdReference?.name?.startsWith('#') || !thirdReference.stringIds?.every((id) => thirdIds.has(id)) || third.stringHasher?.entries?.length <= second.stringHasher?.entries?.length) throw new Error('Chrome candidate did not restore non-empty StringHasher evidence across three stages.')
|
||||
const invalid = JSON.parse(candidate.freecadNamingEvidenceJson(JSON.stringify({ ...firstRequest, history: { ...history, records: [] } })))
|
||||
if (invalid.status !== 'error' || !invalid.error?.includes('requires inputs and native history records')) throw new Error('Chrome candidate did not fail closed for missing history.')
|
||||
await send({ schemaVersion: 1, status: 'pass', browserId: 'chrome', crossOriginIsolated, occtVersion: history.occtVersion, callbacks, firstStage: { mappedNames: first.mappedNames.length, stringHasherEntries: first.stringHasher.entries.length }, chainedStage: { mappedNames: second.mappedNames.length, stringHasherEntries: second.stringHasher.entries.length }, thirdStage: { mappedNames: third.mappedNames.length, stringHasherEntries: third.stringHasher.entries.length }, invalidHistoryRejected: true, candidateOnly: true, productionPublication: false, productionWorkerLinked: false })
|
||||
} catch (error) {
|
||||
await send({ schemaVersion: 1, status: 'failed', browserId: 'chrome', error: error instanceof Error ? error.stack || error.message : String(error), candidateOnly: true, productionPublication: false, productionWorkerLinked: false })
|
||||
}
|
||||
</script>`
|
||||
const contentTypes = { '.js': 'text/javascript', '.wasm': 'application/wasm', '.data': 'application/octet-stream' }
|
||||
const server = createServer((request, response) => {
|
||||
response.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
|
||||
response.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
|
||||
response.setHeader('Cross-Origin-Resource-Policy', 'same-origin')
|
||||
if (request.method === 'POST' && request.url === '/__report') {
|
||||
let body = ''
|
||||
request.setEncoding('utf8')
|
||||
request.on('data', (chunk) => { body += chunk })
|
||||
request.on('end', () => {
|
||||
try { resolveReport(JSON.parse(body)); response.writeHead(204); response.end() }
|
||||
catch (error) { rejectReport(error); response.writeHead(400); response.end(String(error)) }
|
||||
})
|
||||
return
|
||||
}
|
||||
if (request.url === '/' || request.url === '/index.html') { response.setHeader('content-type', 'text/html'); response.end(html); return }
|
||||
if (!request.url?.startsWith('/candidate/')) { response.writeHead(404); response.end('not found'); return }
|
||||
const file = normalize(join(candidateRoot, decodeURIComponent(request.url.slice('/candidate/'.length))))
|
||||
if (!file.startsWith(candidateRoot) || !existsSync(file)) { response.writeHead(404); response.end('not found'); return }
|
||||
response.setHeader('content-type', contentTypes[extname(file)] || 'application/octet-stream')
|
||||
createReadStream(file).pipe(response)
|
||||
})
|
||||
await new Promise((resolveServer) => server.listen(0, '127.0.0.1', resolveServer))
|
||||
const port = server.address().port
|
||||
const profile = await createChromeProfile('freecad-naming-candidate')
|
||||
const child = spawn(chrome, ['--headless=new', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--no-first-run', '--no-default-browser-check', `--user-data-dir=${profile}`, `http://127.0.0.1:${port}/`], { cwd: root, stdio: ['ignore', 'ignore', 'pipe'] })
|
||||
let stderr = ''
|
||||
child.stderr.on('data', (chunk) => { stderr += String(chunk) })
|
||||
const timeout = setTimeout(() => { rejectReport(new Error(`Chrome candidate Worker harness timed out. ${stderr.slice(-2000)}`)); child.kill('SIGTERM') }, 240_000)
|
||||
let report
|
||||
try {
|
||||
report = await reportPromise
|
||||
report = { ...report, artifacts, generatedAt: new Date().toISOString() }
|
||||
await mkdir(resolve(reportPath, '..'), { recursive: true })
|
||||
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
|
||||
console.log(JSON.stringify(report, null, 2))
|
||||
if (report.status !== 'pass') process.exitCode = 1
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
child.kill('SIGTERM')
|
||||
await new Promise((resolveServer) => server.close(resolveServer))
|
||||
await removeChromeProfile(profile)
|
||||
}
|
||||
27
scripts/run-freecad-attachment-mode-oracle.mjs
Normal file
27
scripts/run-freecad-attachment-mode-oracle.mjs
Normal file
@@ -0,0 +1,27 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd')
|
||||
if (!existsSync(executable)) throw new Error(`FreeCAD attachment mode oracle executable is missing: ${executable}`)
|
||||
const sysroot = resolve(root, '.cache/freecad/sysroot')
|
||||
const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), resolve(root, 'scripts/freecad-attachment-mode-oracle.py')], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
timeout: 180_000,
|
||||
maxBuffer: 20 * 1024 * 1024,
|
||||
env: {
|
||||
...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}` : ''}`,
|
||||
},
|
||||
})
|
||||
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
|
||||
const marker = 'FREECAD_ATTACHMENT_MODE_RESULT='
|
||||
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
|
||||
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD attachment mode oracle failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
|
||||
const report = JSON.parse(line.slice(line.indexOf(marker) + marker.length))
|
||||
await writeFile(resolve(root, 'config/freecad-attachment-mode-oracle.json'), `${JSON.stringify(report, null, 2)}\n`)
|
||||
console.log(JSON.stringify({ status: report.status, baselineId: report.baselineId, modes: report.registry.modeCount, engines: Object.fromEntries(Object.entries(report.engines).map(([name, engine]) => [name, engine.implementedModes.length])), successCases: Object.keys(report.success.initial).length, failureCases: Object.keys(report.failures.initial).length, fcstdRoundtrip: true }, null, 2))
|
||||
27
scripts/run-freecad-partdesign-structure-oracle.mjs
Normal file
27
scripts/run-freecad-partdesign-structure-oracle.mjs
Normal file
@@ -0,0 +1,27 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd')
|
||||
if (!existsSync(executable)) throw new Error(`FreeCAD PartDesign structure oracle executable is missing: ${executable}`)
|
||||
const sysroot = resolve(root, '.cache/freecad/sysroot')
|
||||
const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), resolve(root, 'scripts/freecad-partdesign-structure-oracle.py')], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
timeout: 180_000,
|
||||
maxBuffer: 20 * 1024 * 1024,
|
||||
env: {
|
||||
...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}` : ''}`,
|
||||
},
|
||||
})
|
||||
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
|
||||
const marker = 'FREECAD_PARTDESIGN_STRUCTURE_RESULT='
|
||||
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
|
||||
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PartDesign structure oracle failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
|
||||
const report = JSON.parse(line.slice(line.indexOf(marker) + marker.length))
|
||||
await writeFile(resolve(root, 'config/freecad-partdesign-structure-oracle.json'), `${JSON.stringify(report, null, 2)}\n`)
|
||||
console.log(JSON.stringify(report, null, 2))
|
||||
@@ -28,7 +28,22 @@ const chromeTests = Object.keys(scripts)
|
||||
.sort()
|
||||
|
||||
const wasmBuilds = process.env.CI_REAL_REBUILD_WASM === '1'
|
||||
? ['fetch:freecad-source', 'build:occt-history', 'build:planegcs']
|
||||
? [
|
||||
'fetch:freecad-source',
|
||||
'build:qt6-wasm-core',
|
||||
'build:qt6-freecad-wasm',
|
||||
'build:yaml-cpp-wasm',
|
||||
'build:icu-wasm',
|
||||
'build:cpython-wasm',
|
||||
'build:xerces-c-wasm',
|
||||
'build:boost-wasm',
|
||||
'build:freecad-naming-source-probe',
|
||||
'test:freecad-naming-source-probe',
|
||||
'check:freecad-naming-sdk-readiness',
|
||||
'check:freecad-wasm-sdk-build-plan',
|
||||
'build:occt-history',
|
||||
'build:planegcs',
|
||||
]
|
||||
: []
|
||||
if (wasmBuilds.length > 0) {
|
||||
for (const variable of ['OCCT_SOURCE_DIR', 'EMSDK']) {
|
||||
@@ -63,6 +78,8 @@ const lanes = {
|
||||
'probe:freecad-partdesign-loft',
|
||||
'probe:freecad-partdesign-dressup',
|
||||
'probe:freecad-partdesign-transform',
|
||||
'probe:freecad-partdesign-structure',
|
||||
'probe:freecad-attachment-modes',
|
||||
'probe:freecad-partdesign-failures',
|
||||
'probe:freecad-partdesign-revolution-groove',
|
||||
'probe:freecad-part-builders',
|
||||
|
||||
Reference in New Issue
Block a user