feat: add offline Qt6 naming prerequisites and boundary gates
Some checks failed
real-verification / chrome (push) Has been cancelled
real-verification / freecad-oracle (push) Has been cancelled
real-verification / wasm (push) Has been cancelled

This commit is contained in:
2026-08-12 08:17:44 -04:00
parent ca6fe46030
commit 58c0807219
22 changed files with 392 additions and 9 deletions

View File

@@ -0,0 +1,61 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
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"
SHIM_DIR="${ROOT_DIR}/native/freecad-naming-probe/shims"
FREECAD_COMMIT="0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
INDEXED_NAME_SHA256="73e0e60a9d6ee06851252e2071ebdd58698f8f99733903232529b3a194164435"
MAPPED_NAME_SHA256="90173aba5f331ac9453589833f7e5fc63c5c80a35f1600dbc71211606b1c9dbc"
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
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 -
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
fi
for path in \
"${QT6_WASM_DIR}/include/QtCore/QByteArray" \
"${QT6_WASM_DIR}/lib/libQt6Core.a" \
"${QT6_WASM_DIR}/lib/libQt6BundledPcre2.a" \
"${QT6_WASM_DIR}/lib/libQt6BundledZLIB.a"; do
if [[ ! -s "${path}" ]]; then
echo "Missing Qt6 wasm Core prerequisite: ${path}" >&2
exit 1
fi
done
if [[ ! -s "${OFFLINE_INCLUDE_DIR}/boost/signals2/signal.hpp" ]]; then
echo "Missing offline Boost headers: ${OFFLINE_INCLUDE_DIR}" >&2
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" \
-I"${SHIM_DIR}" \
-include "${SHIM_DIR}/App/StringHasher.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 \
-Wl,--start-group \
"${QT6_WASM_DIR}/lib/libQt6Core.a" \
"${QT6_WASM_DIR}/lib/libQt6BundledPcre2.a" \
"${QT6_WASM_DIR}/lib/libQt6BundledZLIB.a" \
-Wl,--end-group \
-sMODULARIZE=1 -sEXPORT_ES6=1 -sENVIRONMENT=web,worker,node \
-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"

96
scripts/build-qt6-wasm-core.sh Executable file
View File

@@ -0,0 +1,96 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
QT_VERSION="6.8.2"
QT_ARCHIVE="qtbase-everywhere-src-${QT_VERSION}.tar.xz"
QT_ARCHIVE_SHA256="012043ce6d411e6e8a91fdc4e05e6bedcfa10fcb1347d3c33908f7fdd10dfe05"
QT_CACHE_DIR="${QT6_WASM_CACHE_DIR:-${ROOT_DIR}/.cache/toolchains/qt6}"
QT_DOWNLOAD_DIR="${QT_CACHE_DIR}/downloads"
QT_SOURCE_DIR="${QT_CACHE_DIR}/src"
QT_BUILD_DIR="${QT_CACHE_DIR}/build-wasm"
QT_INSTALL_DIR="${QT_CACHE_DIR}/install-wasm"
EMSDK_SHIM_DIR="${QT_CACHE_DIR}/emsdk-shim"
JOBS="${JOBS:-$(nproc)}"
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 "Qt wasm Core must use the project-locked Emscripten 3.1.69 toolchain." >&2
exit 1
fi
mkdir -p "${QT_DOWNLOAD_DIR}" "${QT_CACHE_DIR}" "${EMSDK_SHIM_DIR}/upstream"
if [[ ! -f "${QT_DOWNLOAD_DIR}/${QT_ARCHIVE}" ]]; then
if [[ "${WEB_FREECAD_OFFLINE:-0}" == "1" ]]; then
echo "Missing offline Qt source archive: ${QT_DOWNLOAD_DIR}/${QT_ARCHIVE}" >&2
exit 1
fi
QT_ARCHIVE_PART="${QT_DOWNLOAD_DIR}/${QT_ARCHIVE}.part"
curl -fL --retry 3 --retry-delay 2 \
"https://mirrors.ustc.edu.cn/qtproject/archive/qt/6.8/${QT_VERSION}/submodules/${QT_ARCHIVE}" \
-o "${QT_ARCHIVE_PART}"
printf '%s %s\n' "${QT_ARCHIVE_SHA256}" "${QT_ARCHIVE_PART}" | sha256sum -c -
mv "${QT_ARCHIVE_PART}" "${QT_DOWNLOAD_DIR}/${QT_ARCHIVE}"
fi
printf '%s %s\n' "${QT_ARCHIVE_SHA256}" "${QT_DOWNLOAD_DIR}/${QT_ARCHIVE}" | sha256sum -c -
if [[ ! -x "${QT_SOURCE_DIR}/configure" ]]; then
mkdir -p "${QT_SOURCE_DIR}"
tar -xJf "${QT_DOWNLOAD_DIR}/${QT_ARCHIVE}" -C "${QT_SOURCE_DIR}" --strip-components=1
fi
printf '%s\n' "EMSCRIPTEN_ROOT = emsdk_path + '/upstream/emscripten'" > "${EMSDK_SHIM_DIR}/.emscripten"
ln -sfn /usr/share/emscripten "${EMSDK_SHIM_DIR}/upstream/emscripten"
ln -sfn "$(command -v emcc)" "${EMSDK_SHIM_DIR}/emcc"
ln -sfn "$(command -v em++)" "${EMSDK_SHIM_DIR}/em++"
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 \
-no-gui -no-widgets -no-dbus -no-feature-network -no-feature-thread \
-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
cmake --build "${QT_BUILD_DIR}" --target Core --parallel "${JOBS}"
cmake --install "${QT_BUILD_DIR}" --component Devel
cmake --install "${QT_BUILD_DIR}/src/corelib"
for path in \
"${QT_BUILD_DIR}/lib/libQt6Core.a" \
"${QT_BUILD_DIR}/lib/libQt6BundledPcre2.a" \
"${QT_BUILD_DIR}/lib/libQt6BundledZLIB.a"; do
if [[ ! -s "${path}" ]]; then
echo "Qt wasm Core output is missing: ${path}" >&2
exit 1
fi
cp -f "${path}" "${QT_INSTALL_DIR}/lib/$(basename "${path}")"
done
if [[ ! -s "${QT_INSTALL_DIR}/include/QtCore/QByteArray" ]]; then
echo "Qt wasm Core headers were not installed into ${QT_INSTALL_DIR}." >&2
exit 1
fi
sha256sum \
"${QT_DOWNLOAD_DIR}/${QT_ARCHIVE}" \
"${QT_INSTALL_DIR}/lib/libQt6Core.a" \
"${QT_INSTALL_DIR}/lib/libQt6BundledPcre2.a" \
"${QT_INSTALL_DIR}/lib/libQt6BundledZLIB.a"

View File

@@ -0,0 +1,31 @@
import { readFile } from 'node:fs/promises'
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([
load('config/freecad-sketcher-partdesign-abi-contract.json'),
load('config/compatibility-matrix.json'),
load('config/freecad-web-exact-parity-plan.json'),
])
const fail = (message) => { throw new Error(`FreeCAD private naming boundary: ${message}`) }
const tasks = plan.programs.flatMap((program) => program.tasks)
const task = tasks.find((entry) => entry.id === 'EX-TSN-02')
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 (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.')
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,
}, null, 2))

View File

@@ -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:occt-history-artifact', 'test:occt-history', 'test:planegcs', 'build']) {
for (const script of ['check:runtime', 'check:freecad-source', '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 })
}

View File

@@ -81,6 +81,7 @@ const lanes = {
wasm: [
...wasmBuilds,
'check:occt-history-artifact',
'check:freecad-private-naming-boundary',
'test:browser-occt',
'check:browser-occt',
'test:planegcs',