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:
1
config/boost-emscripten-user-config.jam
Normal file
1
config/boost-emscripten-user-config.jam
Normal file
@@ -0,0 +1 @@
|
||||
using emscripten : 3.1.69 : ./boost-emscripten-compiler-wrapper.sh ;
|
||||
33
config/cmake/boost/BoostConfig.cmake
Normal file
33
config/cmake/boost/BoostConfig.cmake
Normal file
@@ -0,0 +1,33 @@
|
||||
set(Boost_VERSION 108300)
|
||||
set(Boost_VERSION_STRING "1.83.0")
|
||||
set(Boost_INCLUDE_DIRS "${BOOST_WASM_ROOT}/include")
|
||||
set(Boost_INCLUDE_DIR "${Boost_INCLUDE_DIRS}")
|
||||
set(Boost_LIBRARIES "")
|
||||
|
||||
foreach(component IN LISTS Boost_FIND_COMPONENTS)
|
||||
string(TOLOWER "${component}" component_lower)
|
||||
set(archive "${BOOST_WASM_ROOT}/lib/libboost_${component_lower}.a")
|
||||
if(NOT EXISTS "${archive}")
|
||||
set(Boost_${component}_FOUND FALSE)
|
||||
set(Boost_FOUND FALSE)
|
||||
if(Boost_FIND_REQUIRED_${component})
|
||||
message(FATAL_ERROR "Missing Boost wasm component ${component}: ${archive}")
|
||||
endif()
|
||||
continue()
|
||||
endif()
|
||||
if(NOT TARGET Boost::${component})
|
||||
add_library(Boost::${component} STATIC IMPORTED)
|
||||
set_target_properties(Boost::${component} PROPERTIES
|
||||
IMPORTED_LOCATION "${archive}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${Boost_INCLUDE_DIRS}"
|
||||
)
|
||||
if(component_lower STREQUAL "thread")
|
||||
set_property(TARGET Boost::${component} APPEND PROPERTY INTERFACE_LINK_LIBRARIES
|
||||
"${BOOST_WASM_ROOT}/lib/libboost_atomic.a")
|
||||
endif()
|
||||
endif()
|
||||
set(Boost_${component}_FOUND TRUE)
|
||||
list(APPEND Boost_LIBRARIES Boost::${component})
|
||||
endforeach()
|
||||
|
||||
set(Boost_FOUND TRUE)
|
||||
9
config/cmake/boost/BoostConfigVersion.cmake
Normal file
9
config/cmake/boost/BoostConfigVersion.cmake
Normal file
@@ -0,0 +1,9 @@
|
||||
set(PACKAGE_VERSION "1.83.0")
|
||||
if(PACKAGE_FIND_VERSION VERSION_GREATER PACKAGE_VERSION)
|
||||
set(PACKAGE_VERSION_COMPATIBLE FALSE)
|
||||
else()
|
||||
set(PACKAGE_VERSION_COMPATIBLE TRUE)
|
||||
if(PACKAGE_FIND_VERSION VERSION_EQUAL PACKAGE_VERSION)
|
||||
set(PACKAGE_VERSION_EXACT TRUE)
|
||||
endif()
|
||||
endif()
|
||||
17
config/cmake/fmt/fmt-config.cmake
Normal file
17
config/cmake/fmt/fmt-config.cmake
Normal file
@@ -0,0 +1,17 @@
|
||||
if(NOT FREECAD_WASM_OFFLINE_SYSROOT)
|
||||
message(FATAL_ERROR "FREECAD_WASM_OFFLINE_SYSROOT is required by the candidate fmt package")
|
||||
endif()
|
||||
set(_freecad_wasm_fmt_include "${FREECAD_WASM_OFFLINE_SYSROOT}/fmt-10.1.1/include")
|
||||
if(NOT EXISTS "${_freecad_wasm_fmt_include}/fmt/format.h")
|
||||
message(FATAL_ERROR "Offline fmt headers are missing: ${_freecad_wasm_fmt_include}/fmt/format.h")
|
||||
endif()
|
||||
if(NOT TARGET fmt)
|
||||
add_library(fmt INTERFACE)
|
||||
target_include_directories(fmt INTERFACE "${_freecad_wasm_fmt_include}")
|
||||
target_compile_definitions(fmt INTERFACE FMT_HEADER_ONLY=1)
|
||||
endif()
|
||||
if(NOT TARGET fmt::fmt)
|
||||
add_library(fmt::fmt ALIAS fmt)
|
||||
endif()
|
||||
set(fmt_FOUND TRUE)
|
||||
set(fmt_VERSION 10.1.1)
|
||||
@@ -102,7 +102,8 @@
|
||||
},
|
||||
"freecadNamingBuild": {
|
||||
"status": "contract-only",
|
||||
"prerequisiteStatus": "qt6-wasm-core-and-locked-mapped-name-source-probe-pass",
|
||||
"prerequisiteStatus": "real-isolated-freecad-private-naming-bridge-pass",
|
||||
"sdkReadinessStatus": "candidate-complete",
|
||||
"sdkEnvironment": "FREECAD_WASM_SDK_DIR",
|
||||
"requiredManifest": "manifest.json",
|
||||
"requiredCallbacks": ["freecadNamingAbiVersion", "freecadNamingCapabilitiesJson", "freecadNamingEvidenceJson"],
|
||||
@@ -111,8 +112,21 @@
|
||||
"requiredEmscriptenVersion": "3.1.69",
|
||||
"buildCommand": "./npmw run build:freecad-naming-worker",
|
||||
"qtCoreBuildCommand": "./npmw run build:qt6-wasm-core",
|
||||
"qtFreecadSdkBuildCommand": "./npmw run build:qt6-freecad-wasm",
|
||||
"yamlCppBuildCommand": "./npmw run build:yaml-cpp-wasm",
|
||||
"icuBuildCommand": "./npmw run build:icu-wasm",
|
||||
"pythonBuildCommand": "./npmw run build:cpython-wasm",
|
||||
"xercesBuildCommand": "./npmw run build:xerces-c-wasm",
|
||||
"boostBuildCommand": "./npmw run build:boost-wasm",
|
||||
"sourceProbeBuildCommand": "./npmw run build:freecad-naming-source-probe",
|
||||
"sourceProbeTestCommand": "./npmw run test:freecad-naming-source-probe",
|
||||
"sdkReadinessCommand": "./npmw run check:freecad-naming-sdk-readiness",
|
||||
"sdkManifestCommand": "./npmw run generate:freecad-naming-sdk-manifest",
|
||||
"isolatedBridgeBuildCommand": "./npmw run build:freecad-naming-bridge-candidate",
|
||||
"isolatedBridgeTestCommand": "./npmw run test:freecad-naming-bridge-candidate",
|
||||
"candidateWorkerTestCommand": "./npmw run test:freecad-naming-worker-candidate",
|
||||
"candidateChromeRunCommand": "./npmw run test:chrome-freecad-naming-worker-candidate",
|
||||
"candidateChromeCheckCommand": "./npmw run check:chrome-freecad-naming-worker-candidate",
|
||||
"boundaryCheckCommand": "./npmw run check:freecad-private-naming-boundary",
|
||||
"checkCommand": "./npmw run check:freecad-naming-sdk",
|
||||
"statusBoundary": "Missing SDK or callback probe keeps EX-TSN-02=in_progress and systemExact=false"
|
||||
|
||||
13
config/emscripten-freecad-candidate-toolchain.cmake
Normal file
13
config/emscripten-freecad-candidate-toolchain.cmake
Normal file
@@ -0,0 +1,13 @@
|
||||
# Candidate-only wrapper around the pinned Emscripten 3.1.69 platform file.
|
||||
include("/usr/share/emscripten/cmake/Modules/Platform/Emscripten.cmake")
|
||||
|
||||
# FreeCAD declares Base/App/Part as SHARED. For this candidate, collect their
|
||||
# wasm object files as static archives and link shared dependencies separately.
|
||||
set(CMAKE_SHARED_LIBRARY_SUFFIX ".a")
|
||||
set(CMAKE_C_CREATE_SHARED_LIBRARY "<CMAKE_AR> rcs <TARGET> <OBJECTS>")
|
||||
set(CMAKE_CXX_CREATE_SHARED_LIBRARY "<CMAKE_AR> rcs <TARGET> <OBJECTS>")
|
||||
|
||||
# FreeCAD's native compiler setup uses the CMake UNIX boolean to add
|
||||
# --undefined,dynamic_lookup. Emscripten emits SHARED as a relocatable object,
|
||||
# so that native-only linker branch must stay disabled for this candidate.
|
||||
set(UNIX FALSE)
|
||||
2340
config/freecad-attachment-mode-oracle.json
Normal file
2340
config/freecad-attachment-mode-oracle.json
Normal file
File diff suppressed because it is too large
Load Diff
87
config/freecad-naming-next-tasks.json
Normal file
87
config/freecad-naming-next-tasks.json
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"scope": "freecad-private-naming-and-parameter-followup",
|
||||
"orderedTasks": [
|
||||
{
|
||||
"id": "SDK-01",
|
||||
"title": "Lock candidate FreeCAD wasm SDK baseline and build boundary",
|
||||
"status": "completed",
|
||||
"dependencies": [],
|
||||
"deliverables": ["Pinned source/toolchain plan", "Candidate-only build-plan checker"],
|
||||
"acceptance": ["FreeCAD commit and Emscripten version are verified", "Production publication remains false"],
|
||||
"evidence": ["check:freecad-wasm-sdk-build-plan"]
|
||||
},
|
||||
{
|
||||
"id": "SDK-02A",
|
||||
"title": "Build the Qt wasm modules required by FreeCAD",
|
||||
"status": "completed",
|
||||
"dependencies": ["SDK-01"],
|
||||
"deliverables": ["Qt Core/Concurrent/Network/Xml archives", "Qt bundled Pcre2/ZLIB archives"],
|
||||
"acceptance": ["Every archive contains only wasm32 objects", "Thread support is enabled for QtConcurrent"],
|
||||
"evidence": ["check:freecad-naming-sdk-readiness"]
|
||||
},
|
||||
{
|
||||
"id": "SDK-02B",
|
||||
"title": "Build remaining wasm dependencies and FreeCADBase/App/Part archives",
|
||||
"status": "completed",
|
||||
"dependencies": ["SDK-02A"],
|
||||
"deliverables": ["Python/ICU/Xerces/yaml-cpp/fmt wasm dependencies", "FreeCADBase/FreeCADApp/Part static archives"],
|
||||
"acceptance": ["No host ELF member is present", "CMake configuration performs no implicit network fetch"],
|
||||
"evidence": ["build:freecad-naming-sdk-candidate", "check:freecad-naming-sdk-readiness"]
|
||||
},
|
||||
{
|
||||
"id": "SDK-04",
|
||||
"title": "Implement an isolated real FreeCAD private naming bridge",
|
||||
"status": "completed",
|
||||
"dependencies": ["SDK-02B"],
|
||||
"deliverables": ["Three versioned callback implementations", "MappedNameRef/StringHasher/ElementMap2 runtime evidence"],
|
||||
"acceptance": ["Candidate bridge passes strict Web validation", "Candidate is not published to the production Worker"],
|
||||
"evidence": ["build:freecad-naming-bridge-candidate", "test:freecad-naming-bridge-candidate", "check:freecad-naming-bridge-candidate", "check:freecad-private-naming-boundary"]
|
||||
},
|
||||
{
|
||||
"id": "SDK-03",
|
||||
"title": "Generate and verify the complete SDK manifest",
|
||||
"status": "completed",
|
||||
"dependencies": ["SDK-02B", "SDK-04"],
|
||||
"deliverables": ["Hashed manifest", "Private header and symbol audit"],
|
||||
"acceptance": ["Manifest generator completes without missing inputs", "Complete SDK checker passes"],
|
||||
"evidence": ["generate:freecad-naming-sdk-manifest", "check:freecad-naming-sdk"]
|
||||
},
|
||||
{
|
||||
"id": "SDK-05",
|
||||
"title": "Link and probe a candidate OCCT Worker",
|
||||
"status": "completed",
|
||||
"dependencies": ["SDK-03", "SDK-04"],
|
||||
"deliverables": ["Candidate Worker artifact", "Real Chrome callback and builder-history evidence"],
|
||||
"acceptance": ["All three callbacks are exported together", "Invalid or incomplete evidence fails closed"],
|
||||
"evidence": ["build:freecad-naming-worker", "test:freecad-naming-worker-candidate", "test:chrome-freecad-naming-worker-candidate", "check:chrome-freecad-naming-worker-candidate"]
|
||||
},
|
||||
{
|
||||
"id": "PAR-01",
|
||||
"title": "Expand Datum/Attachment/ShapeBinder, multi-Body and parameter combinations",
|
||||
"status": "in_progress",
|
||||
"dependencies": ["SDK-05"],
|
||||
"deliverables": ["Native oracle corpus", "Edit/recompute/recovery/FCStd round-trip matrix"],
|
||||
"acceptance": ["Every family has success and failure evidence", "Support/Tip/references survive mutation without silent rebinding"],
|
||||
"evidence": ["check:freecad-sketcher-partdesign-abi", "check:freecad-partdesign-structure", "check:freecad-attachment-modes", "check:chrome-partdesign-lifecycle"]
|
||||
},
|
||||
{
|
||||
"id": "QA-01",
|
||||
"title": "Close CI, offline-resource and browser matrix for the new production path",
|
||||
"status": "pending",
|
||||
"dependencies": ["PAR-01"],
|
||||
"deliverables": ["Execute/check-separated CI lanes", "Offline SDK resources", "Chrome/Firefox/WebKit reports"],
|
||||
"acceptance": ["Static reports cannot substitute for live harnesses", "A clean offline restore can rebuild and verify the candidate"],
|
||||
"evidence": ["ci:real:chrome", "ci:check:chrome", "offline:smoke", "check:browser-matrix"]
|
||||
}
|
||||
],
|
||||
"boundary": {
|
||||
"freecadNamingBuildStatus": "contract-only",
|
||||
"exTsn02": "in_progress",
|
||||
"systemExact": false,
|
||||
"productionWorker": {
|
||||
"availability": "unavailable",
|
||||
"callbacks": []
|
||||
}
|
||||
}
|
||||
}
|
||||
277
config/freecad-naming-sdk-plan.json
Normal file
277
config/freecad-naming-sdk-plan.json
Normal file
@@ -0,0 +1,277 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"scope": "pre-production-sdk-readiness",
|
||||
"baseline": {
|
||||
"freecadVersion": "1.1.1",
|
||||
"sourceCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d",
|
||||
"emscriptenVersion": "3.1.69",
|
||||
"qtVersion": "6.8.2",
|
||||
"target": "wasm32-emscripten"
|
||||
},
|
||||
"includeDirs": [
|
||||
{
|
||||
"name": "FreeCAD",
|
||||
"path": ".cache/toolchains/freecad-naming-sdk/source-occt8/src",
|
||||
"requiredHeaders": [
|
||||
"App/StringHasher.h",
|
||||
"App/MappedName.h",
|
||||
"App/MappedElement.h",
|
||||
"App/ElementMap.h"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "FreeCADBuild",
|
||||
"path": ".cache/toolchains/freecad-naming-sdk/build-wasm/src",
|
||||
"requiredHeaders": ["Base/PersistencePy.h", "Build/Version.h"]
|
||||
},
|
||||
{
|
||||
"name": "PyCXX",
|
||||
"path": ".cache/toolchains/freecad-naming-sdk/source-occt8/src/3rdParty/PyCXX",
|
||||
"requiredHeaders": ["CXX/Objects.hxx", "CXX/Extensions.hxx"]
|
||||
},
|
||||
{
|
||||
"name": "OCCT",
|
||||
"path": ".cache/bitbybit/occt-history-build-offline/include/opencascade",
|
||||
"requiredHeaders": ["Standard.hxx", "TopoDS_Shape.hxx"]
|
||||
},
|
||||
{
|
||||
"name": "OCCTDeprecatedAliases",
|
||||
"path": ".cache/occt/occt/src/Deprecated/NCollectionAliases",
|
||||
"requiredHeaders": ["TopTools_ListOfShape.hxx", "TopTools_HSequenceOfShape.hxx"]
|
||||
},
|
||||
{
|
||||
"name": "QtCore",
|
||||
"path": ".cache/toolchains/qt6/install-wasm-freecad/include",
|
||||
"requiredHeaders": [
|
||||
"QtCore/QByteArray",
|
||||
"QtCore/QJsonDocument",
|
||||
"QtConcurrent/QtConcurrent",
|
||||
"QtNetwork/QNetworkAccessManager",
|
||||
"QtXml/QDomDocument"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "QtCoreModule",
|
||||
"path": ".cache/toolchains/qt6/install-wasm-freecad/include/QtCore",
|
||||
"requiredHeaders": ["QString", "QJsonDocument", "QProcessEnvironment"]
|
||||
},
|
||||
{
|
||||
"name": "Python",
|
||||
"path": ".cache/toolchains/python/install-wasm/include/python3.13",
|
||||
"requiredHeaders": ["Python.h", "pyconfig.h"]
|
||||
},
|
||||
{
|
||||
"name": "XercesC",
|
||||
"path": ".cache/toolchains/xerces-c/install-wasm/include",
|
||||
"requiredHeaders": ["xercesc/util/XercesVersion.hpp"]
|
||||
},
|
||||
{
|
||||
"name": "ICU",
|
||||
"path": ".cache/toolchains/icu/install-wasm/include",
|
||||
"requiredHeaders": ["unicode/uversion.h"]
|
||||
},
|
||||
{
|
||||
"name": "YamlCpp",
|
||||
"path": ".cache/toolchains/yaml-cpp/install-wasm/include",
|
||||
"requiredHeaders": ["yaml-cpp/yaml.h"]
|
||||
},
|
||||
{
|
||||
"name": "OfflineSysroot",
|
||||
"path": ".cache/offline-sysroot/include",
|
||||
"requiredHeaders": ["boost/signals2.hpp", "fmt/format.h", "eigen3/Eigen/Core"]
|
||||
}
|
||||
],
|
||||
"libraries": [
|
||||
{
|
||||
"name": "FreeCADBase",
|
||||
"path": ".cache/toolchains/freecad-naming-sdk/lib/libFreeCADBase.a",
|
||||
"required": true,
|
||||
"expectedSha256": "df02d5527285750a5a34012bb04a177e97719290f4704e8a20110207367698d6"
|
||||
},
|
||||
{
|
||||
"name": "FreeCADApp",
|
||||
"path": ".cache/toolchains/freecad-naming-sdk/lib/libFreeCADApp.a",
|
||||
"required": true,
|
||||
"expectedSha256": "b81c3cb67934b6add3a810a0885baeb9b459cf370b460cb3906d1b20fbe0a500"
|
||||
},
|
||||
{
|
||||
"name": "Part",
|
||||
"path": ".cache/toolchains/freecad-naming-sdk/lib/libPart.a",
|
||||
"required": true,
|
||||
"expectedSha256": "a5d0f1eabd8a3e7e3ad37807ca7cab90810d2fcfe50c2956020485a37bf27d69"
|
||||
},
|
||||
{
|
||||
"name": "QtCore",
|
||||
"path": ".cache/toolchains/qt6/install-wasm-freecad/lib/libQt6Core.a",
|
||||
"required": true,
|
||||
"expectedSha256": "7cf42203ae54fc4706a5dba69a179b340d1a5bf27ce9db881f11d57c66fd3828"
|
||||
},
|
||||
{
|
||||
"name": "Python",
|
||||
"path": ".cache/toolchains/python/install-wasm/lib/libpython3.13.a",
|
||||
"required": true,
|
||||
"expectedSha256": "2e332bc1141eeea51c94510ecc8f237e1f50ff9b3bc03df9cfc6142cd0489bd0"
|
||||
}
|
||||
],
|
||||
"linkDependencies": [
|
||||
{
|
||||
"name": "QtConcurrent",
|
||||
"path": ".cache/toolchains/qt6/install-wasm-freecad/lib/libQt6Concurrent.a",
|
||||
"required": true,
|
||||
"expectedSha256": "52a8ddbc72a420a7cd93593328f2e252cfd696a37b878c623f0ecfe5cb5ade81"
|
||||
},
|
||||
{
|
||||
"name": "QtNetwork",
|
||||
"path": ".cache/toolchains/qt6/install-wasm-freecad/lib/libQt6Network.a",
|
||||
"required": true,
|
||||
"expectedSha256": "1c06acc8b5a18447951b3a53f36f555d80544d804eb9ff920fcac20d9ced019c"
|
||||
},
|
||||
{
|
||||
"name": "QtXml",
|
||||
"path": ".cache/toolchains/qt6/install-wasm-freecad/lib/libQt6Xml.a",
|
||||
"required": true,
|
||||
"expectedSha256": "8694057ac1ac468b3cd40da3bd16312662e434ba4b485780eb7a31ea44381f88"
|
||||
},
|
||||
{
|
||||
"name": "QtBundledPcre2",
|
||||
"path": ".cache/toolchains/qt6/install-wasm-freecad/lib/libQt6BundledPcre2.a",
|
||||
"required": true,
|
||||
"expectedSha256": "9dc72f77f02b13ae7f7de4bcd579a6f04acd6d72cd76d348db1cbfaf6d6d777b"
|
||||
},
|
||||
{
|
||||
"name": "QtBundledZLIB",
|
||||
"path": ".cache/toolchains/qt6/install-wasm-freecad/lib/libQt6BundledZLIB.a",
|
||||
"required": true,
|
||||
"expectedSha256": "bf3c4d7a52c706abec70d83c94e32f0bf920e978aba9bb39fd87f94efcd2201f"
|
||||
},
|
||||
{
|
||||
"name": "yaml-cpp",
|
||||
"path": ".cache/toolchains/yaml-cpp/install-wasm/lib/libyaml-cpp.a",
|
||||
"required": true,
|
||||
"expectedSha256": "213acb389e7e7ff6677e8170b8651a5285af1f8c5c8bf1dfc42591559f253fe5"
|
||||
},
|
||||
{
|
||||
"name": "ICUCommon",
|
||||
"path": ".cache/toolchains/icu/install-wasm/lib/libicuuc.a",
|
||||
"required": true,
|
||||
"expectedSha256": "3cbe58215f7dc0596dacea9e8ac10974e8b8302b9472ff854f35e2e8c21fe7a2"
|
||||
},
|
||||
{
|
||||
"name": "ICUI18N",
|
||||
"path": ".cache/toolchains/icu/install-wasm/lib/libicui18n.a",
|
||||
"required": true,
|
||||
"expectedSha256": "627169cfa4637fe02c5cd5d8358e3faf92f933191049f80f3381c3ab7d7719d7"
|
||||
},
|
||||
{
|
||||
"name": "ICUData",
|
||||
"path": ".cache/toolchains/icu/install-wasm/lib/libicudata.a",
|
||||
"required": true,
|
||||
"expectedSha256": "b146f462e94d061862c27bb11fed116a1e4678f172f6e7551f3a57fedfd17822"
|
||||
},
|
||||
{
|
||||
"name": "XercesC",
|
||||
"path": ".cache/toolchains/xerces-c/install-wasm/lib/libxerces-c.a",
|
||||
"required": true,
|
||||
"expectedSha256": "86fe0646cd7d2cb528708627527ce32cd963cf6b2510178f36c96c0ffa0b89ab"
|
||||
},
|
||||
{
|
||||
"name": "BoostProgramOptions",
|
||||
"path": ".cache/toolchains/boost/install-wasm/lib/libboost_program_options.a",
|
||||
"required": true,
|
||||
"expectedSha256": "5c900c12f5323122d08b696ab095bcfe1b4b636c5a7ccb353962c4c956e11a24"
|
||||
},
|
||||
{
|
||||
"name": "BoostRegex",
|
||||
"path": ".cache/toolchains/boost/install-wasm/lib/libboost_regex.a",
|
||||
"required": true,
|
||||
"expectedSha256": "8fa066fba93dcb06a7e71f21338b288598a1fc4880d8799aaa4934c2239f1f3f"
|
||||
},
|
||||
{
|
||||
"name": "BoostThread",
|
||||
"path": ".cache/toolchains/boost/install-wasm/lib/libboost_thread.a",
|
||||
"required": true,
|
||||
"expectedSha256": "584dbdd2a186ec905d390b62ff3d24c81e4fb18d6b7f101e136ac760e8e4927c"
|
||||
},
|
||||
{
|
||||
"name": "BoostDateTime",
|
||||
"path": ".cache/toolchains/boost/install-wasm/lib/libboost_date_time.a",
|
||||
"required": true,
|
||||
"expectedSha256": "859ed6fa84a44e4dc15dd95b5306d4b2f7c37d4057aa3301edf9804edb83bf29"
|
||||
},
|
||||
{
|
||||
"name": "BoostAtomic",
|
||||
"path": ".cache/toolchains/boost/install-wasm/lib/libboost_atomic.a",
|
||||
"required": true,
|
||||
"expectedSha256": "e7e7b22bb8b380f2ea2ac083136f040908c7f9abcb758dbebf729ec23dc92432"
|
||||
},
|
||||
{
|
||||
"name": "PythonMpdecimal",
|
||||
"path": ".cache/toolchains/python/install-wasm/lib/python-deps/libmpdec.a",
|
||||
"required": true,
|
||||
"expectedSha256": "d69dac9933119c1eafb9d9a1f5e5a81c7e8ef51f0367c55b5349b2b014a07849"
|
||||
},
|
||||
{
|
||||
"name": "PythonExpat",
|
||||
"path": ".cache/toolchains/python/install-wasm/lib/python-deps/libexpat.a",
|
||||
"required": true,
|
||||
"expectedSha256": "6bfc695a31ee252a836e60ba34192dcf047ee4c7fe6a7b68c527e5fbcb714b66"
|
||||
},
|
||||
{
|
||||
"name": "PythonHaclSha2",
|
||||
"path": ".cache/toolchains/python/install-wasm/lib/python-deps/libHacl_Hash_SHA2.a",
|
||||
"required": true,
|
||||
"expectedSha256": "1e622af13d53951c4992f74d2af211dd75c59ec14f2a9d3a3eb3c39b2d9899a4"
|
||||
},
|
||||
{
|
||||
"name": "PythonZlib",
|
||||
"path": ".cache/toolchains/python/install-wasm/lib/python-deps/libz.a",
|
||||
"required": true,
|
||||
"expectedSha256": "42acf281029e51e07d468d2f323055c7c5558e5d6d78738ec1e1571acd4afd67"
|
||||
},
|
||||
{
|
||||
"name": "PythonBzip2",
|
||||
"path": ".cache/toolchains/python/install-wasm/lib/python-deps/libbz2.a",
|
||||
"required": true,
|
||||
"expectedSha256": "991ac951c995d3606ebedc74bc070273005ac9ebff26a7cc38518702d5268de9"
|
||||
},
|
||||
{
|
||||
"name": "PythonSqlite3",
|
||||
"path": ".cache/toolchains/python/install-wasm/lib/python-deps/libsqlite3-mt.a",
|
||||
"required": true,
|
||||
"expectedSha256": "738c819dab8f31a97fc7c1104f73969fee9535222b67fd938450474f7b861fd6"
|
||||
}
|
||||
],
|
||||
"namingBridge": {
|
||||
"path": "native/freecad-naming-bridge/freecad_naming_bridge.cpp",
|
||||
"hostAdapter": "native/freecad-naming-bridge/pre.js",
|
||||
"requiredExports": [
|
||||
"freecadNamingAbiVersion",
|
||||
"freecadNamingCapabilitiesJson",
|
||||
"freecadNamingEvidenceJson"
|
||||
]
|
||||
},
|
||||
"compileOptions": {
|
||||
"cxxStandard": "c++20",
|
||||
"pthread": true,
|
||||
"forceInclude": "config/freecad-wasm-sdk-compat.h",
|
||||
"definitions": ["__linux__=1", "QT_NO_KEYWORDS", "HAVE_CONFIG_H", "PYCXX_6_2_COMPATIBILITY"]
|
||||
},
|
||||
"runtimeAssets": [
|
||||
{
|
||||
"name": "PythonWasmStdlib",
|
||||
"path": ".cache/toolchains/python/install-wasm/share/python-wasm-stdlib",
|
||||
"preloadTo": "/usr/local",
|
||||
"requiredFiles": ["lib/python313.zip", "lib/python3.13/os.py"],
|
||||
"expectedSha256": {
|
||||
"lib/python313.zip": "e3fa6e7ceaf68a2f433a682713ac05288631b3c465114284d49e02e094bb0c12",
|
||||
"lib/python3.13/os.py": "b6b68783d438ff044c096fd0c3d1dfc9479a9b632e0a450a376be6c01655aafe"
|
||||
}
|
||||
}
|
||||
],
|
||||
"productionPublication": false,
|
||||
"boundary": {
|
||||
"freecadNamingBuildStatus": "contract-only",
|
||||
"exTsn02": "in_progress",
|
||||
"systemExact": false
|
||||
}
|
||||
}
|
||||
346
config/freecad-partdesign-structure-oracle.json
Normal file
346
config/freecad-partdesign-structure-oracle.json
Normal file
@@ -0,0 +1,346 @@
|
||||
{
|
||||
"baselineId": "freecad-1.1.1-partdesign-structure-oracle",
|
||||
"crossDocument": {
|
||||
"deleted": {
|
||||
"shapeBinderShapeNull": true,
|
||||
"shapeBinderState": [
|
||||
"Up-to-date"
|
||||
],
|
||||
"shapeBinderStatus": "Valid",
|
||||
"shapeBinderSupport": [],
|
||||
"subShapeBinderCachedArea": 48,
|
||||
"subShapeBinderShapeNull": false,
|
||||
"subShapeBinderState": [
|
||||
"Up-to-date"
|
||||
],
|
||||
"subShapeBinderStatus": "Valid",
|
||||
"subShapeBinderSupport": []
|
||||
},
|
||||
"edited": {
|
||||
"shapeBinderShapeNull": true,
|
||||
"shapeBinderState": [
|
||||
"Up-to-date"
|
||||
],
|
||||
"sourceFaceArea": 48,
|
||||
"sourceVolume": 192,
|
||||
"subShapeBinderArea": 48,
|
||||
"subShapeBinderState": [
|
||||
"Up-to-date"
|
||||
]
|
||||
},
|
||||
"initial": {
|
||||
"shapeBinderExternalLink": {
|
||||
"accepted": false,
|
||||
"error": "PropertyLinkSubList does not support external object",
|
||||
"errorType": "ValueError"
|
||||
},
|
||||
"shapeBinderShapeNull": true,
|
||||
"shapeBinderSupport": [],
|
||||
"shapeBinderSupportType": "App::PropertyLinkSubListGlobal",
|
||||
"shapeBodyTip": null,
|
||||
"sourceFaceArea": 30,
|
||||
"sourceVolume": 120,
|
||||
"subBodyTip": null,
|
||||
"subShapeBinderArea": 30,
|
||||
"subShapeBinderSourceDocument": "PartDesignExternalSource",
|
||||
"subShapeBinderSupport": [
|
||||
{
|
||||
"object": "ExternalBox",
|
||||
"subElements": [
|
||||
"Face1"
|
||||
]
|
||||
}
|
||||
],
|
||||
"subShapeBinderSupportType": "App::PropertyXLinkSubList"
|
||||
},
|
||||
"roundtrip": {
|
||||
"shapeBinderShapeNull": true,
|
||||
"shapeBinderSupport": [],
|
||||
"shapeBodyTip": null,
|
||||
"sourceFaceArea": 30,
|
||||
"sourceVolume": 120,
|
||||
"subBodyTip": null,
|
||||
"subShapeBinderArea": 30,
|
||||
"subShapeBinderSourceDocument": "PartDesignExternalSource",
|
||||
"subShapeBinderSupport": [
|
||||
{
|
||||
"object": "ExternalBox",
|
||||
"subElements": [
|
||||
"Face1"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"edited": {
|
||||
"bodyTips": {
|
||||
"BinderBody": null,
|
||||
"DatumBody": null,
|
||||
"SourceBody": "SourceBox",
|
||||
"SubBinderBody": null
|
||||
},
|
||||
"datumPlaneAttachmentOffset": {
|
||||
"angleDegrees": 45,
|
||||
"axis": [
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"position": [
|
||||
0,
|
||||
0,
|
||||
7
|
||||
]
|
||||
},
|
||||
"datumPlanePlacement": {
|
||||
"angleDegrees": 45.00000000000001,
|
||||
"axis": [
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"position": [
|
||||
0,
|
||||
0,
|
||||
7
|
||||
]
|
||||
},
|
||||
"shapeBinderVolume": 672,
|
||||
"sourceVolume": 672,
|
||||
"subShapeBinderLength": 28
|
||||
},
|
||||
"freecadVersion": "1.1.1",
|
||||
"gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d",
|
||||
"initial": {
|
||||
"bodyGroups": {
|
||||
"BinderBody": [
|
||||
"ShapeBinder"
|
||||
],
|
||||
"DatumBody": [
|
||||
"DatumPlane",
|
||||
"DatumLine",
|
||||
"DatumPoint"
|
||||
],
|
||||
"SourceBody": [
|
||||
"SourceBox"
|
||||
],
|
||||
"SubBinderBody": [
|
||||
"SubShapeBinder"
|
||||
]
|
||||
},
|
||||
"bodyTips": {
|
||||
"BinderBody": null,
|
||||
"DatumBody": null,
|
||||
"SourceBody": "SourceBox",
|
||||
"SubBinderBody": null
|
||||
},
|
||||
"datumLine": {
|
||||
"mapMode": "ObjectX",
|
||||
"state": [
|
||||
"Up-to-date"
|
||||
],
|
||||
"support": [
|
||||
{
|
||||
"object": "XY_Plane",
|
||||
"subElements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"datumPlane": {
|
||||
"attachmentOffset": {
|
||||
"angleDegrees": 29.999999999999996,
|
||||
"axis": [
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"position": [
|
||||
0,
|
||||
0,
|
||||
5
|
||||
]
|
||||
},
|
||||
"mapMode": "FlatFace",
|
||||
"placement": {
|
||||
"angleDegrees": 29.999999999999993,
|
||||
"axis": [
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"position": [
|
||||
0,
|
||||
0,
|
||||
5
|
||||
]
|
||||
},
|
||||
"state": [
|
||||
"Up-to-date"
|
||||
],
|
||||
"support": [
|
||||
{
|
||||
"object": "XY_Plane",
|
||||
"subElements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"datumPoint": {
|
||||
"attachmentOffset": {
|
||||
"angleDegrees": 0,
|
||||
"axis": [
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"position": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
},
|
||||
"mapMode": "ObjectOrigin",
|
||||
"placement": {
|
||||
"angleDegrees": 0,
|
||||
"axis": [
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"position": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
},
|
||||
"state": [
|
||||
"Up-to-date"
|
||||
],
|
||||
"support": [
|
||||
{
|
||||
"object": "XY_Plane",
|
||||
"subElements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"shapeBinder": {
|
||||
"state": [
|
||||
"Up-to-date"
|
||||
],
|
||||
"support": [
|
||||
{
|
||||
"object": "SourceBox",
|
||||
"subElements": []
|
||||
}
|
||||
],
|
||||
"traceSupport": true
|
||||
},
|
||||
"shapeBinderVolume": 480,
|
||||
"sourceVolume": 480,
|
||||
"subShapeBinder": {
|
||||
"state": [
|
||||
"Up-to-date"
|
||||
],
|
||||
"support": [
|
||||
{
|
||||
"object": "SourceBox",
|
||||
"subElements": [
|
||||
"Edge1",
|
||||
"Edge2",
|
||||
"Edge3",
|
||||
"Edge4"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"subShapeBinderLength": 28
|
||||
},
|
||||
"invalidSupport": {
|
||||
"state": [
|
||||
"Touched",
|
||||
"Invalid"
|
||||
],
|
||||
"status": "AttachEngine3D: subshape not found SourceBox.Face999",
|
||||
"support": [
|
||||
{
|
||||
"object": "SourceBox",
|
||||
"subElements": [
|
||||
"Face999"
|
||||
]
|
||||
}
|
||||
],
|
||||
"typeId": "PartDesign::Plane"
|
||||
},
|
||||
"roundtrip": {
|
||||
"bodyGroups": {
|
||||
"BinderBody": [
|
||||
"ShapeBinder"
|
||||
],
|
||||
"DatumBody": [
|
||||
"DatumPlane",
|
||||
"DatumLine",
|
||||
"DatumPoint",
|
||||
"InvalidDatumPlane"
|
||||
],
|
||||
"SourceBody": [
|
||||
"SourceBox"
|
||||
],
|
||||
"SubBinderBody": [
|
||||
"SubShapeBinder"
|
||||
]
|
||||
},
|
||||
"bodyTips": {
|
||||
"BinderBody": null,
|
||||
"DatumBody": null,
|
||||
"SourceBody": "SourceBox",
|
||||
"SubBinderBody": null
|
||||
},
|
||||
"datumPlaneAttachmentOffset": {
|
||||
"angleDegrees": 45,
|
||||
"axis": [
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"position": [
|
||||
0,
|
||||
0,
|
||||
7
|
||||
]
|
||||
},
|
||||
"datumPlaneMapMode": "FlatFace",
|
||||
"datumPlaneSupport": [
|
||||
{
|
||||
"object": "XY_Plane",
|
||||
"subElements": []
|
||||
}
|
||||
],
|
||||
"invalidDatumState": [
|
||||
"Touched",
|
||||
"Invalid"
|
||||
],
|
||||
"invalidDatumStatus": "AttachEngine3D: subshape not found SourceBox.Face999",
|
||||
"shapeBinderSupport": [
|
||||
{
|
||||
"object": "SourceBox",
|
||||
"subElements": []
|
||||
}
|
||||
],
|
||||
"shapeBinderVolume": 672,
|
||||
"sourceVolume": 672,
|
||||
"subShapeBinderLength": 28,
|
||||
"subShapeBinderSupport": [
|
||||
{
|
||||
"object": "SourceBox",
|
||||
"subElements": [
|
||||
"Edge1",
|
||||
"Edge2",
|
||||
"Edge3",
|
||||
"Edge4"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"schemaVersion": 1,
|
||||
"status": "pass",
|
||||
"tolerance": 1e-7
|
||||
}
|
||||
106
config/freecad-private-naming-source-readiness.json
Normal file
106
config/freecad-private-naming-source-readiness.json
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"scope": "standalone-wasm-source-prerequisite",
|
||||
"baseline": {
|
||||
"freecadVersion": "1.1.1",
|
||||
"sourceCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d",
|
||||
"emscriptenVersion": "3.1.69",
|
||||
"qtVersion": "6.8.2"
|
||||
},
|
||||
"linkedOriginalSources": [
|
||||
{
|
||||
"path": "src/App/IndexedName.cpp",
|
||||
"sha256": "73e0e60a9d6ee06851252e2071ebdd58698f8f99733903232529b3a194164435"
|
||||
},
|
||||
{
|
||||
"path": "src/App/MappedName.cpp",
|
||||
"sha256": "90173aba5f331ac9453589833f7e5fc63c5c80a35f1600dbc71211606b1c9dbc"
|
||||
},
|
||||
{
|
||||
"path": "src/App/StringHasher.cpp",
|
||||
"sha256": "fd32f35c9c2a0c21a60fa634ec37ec594d6f331924c5bba7fdbbcb351c3b612c"
|
||||
},
|
||||
{
|
||||
"path": "src/App/MappedElement.cpp",
|
||||
"sha256": "f5448d9de693e319460d5e3ef52c63a54e0948963e4b92786aac38c77bdcd7a5"
|
||||
},
|
||||
{
|
||||
"path": "src/App/ElementNamingUtils.cpp",
|
||||
"sha256": "fc3223857ca6d2990d2b48f437f75841cdf23e1e79fa18ef18296f40acd3cdc3"
|
||||
},
|
||||
{
|
||||
"path": "src/App/ElementMap.cpp",
|
||||
"sha256": "e1ceaedb624688ddffa6a834c1b32539e8ba5bbdc0ec31401ea087e6a9c3eb52"
|
||||
},
|
||||
{
|
||||
"path": "src/Base/Handle.cpp",
|
||||
"sha256": "8f02d27a8b672e9a56764f85e394265b4fcf023aba097f040b03dcb38bc393d2"
|
||||
}
|
||||
],
|
||||
"runtimeAssertions": [
|
||||
"IndexedName parsing",
|
||||
"MappedName postfix tag parsing",
|
||||
"StringHasher duplicate identity",
|
||||
"StringHasher SHA-1 threshold",
|
||||
"StringHasher mapped indexed-name references",
|
||||
"ElementMap StringHasher encoding",
|
||||
"ElementMap bidirectional lookup",
|
||||
"ElementMap history tracing",
|
||||
"ElementMap save and restore",
|
||||
"MappedElement stable ordering",
|
||||
"seven-object wasm static archive linkage"
|
||||
],
|
||||
"hostAdapterScope": [
|
||||
"Base persistence streams",
|
||||
"Base type-system declarations",
|
||||
"Python wrapper objects",
|
||||
"logging",
|
||||
"Application and Document lifecycle hooks"
|
||||
],
|
||||
"isolatedSourceArchive": {
|
||||
"path": "native/freecad-naming-probe/dist/libFreeCADPrivateNamingProbe.a",
|
||||
"target": "wasm32-emscripten",
|
||||
"expectedObjectMembers": 7,
|
||||
"hostAdapterBound": true,
|
||||
"productionEligible": false
|
||||
},
|
||||
"notLinkedProductionComponents": [
|
||||
"FreeCADBase static library",
|
||||
"FreeCADApp static library",
|
||||
"Part static library",
|
||||
"Python static library",
|
||||
"real Application and Document integration",
|
||||
"FreeCAD private naming Worker bridge"
|
||||
],
|
||||
"candidateAbi": {
|
||||
"scope": "isolated-non-production",
|
||||
"exports": [
|
||||
"freecadNamingCandidateAbiVersion",
|
||||
"freecadNamingCandidateCapabilitiesJson",
|
||||
"freecadNamingCandidateEvidenceJson"
|
||||
],
|
||||
"strictWebValidator": "pass",
|
||||
"nativeResources": [
|
||||
"MappedNameRef",
|
||||
"StringHasher",
|
||||
"ElementMap2"
|
||||
],
|
||||
"occtBuilderContext": false,
|
||||
"publishToWorker": false
|
||||
},
|
||||
"production": {
|
||||
"workerArtifact": "native/occt-history/dist/bitbybit-occt-history.wasm",
|
||||
"workerLinked": false,
|
||||
"exportedCallbacks": [],
|
||||
"requiredCallbacks": [
|
||||
"freecadNamingAbiVersion",
|
||||
"freecadNamingCapabilitiesJson",
|
||||
"freecadNamingEvidenceJson"
|
||||
]
|
||||
},
|
||||
"boundary": {
|
||||
"freecadNamingBuildStatus": "contract-only",
|
||||
"exTsn02": "in_progress",
|
||||
"systemExact": false
|
||||
}
|
||||
}
|
||||
38
config/freecad-wasm-sdk-bootstrap.cmake
Normal file
38
config/freecad-wasm-sdk-bootstrap.cmake
Normal file
@@ -0,0 +1,38 @@
|
||||
# Candidate-only cross-build helpers. Keep the locked FreeCAD source tree clean
|
||||
# and forbid its fallback FetchContent path from reaching the network.
|
||||
set(FETCHCONTENT_FULLY_DISCONNECTED ON CACHE BOOL "Disable network fallback" FORCE)
|
||||
set(fmt_DIR "${CMAKE_CURRENT_LIST_DIR}/cmake/fmt" CACHE PATH "Candidate header-only fmt package" FORCE)
|
||||
|
||||
# FreeCAD 1.1.1 has no dedicated Emscripten branch in FCConfig.h. Emscripten's
|
||||
# POSIX layer follows the Linux code paths for this headless candidate.
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
|
||||
if(NOT FREECAD_WASM_OCCT_COMPAT_INCLUDE
|
||||
OR NOT EXISTS "${FREECAD_WASM_OCCT_COMPAT_INCLUDE}/TopTools_ListOfShape.hxx")
|
||||
message(FATAL_ERROR
|
||||
"FREECAD_WASM_OCCT_COMPAT_INCLUDE must point to OCCT's Deprecated/NCollectionAliases directory")
|
||||
endif()
|
||||
add_compile_definitions(__linux__=1)
|
||||
include_directories(BEFORE "${CMAKE_CURRENT_LIST_DIR}/wasm-compat")
|
||||
include_directories(BEFORE SYSTEM "${FREECAD_WASM_OCCT_COMPAT_INCLUDE}")
|
||||
# FreeCAD's native UNIX branch adds --undefined,dynamic_lookup, which is
|
||||
# incompatible with Emscripten's relocatable-object emulation of SHARED.
|
||||
set(UNIX FALSE)
|
||||
# Qt disables its timezone backend on WASM. FreeCAD calls systemTimeZone()
|
||||
# only when formatting diagnostic timestamps, so use deterministic UTC.
|
||||
add_compile_options(
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:-include${CMAKE_CURRENT_LIST_DIR}/freecad-wasm-sdk-compat.h>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:-Wno-deprecated-declarations>"
|
||||
)
|
||||
endif()
|
||||
|
||||
# The headless SDK does not ship Qt LinguistTools, while FreeCAD Base/App invoke
|
||||
# qt_add_translation unconditionally. Keep their resource targets valid without
|
||||
# pulling host-generated .qm files into the wasm candidate.
|
||||
set(FREECAD_WASM_EMBED_TRANSLATIONS OFF CACHE BOOL "Embed FreeCAD translations in the candidate wasm SDK")
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten"
|
||||
AND NOT FREECAD_WASM_EMBED_TRANSLATIONS
|
||||
AND NOT COMMAND qt_add_translation)
|
||||
function(qt_add_translation output_variable)
|
||||
set(${output_variable} "" PARENT_SCOPE)
|
||||
endfunction()
|
||||
endif()
|
||||
112
config/freecad-wasm-sdk-build-plan.json
Normal file
112
config/freecad-wasm-sdk-build-plan.json
Normal file
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"scope": "candidate-only-freecad-wasm-sdk-build",
|
||||
"baseline": {
|
||||
"freecadVersion": "1.1.1",
|
||||
"sourceCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d",
|
||||
"emscriptenVersion": "3.1.69",
|
||||
"qtVersion": "6.8.2",
|
||||
"target": "wasm32-emscripten"
|
||||
},
|
||||
"source": {
|
||||
"path": ".cache/freecad/FreeCAD",
|
||||
"mode": "detached-pinned-commit",
|
||||
"requiredClean": true
|
||||
},
|
||||
"sourceOverlay": {
|
||||
"path": ".cache/toolchains/freecad-naming-sdk/source-occt8",
|
||||
"command": "npm run prepare:freecad-occt8-overlay",
|
||||
"migration": "OCCT 8 migrate_raise_to_throw.py and replace_typedefs.py",
|
||||
"productionPublication": false
|
||||
},
|
||||
"toolchain": {
|
||||
"emscriptenRoot": "EMSDK",
|
||||
"qtPrefix": ".cache/toolchains/qt6/install-wasm-freecad",
|
||||
"cmakeGenerator": "Ninja"
|
||||
},
|
||||
"dependencyBuilds": [
|
||||
{
|
||||
"name": "Qt6",
|
||||
"version": "6.8.2",
|
||||
"command": "npm run build:qt6-freecad-wasm",
|
||||
"targetArchives": ["QtCore", "QtConcurrent", "QtNetwork", "QtXml"]
|
||||
},
|
||||
{
|
||||
"name": "yaml-cpp",
|
||||
"version": "0.8.0",
|
||||
"sourceCommit": "f7320141120f720aecc4c32be25586e7da9eb978",
|
||||
"command": "npm run build:yaml-cpp-wasm",
|
||||
"targetArchives": ["yaml-cpp"]
|
||||
},
|
||||
{
|
||||
"name": "ICU",
|
||||
"version": "68.2",
|
||||
"sourceSha256": "c79193dee3907a2199b8296a93b52c5cb74332c26f3d167269487680d479d625",
|
||||
"command": "npm run build:icu-wasm",
|
||||
"targetArchives": ["icuuc", "icui18n", "icudata"]
|
||||
},
|
||||
{
|
||||
"name": "CPython",
|
||||
"version": "3.13.5",
|
||||
"sourceSha256": "93e583f243454e6e9e4588ca2c2662206ad961659863277afcdb96801647d640",
|
||||
"command": "npm run build:cpython-wasm",
|
||||
"targetArchives": ["python3.13"]
|
||||
},
|
||||
{
|
||||
"name": "Xerces-C",
|
||||
"version": "3.2.4",
|
||||
"sourceSha256": "075bc57940da0f9be6dd183c550c8ce0b9833e4550dc382048377a1a5e3b2bd9",
|
||||
"command": "npm run build:xerces-c-wasm",
|
||||
"targetArchives": ["xerces-c"]
|
||||
},
|
||||
{
|
||||
"name": "Boost",
|
||||
"version": "1.83.0",
|
||||
"sourceSha256": "6478edfe2f3305127cffe8caf73ea0176c53769f4bf1585be237eb30798c3b8e",
|
||||
"command": "npm run build:boost-wasm",
|
||||
"targetArchives": ["boost_program_options", "boost_regex", "boost_thread", "boost_date_time", "boost_atomic"]
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"buildDirectory": ".cache/toolchains/freecad-naming-sdk/build-wasm",
|
||||
"installDirectory": ".cache/toolchains/freecad-naming-sdk/install-wasm",
|
||||
"buildType": "Release",
|
||||
"buildGui": false,
|
||||
"developerTests": false,
|
||||
"python": "required",
|
||||
"modules": ["Part"],
|
||||
"productionPublication": false
|
||||
},
|
||||
"requiredArchives": [
|
||||
"FreeCADBase",
|
||||
"FreeCADApp",
|
||||
"Part",
|
||||
"Python"
|
||||
],
|
||||
"knownConfigurationLimits": [
|
||||
"Qt Core/Concurrent/Network/Xml wasm archives are built locally with thread support for the candidate SDK.",
|
||||
"yaml-cpp 0.8.0 is built as a wasm static archive for the required Material module.",
|
||||
"CPython 3.13.5 development headers and its pthread-enabled wasm static archive are staged for the candidate SDK.",
|
||||
"Xerces-C 3.2.4 is built as a wasm static archive against the staged ICU libraries.",
|
||||
"The headless candidate omits Base/App Qt translation resources because Qt LinguistTools is not part of the target SDK.",
|
||||
"Qt disables its timezone backend on WebAssembly; the candidate formats FreeCAD diagnostic timestamps in deterministic UTC.",
|
||||
"FreeCAD's disabled Linux backtrace block still includes execinfo.h; a candidate-only empty header satisfies that unused include without exposing backtrace APIs.",
|
||||
"OCCT 8.0 keeps FreeCAD 1.1.1's TopTools collection typedefs under Deprecated/NCollectionAliases; the candidate includes that pinned source compatibility directory explicitly.",
|
||||
"OCCT 8.0 removed Standard_*::Raise; a disposable FreeCAD worktree overlay applies the pinned OCCT migrate_raise_to_throw.py phase without modifying the locked source checkout.",
|
||||
"OCCT 8.0 folds legacy iterator typedefs into collection headers; the disposable overlay maps six removed include-only iterator headers after the official typedef migration.",
|
||||
"OCCT 8.0 removes Standard_Failure RTTI; the disposable overlay replaces exception-only DynamicType calls with the documented ExceptionType API.",
|
||||
"OCCT 8.0 removes BRepLProp_CurveTool; its parameter forwarding calls are mapped to BRepAdaptor_Curve's inherited FirstParameter and LastParameter methods.",
|
||||
"OCCT 8.0 consolidates Geom2dLProp into GeomLProp; the overlay maps Geom2dLProp_CLProps2d to the signature-compatible GeomLProp_CLProps2d specialization.",
|
||||
"OCCT 8.0 adds NCollection_List initializer-list construction; three ShapeMapper calls explicitly retain FreeCAD's original std::vector<TopoShape> overload.",
|
||||
"OCCT 8.0 makes Standard_Failure a std::exception; three identical trailing handlers are removed after their std::exception handler to avoid unreachable exception paths.",
|
||||
"FreeCAD declares Base/App/Part as SHARED; the candidate toolchain archives their wasm objects with emar and links dependencies separately.",
|
||||
"The upstream FreeCAD CMake project currently creates shared libraries and requires a full wasm dependency graph; this plan does not treat a host build as a wasm SDK."
|
||||
],
|
||||
"boundary": {
|
||||
"freecadNamingBuildStatus": "contract-only",
|
||||
"exTsn02": "in_progress",
|
||||
"systemExact": false,
|
||||
"workerLinked": false,
|
||||
"callbacks": []
|
||||
}
|
||||
}
|
||||
11
config/freecad-wasm-sdk-compat.h
Normal file
11
config/freecad-wasm-sdk-compat.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#if __has_include(<Standard_Macro.hxx>)
|
||||
#include <TopTools_HSequenceOfShape.hxx>
|
||||
#include <TopTools_ListOfShape.hxx>
|
||||
#endif
|
||||
|
||||
// QTimeZone::UTC remains available when Qt's WASM timezone backend is off.
|
||||
#define systemTimeZone() UTC
|
||||
#endif
|
||||
@@ -56,7 +56,7 @@
|
||||
{ "id": "EX-SK-02", "title": "Match Sketcher editing tools, autoconstraints, virtual space and UI lifecycle", "priority": "P0", "status": "in_progress", "dependencies": ["EX-SK-01", "EX-UI-03"], "deliverables": ["Pointer and keyboard editor oracle", "All editing tools", "Task and focus lifecycle"], "acceptance": ["Every tool supports success, failure, cancel, undo and redo with matching selection"], "evidence": ["check:freecad-sketcher-editor", "check:chrome-sketcher-bspline"], "exactBlockedBy": ["The full GUI tool and focus matrix is incomplete"] },
|
||||
{ "id": "EX-PART-01", "title": "Complete all Part primitives, builders, booleans, healing and inspection", "priority": "P0", "status": "in_progress", "dependencies": ["EX-KER-01", "EX-TSN-04"], "deliverables": ["All Part commands and parameters", "Healing and tolerance tools", "Native history for local operations"], "acceptance": ["Success and failure outputs match native Shape, history and diagnostics"], "evidence": ["check:chrome-part-primitives", "check:freecad-golden-fixtures"], "exactBlockedBy": ["Current golden corpus does not enumerate every Part command and option"] },
|
||||
{ "id": "EX-PD-01", "title": "Complete all PartDesign features, parameters and additive/subtractive combinations", "priority": "P0", "status": "in_progress", "dependencies": ["EX-PART-01", "EX-TSN-04"], "deliverables": ["Complete PartDesign feature matrix", "Body Tip and feature-list semantics", "Per-feature native history and naming"], "acceptance": ["Every feature edit, failure recovery and save-reopen matches FreeCAD"], "evidence": ["check:partdesign-closure", "check:freecad-sketcher-partdesign-abi", "check:chrome-partdesign-lifecycle"], "exactBlockedBy": ["The parameter contract covers the supported Facade subset, not every registered PartDesign TypeId or cross-product"] },
|
||||
{ "id": "EX-PD-02", "title": "Complete attachment, datum, ShapeBinder, SubShapeBinder and body workflows", "priority": "P0", "status": "pending", "dependencies": ["EX-PD-01", "EX-DOC-04"], "deliverables": ["All map modes and support rules", "Datum and binder lifecycle", "Multi-body and cross-document corpus"], "acceptance": ["Support migration, visibility, Tip and references remain exact through mutation"], "evidence": ["check:chrome-partdesign-lifecycle", "check:freecad-fcstd-roundtrip"], "exactBlockedBy": ["Complete datum/binder and multi-body oracle is absent"] }
|
||||
{ "id": "EX-PD-02", "title": "Complete attachment, datum, ShapeBinder, SubShapeBinder and body workflows", "priority": "P0", "status": "pending", "dependencies": ["EX-PD-01", "EX-DOC-04"], "deliverables": ["All map modes and support rules", "Datum and binder lifecycle", "Multi-body and cross-document corpus"], "acceptance": ["Support migration, visibility, Tip and references remain exact through mutation"], "evidence": ["check:chrome-partdesign-lifecycle", "check:freecad-fcstd-roundtrip", "check:freecad-partdesign-structure", "check:freecad-attachment-modes"], "exactBlockedBy": ["Native same-document, first cross-document SubShapeBinder cases, and the 55-mode registry/engine partition pass; executable geometry for every implemented mode/reference combination, other XLink modes, relink recovery and Web Chrome replay remain incomplete"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -167,6 +167,126 @@
|
||||
"libraryPath": "build-caches/qt6-wasm-core-build.tar.zst",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"id": "qt6-wasm-freecad-sdk",
|
||||
"source": ".cache/toolchains/qt6/install-wasm-freecad",
|
||||
"restore": ".cache/toolchains/qt6/install-wasm-freecad",
|
||||
"libraryPath": "toolchains/qt6-wasm-freecad-6.8.2.tar.zst",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "qt6-wasm-freecad-build",
|
||||
"source": ".cache/toolchains/qt6/build-wasm-freecad",
|
||||
"restore": ".cache/toolchains/qt6/build-wasm-freecad",
|
||||
"libraryPath": "build-caches/qt6-wasm-freecad-build.tar.zst",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"id": "yaml-cpp-source",
|
||||
"source": ".cache/toolchains/yaml-cpp/src",
|
||||
"restore": ".cache/toolchains/yaml-cpp/src",
|
||||
"libraryPath": "sources/yaml-cpp-0.8.0-f7320141.tar.zst",
|
||||
"revision": "f7320141120f720aecc4c32be25586e7da9eb978",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "yaml-cpp-wasm-sdk",
|
||||
"source": ".cache/toolchains/yaml-cpp/install-wasm",
|
||||
"restore": ".cache/toolchains/yaml-cpp/install-wasm",
|
||||
"libraryPath": "toolchains/yaml-cpp-wasm-0.8.0.tar.zst",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "yaml-cpp-wasm-build",
|
||||
"source": ".cache/toolchains/yaml-cpp/build-wasm",
|
||||
"restore": ".cache/toolchains/yaml-cpp/build-wasm",
|
||||
"libraryPath": "build-caches/yaml-cpp-wasm-build.tar.zst",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"id": "icu-68.2-download",
|
||||
"source": ".cache/toolchains/icu/downloads",
|
||||
"restore": ".cache/toolchains/icu/downloads",
|
||||
"libraryPath": "sources/icu-68.2-download.tar.zst",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "icu-wasm-sdk",
|
||||
"source": ".cache/toolchains/icu/install-wasm",
|
||||
"restore": ".cache/toolchains/icu/install-wasm",
|
||||
"libraryPath": "toolchains/icu-wasm-68.2.tar.zst",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "emscripten-freecad-cache",
|
||||
"source": ".cache/toolchains/emscripten-freecad-cache",
|
||||
"restore": ".cache/toolchains/emscripten-freecad-cache",
|
||||
"libraryPath": "build-caches/emscripten-freecad-cache.tar.zst",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"id": "cpython-3.13.5-download",
|
||||
"source": ".cache/toolchains/python/downloads",
|
||||
"restore": ".cache/toolchains/python/downloads",
|
||||
"libraryPath": "sources/cpython-3.13.5-download.tar.zst",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "cpython-wasm-sdk",
|
||||
"source": ".cache/toolchains/python/install-wasm",
|
||||
"restore": ".cache/toolchains/python/install-wasm",
|
||||
"libraryPath": "toolchains/cpython-wasm-3.13.5.tar.zst",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "cpython-wasm-build",
|
||||
"source": ".cache/toolchains/python/build-wasm",
|
||||
"restore": ".cache/toolchains/python/build-wasm",
|
||||
"libraryPath": "build-caches/cpython-wasm-build.tar.zst",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"id": "xerces-c-3.2.4-download",
|
||||
"source": ".cache/toolchains/xerces-c/downloads",
|
||||
"restore": ".cache/toolchains/xerces-c/downloads",
|
||||
"libraryPath": "sources/xerces-c-3.2.4-download.tar.zst",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "xerces-c-wasm-sdk",
|
||||
"source": ".cache/toolchains/xerces-c/install-wasm",
|
||||
"restore": ".cache/toolchains/xerces-c/install-wasm",
|
||||
"libraryPath": "toolchains/xerces-c-wasm-3.2.4.tar.zst",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "xerces-c-wasm-build",
|
||||
"source": ".cache/toolchains/xerces-c/build-wasm",
|
||||
"restore": ".cache/toolchains/xerces-c/build-wasm",
|
||||
"libraryPath": "build-caches/xerces-c-wasm-build.tar.zst",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"id": "boost-1.83.0-download",
|
||||
"source": ".cache/toolchains/boost/downloads",
|
||||
"restore": ".cache/toolchains/boost/downloads",
|
||||
"libraryPath": "sources/boost-1.83.0-download.tar.zst",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "boost-wasm-sdk",
|
||||
"source": ".cache/toolchains/boost/install-wasm",
|
||||
"restore": ".cache/toolchains/boost/install-wasm",
|
||||
"libraryPath": "toolchains/boost-wasm-1.83.0.tar.zst",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "boost-wasm-build",
|
||||
"source": ".cache/toolchains/boost/src",
|
||||
"restore": ".cache/toolchains/boost/src",
|
||||
"libraryPath": "build-caches/boost-wasm-build.tar.zst",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"id": "opencamlib-build",
|
||||
"source": ".cache/opencamlib-wasm",
|
||||
@@ -210,6 +330,15 @@
|
||||
"restore": ".cache/offline-sysroot/include/eigen3",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "fmt-headers",
|
||||
"sourceCandidates": [
|
||||
"/usr/include/fmt"
|
||||
],
|
||||
"libraryPath": "system/headers/fmt-10.1.1",
|
||||
"restore": ".cache/offline-sysroot/fmt-10.1.1/include/fmt",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "debian-package-cache",
|
||||
"sourceCandidates": [
|
||||
|
||||
7
config/wasm-compat/execinfo.h
Normal file
7
config/wasm-compat/execinfo.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
// FreeCAD includes this header on its Linux path even when its configure
|
||||
// check has disabled HAVE_BACKTRACE_SYMBOLS. No backtrace API is declared.
|
||||
#if !defined(__EMSCRIPTEN__)
|
||||
#error "This compatibility header is only valid for the Emscripten candidate"
|
||||
#endif
|
||||
@@ -629,7 +629,7 @@ FCStd writer 现在把 Web Sketch 的 `Support`、`MapMode`、`AttachmentOffset`
|
||||
|
||||
锁定 FreeCAD 1.1.1 已实际打开、重算并另存 Web 生成的 `ExternalBox + Sketch`:`MapMode=FlatFace`、`AttachmentSupport=ExternalBox.Face1`、AttachmentOffset Z=0.25 mm,同时保留 10 个内部几何、11 个约束、1 个外部 Edge1 投影和外部 `PointOnObject` 约束。回存 FCStd 由 Web inspector 解码原生字段,并从隐藏元数据精确恢复 Face1 的 topologyVersion=3、generation=2 和 signature。Facade 回归升至 140/140,桌面 oracle 与生产构建通过。
|
||||
|
||||
该增量关闭当前六种 Web Attachment 模式的 FCStd 边界证据,不宣称完整 FreeCAD AttachEngine 的 56 种模式、多引用 ThreePoints/Intersection 支撑、MapReversed/MapPathParameter 或局部 Attachment 几何执行全部等价。FC-06 仍受 Face 多投影、高级 Sketch 约束和批量 FCStd 黄金约束,83 项计划计数保持 30 completed、21 in_progress、32 pending、0 blocked。
|
||||
该增量关闭当前六种 Web Attachment 模式的 FCStd 边界证据,不宣称完整 FreeCAD AttachEngine 的 55 种持久化模式、多引用 ThreePoints/Intersection 支撑、MapReversed/MapPathParameter 或局部 Attachment 几何执行全部等价。FC-06 仍受 Face 多投影、高级 Sketch 约束和批量 FCStd 黄金约束,83 项计划计数保持 30 completed、21 in_progress、32 pending、0 blocked。
|
||||
|
||||
## 82. 2026-08-04 FC-06 Sketcher SnellsLaw/Weight 原生往返增量
|
||||
|
||||
@@ -1790,6 +1790,52 @@ Sketcher 数据模型和 FCStd codec 已覆盖 FreeCAD 1.1.1 的完整原生几
|
||||
|
||||
QtBase 6.8.2 源码包已通过 USTC 国内镜像取得,并用 Qt 发布包官方 SHA-256 `012043ce6d411e6e8a91fdc4e05e6bedcfa10fcb1347d3c33908f7fdd10dfe05` 校验。新增 `build:qt6-wasm-core`,固定 Emscripten 3.1.69,构建静态 wasm32 `Qt6Core`、bundled Pcre2 和 zlib;离线模式只使用本机缓存,缺少已校验源码包时立即失败。源码包、展开源码、可用 SDK 和可选增量构建目录均已纳入项目离线资源清单。
|
||||
|
||||
新增的 `build:freecad-naming-source-probe` 并未假装提供生产命名实现。它固定 FreeCAD 1.1.1 提交 `0108fd4b4850cc46e625b60e53cea7a7bbe69f8d`,同时校验 `IndexedName.cpp` 与 `MappedName.cpp` 的逐文件 SHA-256,再把这两个原始编译单元与 wasm Qt6Core 编译为独立探针。运行时验证 IndexedName 解析、MappedName 追加和 tag 读取;探针明确检查不导出 `freecadNamingAbiVersion`、`freecadNamingCapabilitiesJson`、`freecadNamingEvidenceJson`。这证明的是锁定基础源码与工具链可交叉编译,不包含完整 StringHasher、FreeCADApp/Part 静态库或生产 Worker bridge。
|
||||
新增的 `build:freecad-naming-source-probe` 并未假装提供生产命名实现。它固定 FreeCAD 1.1.1 提交 `0108fd4b4850cc46e625b60e53cea7a7bbe69f8d`,同时校验 `IndexedName.cpp`、`MappedName.cpp`、`StringHasher.cpp` 与 `Base/Handle.cpp` 的逐文件 SHA-256,再把四个原始编译单元与 wasm Qt6Core 编译为独立探针。运行时验证 IndexedName 解析、MappedName 追加/tag 读取、StringHasher 去重、SHA-1 阈值及索引映射名称引用;探针明确检查不导出 `freecadNamingAbiVersion`、`freecadNamingCapabilitiesJson`、`freecadNamingEvidenceJson`。这证明的是锁定 StringHasher 核心源码与工具链可交叉编译和运行,不包含 `ElementMap.cpp`、`MappedElement.cpp`、FreeCADApp/Part 静态库或生产 Worker bridge。
|
||||
|
||||
新增 `check:freecad-private-naming-boundary`,跨 ABI 合同、兼容性矩阵和 exact 计划校验同一状态,并接入默认 `verify`、真实 wasm lane 和离线 smoke。准确边界保持不变:随附 OCCT Worker 仍没有链接 FreeCAD 私有 C++ 命名实现,制品仍无三项私有命名回调;`shippedWorkerImplementation=not-linked`、`freecadNamingBuild.status=contract-only`、`EX-TSN-02=in_progress`、`systemExact=false`。本批只完成可离线复现的前置构建和防止状态误提升的机器门禁。
|
||||
|
||||
## 231. 2026-08-12 ElementMap 原始源码 WASM 探针与隔离 ABI 候选
|
||||
|
||||
私有命名源码探针现已锁定并直接编译 FreeCAD 1.1.1 的 `IndexedName.cpp`、`MappedName.cpp`、`StringHasher.cpp`、`MappedElement.cpp`、`ElementNamingUtils.cpp`、`ElementMap.cpp` 与 `Base/Handle.cpp` 七个原始编译单元。独立 WASM 实跑覆盖 StringHasher 去重/SHA-1 阈值/索引引用、ElementMap StringHasher 编码、双向查找、历史回溯、保存恢复、MappedElement 排序和 DocumentObject tag;Application/Document 生命周期、持久化流、Python 包装、类型系统和日志只在探针中使用最小宿主适配。
|
||||
|
||||
探针现在从真实 `ElementMap::save` 和 `StringHasher::getIDMap` 生成原生资源。ElementMap 保存体按 FreeCAD `ComplexGeoData` 外层协议补入 `BeginElementMap v1` 后,由 Web `parseElementMap2` 读得 `MapCount=1`、4 个名称和 1 个 token;StringHasher schema v2 读得 1 个条目,引用闭包无缺失。三个刻意隔离命名的 `freecadNamingCandidate*` 回调把这些资源送入现有 `probeFreeCadPrivateNamingAbi` 与 `captureFreeCadPrivateNamingEvidence`,严格 Web ABI 校验通过。候选只接受探针 `cut` 协议载荷,没有 OCCT builder context,也不会发布到生产 Worker。
|
||||
|
||||
`config/freecad-private-naming-source-readiness.json` 与 `check:freecad-private-naming-boundary` 已把真实源码、运行时断言、宿主适配、隔离候选和生产缺口分别建模;检查器拒绝候选使用生产回调名,并继续要求生产导出列表为空。真实 WASM lane 与离线 smoke 都会重建和执行源码探针及候选 ABI 测试,不允许只读取历史结果。
|
||||
|
||||
准确边界保持不变:`native/occt-history` 生产制品未链接探针,也未链接 FreeCADApp/Part/Python 静态库、真实 Application/Document 集成或生产 bridge,三项生产 Embind 命名回调仍不存在。因此 `shippedWorkerImplementation=not-linked`、`freecadNamingBuild.status=contract-only`、`EX-TSN-02=in_progress`、`systemExact=false` 全部保持不变。下一步是生成可审计的 wasm FreeCADApp/Part/Python 静态 SDK,将真实 OCCT builder 阶段历史接入生产 bridge,再对每类 builder 执行命名证据和 FCStd 往返门禁。
|
||||
|
||||
## 232. 2026-08-12 FreeCAD 命名 WASM SDK 差距门与静态归档形态
|
||||
|
||||
七个已锁定的 FreeCAD 私有命名编译单元不再直接混入探针最终链接命令,而是分别生成 wasm object,再由 `emar` 封装为隔离的 `libFreeCADPrivateNamingProbe.a`。烟测解析真实 `ar` 成员并确认归档为 900882 bytes、7 个成员、7 个 wasm object、0 个 LLVM bitcode;主探针只从该归档解析命名符号后,StringHasher、ElementMap、MappedNameRef 与 Web ABI 全部断言继续通过。该归档仍使用 Persistence/Python/Application/Document 等宿主适配,机器清单固定 `hostAdapterBound=true`、`productionEligible=false`,不冒充 FreeCADBase/FreeCADApp。
|
||||
|
||||
新增 `config/freecad-naming-sdk-plan.json`、`check:freecad-naming-sdk-readiness` 和 `generate:freecad-naming-sdk-manifest`。就绪检查直接解析静态 `ar`,接受 wasm relocatable object/LLVM bitcode,明确拒绝宿主 ELF;当前本机验证 `libQt6Core.a` 为 219 个 wasm object,并验证其 Bundled Pcre2 27 个、Bundled Zlib 15 个实际链接依赖。FreeCAD 私有头与 QtCore 头齐全,但核心五库仅 `QtCore=1/5`,仍缺 FreeCADBase、FreeCADApp、Part、Python 和 production bridge。生成器在任何缺项下都拒绝写 manifest,完整 SDK 检查也复用相同归档目标校验,不允许仅靠文件非空和 SHA-256 混入 x86 库。
|
||||
|
||||
readiness 已进入默认 `verify`、真实 WASM 重建 lane 与离线 smoke;候选 SDK 目录要等首个真实 FreeCADBase/Python wasm 归档生成后再加入离线资源库,当前不制造空资源记录。准确边界不变:生产 OCCT Worker 未使用上述探针归档,`availability=unavailable`、`callbacks=[]`;`freecadNamingBuild.status=contract-only`、`EX-TSN-02=in_progress`、`systemExact=false`。下一步仍是获取或构建锁定 Emscripten 3.1.69 的 FreeCADBase/FreeCADApp/Part/Python 静态库,实现真实 Document/OCCT builder bridge 后再做 production artifact probe。
|
||||
|
||||
## 233. 2026-08-13 完整候选 SDK、真实私有命名桥与隔离 Worker
|
||||
|
||||
候选 SDK 已从差距门推进为完整可校验闭包。FreeCADBase、FreeCADApp、Part、QtCore 和 CPython 3.13 五个核心 wasm 静态库全部通过 `ar` 成员目标检查;21 个链接依赖包含 Qt/ICU/Xerces/Boost/yaml-cpp 以及 CPython 实际拉入的 mpdecimal、expat、HACL SHA2、zlib、bzip2、sqlite3。CPython 构建固定 `SOURCE_DATE_EPOCH`,显式生成 `pybuilddir.txt` 和 wasm stdlib;连续两次构建得到相同的 `libpython3.13.a` 与 `python313.zip` 哈希。manifest 现在同时哈希 bridge、host adapter、force-include 头和 Python 运行时文件,并固定 C++20、pthread 和四项兼容宏,缺任一项都拒绝生成或加载。
|
||||
|
||||
新增真实隔离 bridge,不再依赖探针 shims。它初始化 FreeCAD `App::Application`,直接调用锁定 1.1.1 的 `ElementMap::encodeElementName/setElementName/beforeSave/save`、`MappedName` 和 `StringHasher`,导出三项版本化 ABI。Node 实跑验证首阶段 postfix、链式 `$#id:index`/3 项 StringHasher 闭包,以及缺 history 的结构化 fail-closed;JS/WASM/DATA 的哈希证据写入本机候选报告,且明确 `productionPublication=false`。
|
||||
|
||||
SDK-05 又建立了独立 pthread OCCT 构建和 `.cache/candidates/freecad-naming-worker` 候选模块。Node 与 Chrome 均在同一个 wasm 中先执行 OCCT 8.0.0 cut history,再调用 FreeCAD 私有命名 ABI;Chrome 在 COOP/COEP 隔离环境中验证三回调、链式 StringHasher 闭包和无 history 拒绝。执行命令与只读报告检查已拆分,检查器会重新计算当前 JS/WASM/DATA 哈希,静态旧报告不能替代真实 harness。本批 WBS 为 SDK-01/02A/02B/03/04/05 completed,PAR-01 in_progress。
|
||||
|
||||
候选桥随后补齐非空 StringHasher 的下一阶段恢复。它只接受 schema/nativeVersion 正确、ID 连续有序、依赖只指向既有 ID、能够由 FreeCAD 公共 `StringHasher` API 逐字段重建的表;二进制/单向哈希、缺表、ID 篡改和多输入不一致表继续 fail-closed。隔离桥、合并 Worker 和真实 Chrome 都执行三阶段链,条目数为 `0→3→5`;隔离负例确认篡改 ID 与不一致表均被拒绝。该能力仍仅存在于候选目录,未发布到生产 Worker。
|
||||
|
||||
准确生产边界没有改变:候选构建强制 `OCCT_HISTORY_PUBLISH=0`,没有覆盖仓库随附的 `native/occt-history` 或 `public/native/occt-history`。因此生产 `availability=unavailable`、`callbacks=[]`、`shippedWorkerImplementation=not-linked`、`freecadNamingBuild.status=contract-only`、`EX-TSN-02=in_progress`、`systemExact=false` 继续成立。下一项是 PAR-01:扩展 Datum/Attachment/ShapeBinder、多 Body、完整参数 mutation/编辑恢复/FCStd 往返,并在覆盖每类 builder 后才评估生产发布。
|
||||
|
||||
## 234. 2026-08-13 PAR-01 首批 Datum/Binder/多 Body 原生结构 oracle
|
||||
|
||||
新增 `freecad-partdesign-structure-oracle`,直接在锁定 FreeCAD 1.1.1 中创建四个 Body、Plane/Line/Point 三类 Datum、ShapeBinder 与 SubShapeBinder。初始证据锁定 SourceBody.Tip=SourceBox,而只包含 Datum 或 Binder 的三个 Body.Tip 均为 null;这些非实体对象不会错误接管 Tip。DatumPlane 使用 XY_Plane/FlatFace 与 Z=5、绕 Z 30° 的 AttachmentOffset,DatumLine/Point 分别使用 ObjectX/ObjectOrigin;四个 Body 的 Group 顺序均被采集。
|
||||
|
||||
源 AdditiveBox 的 Length 编辑使体积 `480→672`,跨 Body ShapeBinder 同步 `480→672`;选定 Edge1..4 的 SubShapeBinder 长度保持 28。DatumPlane offset 编辑为 Z=7/45° 后,其实际 Placement 同步更新。另建 `Face999` 支撑的 DatumPlane,FreeCAD 明确返回 `Touched + Invalid` 与 `AttachEngine3D: subshape not found SourceBox.Face999`,没有把坏引用静默重绑。
|
||||
|
||||
该文档真实保存为 FCStd、关闭、重新打开并重算;四个 Body 的 Group/Tip、Datum 支撑/MapMode/Offset、两类 Binder 引用与形状、无效支持诊断全部保持。执行与检查命令已分离为 `probe:freecad-partdesign-structure` 和 `check:freecad-partdesign-structure`,并纳入真实 oracle lane 和默认 Sketcher/PartDesign 门禁。
|
||||
|
||||
同一 oracle 又增加双 FCStd 跨文档场景。原生 `ShapeBinder.Support` 虽报告为 `App::PropertyLinkSubListGlobal`,但直接赋外部文档对象仍明确返回 `PropertyLinkSubList does not support external object`;`SubShapeBinder.Support` 为 `App::PropertyXLinkSubList`,在源/消费文档均已落盘后接受 `ExternalBox.Face1`。关闭并按源→消费顺序重开后 XLink 仍指向源文档,源 Width 编辑使 Face1 与 SubShapeBinder 面积同步 `30→48`。删除源对象时 FreeCAD 清空 XLink,但保留面积 48 的最后形状缓存,且状态仍为 `Up-to-date/Valid`;检查器锁定了这一非直觉生命周期语义。EX-PD-02 仍为 pending:完整 Attachment 组合、其他 XLink/重新链接恢复与 Web Chrome 生命周期尚未闭合。
|
||||
|
||||
## 235. 2026-08-13 PAR-01 Attachment 注册表与引擎分区 oracle
|
||||
|
||||
新增独立 `freecad-attachment-mode-oracle`,从锁定 FreeCAD 1.1.1 运行时读取 `MapMode` 的永久枚举顺序和 `Attacher` 元数据。真实注册表为 55 项;Plane 与 Sketch 的 `AttachEnginePlane` 各实现 23 项,Line 的 `AttachEngineLine` 实现 18 项,Point 的 `AttachEnginePoint` 实现 9 项,实现并集为 50 项。`Deactivated` 是关闭状态,`TangentU`、`TangentV`、`IntersectionPoint`、`MidPoint` 未出现在这四类引擎的实现集合。报告为每个实现模式保留永久索引和完整 `ReferenceCombinations`,从而将“枚举可写”与“当前引擎实际实现”分开。
|
||||
|
||||
代表性执行场景覆盖 Plane/Sketch 的 `FlatFace`、Line 的 `TwoPointLine` 和 Point 的 `Vertex`,并锁定引用类型建议、Placement、状态与 `positionBySupport`。源 Box 高度从 6 改为 9 后,Vertex Point 的 Z 同步 `6→9`;FCStd 关闭重开后保持。反向案例证明 Plane 写入 `Vertex`、Line/Point 写入 `FlatFace` 虽能保留枚举,却都进入 `Touched + Invalid` 并返回 `Attachment mode ... is not implemented.`,且失败状态同样通过 FCStd 往返。新执行/检查命令已进入真实 oracle lane、Sketcher/PartDesign 聚合门禁及 PAR-01 证据。EX-PD-02 仍保持 pending:尚需为 50 个实现槽位的全部引用组合构造可执行几何,并完成其他 XLink、重新链接恢复和 Web Chrome 回放。
|
||||
|
||||
@@ -393,10 +393,14 @@ PartDesign Mirrored 已形成 experimental whole-shape 垂直切片:Facade 验
|
||||
|
||||
ABI 输入现同时携带文档 `objectId`、FCStd 原生正整数 object tag、上一阶段命名证据、明确的 result object/tag 和阶段 DAG;object tag 会从 `Document.xml` 的 `Object.id` 导入,按稳定值写回 FCStd,并通过 SQLite schema v7、OPFS 重开和 checkpoint 往返。`build:freecad-naming-worker` 是独立的 fail-closed 构建入口,只接受锁定提交、Emscripten 3.1.69、wasm Qt/Python、FreeCADBase/FreeCADApp/Part 静态库及三回调 bridge 的哈希清单;普通 `build:occt-history` 继续是 OCCT-only。`check:occt-history-artifact` 会实际实例化 WASM 并探测三回调,当前结果为 unavailable,因此 `EX-TSN-02=in_progress` 与 `systemExact=false` 保持不变。
|
||||
|
||||
Qt 前置条件已按独立、可离线复现的链路落地:`build:qt6-wasm-core` 使用 Emscripten 3.1.69 构建 QtBase 6.8.2 静态 wasm `Qt6Core`,下载使用 USTC 国内镜像,但必须先通过 Qt 发布包官方 SHA-256;离线模式缺包时直接失败,不允许回退联网。`build:freecad-naming-source-probe` 进一步锁定 FreeCAD 提交及 `IndexedName.cpp`、`MappedName.cpp` 两个原始编译单元的内容哈希,并验证它们能与 wasm Qt6Core 一起生成和执行 wasm32 探针。该探针使用最小编译适配头,不包含完整 `StringHasher`、FreeCADApp/Part 静态库或生产 bridge,也明确禁止导出三项生产命名回调,因此只消除工具链和基础源码可编译性风险,不构成 `FreeCADPrivateNamingABI` 的生产实现。
|
||||
Qt 前置条件已按独立、可离线复现的链路落地:`build:qt6-wasm-core` 使用 Emscripten 3.1.69 构建 QtBase 6.8.2 静态 wasm `Qt6Core`,下载使用 USTC 国内镜像,但必须先通过 Qt 发布包官方 SHA-256;离线模式缺包时直接失败,不允许回退联网。`build:freecad-naming-source-probe` 进一步锁定 FreeCAD 提交及 `IndexedName.cpp`、`MappedName.cpp`、`StringHasher.cpp`、`MappedElement.cpp`、`ElementNamingUtils.cpp`、`ElementMap.cpp`、`Base/Handle.cpp` 七个原始编译单元的内容哈希,并验证它们能与 wasm Qt6Core 一起生成和执行 wasm32 探针。运行时覆盖名称解析、StringHasher 去重/SHA-1 阈值/索引引用、ElementMap 编码/双向查找/历史/保存恢复以及 MappedElement 稳定排序;真实 ElementMap 保存体和 StringHasher 表还会经过 Web 侧 ElementMap2 parser、引用闭包及严格 `FreeCADPrivateNamingABI` 校验。探针只导出隔离的 `freecadNamingCandidate*` 名称,持久化流、Python 包装、类型系统、日志和 Application/Document 生命周期仍由适配头提供;FreeCADApp/Part/Python 静态库、真实文档集成、OCCT builder 上下文和生产 bridge 均未链接。因此它证明的是锁定私有源码子集与 ABI 数据可交叉编译/运行,不构成生产命名实现。
|
||||
|
||||
`check:freecad-private-naming-boundary` 同时读取 ABI 合同、兼容性矩阵和 exact 计划,固定校验 `shippedWorkerImplementation=not-linked`、`freecadNamingBuild.status=contract-only`、`EX-TSN-02=in_progress` 与 `systemExact=false`;该检查已进入默认 `verify`、真实 wasm lane 和离线 smoke。只有生产 OCCT Worker 实际链接锁定的 FreeCAD 私有静态库/bridge,且制品探针取得三项回调和完整命名证据后,才能在后续批次修改这些状态。
|
||||
|
||||
生产 SDK 前置已有完整候选闭包。七个锁定命名编译单元仍保留 `libFreeCADPrivateNamingProbe.a` 作为源码先决条件;实际 bridge 则链接真实 FreeCADBase/FreeCADApp/Part、QtCore、CPython 及 21 个传递依赖,并初始化真实 `App::Application`。`check:freecad-naming-sdk-readiness` 解析所有静态 `ar` 成员并拒绝宿主 ELF;`generate:freecad-naming-sdk-manifest` 还锁定 C++20、pthread、兼容头、host adapter 和预加载的 Python wasm stdlib。完整 `check:freecad-naming-sdk` 对库、源码和运行时文件逐项复算 SHA-256。
|
||||
|
||||
隔离 bridge 的 Node 烟测已用真实 `ElementMap/MappedName/StringHasher` 生成首阶段和链式证据,并验证 `$#id:index` 与 StringHasher 闭包。独立 pthread OCCT 候选模块进一步在 Node 和真实 Chrome 中执行 OCCT cut history,再从同一 wasm 调用三项 FreeCAD 私有命名回调;Chrome 执行与报告检查分开,检查器会拒绝与当前 JS/WASM/DATA 哈希不一致的旧报告。该候选强制写入 `.cache/candidates` 且 `OCCT_HISTORY_PUBLISH=0`,没有替换仓库随附生产 Worker。因此候选 SDK/Worker 验证完成不等于生产链接完成,`shippedWorkerImplementation=not-linked`、`EX-TSN-02=in_progress` 与 `systemExact=false` 仍保持不变。
|
||||
|
||||
Sketcher/PartDesign 的 Web 支持范围新增可执行参数合同:Sketcher 覆盖 FreeCAD 1.1.1 FCStd 表面的 9 类 geometry、19 类 constraint 和 10 类 InternalAlignment 语义;PartDesign 覆盖 Facade 已支持的 21 个 family、202 个属性槽和 72 个语义分区,并在对象创建及属性编辑时执行跨字段校验。`check:freecad-sketcher-partdesign-abi` 会从实现重算这些计数并拒绝陈旧报告;该“完整”限定于 supported Web Facade,不包含尚未实现的 FreeCAD PartDesign TypeId,也不表示 basic TypeScript solver 已求解全部圆锥曲线约束。
|
||||
|
||||
逐 feature 等级目前由 `config/compatibility-matrix.json` 的 `facadeCapabilities.geometry.featureLevels` 管理:Pad/Pocket/Revolution/Groove/Boolean 为有 native history 子集的 `compatible`,Fillet/Chamfer/MultiTransform 保持 `experimental`。`systemExactEvaluation.exact` 必须在所有 feature 的阶段 Shape/history、私有 token 证据和同构来源都闭合后才允许改为 true;当前值固定为 false,阻断项为私有 FreeCAD token 算法、没有原生 builder stage、以及无唯一同构来源。
|
||||
|
||||
783
native/freecad-naming-bridge/freecad_naming_bridge.cpp
Normal file
783
native/freecad-naming-bridge/freecad_naming_bridge.cpp
Normal file
@@ -0,0 +1,783 @@
|
||||
#include <emscripten/bind.h>
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
#include <App/Application.h>
|
||||
#include <App/ElementMap.h>
|
||||
#include <App/ElementNamingUtils.h>
|
||||
#include <App/IndexedName.h>
|
||||
#include <App/MappedName.h>
|
||||
#include <App/StringHasher.h>
|
||||
#include <Base/Exception.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <pwd.h>
|
||||
#include <limits.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
extern "C" ssize_t readlink(const char* path, char* buffer, size_t size)
|
||||
{
|
||||
constexpr const char* executable = "/freecad/bin/freecad-naming-bridge";
|
||||
if (std::strcmp(path, "/proc/self/exe") != 0) {
|
||||
return -1;
|
||||
}
|
||||
const size_t length = std::strlen(executable);
|
||||
if (size < length) {
|
||||
return -1;
|
||||
}
|
||||
std::memcpy(buffer, executable, length);
|
||||
return static_cast<ssize_t>(length);
|
||||
}
|
||||
|
||||
extern "C" char* realpath(const char* path, char* resolvedPath)
|
||||
{
|
||||
static constexpr const char* allowed[] = {
|
||||
"/freecad-user",
|
||||
"/freecad-user/config",
|
||||
"/freecad-user/data",
|
||||
"/freecad-user/cache",
|
||||
"/freecad-user/temp",
|
||||
};
|
||||
if (!path || std::none_of(std::begin(allowed), std::end(allowed), [path](const char* entry) {
|
||||
return std::strcmp(path, entry) == 0;
|
||||
})) {
|
||||
return nullptr;
|
||||
}
|
||||
const size_t length = std::strlen(path);
|
||||
if (length >= PATH_MAX) {
|
||||
return nullptr;
|
||||
}
|
||||
char* output = resolvedPath ? resolvedPath : static_cast<char*>(std::malloc(length + 1));
|
||||
if (!output) {
|
||||
return nullptr;
|
||||
}
|
||||
std::memcpy(output, path, length + 1);
|
||||
return output;
|
||||
}
|
||||
|
||||
extern "C" int getpwuid_r(uid_t uid,
|
||||
struct passwd* password,
|
||||
char* buffer,
|
||||
size_t bufferSize,
|
||||
struct passwd** result)
|
||||
{
|
||||
constexpr const char identity[] = "freecad\0/freecad-user\0/bin/false";
|
||||
if (bufferSize < sizeof(identity)) {
|
||||
*result = nullptr;
|
||||
return -1;
|
||||
}
|
||||
std::memcpy(buffer, identity, sizeof(identity));
|
||||
password->pw_name = buffer;
|
||||
password->pw_passwd = const_cast<char*>("");
|
||||
password->pw_uid = uid;
|
||||
password->pw_gid = getgid();
|
||||
password->pw_gecos = buffer;
|
||||
password->pw_dir = buffer + sizeof("freecad");
|
||||
password->pw_shell = password->pw_dir + sizeof("/freecad-user");
|
||||
*result = password;
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int ABI_VERSION = 1;
|
||||
constexpr const char* FREECAD_VERSION = "1.1.1";
|
||||
constexpr const char* FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d";
|
||||
|
||||
void ensureApplication()
|
||||
{
|
||||
static bool initialized = false;
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
char executable[] = "freecad-naming-bridge";
|
||||
char console[] = "--console";
|
||||
char* argv[] = {executable, console};
|
||||
App::Application::init(2, argv);
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
QJsonObject requireObject(const QJsonObject& object, const char* key)
|
||||
{
|
||||
const QJsonValue value = object.value(key);
|
||||
if (!value.isObject()) {
|
||||
throw std::runtime_error(std::string("FreeCAD naming request requires object ") + key);
|
||||
}
|
||||
return value.toObject();
|
||||
}
|
||||
|
||||
QString requireString(const QJsonObject& object, const char* key)
|
||||
{
|
||||
const QJsonValue value = object.value(key);
|
||||
if (!value.isString() || value.toString().isEmpty()) {
|
||||
throw std::runtime_error(std::string("FreeCAD naming request requires string ") + key);
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
long requirePositiveTag(const QJsonObject& object, const char* key)
|
||||
{
|
||||
const double value = object.value(key).toDouble(-1.0);
|
||||
if (!std::isfinite(value) || value <= 0.0 || std::floor(value) != value
|
||||
|| value > static_cast<double>(0x7fffffff)) {
|
||||
throw std::runtime_error(std::string("FreeCAD naming request requires positive tag ") + key);
|
||||
}
|
||||
return static_cast<long>(value);
|
||||
}
|
||||
|
||||
int requireIndex(const QJsonObject& object, const char* key)
|
||||
{
|
||||
const double value = object.value(key).toDouble(-1.0);
|
||||
if (!std::isfinite(value) || value < 0.0 || std::floor(value) != value
|
||||
|| value > static_cast<double>(0x7ffffffe)) {
|
||||
throw std::runtime_error(std::string("FreeCAD naming request requires non-negative index ")
|
||||
+ key);
|
||||
}
|
||||
return static_cast<int>(value);
|
||||
}
|
||||
|
||||
QString titleKind(const QString& value)
|
||||
{
|
||||
if (value == "face") {
|
||||
return "Face";
|
||||
}
|
||||
if (value == "edge") {
|
||||
return "Edge";
|
||||
}
|
||||
if (value == "vertex") {
|
||||
return "Vertex";
|
||||
}
|
||||
throw std::runtime_error("FreeCAD naming history supports only face, edge, and vertex");
|
||||
}
|
||||
|
||||
QString canonicalOperation(const QString& operation)
|
||||
{
|
||||
QString output;
|
||||
output.reserve(operation.size());
|
||||
for (const QChar character : operation) {
|
||||
if (character.isLetterOrNumber()) {
|
||||
output.append(character.toUpper());
|
||||
}
|
||||
else if (character == '-') {
|
||||
output.append('_');
|
||||
}
|
||||
else {
|
||||
throw std::runtime_error("FreeCAD naming operation contains an unsupported character");
|
||||
}
|
||||
}
|
||||
if (output.isEmpty()) {
|
||||
throw std::runtime_error("FreeCAD naming operation is empty");
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
QJsonObject parseNameToken(const std::string& raw)
|
||||
{
|
||||
if (raw.empty() || (raw[0] != ':' && raw[0] != '$' && raw[0] != ';')) {
|
||||
throw std::runtime_error("FreeCAD ElementMap emitted an invalid name token");
|
||||
}
|
||||
std::vector<std::string> parts;
|
||||
std::size_t start = 0;
|
||||
while (true) {
|
||||
const std::size_t dot = raw.find('.', start);
|
||||
parts.push_back(raw.substr(start, dot == std::string::npos ? dot : dot - start));
|
||||
if (dot == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
start = dot + 1;
|
||||
}
|
||||
const QString marker = QString(QChar(raw[0]));
|
||||
QJsonObject token {{"raw", QString::fromStdString(raw)}, {"marker", marker}};
|
||||
QJsonArray suffix;
|
||||
if (raw[0] == ':') {
|
||||
if (parts.size() < 3) {
|
||||
throw std::runtime_error("FreeCAD ElementMap emitted a truncated indexed token");
|
||||
}
|
||||
bool postfixOk = false;
|
||||
bool indexOk = false;
|
||||
const int postfixIndex = QString::fromStdString(parts[0].substr(1)).toInt(&postfixOk, 16);
|
||||
const int elementIndex = QString::fromStdString(parts[1]).toInt(&indexOk, 16);
|
||||
if (!postfixOk || !indexOk) {
|
||||
throw std::runtime_error("FreeCAD ElementMap emitted a malformed indexed token");
|
||||
}
|
||||
token.insert("postfixIndex", postfixIndex);
|
||||
token.insert("elementIndex", elementIndex);
|
||||
for (std::size_t index = 2; index < parts.size(); ++index) {
|
||||
suffix.append(QString::fromStdString(parts[index]));
|
||||
}
|
||||
}
|
||||
else {
|
||||
token.insert("name", QString::fromStdString(parts[0].substr(1)));
|
||||
for (std::size_t index = 1; index < parts.size(); ++index) {
|
||||
suffix.append(QString::fromStdString(parts[index]));
|
||||
}
|
||||
}
|
||||
token.insert("suffix", suffix);
|
||||
return token;
|
||||
}
|
||||
|
||||
QJsonObject parseElementMap(const std::string& saved)
|
||||
{
|
||||
std::istringstream stream(saved);
|
||||
unsigned rootId = 0;
|
||||
int postfixCount = 0;
|
||||
std::string label;
|
||||
if (!(stream >> rootId >> label >> postfixCount) || rootId == 0 || label != "PostfixCount"
|
||||
|| postfixCount < 0) {
|
||||
throw std::runtime_error("FreeCAD ElementMap root header is invalid");
|
||||
}
|
||||
QJsonArray postfixes;
|
||||
for (int index = 0; index < postfixCount; ++index) {
|
||||
std::string postfix;
|
||||
if (!(stream >> postfix)) {
|
||||
throw std::runtime_error("FreeCAD ElementMap postfix list is truncated");
|
||||
}
|
||||
postfixes.append(QString::fromStdString(postfix));
|
||||
}
|
||||
int mapCount = 0;
|
||||
if (!(stream >> label >> mapCount) || label != "MapCount" || mapCount <= 0) {
|
||||
throw std::runtime_error("FreeCAD ElementMap map count is invalid");
|
||||
}
|
||||
QJsonArray maps;
|
||||
int rootMapIndex = 0;
|
||||
for (int mapOrdinal = 0; mapOrdinal < mapCount; ++mapOrdinal) {
|
||||
int mapIndex = 0;
|
||||
unsigned mapId = 0;
|
||||
int typeCount = 0;
|
||||
if (!(stream >> label >> mapIndex >> mapId >> typeCount) || label != "ElementMap"
|
||||
|| mapIndex <= 0 || typeCount < 0) {
|
||||
throw std::runtime_error("FreeCAD ElementMap map header is invalid");
|
||||
}
|
||||
rootMapIndex = std::max(rootMapIndex, mapIndex);
|
||||
QJsonArray sections;
|
||||
for (int typeOrdinal = 0; typeOrdinal < typeCount; ++typeOrdinal) {
|
||||
std::string sectionName;
|
||||
int childCount = 0;
|
||||
if (!(stream >> sectionName >> label >> childCount) || label != "ChildCount"
|
||||
|| childCount != 0) {
|
||||
throw std::runtime_error("Candidate bridge does not accept child ElementMap records");
|
||||
}
|
||||
int nameCount = 0;
|
||||
if (!(stream >> label >> nameCount) || label != "NameCount" || nameCount < 0) {
|
||||
throw std::runtime_error("FreeCAD ElementMap name count is invalid");
|
||||
}
|
||||
QJsonArray names;
|
||||
for (int nameOrdinal = 0; nameOrdinal < nameCount; ++nameOrdinal) {
|
||||
QJsonArray tokens;
|
||||
std::string raw;
|
||||
std::string token;
|
||||
while (stream >> token) {
|
||||
if (token == "0") {
|
||||
break;
|
||||
}
|
||||
if (!raw.empty()) {
|
||||
raw += ' ';
|
||||
}
|
||||
raw += token;
|
||||
tokens.append(parseNameToken(token));
|
||||
}
|
||||
if (!stream) {
|
||||
throw std::runtime_error("FreeCAD ElementMap name entry is truncated");
|
||||
}
|
||||
names.append(QJsonObject {
|
||||
{"tokens", tokens},
|
||||
{"trailing", "0"},
|
||||
{"raw", QString::fromStdString(raw.empty() ? "0" : raw + " 0")},
|
||||
});
|
||||
}
|
||||
sections.append(QJsonObject {
|
||||
{"name", QString::fromStdString(sectionName)},
|
||||
{"children", QJsonArray {}},
|
||||
{"names", names},
|
||||
});
|
||||
}
|
||||
if (!(stream >> label) || label != "EndMap") {
|
||||
throw std::runtime_error("FreeCAD ElementMap map terminator is missing");
|
||||
}
|
||||
maps.append(QJsonObject {
|
||||
{"index", mapIndex},
|
||||
{"id", static_cast<qint64>(mapId)},
|
||||
{"typeCount", typeCount},
|
||||
{"sections", sections},
|
||||
});
|
||||
}
|
||||
return QJsonObject {
|
||||
{"schemaVersion", 2},
|
||||
{"nativeVersion", 1},
|
||||
{"rootId", static_cast<qint64>(rootId)},
|
||||
{"postfixes", postfixes},
|
||||
{"maps", maps},
|
||||
{"rootMapIndex", rootMapIndex},
|
||||
};
|
||||
}
|
||||
|
||||
QJsonObject stringHasherJson(const App::StringHasherRef& hasher)
|
||||
{
|
||||
QJsonArray entries;
|
||||
for (const auto& [id, reference] : hasher->getIDMap()) {
|
||||
const App::StringID& stringId = reference.deref();
|
||||
int flags = 0;
|
||||
flags |= stringId.isBinary() ? 1 << 0 : 0;
|
||||
flags |= stringId.isHashed() ? 1 << 1 : 0;
|
||||
flags |= stringId.isPostfixEncoded() ? 1 << 2 : 0;
|
||||
flags |= stringId.isPostfixed() ? 1 << 3 : 0;
|
||||
flags |= stringId.isIndexed() ? 1 << 4 : 0;
|
||||
flags |= stringId.isPrefixID() ? 1 << 5 : 0;
|
||||
flags |= stringId.isPrefixIDIndex() ? 1 << 6 : 0;
|
||||
flags |= stringId.isPersistent() ? 1 << 7 : 0;
|
||||
QJsonArray relatedIds;
|
||||
for (const auto& related : reference.relatedIDs()) {
|
||||
relatedIds.append(static_cast<qint64>(related.value()));
|
||||
}
|
||||
entries.append(QJsonObject {
|
||||
{"id", static_cast<qint64>(id)},
|
||||
{"flags", flags},
|
||||
{"relatedIds", relatedIds},
|
||||
{"data", QString::fromUtf8(stringId.data())},
|
||||
{"postfix", QString::fromUtf8(stringId.postfix())},
|
||||
});
|
||||
}
|
||||
return QJsonObject {{"schemaVersion", 2}, {"nativeVersion", 1}, {"entries", entries}};
|
||||
}
|
||||
|
||||
int stringIdFlags(const App::StringID& stringId)
|
||||
{
|
||||
int flags = 0;
|
||||
flags |= stringId.isBinary() ? 1 << 0 : 0;
|
||||
flags |= stringId.isHashed() ? 1 << 1 : 0;
|
||||
flags |= stringId.isPostfixEncoded() ? 1 << 2 : 0;
|
||||
flags |= stringId.isPostfixed() ? 1 << 3 : 0;
|
||||
flags |= stringId.isIndexed() ? 1 << 4 : 0;
|
||||
flags |= stringId.isPrefixID() ? 1 << 5 : 0;
|
||||
flags |= stringId.isPrefixIDIndex() ? 1 << 6 : 0;
|
||||
flags |= stringId.isPersistent() ? 1 << 7 : 0;
|
||||
return flags;
|
||||
}
|
||||
|
||||
long requireStringId(const QJsonValue& value, const char* label)
|
||||
{
|
||||
const double number = value.toDouble(-1.0);
|
||||
if (!std::isfinite(number) || number <= 0.0 || std::floor(number) != number
|
||||
|| number > static_cast<double>(0x7fffffff)) {
|
||||
throw std::runtime_error(std::string("Prior StringHasher requires positive ") + label);
|
||||
}
|
||||
return static_cast<long>(number);
|
||||
}
|
||||
|
||||
void restoreStringHasherTable(const QJsonObject& table, const App::StringHasherRef& hasher)
|
||||
{
|
||||
if (table.value("schemaVersion").toInt() != 2
|
||||
|| table.value("nativeVersion").toInt() != 1) {
|
||||
throw std::runtime_error("Prior StringHasher schema or native version is unsupported");
|
||||
}
|
||||
const QJsonArray entries = table.value("entries").toArray();
|
||||
if (entries.size() > 1000000) {
|
||||
throw std::runtime_error("Prior StringHasher entry count exceeds the candidate limit");
|
||||
}
|
||||
long previousId = 0;
|
||||
for (const QJsonValue entryValue : entries) {
|
||||
if (!entryValue.isObject()) {
|
||||
throw std::runtime_error("Prior StringHasher entry must be an object");
|
||||
}
|
||||
const QJsonObject entry = entryValue.toObject();
|
||||
const long id = requireStringId(entry.value("id"), "entry ID");
|
||||
if (id != previousId + 1) {
|
||||
throw std::runtime_error("Prior StringHasher IDs must be contiguous and ordered");
|
||||
}
|
||||
const int flags = entry.value("flags").toInt(-1);
|
||||
constexpr int knownFlags = 0xff;
|
||||
if (flags < 0 || (flags & ~knownFlags) != 0) {
|
||||
throw std::runtime_error("Prior StringHasher entry contains unknown flags");
|
||||
}
|
||||
if ((flags & ((1 << 0) | (1 << 1))) != 0) {
|
||||
throw std::runtime_error(
|
||||
"Candidate bridge cannot losslessly restore binary or one-way-hashed StringHasher entries");
|
||||
}
|
||||
if (!entry.value("data").isString() || !entry.value("postfix").isString()
|
||||
|| !entry.value("relatedIds").isArray()) {
|
||||
throw std::runtime_error("Prior StringHasher entry payload is incomplete");
|
||||
}
|
||||
const QByteArray data = entry.value("data").toString().toUtf8();
|
||||
const QByteArray postfix = entry.value("postfix").toString().toUtf8();
|
||||
const QJsonArray relatedValues = entry.value("relatedIds").toArray();
|
||||
std::vector<long> relatedIds;
|
||||
relatedIds.reserve(relatedValues.size());
|
||||
for (const QJsonValue relatedValue : relatedValues) {
|
||||
const long relatedId = requireStringId(relatedValue, "related ID");
|
||||
if (relatedId >= id || !hasher->getID(relatedId)) {
|
||||
throw std::runtime_error("Prior StringHasher related ID is unresolved or forward-referenced");
|
||||
}
|
||||
relatedIds.push_back(relatedId);
|
||||
}
|
||||
|
||||
App::StringIDRef restored;
|
||||
if ((flags & (1 << 3)) == 0) {
|
||||
if (flags != 0 && flags != (1 << 7)) {
|
||||
throw std::runtime_error("Prior non-postfixed StringHasher flags cannot be reconstructed");
|
||||
}
|
||||
restored = hasher->getID(data, App::StringHasher::Option::None);
|
||||
}
|
||||
else {
|
||||
QByteArray mappedData = data;
|
||||
if ((flags & ((1 << 4) | (1 << 6))) != 0) {
|
||||
mappedData += '1';
|
||||
}
|
||||
Data::MappedName mapped(mappedData);
|
||||
if (!postfix.isEmpty()) {
|
||||
mapped += postfix;
|
||||
}
|
||||
int internalCount = 0;
|
||||
internalCount += (flags & (1 << 2)) != 0 ? 1 : 0;
|
||||
internalCount += (flags & (1 << 4)) != 0 ? 1 : 0;
|
||||
if (internalCount > static_cast<int>(relatedIds.size())) {
|
||||
throw std::runtime_error("Prior StringHasher internal dependencies are truncated");
|
||||
}
|
||||
Data::ElementIDRefs externalRefs;
|
||||
for (std::size_t index = static_cast<std::size_t>(internalCount);
|
||||
index < relatedIds.size(); ++index) {
|
||||
externalRefs.push_back(hasher->getID(relatedIds[index]));
|
||||
}
|
||||
restored = hasher->getID(mapped, externalRefs);
|
||||
}
|
||||
if (!restored || restored.value() != id) {
|
||||
throw std::runtime_error("Prior StringHasher entry did not restore to its persistent ID");
|
||||
}
|
||||
if ((flags & (1 << 7)) != 0) {
|
||||
restored.setPersistent(true);
|
||||
}
|
||||
const App::StringID& actual = restored.deref();
|
||||
std::vector<long> actualRelated;
|
||||
for (const App::StringIDRef& related : actual.relatedIDs()) {
|
||||
actualRelated.push_back(related.value());
|
||||
}
|
||||
if (stringIdFlags(actual) != flags || actual.data() != data || actual.postfix() != postfix
|
||||
|| actualRelated != relatedIds) {
|
||||
throw std::runtime_error("Prior StringHasher entry changed during strict restoration");
|
||||
}
|
||||
previousId = id;
|
||||
}
|
||||
}
|
||||
|
||||
void restorePriorStringHasher(const QJsonArray& inputs, const App::StringHasherRef& hasher)
|
||||
{
|
||||
QByteArray canonical;
|
||||
QJsonObject selected;
|
||||
for (const QJsonValue inputValue : inputs) {
|
||||
const QJsonObject evidence = inputValue.toObject().value("namingEvidence").toObject();
|
||||
if (evidence.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
const QJsonValue tableValue = evidence.value("stringHasher");
|
||||
if (!tableValue.isObject()) {
|
||||
const QJsonArray mappedNames = evidence.value("mappedNames").toArray();
|
||||
const bool requiresHasher = std::any_of(
|
||||
mappedNames.begin(), mappedNames.end(), [](const QJsonValue& mappedValue) {
|
||||
const QJsonObject reference = mappedValue.toObject().value("reference").toObject();
|
||||
return !reference.value("stringIds").toArray().isEmpty()
|
||||
|| reference.contains("prefixStringId");
|
||||
});
|
||||
if (requiresHasher) {
|
||||
throw std::runtime_error("Prior mapped-name IDs require StringHasher evidence");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const QJsonObject table = tableValue.toObject();
|
||||
const QByteArray encoded = QJsonDocument(table).toJson(QJsonDocument::Compact);
|
||||
if (canonical.isEmpty()) {
|
||||
canonical = encoded;
|
||||
selected = table;
|
||||
}
|
||||
else if (canonical != encoded) {
|
||||
throw std::runtime_error("Candidate bridge cannot merge inconsistent prior StringHasher tables");
|
||||
}
|
||||
}
|
||||
if (!selected.isEmpty()) {
|
||||
restoreStringHasherTable(selected, hasher);
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject findSourceInput(const QJsonArray& inputs, const QString& source)
|
||||
{
|
||||
for (const QJsonValue value : inputs) {
|
||||
const QJsonObject input = value.toObject();
|
||||
if (input.value("inputId").toString() == source || input.value("role").toString() == source) {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
if (source == "object" && !inputs.isEmpty()) {
|
||||
return inputs.first().toObject();
|
||||
}
|
||||
if (source == "tool" && inputs.size() > 1) {
|
||||
return inputs.at(1).toObject();
|
||||
}
|
||||
throw std::runtime_error("FreeCAD naming history references an unknown input");
|
||||
}
|
||||
|
||||
struct SourceName
|
||||
{
|
||||
Data::MappedName name;
|
||||
QString persistentId;
|
||||
};
|
||||
|
||||
SourceName sourceNameFor(const QJsonObject& input,
|
||||
const QString& kind,
|
||||
int sourceIndex,
|
||||
const App::StringHasherRef& hasher)
|
||||
{
|
||||
const QString indexed = titleKind(kind) + QString::number(sourceIndex + 1);
|
||||
const QJsonValue evidenceValue = input.value("namingEvidence");
|
||||
if (!evidenceValue.isObject()) {
|
||||
return {Data::MappedName(indexed.toStdString()), indexed};
|
||||
}
|
||||
const QJsonArray mappedNames = evidenceValue.toObject().value("mappedNames").toArray();
|
||||
for (const QJsonValue value : mappedNames) {
|
||||
const QJsonObject mapped = value.toObject();
|
||||
if (mapped.value("kind").toString() != kind
|
||||
|| mapped.value("resultIndex").toInt(-1) != sourceIndex) {
|
||||
continue;
|
||||
}
|
||||
const QJsonObject reference = requireObject(mapped, "reference");
|
||||
const QJsonArray stringIds = reference.value("stringIds").toArray();
|
||||
for (const QJsonValue idValue : stringIds) {
|
||||
if (!hasher->getID(requireStringId(idValue, "mapped-name string ID"))) {
|
||||
throw std::runtime_error("Prior mapped-name string ID is absent from StringHasher");
|
||||
}
|
||||
}
|
||||
if (reference.contains("prefixStringId")) {
|
||||
const long prefixId = requireStringId(reference.value("prefixStringId"),
|
||||
"mapped-name prefix ID");
|
||||
if (!hasher->getID(prefixId)) {
|
||||
throw std::runtime_error("Prior mapped-name prefix ID is absent from StringHasher");
|
||||
}
|
||||
}
|
||||
Data::MappedName name(requireString(reference, "name").toStdString());
|
||||
const QString postfix = reference.value("postfix").toString();
|
||||
if (!postfix.isEmpty()) {
|
||||
name += postfix.toStdString();
|
||||
}
|
||||
return {name, mapped.value("resultPersistentId").toString(indexed)};
|
||||
}
|
||||
throw std::runtime_error("Prior naming evidence has no matching source subshape");
|
||||
}
|
||||
|
||||
QJsonObject mappedReference(const Data::MappedName& name, const Data::ElementIDRefs& stringIds)
|
||||
{
|
||||
QJsonArray ids;
|
||||
for (const App::StringIDRef& stringId : stringIds) {
|
||||
ids.append(static_cast<qint64>(stringId.value()));
|
||||
}
|
||||
QJsonObject reference {
|
||||
{"name", QString::fromUtf8(name.dataBytes())},
|
||||
{"postfix", QString::fromUtf8(name.postfixBytes())},
|
||||
{"stringIds", ids},
|
||||
};
|
||||
const App::StringID::IndexID prefix = App::StringID::fromString(name.dataBytes());
|
||||
if (prefix) {
|
||||
reference.insert("prefixStringId", static_cast<qint64>(prefix.id));
|
||||
}
|
||||
return reference;
|
||||
}
|
||||
|
||||
std::string evidenceJsonImpl(const std::string& requestJson)
|
||||
{
|
||||
ensureApplication();
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument document =
|
||||
QJsonDocument::fromJson(QByteArray::fromStdString(requestJson), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
|
||||
throw std::runtime_error("FreeCAD naming request is not valid JSON");
|
||||
}
|
||||
const QJsonObject request = document.object();
|
||||
if (request.value("schemaVersion").toInt() != ABI_VERSION) {
|
||||
throw std::runtime_error("FreeCAD naming request schemaVersion is unsupported");
|
||||
}
|
||||
const QString stageId = requireString(request, "stageId");
|
||||
const QString resultObjectId = requireString(request, "resultObjectId");
|
||||
const long resultTag = requirePositiveTag(request, "resultObjectTag");
|
||||
const QString operation = requireString(request, "operation");
|
||||
const QString operationPostfix = canonicalOperation(operation);
|
||||
const QJsonArray inputs = request.value("inputs").toArray();
|
||||
const QJsonObject history = requireObject(request, "history");
|
||||
const QJsonArray records = history.value("records").toArray();
|
||||
if (inputs.isEmpty() || records.isEmpty()) {
|
||||
throw std::runtime_error("FreeCAD naming request requires inputs and native history records");
|
||||
}
|
||||
|
||||
App::StringHasherRef hasher(new App::StringHasher());
|
||||
hasher->setSaveAll(true);
|
||||
restorePriorStringHasher(inputs, hasher);
|
||||
auto elementMap = std::make_shared<Data::ElementMap>();
|
||||
elementMap->hasher = hasher;
|
||||
QJsonArray mappedNames;
|
||||
std::set<std::string> resultKeys;
|
||||
std::map<std::string, int> sourceOrdinals;
|
||||
|
||||
for (const QJsonValue recordValue : records) {
|
||||
const QJsonObject record = recordValue.toObject();
|
||||
const QString relation = requireString(record, "relation");
|
||||
if (relation == "deleted") {
|
||||
continue;
|
||||
}
|
||||
if (relation != "modified" && relation != "generated") {
|
||||
throw std::runtime_error("FreeCAD naming history relation is unsupported");
|
||||
}
|
||||
const QString sourceKind = requireString(record, "kind");
|
||||
const QString resultKind = record.value("resultKind").toString(sourceKind);
|
||||
titleKind(sourceKind);
|
||||
const QString resultType = titleKind(resultKind);
|
||||
const int sourceIndex = requireIndex(record, "sourceIndex");
|
||||
const QString source = record.value("sourceId").toString(
|
||||
requireString(record, "source"));
|
||||
const QJsonObject input = findSourceInput(inputs, source);
|
||||
const QString sourceObjectId = requireString(input, "objectId");
|
||||
const long sourceTag = requirePositiveTag(input, "objectTag");
|
||||
const SourceName sourceName = sourceNameFor(input, sourceKind, sourceIndex, hasher);
|
||||
QJsonArray resultIndexes = record.value("resultIndexes").toArray();
|
||||
if (resultIndexes.isEmpty() && record.contains("resultIndex")) {
|
||||
resultIndexes.append(record.value("resultIndex"));
|
||||
}
|
||||
if (resultIndexes.isEmpty()) {
|
||||
throw std::runtime_error("Non-deleted FreeCAD naming history requires result indexes");
|
||||
}
|
||||
const std::string ordinalKey = (sourceObjectId + '|' + sourceKind + '|'
|
||||
+ QString::number(sourceIndex) + '|' + relation)
|
||||
.toStdString();
|
||||
for (const QJsonValue resultValue : resultIndexes) {
|
||||
const int resultIndex = requireIndex(QJsonObject {{"value", resultValue}}, "value");
|
||||
const std::string resultKey =
|
||||
(resultKind + ':' + QString::number(resultIndex)).toStdString();
|
||||
if (!resultKeys.insert(resultKey).second) {
|
||||
throw std::runtime_error("FreeCAD naming history maps the same result subshape twice");
|
||||
}
|
||||
const int ordinal = ++sourceOrdinals[ordinalKey];
|
||||
std::ostringstream postfix;
|
||||
postfix << (relation == "modified" ? Data::POSTFIX_MOD : Data::POSTFIX_GEN);
|
||||
if (ordinal > 1) {
|
||||
postfix << ordinal;
|
||||
}
|
||||
Data::MappedName encoded(sourceName.name);
|
||||
Data::ElementIDRefs stringIds;
|
||||
elementMap->encodeElementName(
|
||||
resultType.at(0).toLatin1(),
|
||||
encoded,
|
||||
postfix,
|
||||
&stringIds,
|
||||
resultTag,
|
||||
operationPostfix.toUtf8().constData(),
|
||||
sourceTag);
|
||||
const Data::IndexedName resultIndexed(
|
||||
(resultType + QString::number(resultIndex + 1)).toUtf8());
|
||||
const Data::MappedName stored =
|
||||
elementMap->setElementName(resultIndexed, encoded, resultTag, &stringIds);
|
||||
if (!stored) {
|
||||
throw std::runtime_error("FreeCAD ElementMap rejected a native history mapping");
|
||||
}
|
||||
mappedNames.append(QJsonObject {
|
||||
{"kind", resultKind},
|
||||
{"resultIndex", resultIndex},
|
||||
{"resultPersistentId", resultType + QString::number(resultIndex + 1)},
|
||||
{"relation", relation},
|
||||
{"reference", mappedReference(stored, stringIds)},
|
||||
{"sourceRefs", QJsonArray {QJsonObject {
|
||||
{"objectId", sourceObjectId},
|
||||
{"persistentId", sourceName.persistentId},
|
||||
{"stageId", input.value("stageId")},
|
||||
}}},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (mappedNames.isEmpty()) {
|
||||
throw std::runtime_error("FreeCAD naming history has no result subshape evidence");
|
||||
}
|
||||
|
||||
elementMap->beforeSave(hasher);
|
||||
std::ostringstream saved;
|
||||
elementMap->save(saved);
|
||||
const QJsonObject response {
|
||||
{"schemaVersion", ABI_VERSION},
|
||||
{"stageId", stageId},
|
||||
{"resultObjectId", resultObjectId},
|
||||
{"status", "native-evidence"},
|
||||
{"mappedNames", mappedNames},
|
||||
{"stringHasher", stringHasherJson(hasher)},
|
||||
{"elementMap2", parseElementMap(saved.str())},
|
||||
};
|
||||
return QJsonDocument(response).toJson(QJsonDocument::Compact).toStdString();
|
||||
}
|
||||
|
||||
std::string evidenceJson(const std::string& requestJson)
|
||||
{
|
||||
try {
|
||||
return evidenceJsonImpl(requestJson);
|
||||
}
|
||||
catch (const Base::Exception& exception) {
|
||||
return QJsonDocument(QJsonObject {
|
||||
{"schemaVersion", ABI_VERSION},
|
||||
{"status", "error"},
|
||||
{"error", QString::fromStdString("FreeCAD private naming failure: "
|
||||
+ exception.getMessage())},
|
||||
}).toJson(QJsonDocument::Compact).toStdString();
|
||||
}
|
||||
catch (const std::exception& exception) {
|
||||
return QJsonDocument(QJsonObject {
|
||||
{"schemaVersion", ABI_VERSION},
|
||||
{"status", "error"},
|
||||
{"error", QString::fromUtf8(exception.what())},
|
||||
}).toJson(QJsonDocument::Compact).toStdString();
|
||||
}
|
||||
catch (...) {
|
||||
return R"({"schemaVersion":1,"status":"error","error":"Unknown native exception"})";
|
||||
}
|
||||
}
|
||||
|
||||
int freecadNamingAbiVersion()
|
||||
{
|
||||
return ABI_VERSION;
|
||||
}
|
||||
|
||||
std::string freecadNamingCapabilitiesJson()
|
||||
{
|
||||
return QJsonDocument(QJsonObject {
|
||||
{"schemaVersion", ABI_VERSION},
|
||||
{"freecadVersion", FREECAD_VERSION},
|
||||
{"sourceCommit", FREECAD_COMMIT},
|
||||
{"mappedNameRef", true},
|
||||
{"stringHasher", true},
|
||||
{"elementMap2", true},
|
||||
{"operations", QJsonArray {
|
||||
"fuse", "cut", "common", "rotate", "pad", "pocket", "loft", "pipe",
|
||||
"revolution", "groove", "fillet", "chamfer", "hole", "draft", "thickness",
|
||||
"linear-pattern", "polar-pattern", "mirrored", "multi-transform",
|
||||
}},
|
||||
{"maxRequestBytes", 16 * 1024 * 1024},
|
||||
{"maxResponseBytes", 32 * 1024 * 1024},
|
||||
}).toJson(QJsonDocument::Compact).toStdString();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
EMSCRIPTEN_BINDINGS(freecad_private_naming_bridge)
|
||||
{
|
||||
emscripten::function("freecadNamingAbiVersion", &freecadNamingAbiVersion);
|
||||
emscripten::function("freecadNamingCapabilitiesJson", &freecadNamingCapabilitiesJson);
|
||||
emscripten::function("freecadNamingEvidenceJson", &evidenceJson);
|
||||
}
|
||||
26
native/freecad-naming-bridge/pre.js
Normal file
26
native/freecad-naming-bridge/pre.js
Normal file
@@ -0,0 +1,26 @@
|
||||
Module.preRun = Module.preRun || [];
|
||||
Module.preRun.push(() => {
|
||||
for (const path of [
|
||||
'/freecad-user',
|
||||
'/freecad-user/config',
|
||||
'/freecad-user/data',
|
||||
'/freecad-user/cache',
|
||||
'/freecad-user/temp',
|
||||
]) {
|
||||
try {
|
||||
FS.mkdir(path)
|
||||
} catch (error) {
|
||||
if (!FS.analyzePath(path).exists) throw error
|
||||
}
|
||||
}
|
||||
Object.assign(ENV, {
|
||||
HOME: '/freecad-user',
|
||||
FREECAD_USER_HOME: '/freecad-user',
|
||||
FREECAD_USER_DATA: '/freecad-user/data',
|
||||
FREECAD_USER_TEMP: '/freecad-user/temp',
|
||||
XDG_CONFIG_HOME: '/freecad-user/config',
|
||||
XDG_DATA_HOME: '/freecad-user/data',
|
||||
XDG_CACHE_HOME: '/freecad-user/cache',
|
||||
TMPDIR: '/freecad-user/temp',
|
||||
})
|
||||
})
|
||||
185
native/freecad-naming-bridge/smoke-test.ts
Normal file
185
native/freecad-naming-bridge/smoke-test.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import createBridge from './dist/freecad-private-naming-bridge.js'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import {
|
||||
captureFreeCadPrivateNamingEvidence,
|
||||
createFreeCadPrivateNamingAbiRequest,
|
||||
probeFreeCadPrivateNamingAbi,
|
||||
type NativeFreeCadNamingAbiModule,
|
||||
} from '../../src/facade/nativeNamingAbi'
|
||||
|
||||
const distUrl = new URL('./dist/', import.meta.url)
|
||||
const bridge = await createBridge({ locateFile: (path: string) => fileURLToPath(new URL(path, distUrl)) }) as NativeFreeCadNamingAbiModule
|
||||
const probe = probeFreeCadPrivateNamingAbi(bridge)
|
||||
if (probe.availability !== 'available') throw new Error(`Isolated bridge ABI probe failed: ${probe.reason}`)
|
||||
|
||||
const request = createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'isolated-bridge-1',
|
||||
documentId: 'isolated-document',
|
||||
documentVersion: 1,
|
||||
operationId: 'isolated-cut-1',
|
||||
operation: 'cut',
|
||||
stageId: 'isolated:stage:1',
|
||||
resultObjectId: 'isolated:result:1',
|
||||
resultObjectTag: 99,
|
||||
inputs: [
|
||||
{ inputId: 'object', objectId: 'source-object', role: 'object', step: 'ISO-10303-21; object', objectTag: 42 },
|
||||
{ inputId: 'tool', objectId: 'source-tool', role: 'tool', step: 'ISO-10303-21; tool', objectTag: 43 },
|
||||
],
|
||||
stages: [{ stageId: 'isolated:stage:1', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }],
|
||||
resultStep: 'ISO-10303-21; result',
|
||||
history: {
|
||||
provider: 'occt-native',
|
||||
occtVersion: '8.0.0',
|
||||
hasModified: true,
|
||||
hasGenerated: false,
|
||||
hasDeleted: false,
|
||||
records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 0 }],
|
||||
},
|
||||
})
|
||||
const first = captureFreeCadPrivateNamingEvidence(bridge, request, probe)
|
||||
if (!first || first.status !== 'native-evidence' || first.mappedNames?.length !== 1 || !first.elementMap2 || !first.stringHasher) throw new Error('Isolated bridge returned incomplete first-stage evidence.')
|
||||
if (!first.mappedNames[0].reference.postfix?.includes(';:M;CUT;:H2a:7,F')) throw new Error(`Unexpected first-stage FreeCAD postfix: ${first.mappedNames[0].reference.postfix}`)
|
||||
|
||||
const chained = captureFreeCadPrivateNamingEvidence(bridge, createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'isolated-bridge-2',
|
||||
documentId: 'isolated-document',
|
||||
documentVersion: 2,
|
||||
operationId: 'isolated-cut-2',
|
||||
operation: 'cut',
|
||||
stageId: 'isolated:stage:2',
|
||||
resultObjectId: 'isolated:result:2',
|
||||
resultObjectTag: 100,
|
||||
inputs: [{ inputId: 'object', objectId: 'isolated:result:1', role: 'object', stageId: 'isolated:stage:1', step: 'ISO-10303-21; first result', objectTag: 99, namingEvidence: first }],
|
||||
stages: [{ stageId: 'isolated:stage:2', operation: 'cut', inputIds: ['object'], ordinal: 0 }],
|
||||
resultStep: 'ISO-10303-21; second result',
|
||||
history: {
|
||||
provider: 'occt-native',
|
||||
occtVersion: '8.0.0',
|
||||
hasModified: true,
|
||||
hasGenerated: false,
|
||||
hasDeleted: false,
|
||||
records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 1 }],
|
||||
},
|
||||
}), probe)
|
||||
if (!chained || chained.elementMap2?.maps.length !== 1) throw new Error('Isolated bridge did not preserve chained ElementMap2 evidence.')
|
||||
const chainedReference = chained.mappedNames?.[0].reference
|
||||
const prefixStringId = chainedReference?.prefixStringId
|
||||
const stringHasherIds = new Set(chained.stringHasher?.entries.map(({ id }) => id))
|
||||
const chainedTokens = chained.elementMap2.maps.flatMap((map) => map.sections.flatMap((section) => section.names.flatMap((name) => name.tokens)))
|
||||
if (!chainedReference?.name.startsWith('#') || !Number.isSafeInteger(prefixStringId) || !chainedReference.stringIds?.includes(prefixStringId!) || !stringHasherIds.has(prefixStringId!) || !chainedTokens.some((token) => token.marker === '$' && token.name === chainedReference.name)) throw new Error('Isolated bridge did not emit a closed native hashed MappedNameRef/StringHasher/ElementMap2 chain.')
|
||||
|
||||
const third = captureFreeCadPrivateNamingEvidence(bridge, createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'isolated-bridge-3',
|
||||
documentId: 'isolated-document',
|
||||
documentVersion: 3,
|
||||
operationId: 'isolated-cut-3',
|
||||
operation: 'cut',
|
||||
stageId: 'isolated:stage:3',
|
||||
resultObjectId: 'isolated:result:3',
|
||||
resultObjectTag: 101,
|
||||
inputs: [{ inputId: 'object', objectId: 'isolated:result:2', role: 'object', stageId: 'isolated:stage:2', step: 'ISO-10303-21; second result', objectTag: 100, namingEvidence: chained }],
|
||||
stages: [{ stageId: 'isolated:stage:3', operation: 'cut', inputIds: ['object'], ordinal: 0 }],
|
||||
resultStep: 'ISO-10303-21; third result',
|
||||
history: {
|
||||
provider: 'occt-native',
|
||||
occtVersion: '8.0.0',
|
||||
hasModified: true,
|
||||
hasGenerated: false,
|
||||
hasDeleted: false,
|
||||
records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 1, resultIndex: 2 }],
|
||||
},
|
||||
}), probe)
|
||||
const thirdReference = third?.mappedNames?.[0]?.reference
|
||||
const thirdHasherIds = new Set(third?.stringHasher?.entries.map(({ id }) => id))
|
||||
if (!third || third.status !== 'native-evidence' || !thirdReference?.name.startsWith('#') || !thirdReference.stringIds?.every((id) => thirdHasherIds.has(id)) || (third.stringHasher?.entries.length ?? 0) <= (chained.stringHasher?.entries.length ?? 0)) throw new Error('Isolated bridge did not restore prior StringHasher evidence for a third naming stage.')
|
||||
|
||||
let tamperedHasherRejected = false
|
||||
try {
|
||||
const validRequest = createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'isolated-bridge-tampered',
|
||||
documentId: 'isolated-document',
|
||||
documentVersion: 3,
|
||||
operationId: 'isolated-cut-tampered',
|
||||
operation: 'cut',
|
||||
stageId: 'isolated:stage:tampered',
|
||||
resultObjectId: 'isolated:result:tampered',
|
||||
resultObjectTag: 102,
|
||||
inputs: [{ inputId: 'object', objectId: 'isolated:result:2', role: 'object', stageId: 'isolated:stage:2', step: 'ISO-10303-21; second result', objectTag: 100, namingEvidence: chained }],
|
||||
stages: [{ stageId: 'isolated:stage:tampered', operation: 'cut', inputIds: ['object'], ordinal: 0 }],
|
||||
resultStep: 'ISO-10303-21; tampered result',
|
||||
history: { provider: 'occt-native', occtVersion: '8.0.0', hasModified: true, hasGenerated: false, hasDeleted: false, records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 1, resultIndex: 2 }] },
|
||||
})
|
||||
const tamperedRequest = structuredClone(validRequest)
|
||||
const tamperedEntry = tamperedRequest.inputs[0].namingEvidence?.stringHasher?.entries[0]
|
||||
if (!tamperedEntry) throw new Error('Chained evidence has no StringHasher entry to tamper.')
|
||||
tamperedEntry.id += 1
|
||||
const response = JSON.parse(bridge.freecadNamingEvidenceJson!(JSON.stringify(tamperedRequest))) as { status?: string; error?: string }
|
||||
if (response.status !== 'error' || !response.error?.includes('StringHasher IDs must be contiguous and ordered')) throw new Error(`Unexpected tampered StringHasher response: ${JSON.stringify(response)}`)
|
||||
tamperedHasherRejected = true
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
if (!tamperedHasherRejected) throw new Error('Isolated bridge accepted tampered prior StringHasher evidence.')
|
||||
|
||||
let inconsistentTablesRejected = false
|
||||
{
|
||||
const inconsistentRequest = createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'isolated-bridge-inconsistent',
|
||||
documentId: 'isolated-document',
|
||||
documentVersion: 3,
|
||||
operationId: 'isolated-cut-inconsistent',
|
||||
operation: 'cut',
|
||||
stageId: 'isolated:stage:inconsistent',
|
||||
resultObjectId: 'isolated:result:inconsistent',
|
||||
resultObjectTag: 103,
|
||||
inputs: [
|
||||
{ inputId: 'object', objectId: 'isolated:result:2', role: 'object', stageId: 'isolated:stage:2', step: 'ISO-10303-21; second result', objectTag: 100, namingEvidence: chained },
|
||||
{ inputId: 'tool', objectId: 'isolated:result:2-copy', role: 'tool', stageId: 'isolated:stage:2', step: 'ISO-10303-21; second result copy', objectTag: 104, namingEvidence: chained },
|
||||
],
|
||||
stages: [{ stageId: 'isolated:stage:inconsistent', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }],
|
||||
resultStep: 'ISO-10303-21; inconsistent result',
|
||||
history: { provider: 'occt-native', occtVersion: '8.0.0', hasModified: true, hasGenerated: false, hasDeleted: false, records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 1, resultIndex: 2 }] },
|
||||
})
|
||||
const secondTable = inconsistentRequest.inputs[1].namingEvidence?.stringHasher
|
||||
if (!secondTable?.entries[0]) throw new Error('Chained evidence has no StringHasher table to make inconsistent.')
|
||||
secondTable.entries[0].data += '-different'
|
||||
const response = JSON.parse(bridge.freecadNamingEvidenceJson!(JSON.stringify(inconsistentRequest))) as { status?: string; error?: string }
|
||||
if (response.status !== 'error' || !response.error?.includes('cannot merge inconsistent prior StringHasher tables')) throw new Error(`Unexpected inconsistent StringHasher response: ${JSON.stringify(response)}`)
|
||||
inconsistentTablesRejected = true
|
||||
}
|
||||
|
||||
let rejected = false
|
||||
try {
|
||||
captureFreeCadPrivateNamingEvidence(bridge, { ...request, history: { ...request.history, records: [] } }, probe)
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error) || !error.message.includes('requires inputs and native history records')) throw error
|
||||
rejected = true
|
||||
}
|
||||
if (!rejected) throw new Error('Isolated bridge accepted a request without native history records.')
|
||||
|
||||
const result = {
|
||||
status: 'freecad-private-naming-isolated-bridge-pass',
|
||||
descriptor: probe.descriptor,
|
||||
firstStage: { mappedNames: first.mappedNames.length, stringHasherEntries: first.stringHasher.entries.length },
|
||||
chainedStage: { mappedNames: chained.mappedNames.length, stringHasherEntries: chained.stringHasher.entries.length },
|
||||
thirdStage: { mappedNames: third.mappedNames.length, stringHasherEntries: third.stringHasher.entries.length },
|
||||
tamperedHasherRejected,
|
||||
inconsistentTablesRejected,
|
||||
productionPublication: false,
|
||||
productionWorkerLinked: false,
|
||||
}
|
||||
const reportPath = process.env.FREECAD_NAMING_BRIDGE_REPORT
|
||||
if (reportPath) {
|
||||
const artifacts = await Promise.all(['freecad-private-naming-bridge.js', 'freecad-private-naming-bridge.wasm', 'freecad-private-naming-bridge.data'].map(async (name) => {
|
||||
const path = fileURLToPath(new URL(name, distUrl))
|
||||
const [bytes, content] = await Promise.all([stat(path).then(({ size }) => size), readFile(path)])
|
||||
return { name, bytes, sha256: createHash('sha256').update(content).digest('hex') }
|
||||
}))
|
||||
const absoluteReportPath = resolve(reportPath)
|
||||
await mkdir(dirname(absoluteReportPath), { recursive: true })
|
||||
await writeFile(absoluteReportPath, `${JSON.stringify({ schemaVersion: 1, ...result, artifacts }, null, 2)}\n`)
|
||||
}
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
74
native/freecad-naming-probe/abi-candidate-smoke.ts
Normal file
74
native/freecad-naming-probe/abi-candidate-smoke.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import createProbe from './dist/freecad-private-naming-source-probe.js'
|
||||
import { parseElementMap2 } from '../../src/facade/elementMap2'
|
||||
import {
|
||||
captureFreeCadPrivateNamingEvidence,
|
||||
createFreeCadPrivateNamingAbiRequest,
|
||||
probeFreeCadPrivateNamingAbi,
|
||||
type NativeFreeCadNamingAbiModule,
|
||||
} from '../../src/facade/nativeNamingAbi'
|
||||
|
||||
type CandidateProbe = {
|
||||
freecadNamingCandidateAbiVersion(): number
|
||||
freecadNamingCandidateCapabilitiesJson(): string
|
||||
freecadNamingCandidateEvidenceJson(requestJson: string): string
|
||||
}
|
||||
|
||||
const native = await createProbe() as CandidateProbe
|
||||
const candidate: NativeFreeCadNamingAbiModule = {
|
||||
freecadNamingAbiVersion: () => native.freecadNamingCandidateAbiVersion(),
|
||||
freecadNamingCapabilitiesJson: () => native.freecadNamingCandidateCapabilitiesJson(),
|
||||
freecadNamingEvidenceJson: (requestJson) => {
|
||||
const response = JSON.parse(native.freecadNamingCandidateEvidenceJson(requestJson)) as Record<string, unknown>
|
||||
const elementMap2Text = response.elementMap2Text
|
||||
if (typeof elementMap2Text !== 'string') throw new TypeError('Candidate response omitted native ElementMap2 text.')
|
||||
delete response.elementMap2Text
|
||||
response.elementMap2 = parseElementMap2(elementMap2Text)
|
||||
return JSON.stringify(response)
|
||||
},
|
||||
}
|
||||
|
||||
const abiProbe = probeFreeCadPrivateNamingAbi(candidate)
|
||||
if (abiProbe.availability !== 'available') throw new Error(`Candidate ABI probe failed: ${abiProbe.reason}`)
|
||||
const request = createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'source-candidate-request',
|
||||
documentId: 'source-candidate-document',
|
||||
documentVersion: 1,
|
||||
operationId: 'source-candidate-cut',
|
||||
operation: 'cut',
|
||||
stageId: 'source-candidate:stage:0',
|
||||
resultObjectId: 'source-candidate:result',
|
||||
resultObjectTag: 99,
|
||||
inputs: [
|
||||
{ inputId: 'object', objectId: 'source-candidate:object', role: 'object', step: 'ISO-10303-21; probe object', objectTag: 42 },
|
||||
{ inputId: 'tool', objectId: 'source-candidate:tool', role: 'tool', step: 'ISO-10303-21; probe tool', objectTag: 43 },
|
||||
],
|
||||
stages: [{ stageId: 'source-candidate:stage:0', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }],
|
||||
resultStep: 'ISO-10303-21; probe result',
|
||||
history: {
|
||||
provider: 'occt-native',
|
||||
occtVersion: '8.0.0',
|
||||
hasModified: true,
|
||||
hasGenerated: false,
|
||||
hasDeleted: false,
|
||||
resultStep: 'ISO-10303-21; probe result',
|
||||
records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 2 }],
|
||||
},
|
||||
})
|
||||
const evidence = captureFreeCadPrivateNamingEvidence(candidate, request, abiProbe)
|
||||
if (!evidence || evidence.status !== 'native-evidence') throw new Error('Candidate ABI returned no native evidence.')
|
||||
if (evidence.mappedNames?.[0]?.reference.name !== '#1' || evidence.mappedNames[0].reference.postfix !== ';:H2a,F') throw new Error('Candidate ABI mapped-name reference is unexpected.')
|
||||
if (evidence.stringHasher?.entries.length !== 1 || evidence.elementMap2?.maps.length !== 1) throw new Error('Candidate ABI native resource evidence is incomplete.')
|
||||
|
||||
console.log(JSON.stringify({
|
||||
status: 'freecad-private-naming-abi-candidate-pass',
|
||||
descriptor: abiProbe.descriptor,
|
||||
evidence: {
|
||||
stageId: evidence.stageId,
|
||||
resultObjectId: evidence.resultObjectId,
|
||||
mappedNames: evidence.mappedNames.length,
|
||||
stringHasherEntries: evidence.stringHasher.entries.length,
|
||||
elementMaps: evidence.elementMap2.maps.length,
|
||||
},
|
||||
productionCallbacksExported: false,
|
||||
productionWorkerLinked: false,
|
||||
}, null, 2))
|
||||
@@ -1,10 +1,21 @@
|
||||
#include <emscripten/bind.h>
|
||||
|
||||
#include <App/IndexedName.h>
|
||||
#include <App/ElementMap.h>
|
||||
#include <App/ElementNamingUtils.h>
|
||||
#include <App/MappedName.h>
|
||||
#include <App/MappedElement.h>
|
||||
#include <App/StringHasher.h>
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -26,9 +37,248 @@ std::string freecadPrivateNamingSourceProbe()
|
||||
}
|
||||
return name.toString();
|
||||
}
|
||||
|
||||
std::string freecadPrivateStringHasherSourceProbe()
|
||||
{
|
||||
App::StringHasher hasher;
|
||||
hasher.setSaveAll(true);
|
||||
|
||||
const App::StringIDRef plain = hasher.getID("stable-name");
|
||||
const App::StringIDRef duplicate = hasher.getID("stable-name");
|
||||
if (!plain || plain.value() != duplicate.value() || plain.dataToText() != "stable-name") {
|
||||
throw std::runtime_error("FreeCAD StringHasher source deduplication probe failed");
|
||||
}
|
||||
|
||||
hasher.setThreshold(8);
|
||||
const App::StringIDRef hashed = hasher.getID("hashable-source-value", -1, true);
|
||||
if (!hashed.isHashed() || hashed.deref().data().size() != 20) {
|
||||
throw std::runtime_error("FreeCAD StringHasher source SHA-1 threshold probe failed");
|
||||
}
|
||||
|
||||
Data::MappedName mapped(Data::IndexedName("Edge12"));
|
||||
mapped += ";:M;CUT;:H2a:7,E";
|
||||
const App::StringIDRef mappedID = hasher.getID(mapped, {});
|
||||
if (!mappedID || mappedID.getIndex() != 12 || mappedID.relatedIDs().size() != 2) {
|
||||
throw std::runtime_error("FreeCAD StringHasher mapped-name source probe failed");
|
||||
}
|
||||
|
||||
return "plain=" + std::to_string(plain.value()) + ";hashed="
|
||||
+ std::to_string(hashed.value()) + ";mapped=" + std::to_string(mappedID.value())
|
||||
+ ";index=" + std::to_string(mappedID.getIndex()) + ";related="
|
||||
+ std::to_string(mappedID.relatedIDs().size());
|
||||
}
|
||||
|
||||
std::string freecadPrivateElementMapSourceProbe()
|
||||
{
|
||||
App::StringHasherRef hasher(new App::StringHasher());
|
||||
hasher->setSaveAll(true);
|
||||
|
||||
auto elementMap = std::make_shared<Data::ElementMap>();
|
||||
elementMap->hasher = hasher;
|
||||
Data::MappedName encoded("Edge12;:M;CUT");
|
||||
Data::ElementIDRefs stringIDs;
|
||||
std::ostringstream postfix;
|
||||
elementMap->encodeElementName('F', encoded, postfix, &stringIDs, 99, nullptr, 42, true);
|
||||
if (!encoded.startsWith("#") || encoded.find(";:H2a,F") < 0 || stringIDs.size() != 1) {
|
||||
throw std::runtime_error("FreeCAD ElementMap StringHasher encoding probe failed");
|
||||
}
|
||||
|
||||
const Data::IndexedName face("Face3");
|
||||
const Data::MappedName stored = elementMap->setElementName(face, encoded, 99, &stringIDs);
|
||||
if (!stored || elementMap->find(stored) != face || elementMap->find(face) != stored) {
|
||||
throw std::runtime_error("FreeCAD ElementMap bidirectional lookup probe failed");
|
||||
}
|
||||
|
||||
Data::MappedName original;
|
||||
std::vector<Data::MappedName> history;
|
||||
const long historyTag = elementMap->getElementHistory(stored, 99, &original, &history);
|
||||
if (historyTag != 42 || original.toString() != "Edge12;:M;CUT") {
|
||||
throw std::runtime_error("FreeCAD ElementMap history probe failed");
|
||||
}
|
||||
|
||||
elementMap->beforeSave(hasher);
|
||||
std::ostringstream saved;
|
||||
elementMap->save(saved);
|
||||
auto restored = std::make_shared<Data::ElementMap>();
|
||||
std::istringstream input(saved.str());
|
||||
restored = restored->restore(hasher, input);
|
||||
if (!restored || restored->find(face) != stored || restored->find(stored) != face) {
|
||||
throw std::runtime_error("FreeCAD ElementMap save/restore probe failed");
|
||||
}
|
||||
|
||||
std::vector<Data::MappedName> ordered {
|
||||
Data::MappedName("#b"),
|
||||
Data::MappedName("Edge10"),
|
||||
Data::MappedName("#a"),
|
||||
Data::MappedName("Edge2"),
|
||||
};
|
||||
std::sort(ordered.begin(), ordered.end(), Data::ElementNameComparator {});
|
||||
if (ordered[0].toString() != "Edge2" || ordered[1].toString() != "Edge10"
|
||||
|| ordered[2].toString() != "#a" || ordered[3].toString() != "#b") {
|
||||
throw std::runtime_error("FreeCAD MappedElement stable ordering probe failed");
|
||||
}
|
||||
|
||||
App::DocumentObject object(77);
|
||||
const Data::HistoryItem item(&object, stored);
|
||||
if (item.tag != 77 || Data::oldElementName("Body.;mapped.Face3") != "Body.Face3") {
|
||||
throw std::runtime_error("FreeCAD mapped element host-boundary probe failed");
|
||||
}
|
||||
|
||||
return "stored=" + stored.toString() + ";tag=" + std::to_string(historyTag)
|
||||
+ ";original=" + original.toString() + ";serialized="
|
||||
+ std::to_string(saved.str().size()) + ";restored=" + std::to_string(restored->size());
|
||||
}
|
||||
|
||||
std::string freecadPrivateElementMapResourcesProbe()
|
||||
{
|
||||
App::StringHasherRef hasher(new App::StringHasher());
|
||||
hasher->setSaveAll(true);
|
||||
auto elementMap = std::make_shared<Data::ElementMap>();
|
||||
elementMap->hasher = hasher;
|
||||
|
||||
Data::MappedName encoded("Edge12;:M;CUT");
|
||||
Data::ElementIDRefs stringIDs;
|
||||
std::ostringstream postfix;
|
||||
elementMap->encodeElementName('F', encoded, postfix, &stringIDs, 99, nullptr, 42, true);
|
||||
elementMap->setElementName(Data::IndexedName("Face3"), encoded, 99, &stringIDs);
|
||||
elementMap->beforeSave(hasher);
|
||||
|
||||
std::ostringstream elementMapStream;
|
||||
elementMap->save(elementMapStream);
|
||||
QJsonArray referenceStringIDs;
|
||||
for (const auto& stringID : stringIDs) {
|
||||
referenceStringIDs.append(static_cast<qint64>(stringID.value()));
|
||||
}
|
||||
const App::StringID::IndexID prefixID = App::StringID::fromString(encoded.dataBytes());
|
||||
if (!prefixID || prefixID.index != 0 || referenceStringIDs.isEmpty()) {
|
||||
throw std::runtime_error("FreeCAD ElementMap MappedNameRef probe failed");
|
||||
}
|
||||
const QJsonObject mappedNameReference {
|
||||
{"name", QString::fromUtf8(encoded.dataBytes())},
|
||||
{"postfix", QString::fromUtf8(encoded.postfixBytes())},
|
||||
{"prefixStringId", static_cast<qint64>(prefixID.id)},
|
||||
{"stringIds", referenceStringIDs},
|
||||
};
|
||||
QJsonArray entries;
|
||||
for (const auto& [id, reference] : hasher->getIDMap()) {
|
||||
const App::StringID& stringID = reference.deref();
|
||||
int flags = 0;
|
||||
flags |= stringID.isBinary() ? 1 << 0 : 0;
|
||||
flags |= stringID.isHashed() ? 1 << 1 : 0;
|
||||
flags |= stringID.isPostfixEncoded() ? 1 << 2 : 0;
|
||||
flags |= stringID.isPostfixed() ? 1 << 3 : 0;
|
||||
flags |= stringID.isIndexed() ? 1 << 4 : 0;
|
||||
flags |= stringID.isPrefixID() ? 1 << 5 : 0;
|
||||
flags |= stringID.isPrefixIDIndex() ? 1 << 6 : 0;
|
||||
flags |= stringID.isPersistent() ? 1 << 7 : 0;
|
||||
QJsonArray relatedIDs;
|
||||
for (const auto& related : reference.relatedIDs()) {
|
||||
relatedIDs.append(static_cast<qint64>(related.value()));
|
||||
}
|
||||
entries.append(QJsonObject {
|
||||
{"id", static_cast<qint64>(id)},
|
||||
{"flags", flags},
|
||||
{"relatedIds", relatedIDs},
|
||||
{"data", QString::fromUtf8(stringID.data())},
|
||||
{"postfix", QString::fromUtf8(stringID.postfix())},
|
||||
});
|
||||
}
|
||||
|
||||
return QJsonDocument(QJsonObject {
|
||||
{"elementMapText", QString::fromStdString("BeginElementMap v1\n" + elementMapStream.str())},
|
||||
{"mappedNameReference", mappedNameReference},
|
||||
{"stringHasher", QJsonObject {
|
||||
{"schemaVersion", 2},
|
||||
{"nativeVersion", 1},
|
||||
{"entries", entries},
|
||||
}},
|
||||
}).toJson(QJsonDocument::Compact).toStdString();
|
||||
}
|
||||
|
||||
int freecadNamingCandidateAbiVersion()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string freecadNamingCandidateCapabilitiesJson()
|
||||
{
|
||||
return QJsonDocument(QJsonObject {
|
||||
{"schemaVersion", 1},
|
||||
{"freecadVersion", "1.1.1"},
|
||||
{"sourceCommit", "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"},
|
||||
{"mappedNameRef", true},
|
||||
{"stringHasher", true},
|
||||
{"elementMap2", true},
|
||||
{"operations", QJsonArray {"cut"}},
|
||||
}).toJson(QJsonDocument::Compact).toStdString();
|
||||
}
|
||||
|
||||
std::string freecadNamingCandidateEvidenceJson(const std::string& requestJson)
|
||||
{
|
||||
QJsonParseError requestError;
|
||||
const QJsonDocument requestDocument =
|
||||
QJsonDocument::fromJson(QByteArray::fromStdString(requestJson), &requestError);
|
||||
const QJsonObject request = requestDocument.object();
|
||||
if (requestError.error != QJsonParseError::NoError || request.value("schemaVersion").toInt() != 1
|
||||
|| request.value("operation").toString() != "cut"
|
||||
|| request.value("stageId").toString().isEmpty()
|
||||
|| request.value("resultObjectId").toString().isEmpty()) {
|
||||
throw std::runtime_error("FreeCAD naming candidate request is invalid");
|
||||
}
|
||||
|
||||
QJsonParseError resourcesError;
|
||||
const QJsonDocument resourcesDocument = QJsonDocument::fromJson(
|
||||
QByteArray::fromStdString(freecadPrivateElementMapResourcesProbe()), &resourcesError);
|
||||
if (resourcesError.error != QJsonParseError::NoError) {
|
||||
throw std::runtime_error("FreeCAD naming candidate resources are invalid");
|
||||
}
|
||||
const QJsonObject resources = resourcesDocument.object();
|
||||
QString sourceObjectId = "probe-source";
|
||||
const QJsonArray inputs = request.value("inputs").toArray();
|
||||
if (!inputs.isEmpty() && !inputs[0].toObject().value("objectId").toString().isEmpty()) {
|
||||
sourceObjectId = inputs[0].toObject().value("objectId").toString();
|
||||
}
|
||||
|
||||
const QJsonObject reference = resources.value("mappedNameReference").toObject();
|
||||
if (reference.isEmpty()) {
|
||||
throw std::runtime_error("FreeCAD naming candidate omitted its native MappedNameRef");
|
||||
}
|
||||
const QJsonObject mappedName {
|
||||
{"kind", "face"},
|
||||
{"resultIndex", 2},
|
||||
{"resultPersistentId", "probe-face-3"},
|
||||
{"relation", "modified"},
|
||||
{"reference", reference},
|
||||
{"sourceRefs", QJsonArray {QJsonObject {
|
||||
{"objectId", sourceObjectId},
|
||||
{"persistentId", "Edge12"},
|
||||
}}},
|
||||
};
|
||||
|
||||
return QJsonDocument(QJsonObject {
|
||||
{"schemaVersion", 1},
|
||||
{"stageId", request.value("stageId")},
|
||||
{"resultObjectId", request.value("resultObjectId")},
|
||||
{"status", "native-evidence"},
|
||||
{"mappedNames", QJsonArray {mappedName}},
|
||||
{"stringHasher", resources.value("stringHasher")},
|
||||
{"elementMap2Text", resources.value("elementMapText")},
|
||||
}).toJson(QJsonDocument::Compact).toStdString();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
EMSCRIPTEN_BINDINGS(freecad_private_naming_source_probe)
|
||||
{
|
||||
emscripten::function("freecadPrivateNamingSourceProbe", &freecadPrivateNamingSourceProbe);
|
||||
emscripten::function("freecadPrivateStringHasherSourceProbe",
|
||||
&freecadPrivateStringHasherSourceProbe);
|
||||
emscripten::function("freecadPrivateElementMapSourceProbe",
|
||||
&freecadPrivateElementMapSourceProbe);
|
||||
emscripten::function("freecadPrivateElementMapResourcesProbe",
|
||||
&freecadPrivateElementMapResourcesProbe);
|
||||
emscripten::function("freecadNamingCandidateAbiVersion", &freecadNamingCandidateAbiVersion);
|
||||
emscripten::function("freecadNamingCandidateCapabilitiesJson",
|
||||
&freecadNamingCandidateCapabilitiesJson);
|
||||
emscripten::function("freecadNamingCandidateEvidenceJson",
|
||||
&freecadNamingCandidateEvidenceJson);
|
||||
}
|
||||
|
||||
41
native/freecad-naming-probe/shims/App/Application.h
Normal file
41
native/freecad-naming-probe/shims/App/Application.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#ifndef SRC_APP_APPLICATION_H_
|
||||
#define SRC_APP_APPLICATION_H_
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
namespace App {
|
||||
|
||||
class Document;
|
||||
|
||||
class ProbeSignal
|
||||
{
|
||||
public:
|
||||
template<typename Callback>
|
||||
void connect(Callback&&)
|
||||
{}
|
||||
};
|
||||
|
||||
class Application
|
||||
{
|
||||
public:
|
||||
Document* getActiveDocument() const
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ProbeSignal signalStartSaveDocument;
|
||||
ProbeSignal signalFinishSaveDocument;
|
||||
ProbeSignal signalStartRestoreDocument;
|
||||
ProbeSignal signalFinishRestoreDocument;
|
||||
};
|
||||
|
||||
inline Application& GetApplication()
|
||||
{
|
||||
static Application application;
|
||||
return application;
|
||||
}
|
||||
|
||||
} // namespace App
|
||||
|
||||
#endif
|
||||
19
native/freecad-naming-probe/shims/App/Document.h
Normal file
19
native/freecad-naming-probe/shims/App/Document.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#ifndef SRC_APP_DOCUMENT_H_
|
||||
#define SRC_APP_DOCUMENT_H_
|
||||
|
||||
#include <App/DocumentObject.h>
|
||||
|
||||
namespace App {
|
||||
|
||||
class Document
|
||||
{
|
||||
public:
|
||||
DocumentObject* getObjectByID(long) const
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace App
|
||||
|
||||
#endif
|
||||
31
native/freecad-naming-probe/shims/App/DocumentObject.h
Normal file
31
native/freecad-naming-probe/shims/App/DocumentObject.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#ifndef SRC_APP_DOCUMENTOBJECT_H_
|
||||
#define SRC_APP_DOCUMENTOBJECT_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace App {
|
||||
|
||||
class DocumentObject
|
||||
{
|
||||
public:
|
||||
explicit DocumentObject(long id = 0)
|
||||
: objectId(id)
|
||||
{}
|
||||
|
||||
long getID() const
|
||||
{
|
||||
return objectId;
|
||||
}
|
||||
|
||||
std::string getFullName() const
|
||||
{
|
||||
return "ProbeObject";
|
||||
}
|
||||
|
||||
private:
|
||||
long objectId;
|
||||
};
|
||||
|
||||
} // namespace App
|
||||
|
||||
#endif
|
||||
@@ -1,27 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#define APP_STRING_ID_H
|
||||
|
||||
#include <QByteArray>
|
||||
|
||||
namespace App
|
||||
{
|
||||
class StringIDRef
|
||||
{
|
||||
public:
|
||||
void toBytes(QByteArray& bytes) const
|
||||
{
|
||||
bytes.clear();
|
||||
}
|
||||
|
||||
bool operator<(const StringIDRef&) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool operator==(const StringIDRef&) const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
} // namespace App
|
||||
@@ -1,3 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <Base/Exception.h>
|
||||
|
||||
struct FreeCadNamingProbeLogInstance
|
||||
{
|
||||
bool isEnabled(int) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
inline FreeCadNamingProbeLogInstance freecad_naming_probe_log_instance;
|
||||
|
||||
#define FC_LOGLEVEL_LOG 3
|
||||
#define FC_LOGLEVEL_TRACE 4
|
||||
#define FC_LOG_INSTANCE freecad_naming_probe_log_instance
|
||||
#define FC_LOG_LEVEL_INIT(...)
|
||||
#define FC_WARN(message) ((void)0)
|
||||
#define FC_ERR(message) ((void)0)
|
||||
#define FC_LOG(message) ((void)0)
|
||||
#define FC_TRACE(message) ((void)0)
|
||||
|
||||
35
native/freecad-naming-probe/shims/Base/Exception.h
Normal file
35
native/freecad-naming-probe/shims/Base/Exception.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace Base {
|
||||
|
||||
class Exception : public std::runtime_error
|
||||
{
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
|
||||
void reportException() const {}
|
||||
};
|
||||
|
||||
class RuntimeError : public Exception
|
||||
{
|
||||
public:
|
||||
using Exception::Exception;
|
||||
};
|
||||
|
||||
class ValueError : public Exception
|
||||
{
|
||||
public:
|
||||
using Exception::Exception;
|
||||
};
|
||||
|
||||
} // namespace Base
|
||||
|
||||
#define FC_THROWM(type, message) \
|
||||
do { \
|
||||
std::ostringstream freecad_naming_probe_exception_stream; \
|
||||
freecad_naming_probe_exception_stream << message; \
|
||||
throw type(freecad_naming_probe_exception_stream.str()); \
|
||||
} while (false)
|
||||
38
native/freecad-naming-probe/shims/Base/Persistence.h
Normal file
38
native/freecad-naming-probe/shims/Base/Persistence.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <CXX/Objects.hxx>
|
||||
|
||||
namespace Base {
|
||||
|
||||
class Reader;
|
||||
class Writer;
|
||||
class XMLReader;
|
||||
|
||||
class BaseClass
|
||||
{
|
||||
public:
|
||||
virtual ~BaseClass() = default;
|
||||
virtual PyObject* getPyObject()
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
class Persistence : public BaseClass
|
||||
{
|
||||
public:
|
||||
~Persistence() override = default;
|
||||
|
||||
virtual unsigned int getMemSize() const = 0;
|
||||
virtual void Save(Writer&) const = 0;
|
||||
virtual void Restore(XMLReader&) = 0;
|
||||
virtual void SaveDocFile(Writer&) const = 0;
|
||||
virtual void RestoreDocFile(Reader&) = 0;
|
||||
};
|
||||
|
||||
} // namespace Base
|
||||
|
||||
#define TYPESYSTEM_HEADER()
|
||||
#define TYPESYSTEM_HEADER_WITH_OVERRIDE()
|
||||
#define TYPESYSTEM_SOURCE(...)
|
||||
#define TYPESYSTEM_SOURCE_ABSTRACT(...)
|
||||
3
native/freecad-naming-probe/shims/Base/PyObjectBase.h
Normal file
3
native/freecad-naming-probe/shims/Base/PyObjectBase.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include <CXX/Objects.hxx>
|
||||
45
native/freecad-naming-probe/shims/Base/Reader.h
Normal file
45
native/freecad-naming-probe/shims/Base/Reader.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <istream>
|
||||
#include <sstream>
|
||||
|
||||
namespace Base {
|
||||
|
||||
class Reader : public std::istream
|
||||
{
|
||||
public:
|
||||
Reader()
|
||||
: std::istream(nullptr)
|
||||
{}
|
||||
};
|
||||
|
||||
class XMLReader
|
||||
{
|
||||
public:
|
||||
template<typename T>
|
||||
T getAttribute(const char*) const
|
||||
{
|
||||
return T {};
|
||||
}
|
||||
|
||||
bool hasAttribute(const char*) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void readElement(const char*) {}
|
||||
void readEndElement(const char*) {}
|
||||
void addFile(const char*, void*) {}
|
||||
|
||||
std::istream& beginCharStream()
|
||||
{
|
||||
return stream;
|
||||
}
|
||||
|
||||
int FileVersion = 1;
|
||||
|
||||
private:
|
||||
std::istringstream stream;
|
||||
};
|
||||
|
||||
} // namespace Base
|
||||
43
native/freecad-naming-probe/shims/Base/Stream.h
Normal file
43
native/freecad-naming-probe/shims/Base/Stream.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <istream>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
|
||||
namespace Base {
|
||||
|
||||
class TextOutputStream
|
||||
{
|
||||
public:
|
||||
explicit TextOutputStream(std::ostream& stream)
|
||||
: output(stream)
|
||||
{}
|
||||
|
||||
TextOutputStream& operator<<(const char* value)
|
||||
{
|
||||
output << value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
private:
|
||||
std::ostream& output;
|
||||
};
|
||||
|
||||
class TextInputStream
|
||||
{
|
||||
public:
|
||||
explicit TextInputStream(std::istream& stream)
|
||||
: input(stream)
|
||||
{}
|
||||
|
||||
TextInputStream& operator>>(std::string& value)
|
||||
{
|
||||
input >> value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
private:
|
||||
std::istream& input;
|
||||
};
|
||||
|
||||
} // namespace Base
|
||||
40
native/freecad-naming-probe/shims/Base/Writer.h
Normal file
40
native/freecad-naming-probe/shims/Base/Writer.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <ostream>
|
||||
#include <sstream>
|
||||
|
||||
namespace Base {
|
||||
|
||||
class Writer
|
||||
{
|
||||
public:
|
||||
std::ostream& Stream()
|
||||
{
|
||||
return stream;
|
||||
}
|
||||
|
||||
const char* ind() const
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
const char* addFile(const char* name, const void*)
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
std::ostream& beginCharStream()
|
||||
{
|
||||
return stream;
|
||||
}
|
||||
|
||||
std::ostream& endCharStream()
|
||||
{
|
||||
return stream;
|
||||
}
|
||||
|
||||
private:
|
||||
std::ostringstream stream;
|
||||
};
|
||||
|
||||
} // namespace Base
|
||||
9
native/freecad-naming-probe/shims/CXX/Objects.hxx
Normal file
9
native/freecad-naming-probe/shims/CXX/Objects.hxx
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
struct _object {};
|
||||
using PyObject = _object;
|
||||
|
||||
inline _object freecad_naming_probe_py_none;
|
||||
|
||||
#define Py_None (&freecad_naming_probe_py_none)
|
||||
#define Py_INCREF(object) ((void)(object))
|
||||
11
native/freecad-naming-probe/shims/FCConfig.h
Normal file
11
native/freecad-naming-probe/shims/FCConfig.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
|
||||
#ifndef AppExport
|
||||
#define AppExport
|
||||
#endif
|
||||
|
||||
#ifndef BaseExport
|
||||
#define BaseExport
|
||||
#endif
|
||||
5
native/freecad-naming-probe/shims/ProbeAppHost.h
Normal file
5
native/freecad-naming-probe/shims/ProbeAppHost.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <App/DocumentObject.h>
|
||||
#include <App/Document.h>
|
||||
#include <App/Application.h>
|
||||
15
native/freecad-naming-probe/shims/StringHasherPy.h
Normal file
15
native/freecad-naming-probe/shims/StringHasherPy.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <CXX/Objects.hxx>
|
||||
|
||||
namespace App {
|
||||
|
||||
class StringHasher;
|
||||
|
||||
class StringHasherPy : public _object
|
||||
{
|
||||
public:
|
||||
explicit StringHasherPy(StringHasher*) {}
|
||||
};
|
||||
|
||||
} // namespace App
|
||||
17
native/freecad-naming-probe/shims/StringIDPy.h
Normal file
17
native/freecad-naming-probe/shims/StringIDPy.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <CXX/Objects.hxx>
|
||||
|
||||
namespace App {
|
||||
|
||||
class StringID;
|
||||
|
||||
class StringIDPy : public _object
|
||||
{
|
||||
public:
|
||||
explicit StringIDPy(StringID*) {}
|
||||
|
||||
int _index = 0;
|
||||
};
|
||||
|
||||
} // namespace App
|
||||
@@ -1,8 +1,22 @@
|
||||
import createProbe from './dist/freecad-private-naming-source-probe.js'
|
||||
import { inspectWasmStaticArchive } from '../../scripts/freecad-naming-sdk-lib.mjs'
|
||||
|
||||
const archive = await inspectWasmStaticArchive(new URL('./dist/libFreeCADPrivateNamingProbe.a', import.meta.url))
|
||||
if (archive.memberCount !== 7 || archive.wasmObjectCount !== 7 || archive.llvmBitcodeCount !== 0) {
|
||||
throw new Error(`Unexpected FreeCAD private naming source archive: ${JSON.stringify(archive)}`)
|
||||
}
|
||||
|
||||
const probe = await createProbe()
|
||||
const result = probe.freecadPrivateNamingSourceProbe()
|
||||
if (result !== 'Edge12;:M;CUT;:H2a:7,E') throw new Error(`Unexpected FreeCAD private naming source probe result: ${result}`)
|
||||
const stringHasherResult = probe.freecadPrivateStringHasherSourceProbe()
|
||||
if (stringHasherResult !== 'plain=1;hashed=2;mapped=5;index=12;related=2') {
|
||||
throw new Error(`Unexpected FreeCAD StringHasher source probe result: ${stringHasherResult}`)
|
||||
}
|
||||
const elementMapResult = probe.freecadPrivateElementMapSourceProbe()
|
||||
if (elementMapResult !== 'stored=#1;:H2a,F;tag=42;original=Edge12;:M;CUT;serialized=115;restored=1') {
|
||||
throw new Error(`Unexpected FreeCAD ElementMap source probe result: ${elementMapResult}`)
|
||||
}
|
||||
for (const forbidden of ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson']) {
|
||||
if (typeof probe[forbidden] === 'function') throw new Error(`Source prerequisite probe must not export production ABI callback ${forbidden}.`)
|
||||
}
|
||||
@@ -10,7 +24,22 @@ console.log(JSON.stringify({
|
||||
status: 'source-prerequisite-pass',
|
||||
freecadVersion: '1.1.1',
|
||||
sourceCommit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d',
|
||||
linkedSources: ['App/IndexedName.cpp', 'App/MappedName.cpp'],
|
||||
linkedSources: [
|
||||
'App/IndexedName.cpp',
|
||||
'App/MappedName.cpp',
|
||||
'App/StringHasher.cpp',
|
||||
'App/MappedElement.cpp',
|
||||
'App/ElementNamingUtils.cpp',
|
||||
'App/ElementMap.cpp',
|
||||
'Base/Handle.cpp',
|
||||
],
|
||||
sourceArchive: {
|
||||
name: 'libFreeCADPrivateNamingProbe.a',
|
||||
...archive,
|
||||
productionEligible: false,
|
||||
},
|
||||
result,
|
||||
stringHasherResult,
|
||||
elementMapResult,
|
||||
productionWorkerLinked: false,
|
||||
}, null, 2))
|
||||
|
||||
@@ -44,19 +44,48 @@ establishes ABI linkage; exact promotion additionally requires valid
|
||||
MappedNameRef, StringHasher, ElementMap2, stage history, and round-trip evidence.
|
||||
|
||||
The repository also carries a prerequisite-only source probe. It builds QtBase
|
||||
6.8.2 `Qt6Core` for wasm, then compiles the locked FreeCAD `IndexedName.cpp` and
|
||||
`MappedName.cpp` sources without exporting the production naming callbacks:
|
||||
6.8.2 `Qt6Core` for wasm, then compiles the locked FreeCAD `IndexedName.cpp`,
|
||||
`MappedName.cpp`, `StringHasher.cpp`, `MappedElement.cpp`,
|
||||
`ElementNamingUtils.cpp`, `ElementMap.cpp`, and `Base/Handle.cpp` sources without
|
||||
exporting the production naming callbacks:
|
||||
|
||||
```bash
|
||||
./npmw run build:qt6-wasm-core
|
||||
./npmw run build:freecad-naming-source-probe
|
||||
./npmw run test:freecad-naming-source-probe
|
||||
./npmw run check:freecad-naming-sdk-readiness
|
||||
./npmw run check:freecad-private-naming-boundary
|
||||
```
|
||||
|
||||
Passing this probe establishes that the private source subset and toolchain are
|
||||
cross-compilable. It does not link the production OCCT Worker, does not close
|
||||
EX-TSN-02, and does not change `systemExact=false`.
|
||||
cross-compilable. Its runtime test covers mapped-name parsing, StringHasher
|
||||
deduplication, SHA-1 threshold handling, indexed mapped-name references,
|
||||
ElementMap encoding/lookup/history/save/restore, and stable mapped-element
|
||||
ordering. It also feeds native MappedNameRef, StringHasher, and ElementMap2
|
||||
resources through the strict Web ABI validator using deliberately separate
|
||||
`freecadNamingCandidate*` callback names.
|
||||
|
||||
The seven locked FreeCAD source units are first compiled into the isolated
|
||||
`libFreeCADPrivateNamingProbe.a` archive. The smoke test checks that it contains
|
||||
exactly seven wasm object members before linking the runnable probe. This
|
||||
archive remains host-adapter-bound and is not a substitute for FreeCADBase or
|
||||
FreeCADApp.
|
||||
|
||||
Persistence, Python wrappers, type-system, logging, and Application/Document
|
||||
lifecycle interfaces remain standalone host adapters. FreeCADApp/Part/Python
|
||||
static libraries, real Application/Document integration, OCCT builder context,
|
||||
and the production bridge are not linked. The candidate callbacks are never
|
||||
published to the production Worker. The probe therefore does not close
|
||||
EX-TSN-02 and does not change `systemExact=false`.
|
||||
|
||||
`check:freecad-naming-sdk-readiness` audits the pre-production SDK plan without
|
||||
publishing anything. It parses every present `.a` archive and rejects native
|
||||
ELF members; the current local resource set verifies QtCore plus its bundled
|
||||
Pcre2/Zlib wasm dependencies, but still lacks FreeCADBase, FreeCADApp, Part,
|
||||
Python, and the production bridge. `generate:freecad-naming-sdk-manifest` is
|
||||
fail-closed and writes no manifest until all inputs are present. The complete
|
||||
SDK checker applies the same wasm-archive rule, so an x86 static library cannot
|
||||
satisfy the manifest by hash alone.
|
||||
|
||||
The versioned JSON request carries document `objectId`, stable positive
|
||||
`objectTag`, prior naming evidence, result object identity, result tag, stage
|
||||
|
||||
@@ -20,6 +20,7 @@ if [[ -z "${OCCT_BUILD_DIR:-}" ]]; then
|
||||
fi
|
||||
DIST_DIR="${OCCT_HISTORY_DIST_DIR:-${ROOT_DIR}/native/occt-history/dist}"
|
||||
PUBLIC_DIR="${OCCT_HISTORY_PUBLIC_DIR:-${ROOT_DIR}/public/native/occt-history}"
|
||||
PUBLISH_ARTIFACT="${OCCT_HISTORY_PUBLISH:-1}"
|
||||
JOBS="${JOBS:-$(nproc)}"
|
||||
FREECAD_WASM_SDK_DIR="${FREECAD_WASM_SDK_DIR:-}"
|
||||
|
||||
@@ -40,11 +41,18 @@ if ! command -v emcmake >/dev/null || ! command -v em++ >/dev/null; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OCCT_CXX_FLAGS="-fexceptions"
|
||||
OCCT_CXX_RELEASE_FLAGS="-O0 -DNDEBUG -fexceptions"
|
||||
if [[ -n "${FREECAD_WASM_SDK_DIR}" ]]; then
|
||||
OCCT_CXX_FLAGS+=" -pthread"
|
||||
OCCT_CXX_RELEASE_FLAGS+=" -pthread"
|
||||
fi
|
||||
|
||||
emcmake cmake -S "${OCCT_SOURCE_DIR}" -B "${OCCT_BUILD_DIR}" -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_MAKE_PROGRAM="$(command -v ninja)" \
|
||||
-DCMAKE_CXX_FLAGS="-fexceptions" \
|
||||
-DCMAKE_CXX_FLAGS_RELEASE="-O0 -DNDEBUG -fexceptions" \
|
||||
-DCMAKE_CXX_FLAGS="${OCCT_CXX_FLAGS}" \
|
||||
-DCMAKE_CXX_FLAGS_RELEASE="${OCCT_CXX_RELEASE_FLAGS}" \
|
||||
-DBUILD_RELEASE_DISABLE_EXCEPTIONS=OFF \
|
||||
-DBUILD_LIBRARY_TYPE=Static \
|
||||
-DBUILD_MODULE_FoundationClasses=ON \
|
||||
@@ -74,18 +82,29 @@ SOURCES=("${ROOT_DIR}/native/occt-history/occt_history.cpp")
|
||||
INCLUDES=("-I${OCCT_BUILD_DIR}/include/opencascade" "-I${OCCT_SOURCE_DIR}/src")
|
||||
DEFINES=()
|
||||
FREECAD_LIBRARIES=()
|
||||
FREECAD_LINK_OPTIONS=()
|
||||
if [[ -n "${FREECAD_WASM_SDK_DIR}" ]]; then
|
||||
BRIDGE_SOURCE="$(node -e 'const fs=require("node:fs"); const p=require("node:path"); const m=JSON.parse(fs.readFileSync(p.resolve(process.argv[1],"manifest.json"),"utf8")); console.log(p.isAbsolute(m.namingBridge.source) ? m.namingBridge.source : p.resolve(process.argv[1],m.namingBridge.source))' "${FREECAD_WASM_SDK_DIR}")"
|
||||
SOURCES+=("${BRIDGE_SOURCE}")
|
||||
while IFS= read -r include_dir; do INCLUDES+=("-I${include_dir}"); done < <(node -e 'const fs=require("node:fs"); const p=require("node:path"); const root=p.resolve(process.argv[1]); const m=JSON.parse(fs.readFileSync(p.resolve(root,"manifest.json"),"utf8")); for (const d of m.includeDirs) console.log(p.isAbsolute(d) ? d : p.resolve(root,d))' "${FREECAD_WASM_SDK_DIR}")
|
||||
while IFS= read -r library; do FREECAD_LIBRARIES+=("${library}"); done < <(node -e 'const fs=require("node:fs"); const p=require("node:path"); const root=p.resolve(process.argv[1]); const m=JSON.parse(fs.readFileSync(p.resolve(root,"manifest.json"),"utf8")); for (const l of m.libraries) console.log(p.isAbsolute(l.path) ? l.path : p.resolve(root,l.path))' "${FREECAD_WASM_SDK_DIR}")
|
||||
while IFS= read -r definition; do DEFINES+=("-D${definition}"); done < <(node -e 'const fs=require("node:fs"); const p=require("node:path"); const m=JSON.parse(fs.readFileSync(p.resolve(process.argv[1],"manifest.json"),"utf8")); for (const d of m.compileOptions.definitions) console.log(d)' "${FREECAD_WASM_SDK_DIR}")
|
||||
CXX_STANDARD="$(node -e 'const fs=require("node:fs"); const p=require("node:path"); const m=JSON.parse(fs.readFileSync(p.resolve(process.argv[1],"manifest.json"),"utf8")); console.log(m.compileOptions.cxxStandard)' "${FREECAD_WASM_SDK_DIR}")"
|
||||
FORCE_INCLUDE="$(node -e 'const fs=require("node:fs"); const p=require("node:path"); const root=p.resolve(process.argv[1]); const m=JSON.parse(fs.readFileSync(p.resolve(root,"manifest.json"),"utf8")); const v=m.compileOptions.forceInclude; console.log(p.isAbsolute(v) ? v : p.resolve(root,v))' "${FREECAD_WASM_SDK_DIR}")"
|
||||
HOST_ADAPTER="$(node -e 'const fs=require("node:fs"); const p=require("node:path"); const root=p.resolve(process.argv[1]); const m=JSON.parse(fs.readFileSync(p.resolve(root,"manifest.json"),"utf8")); const v=m.namingBridge.hostAdapter; console.log(p.isAbsolute(v) ? v : p.resolve(root,v))' "${FREECAD_WASM_SDK_DIR}")"
|
||||
FREECAD_LINK_OPTIONS+=("-include" "${FORCE_INCLUDE}" "-pthread" "--pre-js" "${HOST_ADAPTER}" "-sPTHREAD_POOL_SIZE=2")
|
||||
while IFS=$'\t' read -r asset_path preload_to; do
|
||||
FREECAD_LINK_OPTIONS+=("--preload-file" "${asset_path}@${preload_to}")
|
||||
done < <(node -e 'const fs=require("node:fs"); const p=require("node:path"); const root=p.resolve(process.argv[1]); const m=JSON.parse(fs.readFileSync(p.resolve(root,"manifest.json"),"utf8")); for (const a of m.runtimeAssets) { const v=p.isAbsolute(a.path) ? a.path : p.resolve(root,a.path); console.log(`${v}\t${a.preloadTo}`) }' "${FREECAD_WASM_SDK_DIR}")
|
||||
DEFINES+=("-DBITBYBIT_FREECAD_NAMING_LINKED=1")
|
||||
else
|
||||
CXX_STANDARD="c++17"
|
||||
fi
|
||||
|
||||
em++ "${SOURCES[@]}" "${INCLUDES[@]}" "${DEFINES[@]}" \
|
||||
em++ "${SOURCES[@]}" "${INCLUDES[@]}" "${DEFINES[@]}" "${FREECAD_LINK_OPTIONS[@]}" \
|
||||
-I"${OCCT_BUILD_DIR}/include/opencascade" \
|
||||
-I"${OCCT_SOURCE_DIR}/src" \
|
||||
-std=c++17 -O2 -fexceptions --bind \
|
||||
-std="${CXX_STANDARD}" -O2 -fexceptions --bind \
|
||||
-Wl,--start-group \
|
||||
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKBO.a" \
|
||||
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKFillet.a" \
|
||||
@@ -112,7 +131,13 @@ em++ "${SOURCES[@]}" "${INCLUDES[@]}" "${DEFINES[@]}" \
|
||||
-o "${DIST_DIR}/bitbybit-occt-history.js"
|
||||
|
||||
cp "${ROOT_DIR}/native/occt-history/package.json" "${DIST_DIR}/package.json"
|
||||
if [[ "${PUBLISH_ARTIFACT}" == "1" ]]; then
|
||||
mkdir -p "${PUBLIC_DIR}"
|
||||
cp "${DIST_DIR}/bitbybit-occt-history.js" "${PUBLIC_DIR}/bitbybit-occt-history.js"
|
||||
cp "${DIST_DIR}/bitbybit-occt-history.wasm" "${PUBLIC_DIR}/bitbybit-occt-history.wasm"
|
||||
sha256sum "${DIST_DIR}/bitbybit-occt-history.js" "${DIST_DIR}/bitbybit-occt-history.wasm"
|
||||
for artifact in "${DIST_DIR}"/bitbybit-occt-history.{js,wasm,data,worker.js}; do
|
||||
[[ -f "${artifact}" ]] && cp "${artifact}" "${PUBLIC_DIR}/$(basename "${artifact}")"
|
||||
done
|
||||
elif [[ "${PUBLISH_ARTIFACT}" != "0" ]]; then
|
||||
echo "OCCT_HISTORY_PUBLISH must be 0 or 1." >&2
|
||||
exit 1
|
||||
fi
|
||||
sha256sum "${DIST_DIR}"/bitbybit-occt-history.{js,wasm,data} 2>/dev/null || sha256sum "${DIST_DIR}/bitbybit-occt-history.js" "${DIST_DIR}/bitbybit-occt-history.wasm"
|
||||
|
||||
149
native/occt-history/freecad-naming-candidate-smoke.ts
Normal file
149
native/occt-history/freecad-naming-candidate-smoke.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
|
||||
import { dirname } from 'node:path'
|
||||
import { resolve } from 'node:path'
|
||||
import {
|
||||
captureFreeCadPrivateNamingEvidence,
|
||||
createFreeCadPrivateNamingAbiRequest,
|
||||
probeFreeCadPrivateNamingAbi,
|
||||
type NativeFreeCadNamingAbiModule,
|
||||
} from '../../src/facade/nativeNamingAbi'
|
||||
|
||||
type CandidateModule = NativeFreeCadNamingAbiModule & {
|
||||
makeBox(x: number, y: number, z: number): unknown
|
||||
shapeToStep(shape: unknown): string
|
||||
booleanHistoryFromStep(objectStep: string, toolStep: string, operation: 'cut'): {
|
||||
provider: 'occt-native'
|
||||
occtVersion: string
|
||||
hasModified: boolean
|
||||
hasGenerated: boolean
|
||||
hasDeleted: boolean
|
||||
resultStep: string
|
||||
records: Array<{
|
||||
relation: 'modified' | 'generated' | 'deleted'
|
||||
source: string
|
||||
kind: 'face' | 'edge' | 'vertex'
|
||||
resultKind?: 'face' | 'edge' | 'vertex'
|
||||
sourceIndex: number
|
||||
resultIndex?: number
|
||||
resultIndexes?: number[]
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
const dist = resolve(process.env.FREECAD_NAMING_WORKER_DIST ?? '.cache/candidates/freecad-naming-worker')
|
||||
const moduleUrl = pathToFileURL(resolve(dist, 'bitbybit-occt-history.js')).href
|
||||
const createCandidate = (await import(moduleUrl)).default as (options: { locateFile(path: string): string }) => Promise<CandidateModule>
|
||||
const candidate = await createCandidate({ locateFile: (path) => resolve(dist, path) })
|
||||
const probe = probeFreeCadPrivateNamingAbi(candidate)
|
||||
if (probe.availability !== 'available') throw new Error(`Candidate Worker naming ABI probe failed: ${probe.reason}`)
|
||||
|
||||
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('Candidate Worker OCCT cut returned no usable native history record.')
|
||||
const resultIndex = Number.isSafeInteger(record.resultIndex) ? record.resultIndex! : record.resultIndexes!.find(Number.isSafeInteger)!
|
||||
const selectedRecord = { ...record, resultIndex, resultIndexes: undefined }
|
||||
const evidence = captureFreeCadPrivateNamingEvidence(candidate, createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'candidate-worker-cut-1',
|
||||
documentId: 'candidate-worker-document',
|
||||
documentVersion: 1,
|
||||
operationId: 'candidate-worker-cut',
|
||||
operation: 'cut',
|
||||
stageId: 'candidate-worker:stage:1',
|
||||
resultObjectId: 'candidate-worker:result:1',
|
||||
resultObjectTag: 100,
|
||||
inputs: [
|
||||
{ inputId: 'object', objectId: 'candidate-object', role: 'object', objectTag: 42, step: objectStep },
|
||||
{ inputId: 'tool', objectId: 'candidate-tool', role: 'tool', objectTag: 43, step: toolStep },
|
||||
],
|
||||
stages: [{ stageId: 'candidate-worker:stage:1', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }],
|
||||
resultStep: history.resultStep,
|
||||
history: { ...history, records: [selectedRecord] },
|
||||
}), probe)
|
||||
if (!evidence?.elementMap2 || !evidence.stringHasher || evidence.mappedNames?.length !== 1) throw new Error('Candidate Worker returned incomplete FreeCAD naming evidence for OCCT cut history.')
|
||||
const secondEvidence = captureFreeCadPrivateNamingEvidence(candidate, createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'candidate-worker-cut-2',
|
||||
documentId: 'candidate-worker-document',
|
||||
documentVersion: 2,
|
||||
operationId: 'candidate-worker-cut-2',
|
||||
operation: 'cut',
|
||||
stageId: 'candidate-worker:stage:2',
|
||||
resultObjectId: 'candidate-worker:result:2',
|
||||
resultObjectTag: 101,
|
||||
inputs: [{ inputId: 'object', objectId: 'candidate-worker:result:1', role: 'object', stageId: 'candidate-worker:stage:1', objectTag: 100, step: history.resultStep, namingEvidence: evidence }],
|
||||
stages: [{ stageId: 'candidate-worker:stage:2', operation: 'cut', inputIds: ['object'], ordinal: 0 }],
|
||||
resultStep: history.resultStep,
|
||||
history: { ...history, records: [{ relation: 'modified', source: 'object', kind: record.resultKind ?? record.kind, sourceIndex: resultIndex, resultIndex: resultIndex + 1 }] },
|
||||
}), probe)
|
||||
const thirdEvidence = secondEvidence && captureFreeCadPrivateNamingEvidence(candidate, createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'candidate-worker-cut-3',
|
||||
documentId: 'candidate-worker-document',
|
||||
documentVersion: 3,
|
||||
operationId: 'candidate-worker-cut-3',
|
||||
operation: 'cut',
|
||||
stageId: 'candidate-worker:stage:3',
|
||||
resultObjectId: 'candidate-worker:result:3',
|
||||
resultObjectTag: 102,
|
||||
inputs: [{ inputId: 'object', objectId: 'candidate-worker:result:2', role: 'object', stageId: 'candidate-worker:stage:2', objectTag: 101, step: history.resultStep, namingEvidence: secondEvidence }],
|
||||
stages: [{ stageId: 'candidate-worker:stage:3', operation: 'cut', inputIds: ['object'], ordinal: 0 }],
|
||||
resultStep: history.resultStep,
|
||||
history: { ...history, records: [{ relation: 'modified', source: 'object', kind: record.resultKind ?? record.kind, sourceIndex: resultIndex + 1, resultIndex: resultIndex + 2 }] },
|
||||
}), probe)
|
||||
if (!secondEvidence?.stringHasher || !thirdEvidence?.stringHasher || secondEvidence.stringHasher.entries.length < 1 || thirdEvidence.stringHasher.entries.length <= secondEvidence.stringHasher.entries.length) throw new Error('Candidate Worker did not restore non-empty StringHasher evidence across three stages.')
|
||||
|
||||
let rejected = false
|
||||
try {
|
||||
captureFreeCadPrivateNamingEvidence(candidate, createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'candidate-worker-invalid',
|
||||
documentId: 'candidate-worker-document',
|
||||
documentVersion: 2,
|
||||
operationId: 'candidate-worker-invalid',
|
||||
operation: 'cut',
|
||||
stageId: 'candidate-worker:invalid',
|
||||
resultObjectId: 'candidate-worker:invalid-result',
|
||||
resultObjectTag: 101,
|
||||
inputs: [{ inputId: 'object', objectId: 'candidate-object', role: 'object', objectTag: 42, step: objectStep }],
|
||||
stages: [{ stageId: 'candidate-worker:invalid', operation: 'cut', inputIds: ['object'], ordinal: 0 }],
|
||||
history: { ...history, records: [] },
|
||||
}), probe)
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error) || !error.message.includes('requires inputs and native history records')) throw error
|
||||
rejected = true
|
||||
}
|
||||
if (!rejected) throw new Error('Candidate Worker accepted incomplete native history.')
|
||||
|
||||
const result = {
|
||||
status: 'freecad-naming-candidate-worker-pass',
|
||||
occtVersion: history.occtVersion,
|
||||
callbacks: ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson'],
|
||||
namingEvidence: {
|
||||
mappedNames: evidence.mappedNames.length,
|
||||
stringHasherEntries: evidence.stringHasher.entries.length,
|
||||
elementMaps: evidence.elementMap2.maps.length,
|
||||
},
|
||||
chainedStage: { mappedNames: secondEvidence.mappedNames.length, stringHasherEntries: secondEvidence.stringHasher.entries.length },
|
||||
thirdStage: { mappedNames: thirdEvidence.mappedNames.length, stringHasherEntries: thirdEvidence.stringHasher.entries.length },
|
||||
invalidHistoryRejected: true,
|
||||
candidateOnly: true,
|
||||
productionPublication: false,
|
||||
productionWorkerLinked: false,
|
||||
}
|
||||
const reportPath = process.env.FREECAD_NAMING_WORKER_REPORT
|
||||
if (reportPath) {
|
||||
const artifacts = await Promise.all(['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm', 'bitbybit-occt-history.data'].map(async (name) => {
|
||||
const path = resolve(dist, name)
|
||||
const [bytes, content] = await Promise.all([stat(path).then(({ size }) => size), readFile(path)])
|
||||
return { name, bytes, sha256: createHash('sha256').update(content).digest('hex') }
|
||||
}))
|
||||
const absoluteReportPath = resolve(reportPath)
|
||||
await mkdir(dirname(absoluteReportPath), { recursive: true })
|
||||
await writeFile(absoluteReportPath, `${JSON.stringify({ schemaVersion: 1, ...result, artifacts }, null, 2)}\n`)
|
||||
}
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
@@ -14,15 +14,60 @@
|
||||
{ "name": "FreeCADApp", "path": "lib/libFreeCADApp.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "Part", "path": "lib/libPart.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "QtCore", "path": "lib/libQt6Core.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "Python", "path": "lib/libpython3.a", "sha256": "replace-with-64-lowercase-hex-digits" }
|
||||
{ "name": "QtConcurrent", "path": "lib/libQt6Concurrent.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "QtNetwork", "path": "lib/libQt6Network.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "QtXml", "path": "lib/libQt6Xml.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "QtBundledPcre2", "path": "lib/libQt6BundledPcre2.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "QtBundledZLIB", "path": "lib/libQt6BundledZLIB.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "yaml-cpp", "path": "lib/libyaml-cpp.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "ICUCommon", "path": "lib/libicuuc.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "ICUI18N", "path": "lib/libicui18n.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "ICUData", "path": "lib/libicudata.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "XercesC", "path": "lib/libxerces-c.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "BoostProgramOptions", "path": "lib/libboost_program_options.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "BoostRegex", "path": "lib/libboost_regex.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "BoostThread", "path": "lib/libboost_thread.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "BoostDateTime", "path": "lib/libboost_date_time.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "BoostAtomic", "path": "lib/libboost_atomic.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "Python", "path": "lib/libpython3.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "PythonMpdecimal", "path": "lib/python-deps/libmpdec.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "PythonExpat", "path": "lib/python-deps/libexpat.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "PythonHaclSha2", "path": "lib/python-deps/libHacl_Hash_SHA2.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "PythonZlib", "path": "lib/python-deps/libz.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "PythonBzip2", "path": "lib/python-deps/libbz2.a", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "name": "PythonSqlite3", "path": "lib/python-deps/libsqlite3-mt.a", "sha256": "replace-with-64-lowercase-hex-digits" }
|
||||
],
|
||||
"namingBridge": {
|
||||
"source": "src/freecad_naming_bridge.cpp",
|
||||
"sha256": "replace-with-64-lowercase-hex-digits",
|
||||
"hostAdapter": "runtime/freecad_naming_pre.js",
|
||||
"hostAdapterSha256": "replace-with-64-lowercase-hex-digits",
|
||||
"exports": [
|
||||
"freecadNamingAbiVersion",
|
||||
"freecadNamingCapabilitiesJson",
|
||||
"freecadNamingEvidenceJson"
|
||||
]
|
||||
},
|
||||
"compileOptions": {
|
||||
"cxxStandard": "c++20",
|
||||
"pthread": true,
|
||||
"forceInclude": "include/freecad-wasm-sdk-compat.h",
|
||||
"forceIncludeSha256": "replace-with-64-lowercase-hex-digits",
|
||||
"definitions": ["__linux__=1", "QT_NO_KEYWORDS", "HAVE_CONFIG_H", "PYCXX_6_2_COMPATIBILITY"]
|
||||
},
|
||||
"runtimeAssets": [{
|
||||
"name": "PythonWasmStdlib",
|
||||
"path": "runtime/python",
|
||||
"preloadTo": "/usr/local",
|
||||
"files": [
|
||||
{ "path": "lib/python313.zip", "sha256": "replace-with-64-lowercase-hex-digits" },
|
||||
{ "path": "lib/python3.13/os.py", "sha256": "replace-with-64-lowercase-hex-digits" }
|
||||
]
|
||||
}],
|
||||
"productionPublication": false,
|
||||
"boundary": {
|
||||
"freecadNamingBuildStatus": "contract-only",
|
||||
"exTsn02": "in_progress",
|
||||
"systemExact": false
|
||||
}
|
||||
}
|
||||
|
||||
31
package.json
31
package.json
@@ -48,7 +48,16 @@
|
||||
"test:golden:freecad:failures": "node scripts/run-freecad-golden-failures.mjs",
|
||||
"test:golden:freecad:family-failures": "node scripts/run-freecad-golden-failures.mjs --manifest=fixtures/freecad-golden/feature-families/failures/manifest.json",
|
||||
"check:occt-history-artifact": "node scripts/check-occt-history-browser-artifact.mjs",
|
||||
"check:freecad-naming-sdk-readiness": "node scripts/check-freecad-naming-sdk-readiness.mjs",
|
||||
"check:freecad-wasm-sdk-build-plan": "node scripts/check-freecad-wasm-sdk-build-plan.mjs",
|
||||
"check:freecad-naming-next-tasks": "node scripts/check-freecad-naming-next-tasks.mjs",
|
||||
"configure:freecad-naming-sdk-candidate": "node scripts/configure-freecad-naming-sdk-candidate.mjs",
|
||||
"build:freecad-naming-sdk-candidate": "bash scripts/build-freecad-naming-sdk-candidate.sh",
|
||||
"build:freecad-naming-bridge-candidate": "bash scripts/build-freecad-naming-bridge-candidate.sh",
|
||||
"test:freecad-naming-bridge-candidate": "tsx native/freecad-naming-bridge/smoke-test.ts",
|
||||
"check:freecad-naming-bridge-candidate": "node scripts/check-freecad-naming-bridge-candidate.mjs",
|
||||
"check:freecad-naming-sdk": "node scripts/check-freecad-naming-sdk.mjs",
|
||||
"generate:freecad-naming-sdk-manifest": "node scripts/generate-freecad-naming-sdk-manifest.mjs",
|
||||
"check:freecad-private-naming-boundary": "node scripts/check-freecad-private-naming-boundary.mjs",
|
||||
"test:browser-occt": "node scripts/run-browser-occt-harness.mjs",
|
||||
"test:freecad:threeway": "node scripts/run-freecad-threeway-boolean.mjs",
|
||||
@@ -63,7 +72,7 @@
|
||||
"build:freecad-native": "node scripts/build-freecad-native.mjs",
|
||||
"check:freecad-desktop-oracle": "node scripts/check-freecad-desktop-oracle.mjs",
|
||||
"probe:freecad-sketcher-constraints": "node scripts/run-freecad-sketcher-constraint-oracle.mjs",
|
||||
"check:freecad-sketcher-constraints": "node scripts/check-freecad-sketcher-constraint-oracle.mjs && node scripts/check-freecad-partdesign-profile-oracle.mjs && node scripts/check-freecad-partdesign-base-oracle.mjs && node scripts/check-freecad-partdesign-loft-oracle.mjs && node scripts/check-freecad-partdesign-dressup-oracle.mjs && node scripts/check-freecad-partdesign-transform-oracle.mjs && node scripts/check-freecad-partdesign-failure-oracle.mjs && node scripts/check-freecad-partdesign-revolution-groove-oracle.mjs && node scripts/check-freecad-part-builders-oracle.mjs && node scripts/check-freecad-sketcher-editor-oracle.mjs && npm run check:freecad-sketcher-partdesign-abi",
|
||||
"check:freecad-sketcher-constraints": "node scripts/check-freecad-sketcher-constraint-oracle.mjs && node scripts/check-freecad-partdesign-profile-oracle.mjs && node scripts/check-freecad-partdesign-base-oracle.mjs && node scripts/check-freecad-partdesign-loft-oracle.mjs && node scripts/check-freecad-partdesign-dressup-oracle.mjs && node scripts/check-freecad-partdesign-transform-oracle.mjs && node scripts/check-freecad-partdesign-structure-oracle.mjs && node scripts/check-freecad-attachment-mode-oracle.mjs && node scripts/check-freecad-partdesign-failure-oracle.mjs && node scripts/check-freecad-partdesign-revolution-groove-oracle.mjs && node scripts/check-freecad-part-builders-oracle.mjs && node scripts/check-freecad-sketcher-editor-oracle.mjs && npm run check:freecad-sketcher-partdesign-abi",
|
||||
"check:freecad-sketcher-partdesign-abi": "tsx scripts/check-freecad-sketcher-partdesign-abi-contract.mjs",
|
||||
"probe:freecad-partdesign-profiles": "node scripts/run-freecad-partdesign-profile-oracle.mjs",
|
||||
"check:freecad-partdesign-profiles": "node scripts/check-freecad-partdesign-profile-oracle.mjs",
|
||||
@@ -75,6 +84,10 @@
|
||||
"check:freecad-partdesign-dressup": "node scripts/check-freecad-partdesign-dressup-oracle.mjs",
|
||||
"probe:freecad-partdesign-transform": "node scripts/run-freecad-partdesign-transform-oracle.mjs",
|
||||
"check:freecad-partdesign-transform": "node scripts/check-freecad-partdesign-transform-oracle.mjs",
|
||||
"probe:freecad-partdesign-structure": "node scripts/run-freecad-partdesign-structure-oracle.mjs",
|
||||
"check:freecad-partdesign-structure": "node scripts/check-freecad-partdesign-structure-oracle.mjs",
|
||||
"probe:freecad-attachment-modes": "node scripts/run-freecad-attachment-mode-oracle.mjs",
|
||||
"check:freecad-attachment-modes": "node scripts/check-freecad-attachment-mode-oracle.mjs",
|
||||
"probe:freecad-sketcher-editor": "node scripts/run-freecad-sketcher-editor-oracle.mjs",
|
||||
"check:freecad-sketcher-editor": "node scripts/check-freecad-sketcher-editor-oracle.mjs",
|
||||
"test:freecad-fcstd-native": "tsx scripts/run-freecad-fcstd-native-oracle.ts",
|
||||
@@ -82,10 +95,20 @@
|
||||
"check:freecad-cam-path": "node scripts/check-freecad-cam-path-oracle.mjs",
|
||||
"test:freecad-cam-path-report": "node --test tests/freecadCamPathOracle.test.mjs",
|
||||
"build:occt-history": "bash native/occt-history/build.sh",
|
||||
"build:freecad-naming-worker": "FREECAD_WASM_NAMING_REQUIRED=1 bash native/occt-history/build.sh",
|
||||
"build:freecad-naming-worker": "FREECAD_WASM_NAMING_REQUIRED=1 FREECAD_WASM_SDK_DIR=.cache/toolchains/freecad-naming-sdk OCCT_BUILD_DIR=.cache/bitbybit/occt-history-build-freecad-pthread OCCT_HISTORY_DIST_DIR=.cache/candidates/freecad-naming-worker OCCT_HISTORY_PUBLISH=0 bash native/occt-history/build.sh",
|
||||
"test:freecad-naming-worker-candidate": "FREECAD_NAMING_WORKER_REPORT=.cache/toolchains/freecad-naming-sdk/candidate-worker-report.json tsx native/occt-history/freecad-naming-candidate-smoke.ts",
|
||||
"test:chrome-freecad-naming-worker-candidate": "node scripts/run-chrome-freecad-naming-worker-candidate.mjs",
|
||||
"check:chrome-freecad-naming-worker-candidate": "node scripts/check-chrome-freecad-naming-worker-candidate.mjs",
|
||||
"build:qt6-wasm-core": "bash scripts/build-qt6-wasm-core.sh",
|
||||
"build:qt6-freecad-wasm": "bash scripts/build-qt6-freecad-wasm.sh",
|
||||
"build:yaml-cpp-wasm": "bash scripts/build-yaml-cpp-wasm.sh",
|
||||
"build:icu-wasm": "bash scripts/build-icu-wasm.sh",
|
||||
"build:cpython-wasm": "bash scripts/build-cpython-wasm.sh",
|
||||
"build:xerces-c-wasm": "bash scripts/build-xerces-c-wasm.sh",
|
||||
"build:boost-wasm": "bash scripts/build-boost-wasm.sh",
|
||||
"prepare:freecad-occt8-overlay": "bash scripts/prepare-freecad-occt8-overlay.sh",
|
||||
"build:freecad-naming-source-probe": "bash scripts/build-freecad-naming-source-probe.sh",
|
||||
"test:freecad-naming-source-probe": "node native/freecad-naming-probe/smoke-test.mjs",
|
||||
"test:freecad-naming-source-probe": "node native/freecad-naming-probe/smoke-test.mjs && tsx native/freecad-naming-probe/abi-candidate-smoke.ts",
|
||||
"test:freecad-private-naming-boundary": "node --test tests/freecadPrivateNamingBoundary.test.mjs",
|
||||
"test:occt-history": "node native/occt-history/smoke-test.mjs",
|
||||
"build:planegcs": "bash native/planegcs/build.sh",
|
||||
@@ -274,7 +297,7 @@
|
||||
"test:browser-matrix": "node scripts/run-browser-matrix.mjs",
|
||||
"check:browser-matrix": "node scripts/check-browser-matrix.mjs",
|
||||
"check:runtime": "node scripts/check-runtime.mjs",
|
||||
"verify": "npm run check:runtime && npm run check:baseline && npm run check:bitbybit-history && npm run check:execution-plan && npm run check:freecad-sketcher-constraints && npm run check:freecad-inventory && npm run check:freecad-type-properties && npm run check:freecad-gui-commands && npm run check:freecad-entrypoints && npm run check:freecad-golden-fixtures && npm run check:freecad-semantic-comparator && npm run check:freecad-fcstd-roundtrip && npm run check:freecad-composite-history-elementmap && npm run check:freecad-exact-history-elementmap-gate && npm run check:freecad-native-naming-evidence && npm run check:fcstd-hardening && npm run check:freecad-naming-sdk && npm run check:freecad-private-naming-boundary && npm run check:occt-history-artifact && npm run check:planegcs-artifact && npm run check:browser-occt && npm run check:chrome-topology && npm run check:chrome-planegcs && npm run check:chrome-fault-injection && npm run check:chrome-opfs-migration && npm run check:chrome-persistence && npm run check:chrome-app-e2e && npm run check:chrome-partdesign-lifecycle && npm run check:pwa && npm run check:chrome-offline && npm run check:chrome-geometry-features && npm run check:chrome-native-history && npm run check:chrome-native-pad-history && npm run check:chrome-native-pocket-history && npm run check:chrome-native-loft-history && npm run check:chrome-native-pipe-history && npm run check:chrome-native-revolution-history && npm run check:chrome-native-groove-history && npm run check:chrome-native-fillet-history && npm run check:chrome-native-chamfer-history && npm run check:chrome-native-hole-history && npm run check:chrome-native-draft-history && npm run check:chrome-native-thickness-history && npm run check:chrome-native-linear-pattern-history && npm run check:chrome-native-polar-pattern-history && npm run check:chrome-native-mirrored-history && npm run check:chrome-native-multi-transform-history && npm run check:chrome-sketcher-diagnostics && npm run check:chrome-profile-validation && npm run check:chrome-sketcher-editor && npm run check:chrome-sketcher-bspline && npm run check:chrome-sketcher-stress && npm run check:chrome-part-primitives && npm run check:chrome-partdesign-transform && npm run check:chrome-partdesign-loft && npm run check:partdesign-closure && npm run check:chrome-fcstd-semantic && npm run check:chrome-fcstd-roundtrip && npm run check:fcstd-closure && npm run check:chrome-spreadsheet && npm run check:chrome-plot && npm run check:chrome-draft && npm run check:chrome-techdraw && npm run check:chrome-assembly && npm run check:chrome-mesh && npm run check:chrome-inspection && npm run check:chrome-bim && npm run check:chrome-cam && npm run check:cam-parity && npm run check:cam-pipeline && npm run check:opencamlib-wasm && npm run check:camotics-native && npm run check:camotics-gui-tpl && npm run check:camotics-wasm && npm run check:chrome-camotics-wasm && npm run check:chrome-cam-native-simulation && npm run check:chrome-cam-linuxcnc-machine && npm run check:chrome-fem && npm run check:chrome-robot && npm run check:chrome-data && npm run check:chrome-addon && npm run check:chrome-script && npm run check:chrome-surface && npm run check:chrome-qa08 && npm run check:chrome-secondary-formats && npm run check:chrome-performance && npm run check:chrome-production-document && npm run check:document-closure && npm run check:chrome-engineering && npm run check:chrome-security && npm run check:chrome-fcstd-golden && npm run check:sbom && npm run check:project-migration && npm run check:security-baseline && npm run check:dependency-audit && npm run check:freecad:threeway && npm run check:occt-upstream-drift && npm run check:facade-boundary && npm run test:golden && npm run test:spreadsheet && npm run test:plot && npm run test:draft && npm run test:techdraw && npm run test:assembly && npm run test:mesh && npm run test:inspection && npm run test:bim && npm run test:cam && npm run test:camotics-wasm && npm run test:fem && npm run test:robot && npm run test:data && npm run test:addon && npm run test:script && npm run test:surface && npm run test:locale && npm run test:secondary-formats && npm run test:performance && npm run test:production-document && npm run test:engineering && npm run test:security-preflight && npm run test:fcstd-roundtrip && npm run test:facade && npm run test:fcstd-fuzz && npm run test:geometry-input-fuzz && npm run test:sketch-solver-fuzz && npm run check:quality-closure && npm run test:planegcs && npm run test:project-migration && npm run test:sketcher-stress && npm run test:topology-replay && npm run build && npm run check:built-pwa && npm run generate:release-artifacts && npm run check:release-artifacts && npm run check:release-closure && npm run test:release-signature",
|
||||
"verify": "npm run check:runtime && npm run check:baseline && npm run check:bitbybit-history && npm run check:execution-plan && npm run check:freecad-sketcher-constraints && npm run check:freecad-inventory && npm run check:freecad-type-properties && npm run check:freecad-gui-commands && npm run check:freecad-entrypoints && npm run check:freecad-golden-fixtures && npm run check:freecad-semantic-comparator && npm run check:freecad-fcstd-roundtrip && npm run check:freecad-composite-history-elementmap && npm run check:freecad-exact-history-elementmap-gate && npm run check:freecad-native-naming-evidence && npm run check:fcstd-hardening && npm run check:freecad-naming-sdk-readiness && npm run check:freecad-wasm-sdk-build-plan && npm run check:freecad-naming-next-tasks && npm run check:freecad-naming-sdk && npm run check:freecad-private-naming-boundary && npm run check:occt-history-artifact && npm run check:planegcs-artifact && npm run check:browser-occt && npm run check:chrome-topology && npm run check:chrome-planegcs && npm run check:chrome-fault-injection && npm run check:chrome-opfs-migration && npm run check:chrome-persistence && npm run check:chrome-app-e2e && npm run check:chrome-partdesign-lifecycle && npm run check:pwa && npm run check:chrome-offline && npm run check:chrome-geometry-features && npm run check:chrome-native-history && npm run check:chrome-native-pad-history && npm run check:chrome-native-pocket-history && npm run check:chrome-native-loft-history && npm run check:chrome-native-pipe-history && npm run check:chrome-native-revolution-history && npm run check:chrome-native-groove-history && npm run check:chrome-native-fillet-history && npm run check:chrome-native-chamfer-history && npm run check:chrome-native-hole-history && npm run check:chrome-native-draft-history && npm run check:chrome-native-thickness-history && npm run check:chrome-native-linear-pattern-history && npm run check:chrome-native-polar-pattern-history && npm run check:chrome-native-mirrored-history && npm run check:chrome-native-multi-transform-history && npm run check:chrome-sketcher-diagnostics && npm run check:chrome-profile-validation && npm run check:chrome-sketcher-editor && npm run check:chrome-sketcher-bspline && npm run check:chrome-sketcher-stress && npm run check:chrome-part-primitives && npm run check:chrome-partdesign-transform && npm run check:chrome-partdesign-loft && npm run check:partdesign-closure && npm run check:chrome-fcstd-semantic && npm run check:chrome-fcstd-roundtrip && npm run check:fcstd-closure && npm run check:chrome-spreadsheet && npm run check:chrome-plot && npm run check:chrome-draft && npm run check:chrome-techdraw && npm run check:chrome-assembly && npm run check:chrome-mesh && npm run check:chrome-inspection && npm run check:chrome-bim && npm run check:chrome-cam && npm run check:cam-parity && npm run check:cam-pipeline && npm run check:opencamlib-wasm && npm run check:camotics-native && npm run check:camotics-gui-tpl && npm run check:camotics-wasm && npm run check:chrome-camotics-wasm && npm run check:chrome-cam-native-simulation && npm run check:chrome-cam-linuxcnc-machine && npm run check:chrome-fem && npm run check:chrome-robot && npm run check:chrome-data && npm run check:chrome-addon && npm run check:chrome-script && npm run check:chrome-surface && npm run check:chrome-qa08 && npm run check:chrome-secondary-formats && npm run check:chrome-performance && npm run check:chrome-production-document && npm run check:document-closure && npm run check:chrome-engineering && npm run check:chrome-security && npm run check:chrome-fcstd-golden && npm run check:sbom && npm run check:project-migration && npm run check:security-baseline && npm run check:dependency-audit && npm run check:freecad:threeway && npm run check:occt-upstream-drift && npm run check:facade-boundary && npm run test:golden && npm run test:spreadsheet && npm run test:plot && npm run test:draft && npm run test:techdraw && npm run test:assembly && npm run test:mesh && npm run test:inspection && npm run test:bim && npm run test:cam && npm run test:camotics-wasm && npm run test:fem && npm run test:robot && npm run test:data && npm run test:addon && npm run test:script && npm run test:surface && npm run test:locale && npm run test:secondary-formats && npm run test:performance && npm run test:production-document && npm run test:engineering && npm run test:security-preflight && npm run test:fcstd-roundtrip && npm run test:facade && npm run test:fcstd-fuzz && npm run test:geometry-input-fuzz && npm run test:sketch-solver-fuzz && npm run check:quality-closure && npm run test:planegcs && npm run test:project-migration && npm run test:sketcher-stress && npm run test:topology-replay && npm run build && npm run check:built-pwa && npm run generate:release-artifacts && npm run check:release-artifacts && npm run check:release-closure && npm run test:release-signature",
|
||||
"test:chrome-native-thickness-history": "node scripts/run-chrome-native-thickness-history.mjs",
|
||||
"check:chrome-native-thickness-history": "node scripts/check-chrome-native-thickness-history.mjs",
|
||||
"test:chrome-native-linear-pattern-history": "node scripts/run-chrome-native-linear-pattern-history.mjs",
|
||||
|
||||
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',
|
||||
|
||||
@@ -33,6 +33,12 @@ export type NativeFreeCadNamingAbiModule = {
|
||||
freecadNamingEvidenceJson?(requestJson: string): string
|
||||
}
|
||||
|
||||
type FreeCadPrivateNamingAbiError = {
|
||||
schemaVersion: 1
|
||||
status: 'error'
|
||||
error: string
|
||||
}
|
||||
|
||||
export type FreeCadPrivateNamingAbiRequest = {
|
||||
schemaVersion: 1
|
||||
requestId: string
|
||||
@@ -124,7 +130,12 @@ export const captureFreeCadPrivateNamingEvidence = (
|
||||
const responseBytes = encoder.encode(responseJson).byteLength
|
||||
const responseLimit = boundedLimit(probe.descriptor.maxResponseBytes, FREECAD_PRIVATE_NAMING_MAX_RESPONSE_BYTES)
|
||||
if (responseBytes > responseLimit) throw new RangeError(`FreeCAD naming ABI response exceeds ${responseLimit} bytes.`)
|
||||
const evidence = assertNativeNamingEvidence(JSON.parse(responseJson) as NativeStageNamingEvidence)
|
||||
const parsed = JSON.parse(responseJson) as NativeStageNamingEvidence | FreeCadPrivateNamingAbiError
|
||||
if (parsed && parsed.status === 'error') {
|
||||
if (parsed.schemaVersion !== FREECAD_PRIVATE_NAMING_ABI_VERSION || typeof parsed.error !== 'string' || !parsed.error.trim()) throw new TypeError('FreeCAD naming ABI returned a malformed error response.')
|
||||
throw new Error(`FreeCAD naming ABI failed: ${parsed.error}`)
|
||||
}
|
||||
const evidence = assertNativeNamingEvidence(parsed)
|
||||
if (evidence.stageId !== request.stageId || evidence.resultObjectId !== request.resultObjectId) throw new RangeError('FreeCAD naming ABI response does not match its stage and result object context.')
|
||||
if (evidence.status !== 'native-evidence' && evidence.status !== 'ambiguous') throw new TypeError(`FreeCAD naming ABI cannot return non-native status '${evidence.status}'.`)
|
||||
if (!evidence.stringHasher || !evidence.elementMap2) throw new TypeError('FreeCAD naming ABI response requires both StringHasher and ElementMap2 evidence.')
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { inspectWasmStaticArchive } from '../scripts/freecad-naming-sdk-lib.mjs'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
|
||||
const arArchive = (member, bytes) => {
|
||||
const field = (value, length) => String(value).padEnd(length, ' ')
|
||||
const header = `${field(`${member}/`, 16)}${field(0, 12)}${field(0, 6)}${field(0, 6)}${field('100644', 8)}${field(bytes.length, 10)}` + '`\n'
|
||||
return Buffer.concat([Buffer.from('!<arch>\n'), Buffer.from(header), bytes, ...(bytes.length % 2 ? [Buffer.from('\n')] : [])])
|
||||
}
|
||||
|
||||
test('Qt wasm Core build pins the released QtBase source and project Emscripten baseline', async () => {
|
||||
const source = await readFile(resolve(root, 'scripts/build-qt6-wasm-core.sh'), 'utf8')
|
||||
const freecadQt = await readFile(resolve(root, 'scripts/build-qt6-freecad-wasm.sh'), 'utf8')
|
||||
assert.match(source, /QT_VERSION="6\.8\.2"/)
|
||||
assert.match(source, /012043ce6d411e6e8a91fdc4e05e6bedcfa10fcb1347d3c33908f7fdd10dfe05/)
|
||||
assert.match(source, /Emscripten 3\.1\.69/)
|
||||
@@ -14,19 +23,45 @@ test('Qt wasm Core build pins the released QtBase source and project Emscripten
|
||||
assert.match(source, /WEB_FREECAD_OFFLINE/)
|
||||
assert.match(source, /cmake --install "\$\{QT_BUILD_DIR\}\/src\/corelib"/)
|
||||
assert.doesNotMatch(source, /cmake --install[^\n]+\|\| true/)
|
||||
assert.match(freecadQt, /Core Concurrent Network Xml/)
|
||||
assert.match(freecadQt, /feature-thread/)
|
||||
assert.match(freecadQt, /mkspecs\/wasm-emscripten/)
|
||||
assert.match(freecadQt, /inspectWasmStaticArchive/)
|
||||
})
|
||||
|
||||
test('FreeCAD source probe pins the exact private source inputs and does not export the production ABI', async () => {
|
||||
const build = await readFile(resolve(root, 'scripts/build-freecad-naming-source-probe.sh'), 'utf8')
|
||||
const smoke = await readFile(resolve(root, 'native/freecad-naming-probe/smoke-test.mjs'), 'utf8')
|
||||
const candidate = await readFile(resolve(root, 'native/freecad-naming-probe/abi-candidate-smoke.ts'), 'utf8')
|
||||
assert.match(build, /0108fd4b4850cc46e625b60e53cea7a7bbe69f8d/)
|
||||
assert.match(build, /73e0e60a9d6ee06851252e2071ebdd58698f8f99733903232529b3a194164435/)
|
||||
assert.match(build, /90173aba5f331ac9453589833f7e5fc63c5c80a35f1600dbc71211606b1c9dbc/)
|
||||
assert.match(build, /fd32f35c9c2a0c21a60fa634ec37ec594d6f331924c5bba7fdbbcb351c3b612c/)
|
||||
assert.match(build, /8f02d27a8b672e9a56764f85e394265b4fcf023aba097f040b03dcb38bc393d2/)
|
||||
assert.match(build, /StringHasher\.cpp/)
|
||||
assert.match(build, /Base\/Handle\.cpp/)
|
||||
assert.match(build, /f5448d9de693e319460d5e3ef52c63a54e0948963e4b92786aac38c77bdcd7a5/)
|
||||
assert.match(build, /fc3223857ca6d2990d2b48f437f75841cdf23e1e79fa18ef18296f40acd3cdc3/)
|
||||
assert.match(build, /e1ceaedb624688ddffa6a834c1b32539e8ba5bbdc0ec31401ea087e6a9c3eb52/)
|
||||
assert.match(build, /MappedElement\.cpp/)
|
||||
assert.match(build, /ElementNamingUtils\.cpp/)
|
||||
assert.match(build, /ElementMap\.cpp/)
|
||||
assert.match(build, /libFreeCADPrivateNamingProbe\.a/)
|
||||
assert.match(build, /emar rcs/)
|
||||
assert.match(build, /git -C "\$\{FREECAD_SOURCE_DIR\}" diff --quiet/)
|
||||
assert.doesNotMatch(build, /public\/native\/freecad-naming-probe/)
|
||||
for (const callback of ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson']) {
|
||||
assert.match(smoke, new RegExp(callback))
|
||||
}
|
||||
assert.match(smoke, /productionWorkerLinked: false/)
|
||||
assert.match(smoke, /freecadPrivateStringHasherSourceProbe/)
|
||||
assert.match(smoke, /freecadPrivateElementMapSourceProbe/)
|
||||
assert.match(smoke, /archive\.memberCount !== 7/)
|
||||
assert.match(smoke, /productionEligible: false/)
|
||||
assert.match(candidate, /captureFreeCadPrivateNamingEvidence/)
|
||||
assert.match(candidate, /parseElementMap2/)
|
||||
assert.match(candidate, /productionCallbacksExported: false/)
|
||||
assert.match(candidate, /productionWorkerLinked: false/)
|
||||
})
|
||||
|
||||
test('private naming boundary checker cross-validates all authoritative status files', async () => {
|
||||
@@ -35,8 +70,148 @@ test('private naming boundary checker cross-validates all authoritative status f
|
||||
'config/freecad-sketcher-partdesign-abi-contract.json',
|
||||
'config/compatibility-matrix.json',
|
||||
'config/freecad-web-exact-parity-plan.json',
|
||||
'config/freecad-private-naming-source-readiness.json',
|
||||
'config/freecad-naming-sdk-plan.json',
|
||||
]) assert.match(source, new RegExp(path.replaceAll('.', '\\.')))
|
||||
assert.match(source, /shippedWorkerImplementation !== 'not-linked'/)
|
||||
assert.match(source, /task\.status !== 'in_progress'/)
|
||||
assert.match(source, /systemExactEvaluation\?\.exact !== false/)
|
||||
assert.match(source, /production\?\.workerLinked !== false/)
|
||||
assert.match(source, /candidateAbi\?\.scope !== 'isolated-non-production'/)
|
||||
assert.match(source, /callback\.startsWith\('freecadNamingCandidate'\)/)
|
||||
assert.match(source, /FreeCAD private naming Worker bridge/)
|
||||
assert.match(source, /sdkPlan\.productionPublication !== false/)
|
||||
})
|
||||
|
||||
test('SDK readiness verifies the complete candidate wasm closure and keeps it pre-production', async () => {
|
||||
const plan = JSON.parse(await readFile(resolve(root, 'config/freecad-naming-sdk-plan.json'), 'utf8'))
|
||||
const readiness = await readFile(resolve(root, 'scripts/check-freecad-naming-sdk-readiness.mjs'), 'utf8')
|
||||
const sdkBuildPlan = await readFile(resolve(root, 'config/freecad-wasm-sdk-build-plan.json'), 'utf8')
|
||||
const sdkBootstrap = await readFile(resolve(root, 'config/freecad-wasm-sdk-bootstrap.cmake'), 'utf8')
|
||||
const sdkCompat = await readFile(resolve(root, 'config/freecad-wasm-sdk-compat.h'), 'utf8')
|
||||
const sdkExecinfo = await readFile(resolve(root, 'config/wasm-compat/execinfo.h'), 'utf8')
|
||||
const sdkToolchain = await readFile(resolve(root, 'config/emscripten-freecad-candidate-toolchain.cmake'), 'utf8')
|
||||
const sdkConfigure = await readFile(resolve(root, 'scripts/configure-freecad-naming-sdk-candidate.mjs'), 'utf8')
|
||||
const sdkBuild = await readFile(resolve(root, 'scripts/build-freecad-naming-sdk-candidate.sh'), 'utf8')
|
||||
const sdkOverlay = await readFile(resolve(root, 'scripts/prepare-freecad-occt8-overlay.sh'), 'utf8')
|
||||
const sdkCheck = await readFile(resolve(root, 'scripts/check-freecad-naming-sdk.mjs'), 'utf8')
|
||||
const sdkManifest = await readFile(resolve(root, 'scripts/generate-freecad-naming-sdk-manifest.mjs'), 'utf8')
|
||||
const bridgeBuild = await readFile(resolve(root, 'scripts/build-freecad-naming-bridge-candidate.sh'), 'utf8')
|
||||
const bridgeSmoke = await readFile(resolve(root, 'native/freecad-naming-bridge/smoke-test.ts'), 'utf8')
|
||||
const bridgeCheck = await readFile(resolve(root, 'scripts/check-freecad-naming-bridge-candidate.mjs'), 'utf8')
|
||||
const workerBuild = await readFile(resolve(root, 'native/occt-history/build.sh'), 'utf8')
|
||||
const workerSmoke = await readFile(resolve(root, 'native/occt-history/freecad-naming-candidate-smoke.ts'), 'utf8')
|
||||
const chromeRun = await readFile(resolve(root, 'scripts/run-chrome-freecad-naming-worker-candidate.mjs'), 'utf8')
|
||||
const chromeCheck = await readFile(resolve(root, 'scripts/check-chrome-freecad-naming-worker-candidate.mjs'), 'utf8')
|
||||
assert.equal(plan.productionPublication, false)
|
||||
assert.deepEqual(plan.libraries.map(({ name }) => name), ['FreeCADBase', 'FreeCADApp', 'Part', 'QtCore', 'Python'])
|
||||
assert.deepEqual(plan.linkDependencies.map(({ name }) => name), ['QtConcurrent', 'QtNetwork', 'QtXml', 'QtBundledPcre2', 'QtBundledZLIB', 'yaml-cpp', 'ICUCommon', 'ICUI18N', 'ICUData', 'XercesC', 'BoostProgramOptions', 'BoostRegex', 'BoostThread', 'BoostDateTime', 'BoostAtomic', 'PythonMpdecimal', 'PythonExpat', 'PythonHaclSha2', 'PythonZlib', 'PythonBzip2', 'PythonSqlite3'])
|
||||
assert.equal(plan.compileOptions.cxxStandard, 'c++20')
|
||||
assert.equal(plan.compileOptions.pthread, true)
|
||||
assert.deepEqual(plan.runtimeAssets.map(({ name }) => name), ['PythonWasmStdlib'])
|
||||
assert.match(readiness, /inspectPlannedLibrary/)
|
||||
assert.match(readiness, /publishToWorker: false/)
|
||||
assert.match(readiness, /inspectPlannedRuntimeAsset/)
|
||||
assert.match(sdkCheck, /inspectWasmStaticArchive/)
|
||||
assert.match(sdkCheck, /hostAdapterSha256/)
|
||||
assert.match(sdkCheck, /forceIncludeSha256/)
|
||||
assert.match(sdkCheck, /PythonWasmStdlib/)
|
||||
assert.match(sdkManifest, /runtimeAssets/)
|
||||
assert.match(sdkManifest, /productionPublication: false/)
|
||||
assert.match(sdkBuildPlan, /candidate-only-freecad-wasm-sdk-build/)
|
||||
assert.match(sdkBuildPlan, /productionPublication": false/)
|
||||
assert.match(sdkBootstrap, /FREECAD_WASM_EMBED_TRANSLATIONS OFF/)
|
||||
assert.match(sdkBootstrap, /function\(qt_add_translation output_variable\)/)
|
||||
assert.match(sdkBootstrap, /add_compile_definitions\(__linux__=1\)/)
|
||||
assert.match(sdkBootstrap, /FREECAD_WASM_OCCT_COMPAT_INCLUDE/)
|
||||
assert.match(sdkBootstrap, /TopTools_ListOfShape\.hxx/)
|
||||
assert.match(sdkBootstrap, /set\(UNIX FALSE\)/)
|
||||
assert.match(sdkToolchain, /Platform\/Emscripten\.cmake/)
|
||||
assert.match(sdkToolchain, /set\(UNIX FALSE\)/)
|
||||
assert.match(sdkToolchain, /CMAKE_CXX_CREATE_SHARED_LIBRARY/)
|
||||
assert.match(sdkBootstrap, /freecad-wasm-sdk-compat\.h/)
|
||||
assert.match(sdkCompat, /#include <TopTools_ListOfShape\.hxx>/)
|
||||
assert.match(sdkCompat, /#include <TopTools_HSequenceOfShape\.hxx>/)
|
||||
assert.match(sdkCompat, /#define systemTimeZone\(\) UTC/)
|
||||
assert.doesNotMatch(sdkExecinfo, /backtrace\s*\(/)
|
||||
assert.match(sdkConfigure, /Qt6_DIR/)
|
||||
assert.match(sdkConfigure, /Python3_INCLUDE_DIRS/)
|
||||
assert.match(sdkConfigure, /FREECAD_WASM_OFFLINE_SYSROOT/)
|
||||
assert.match(sdkConfigure, /src\/Deprecated\/NCollectionAliases/)
|
||||
assert.match(sdkConfigure, /source-occt8/)
|
||||
assert.match(sdkOverlay, /migrate_raise_to_throw\.py/)
|
||||
assert.match(sdkOverlay, /replace_typedefs\.py/)
|
||||
assert.match(sdkOverlay, /collected_typedefs\.json/)
|
||||
assert.match(sdkOverlay, /TopTools_ListIteratorOfListOfShape\.hxx\|TopTools_ListOfShape\.hxx/)
|
||||
assert.match(sdkOverlay, /removed_iterator_headers/)
|
||||
assert.match(sdkOverlay, /e\.DynamicType\(\)->get_type_name\(\)\|e\.ExceptionType\(\)/)
|
||||
assert.match(sdkOverlay, /legacy_exception_rtti/)
|
||||
assert.match(sdkOverlay, /BRepLProp_CurveTool::FirstParameter\(adapt\)\|adapt\.FirstParameter\(\)/)
|
||||
assert.match(sdkOverlay, /legacy_curve_tool/)
|
||||
assert.match(sdkOverlay, /Geom2dLProp_CLProps2d\.hxx>\|#include <GeomLProp_CLProps\.hxx>/)
|
||||
assert.match(sdkOverlay, /legacy_geom2d_lprop/)
|
||||
assert.match(sdkOverlay, /std::vector<TopoShape>\{e1, e2, e3, e4\}/)
|
||||
assert.match(sdkOverlay, /ambiguous_shape_mapper_calls/)
|
||||
assert.match(sdkOverlay, /Standard_Failure inherits std::exception/)
|
||||
assert.match(sdkOverlay, /unreachable_occt_exception_fallbacks/)
|
||||
assert.match(sdkOverlay, /worktree add --detach/)
|
||||
assert.match(sdkOverlay, /git -C "\$\{FREECAD_SOURCE_DIR\}" diff --quiet/)
|
||||
assert.match(sdkConfigure, /FreeCADBase|BUILD_PART/)
|
||||
assert.match(sdkConfigure, /callbacks: \[\]/)
|
||||
assert.match(sdkBuild, /cmake --build "\$\{BUILD_DIR\}" --target Part/)
|
||||
assert.match(sdkBuild, /libFreeCADBase\.a/)
|
||||
assert.match(sdkBuild, /ElementMap\.cpp\.o/)
|
||||
assert.match(sdkBuild, /productionManifestGenerated: false/)
|
||||
assert.match(sdkBuild, /availability: 'unavailable'/)
|
||||
assert.match(bridgeBuild, /libHacl_Hash_SHA2\.a/)
|
||||
assert.match(bridgeBuild, /--preload-file/)
|
||||
assert.match(bridgeBuild, /FREECAD_NAMING_BRIDGE_REPORT/)
|
||||
assert.match(bridgeBuild, /public\/native\/freecad-naming-bridge/)
|
||||
assert.match(bridgeSmoke, /freecad-private-naming-isolated-bridge-pass/)
|
||||
assert.match(bridgeSmoke, /thirdStage/)
|
||||
assert.match(bridgeSmoke, /tamperedHasherRejected/)
|
||||
assert.match(bridgeSmoke, /inconsistentTablesRejected/)
|
||||
assert.match(bridgeSmoke, /productionWorkerLinked: false/)
|
||||
assert.match(bridgeCheck, /thirdStage\?\.stringHasherEntries <= report\.chainedStage\.stringHasherEntries/)
|
||||
assert.match(bridgeCheck, /stale artifact evidence/)
|
||||
assert.match(workerBuild, /OCCT_HISTORY_PUBLISH/)
|
||||
assert.match(workerBuild, /m\.runtimeAssets/)
|
||||
assert.match(workerBuild, /m\.compileOptions\.definitions/)
|
||||
assert.match(workerBuild, /OCCT_CXX_FLAGS\+=" -pthread"/)
|
||||
assert.match(workerSmoke, /booleanHistoryFromStep/)
|
||||
assert.match(workerSmoke, /thirdStage/)
|
||||
assert.match(workerSmoke, /invalidHistoryRejected: true/)
|
||||
assert.match(chromeRun, /crossOriginIsolated/)
|
||||
assert.match(chromeRun, /thirdStage/)
|
||||
assert.match(chromeRun, /productionWorkerLinked: false/)
|
||||
assert.match(chromeCheck, /thirdStage\?\.stringHasherEntries <= report\.chainedStage\.stringHasherEntries/)
|
||||
assert.match(chromeCheck, /stale artifact evidence/)
|
||||
const fixtureDir = await mkdtemp(join(tmpdir(), 'freecad-naming-sdk-'))
|
||||
try {
|
||||
const wasmArchive = join(fixtureDir, 'libwasm.a')
|
||||
const hostArchive = join(fixtureDir, 'libhost.a')
|
||||
await writeFile(wasmArchive, arArchive('wasm.o', Buffer.from([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00])))
|
||||
await writeFile(hostArchive, arArchive('host.o', Buffer.from([0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00])))
|
||||
const wasm = await inspectWasmStaticArchive(wasmArchive)
|
||||
assert.equal(wasm.target, 'wasm32-emscripten')
|
||||
assert.equal(wasm.wasmObjectCount, 1)
|
||||
await assert.rejects(inspectWasmStaticArchive(hostArchive), /host ELF object/)
|
||||
} finally {
|
||||
await rm(fixtureDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('real wasm and offline smoke lanes rebuild and execute the source prerequisite', async () => {
|
||||
const real = await readFile(resolve(root, 'scripts/run-real-verification.mjs'), 'utf8')
|
||||
const offline = await readFile(resolve(root, 'scripts/offline-resource-lib.mjs'), 'utf8')
|
||||
for (const source of [real, offline]) {
|
||||
assert.match(source, /build:freecad-naming-source-probe/)
|
||||
assert.match(source, /test:freecad-naming-source-probe/)
|
||||
assert.match(source, /check:freecad-naming-sdk-readiness/)
|
||||
assert.match(source, /check:freecad-wasm-sdk-build-plan/)
|
||||
}
|
||||
assert.match(real, /build:qt6-wasm-core/)
|
||||
assert.match(real, /probe:freecad-attachment-modes/)
|
||||
assert.match(real, /name\.startsWith\('test:chrome-'\)/)
|
||||
assert.match(real, /requestedPhase === 'execute' \? chromeExecution : chromeChecks/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user