From c87b2871bedfdf79048136ec07cc8f1e21656d66 Mon Sep 17 00:00:00 2001 From: wangdequan Date: Fri, 21 Aug 2026 06:51:31 +0800 Subject: [PATCH] Add CAM and Cloud WebAssembly probes --- CMakeLists.txt | 18 + cMake/FindOCC.cmake | 7 + cMake/FreeCADWebDependencies.cmake | 45 + src/Mod/CAM/libarea/CMakeLists.txt | 8 +- src/Mod/Cloud/App/AppCloud.cpp | 23 +- src/Mod/Cloud/App/AppCloud.h | 4 + src/Mod/Cloud/CMakeLists.txt | 14 +- .../FreeCADCoreProbe/CAMRuntimeProbe.cpp | 691 +++++++++++++ .../FreeCADCoreProbe/CMakeLists.txt | 114 ++- .../FreeCADCoreProbe/CloudKernelProbe.cpp | 953 ++++++++++++++++++ .../FreeCADCoreProbe/CloudRuntimeProbe.cpp | 164 +++ .../FreeCADCoreProbe/cam-kernel-smoke.js | 41 + .../FreeCADCoreProbe/cam-persistence-smoke.js | 40 + .../FreeCADCoreProbe/cam-recovery-smoke.js | 59 ++ .../FreeCADCoreProbe/cam-runtime-smoke.js | 87 ++ .../cloud-inspect-persistence-smoke.js | 56 + .../cloud-inspect-recovery-smoke.js | 59 ++ .../FreeCADCoreProbe/cloud-inspect-smoke.js | 72 ++ .../FreeCADCoreProbe/cloud-kernel-smoke.js | 68 ++ .../cloud-persistence-boundary-smoke.js | 80 ++ .../FreeCADCoreProbe/cloud-recovery-smoke.js | 75 ++ .../FreeCADCoreProbe/cloud-runtime-smoke.js | 69 ++ tools/prepare_web_cloud_dependencies.sh | 133 +++ 23 files changed, 2865 insertions(+), 15 deletions(-) create mode 100644 src/WebAssembly/FreeCADCoreProbe/CAMRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/CloudKernelProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/CloudRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/cam-kernel-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/cam-persistence-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/cam-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/cam-runtime-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/cloud-inspect-persistence-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/cloud-inspect-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/cloud-inspect-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/cloud-kernel-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/cloud-persistence-boundary-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/cloud-recovery-smoke.js create mode 100644 src/WebAssembly/FreeCADCoreProbe/cloud-runtime-smoke.js create mode 100755 tools/prepare_web_cloud_dependencies.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 3982c0d5..07175ee1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,8 @@ project(FreeCAD) # block narrows the module graph to the first native-kernel probe. option(FREECAD_WEB "Build the FreeCAD WebAssembly core configuration" OFF) option(FREECAD_WEB_ENABLE_ASSEMBLY "Enable the Assembly Web probe and its dependency closure" OFF) +option(FREECAD_WEB_ENABLE_CAM "Enable the CAM Path App Web probe and its dependency closure" OFF) +option(FREECAD_WEB_ENABLE_CLOUD "Enable the Cloud App Web probe and its dependency closure" OFF) if(FREECAD_WEB) if(NOT EMSCRIPTEN OR NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") message(FATAL_ERROR @@ -108,6 +110,14 @@ if(FREECAD_WEB) list(REMOVE_ITEM _freecad_web_disabled_options BUILD_ASSEMBLY BUILD_SPREADSHEET BUILD_DRAFT BUILD_TECHDRAW) endif() + if(FREECAD_WEB_ENABLE_CAM) + # CAM's Path App target is GUI-free, but its build graph requires Mesh + # in addition to the existing Part and Import Web modules. + list(REMOVE_ITEM _freecad_web_disabled_options BUILD_CAM BUILD_MESH) + endif() + if(FREECAD_WEB_ENABLE_CLOUD) + list(REMOVE_ITEM _freecad_web_disabled_options BUILD_CLOUD) + endif() foreach(_freecad_web_disabled_option IN LISTS _freecad_web_disabled_options) set(${_freecad_web_disabled_option} OFF CACHE BOOL "Disabled by FREECAD_WEB core configuration" FORCE) endforeach() @@ -121,6 +131,14 @@ if(FREECAD_WEB) set(${_freecad_web_assembly_option} ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_ASSEMBLY" FORCE) endforeach() endif() + if(FREECAD_WEB_ENABLE_CAM) + foreach(_freecad_web_cam_option BUILD_CAM BUILD_MESH) + set(${_freecad_web_cam_option} ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_CAM" FORCE) + endforeach() + endif() + if(FREECAD_WEB_ENABLE_CLOUD) + set(BUILD_CLOUD ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_CLOUD" FORCE) + endif() include(cMake/FreeCADWebDependencies.cmake) endif() diff --git a/cMake/FindOCC.cmake b/cMake/FindOCC.cmake index b6b742fc..226a4805 100644 --- a/cMake/FindOCC.cmake +++ b/cMake/FindOCC.cmake @@ -127,6 +127,13 @@ if (OCC_FOUND) TKV3d TKRWMesh ) + if(FREECAD_WEB) + # The Web kernel does not build OCCT's MeshVS presentation library. + # Part/CAM App targets do not reference MeshVS symbols; keeping this + # desktop-only visualisation dependency in the Web OCAF closure makes + # otherwise GUI-free probes fail at link time. + list(REMOVE_ITEM OCC_OCAF_LIBRARIES TKMeshVS) + endif() if (OCC_VERSION_STRING VERSION_LESS 7.8.0) list(APPEND OCC_LIBRARIES TKIGES TKSTL TKSTEPBase TKSTEPAttr TKSTEP209 TKSTEP) diff --git a/cMake/FreeCADWebDependencies.cmake b/cMake/FreeCADWebDependencies.cmake index 7f667d8f..0e60b5cf 100644 --- a/cMake/FreeCADWebDependencies.cmake +++ b/cMake/FreeCADWebDependencies.cmake @@ -20,6 +20,10 @@ set(FREECAD_WEB_XERCESC_ROOT "${FREECAD_WEB_DEPENDENCY_ROOT}/xerces-install" CAC "Xerces-C 3.2.4 Emscripten installation prefix") set(FREECAD_WEB_OCCT_ROOT "${FREECAD_WEB_DEPENDENCY_ROOT}/occt-install" CACHE PATH "OpenCASCADE 7.6.3 Emscripten installation prefix") +set(FREECAD_WEB_OPENSSL_ROOT "${FREECAD_WEB_DEPENDENCY_ROOT}/openssl-install" CACHE PATH + "OpenSSL 3.2.2 Emscripten installation prefix") +set(FREECAD_WEB_CURL_ROOT "${FREECAD_WEB_DEPENDENCY_ROOT}/curl-install" CACHE PATH + "CURL 8.8.0 Emscripten installation prefix") set(FREECAD_WEB_HOST_PYTHON_EXECUTABLE "" CACHE FILEPATH "Native Python used for configure-time generators in a Web build") @@ -49,6 +53,14 @@ set(_freecad_web_xerces_include "${FREECAD_WEB_XERCESC_ROOT}/include") set(_freecad_web_xerces_library "${FREECAD_WEB_XERCESC_ROOT}/lib/libxerces-c.a") set(_freecad_web_occt_dir "${FREECAD_WEB_OCCT_ROOT}/lib/cmake/opencascade") +if(FREECAD_WEB_ENABLE_CLOUD) + set(_freecad_web_openssl_include "${FREECAD_WEB_OPENSSL_ROOT}/include") + set(_freecad_web_openssl_crypto "${FREECAD_WEB_OPENSSL_ROOT}/lib/libcrypto.a") + set(_freecad_web_openssl_ssl "${FREECAD_WEB_OPENSSL_ROOT}/lib/libssl.a") + set(_freecad_web_curl_include "${FREECAD_WEB_CURL_ROOT}/include") + set(_freecad_web_curl_library "${FREECAD_WEB_CURL_ROOT}/lib/libcurl.a") +endif() + _freecad_web_require("${_freecad_web_yaml_dir}/yaml-cpp-config.cmake" "yaml-cpp CMake package") _freecad_web_require("${_freecad_web_eigen_include}/signature_of_eigen3_matrix_library" "Eigen Web headers") _freecad_web_require("${_freecad_web_eigen_include}/Eigen/src/Core/util/Macros.h" "Eigen version header") @@ -74,6 +86,13 @@ _freecad_web_require("${_freecad_web_xerces_library}" "Xerces-C Web static libra _freecad_web_require("${_freecad_web_occt_dir}/OpenCASCADEConfig.cmake" "OpenCASCADE Web CMake package") _freecad_web_require("${FREECAD_WEB_OCCT_ROOT}/include/opencascade/Standard_Version.hxx" "OpenCASCADE Web headers") _freecad_web_require("${FREECAD_WEB_OCCT_ROOT}/lib/libTKernel.a" "OpenCASCADE Web foundation library") +if(FREECAD_WEB_ENABLE_CLOUD) + _freecad_web_require("${_freecad_web_openssl_include}/openssl/opensslv.h" "OpenSSL Web headers") + _freecad_web_require("${_freecad_web_openssl_crypto}" "OpenSSL Web crypto static library") + _freecad_web_require("${_freecad_web_openssl_ssl}" "OpenSSL Web TLS static library") + _freecad_web_require("${_freecad_web_curl_include}/curl/curl.h" "CURL Web headers") + _freecad_web_require("${_freecad_web_curl_library}" "CURL Web static library") +endif() if(NOT FREECAD_WEB_HOST_PYTHON_EXECUTABLE) find_program(_freecad_web_host_python NAMES python3.12 python3) @@ -137,6 +156,27 @@ set(OpenCASCADE_DIR "${_freecad_web_occt_dir}" CACHE PATH "OpenCASCADE Web CMake package" FORCE) set(OCCT_CMAKE_FALLBACK OFF CACHE BOOL "Use the OpenCASCADE Web CMake package" FORCE) +if(FREECAD_WEB_ENABLE_CLOUD) + set(OPENSSL_ROOT_DIR "${FREECAD_WEB_OPENSSL_ROOT}" CACHE PATH + "OpenSSL Web installation prefix" FORCE) + set(OPENSSL_USE_STATIC_LIBS TRUE CACHE BOOL "Use OpenSSL Web static libraries" FORCE) + set(OPENSSL_INCLUDE_DIR "${_freecad_web_openssl_include}" CACHE PATH + "OpenSSL Web headers" FORCE) + set(OPENSSL_CRYPTO_LIBRARY "${_freecad_web_openssl_crypto}" CACHE FILEPATH + "OpenSSL Web crypto static library" FORCE) + set(OPENSSL_SSL_LIBRARY "${_freecad_web_openssl_ssl}" CACHE FILEPATH + "OpenSSL Web TLS static library" FORCE) + + set(CURL_NO_CURL_CMAKE ON CACHE BOOL "Use CMake's FindCURL for the Web archive" FORCE) + set(CURL_USE_STATIC_LIBS ON CACHE BOOL "Use the CURL Web static library" FORCE) + set(CURL_INCLUDE_DIR "${_freecad_web_curl_include}" CACHE PATH + "CURL Web headers" FORCE) + set(CURL_LIBRARY "${_freecad_web_curl_library}" CACHE FILEPATH + "CURL Web static library" FORCE) + set(CURL_LIBRARY_RELEASE "${_freecad_web_curl_library}" CACHE FILEPATH + "CURL Web release static library" FORCE) +endif() + unset(_freecad_web_yaml_dir) unset(_freecad_web_eigen_include) unset(_freecad_web_boost_include) @@ -153,4 +193,9 @@ unset(_freecad_web_zlib_library) unset(_freecad_web_xerces_include) unset(_freecad_web_xerces_library) unset(_freecad_web_occt_dir) +unset(_freecad_web_openssl_include) +unset(_freecad_web_openssl_crypto) +unset(_freecad_web_openssl_ssl) +unset(_freecad_web_curl_include) +unset(_freecad_web_curl_library) unset(_freecad_web_host_python) diff --git a/src/Mod/CAM/libarea/CMakeLists.txt b/src/Mod/CAM/libarea/CMakeLists.txt index b9e498d4..9887a41f 100644 --- a/src/Mod/CAM/libarea/CMakeLists.txt +++ b/src/Mod/CAM/libarea/CMakeLists.txt @@ -18,9 +18,6 @@ include_directories( link_directories(${OCC_LIBRARY_DIR}) -include_directories(${pybind11_INCLUDE_DIR}) - - # this defines the source-files for library set(AREA_SRC_COMMON Arc.cpp @@ -73,6 +70,11 @@ add_library( ${PYAREA_SRC} ) +# Keep pybind11's host include path local to the Python shim. Propagating it +# to area-native makes Emscripten compile unrelated geometry sources against +# the host libc headers. +target_include_directories(area PRIVATE ${pybind11_INCLUDE_DIRS}) + if(MSVC) set(area_native_LIBS diff --git a/src/Mod/Cloud/App/AppCloud.cpp b/src/Mod/Cloud/App/AppCloud.cpp index 638c1bb8..9d04b051 100644 --- a/src/Mod/Cloud/App/AppCloud.cpp +++ b/src/Mod/Cloud/App/AppCloud.cpp @@ -34,9 +34,11 @@ #include #include #include -#include - -#include +#include + +#include +#include +#include #include #include @@ -1484,7 +1486,11 @@ void Cloud::Module::LinkXSetValue(std::string filename) string newName; Document* currentDoc = GetApplication().getActiveDocument(); newName = GetApplication().getUniqueDocumentName("unnamed"); - newDoc = GetApplication().newDocument(newName.c_str(), (const char*)path.c_str(), true); + newDoc = GetApplication().newDocument( + newName.c_str(), + (const char*)path.c_str(), + App::DocumentInitFlags{true, true} + ); GetApplication().setActiveDocument(newDoc); this->cloudRestore((const char*)path.c_str()); GetApplication().setActiveDocument(currentDoc); @@ -1496,7 +1502,9 @@ bool Cloud::Module::cloudRestore(const char* BucketName) Document* doc = GetApplication().getActiveDocument(); // clean up if the document is not empty // !TODO: mind exceptions while restoring! - doc->signalLinkXsetValue.connect(boost::bind(&Cloud::Module::LinkXSetValue, this, _1)); + fastsignals::scoped_connection linkConnection = doc->signalLinkXsetValue.connect( + boost::bind(&Cloud::Module::LinkXSetValue, this, _1) + ); doc->clearUndos(); @@ -1553,6 +1561,5 @@ bool Cloud::Module::cloudRestore(const char* BucketName) string uri; uri = this->URL.getStrValue() + ":" + this->TCPPort.getStrValue() + "/" + string(BucketName); doc->FileName.setValue(uri); - doc->signalLinkXsetValue.disconnect(boost::bind(&Cloud::Module::LinkXSetValue, this, _1)); - return (true); -} + return (true); +} diff --git a/src/Mod/Cloud/App/AppCloud.h b/src/Mod/Cloud/App/AppCloud.h index 7edcfe87..d842e235 100644 --- a/src/Mod/Cloud/App/AppCloud.h +++ b/src/Mod/Cloud/App/AppCloud.h @@ -247,6 +247,10 @@ public: { return FileStream; } + virtual const std::ostream& Stream(void) const + { + return FileStream; + } virtual bool shouldWrite(const std::string& name, const Base::Persistence* Object) const; void checkText(XERCES_CPP_NAMESPACE_QUALIFIER DOMText* text); void checkXML(XERCES_CPP_NAMESPACE_QUALIFIER DOMNode* node); diff --git a/src/Mod/Cloud/CMakeLists.txt b/src/Mod/Cloud/CMakeLists.txt index 300beaca..2cb14356 100644 --- a/src/Mod/Cloud/CMakeLists.txt +++ b/src/Mod/Cloud/CMakeLists.txt @@ -52,9 +52,17 @@ IF(WIN32) SET( CURL_FOUND False) message( FATAL_ERROR "curl not found!" ) endif(EXISTS ${FREECAD_LIBPACK_DIR}/lib/libcurl.lib) -ELSE() - find_package(CURL REQUIRED) -ENDIF(WIN32) +ELSE() + find_package(CURL REQUIRED) +ENDIF(WIN32) + +if(FREECAD_WEB) + # FindOpenSSL may append host pkg-config transitive flags even when its + # primary archive paths are pinned. The Web dependency closure is fully + # static and has no host-side dl/thread dependencies. + set(OPENSSL_LIBRARIES ${OPENSSL_SSL_LIBRARY} ${OPENSSL_CRYPTO_LIBRARY}) + set(CURL_LIBRARIES ${CURL_LIBRARY}) +endif() if(CURL_FOUND) diff --git a/src/WebAssembly/FreeCADCoreProbe/CAMRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/CAMRuntimeProbe.cpp new file mode 100644 index 00000000..77c6ecdd --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/CAMRuntimeProbe.cpp @@ -0,0 +1,691 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +extern "C" PyObject* PyInit_Materials(); +extern "C" PyObject* PyInit_Part(); +extern "C" PyObject* PyInit_PathApp(); + +namespace { + +std::string lastErrorCode; +std::string lastErrorMessage; +std::string lastTypeName; +unsigned int initializationCalls = 0; +unsigned int initializationRuns = 0; +bool pathInitialized = false; +std::vector handles; +std::string kernelError; +std::string kernelGCode; +unsigned int kernelCommandCount = 0; +double kernelLength = 0.0; +int kernelRecomputeCount = 0; +int kernelRecomputeHasError = 0; +std::string kernelStage; +std::string recoveryErrorCode; +std::string recoveryErrorMessage; +std::string recoveryBaselineGCode; +std::string recoveryFinalGCode; +unsigned int recoveryBaselineCommandCount = 0; +unsigned int recoveryFinalCommandCount = 0; +double recoveryBaselineLength = 0.0; +double recoveryFinalLength = 0.0; +int recoveryBaselineRecomputeCount = 0; +int recoveryFinalRecomputeCount = 0; +int recoveryHasError = 0; +int recoveryDocumentCountAfterClose = 0; +int recoveryUnchangedAfterReject = 0; +std::string persistencePath; +unsigned int persistenceSize = 0; +std::string persistenceSaveError; +std::string persistenceOpenError; +std::string persistenceStage; +int persistenceOpenedDocumentCount = 0; +int persistenceOpenedObjectCount = 0; +std::string persistenceOpenedObjectType; +std::string persistenceOpenedGCode; +int persistenceRoundTrip = 0; +int persistenceOpenedRecompute = 0; +int persistenceOpenedHasError = 0; + +void setError(const char* code, const std::string& message) +{ + lastErrorCode = code; + lastErrorMessage = message; +} + +void clearError() +{ + lastErrorCode.clear(); + lastErrorMessage.clear(); +} + +std::string pythonError(const char* fallback) +{ + if (!PyErr_Occurred()) { + return fallback; + } + PyObject* type = nullptr; + PyObject* value = nullptr; + PyObject* traceback = nullptr; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + std::string message = fallback; + if (value) { + PyObject* text = PyObject_Str(value); + if (text) { + const char* utf8 = PyUnicode_AsUTF8(text); + if (utf8 && *utf8) { + message += ": "; + message += utf8; + } + Py_DECREF(text); + } + } + Py_XDECREF(type); + Py_XDECREF(value); + Py_XDECREF(traceback); + return message; +} + +void addPythonModule(const char* name, PyObject* (*initialize)()) +{ + auto* modules = PyImport_GetModuleDict(); + if (PyDict_GetItemString(modules, name)) { + return; + } + PyObject* module = initialize(); + if (!module) { + throw Base::RuntimeError(pythonError((std::string("PyInit_") + name + " failed").c_str())); + } + if (PyDict_SetItemString(modules, name, module) != 0) { + Py_DECREF(module); + throw Base::RuntimeError(std::string("failed to register Python module ") + name); + } + Py_DECREF(module); +} + +void addPythonModuleAlias(const char* alias, const char* moduleName) +{ + auto* modules = PyImport_GetModuleDict(); + if (PyDict_GetItemString(modules, alias)) { + return; + } + PyObject* module = PyDict_GetItemString(modules, moduleName); + if (!module) { + throw Base::RuntimeError(std::string("Python module is not registered: ") + moduleName); + } + if (PyDict_SetItemString(modules, alias, module) != 0) { + throw Base::RuntimeError(std::string("failed to register Python module alias ") + alias); + } +} + +bool ensurePath() +{ + ++initializationCalls; + if (pathInitialized) { + clearError(); + return true; + } + + ++initializationRuns; + try { + App::Application::initWebCore(); + addPythonModule("Materials", PyInit_Materials); + addPythonModule("Part", PyInit_Part); + addPythonModule("PathApp", PyInit_PathApp); + // Desktop startup exposes PathApp through Path/__init__.py. The + // embedded Web probe has no Python package tree, so provide the same + // import name without pulling GUI-side CAM modules into the kernel. + addPythonModuleAlias("Path", "PathApp"); + if (Base::Type::fromName("Path::Feature").isBad()) { + throw Base::RuntimeError("Path::Feature was not registered"); + } + pathInitialized = true; + clearError(); + return true; + } + catch (const Base::Exception& error) { + setError("initialization_failed", error.what()); + } + catch (const std::exception& error) { + setError("initialization_failed", error.what()); + } + catch (...) { + setError("initialization_failed", "unknown CAM Path initialization failure"); + } + return false; +} + +bool requireModule(const char* moduleName) +{ + if (!moduleName || *moduleName == '\0') { + setError("invalid_argument", "module name is empty"); + return false; + } + const std::string name(moduleName); + if (name == "PathGui") { + setError("gui_dependency_unavailable", "PathGui is unavailable in the GUI-free Web kernel"); + return false; + } + if (name != "PathApp") { + setError("module_unavailable", "module is not available in the GUI-free Web kernel: " + name); + return false; + } + return ensurePath(); +} + +} + +extern "C" { + +EMSCRIPTEN_KEEPALIVE int fc_web_cam_initialize(const char* moduleName) +{ + return requireModule(moduleName) ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cam_type_probe(const char* typeName) +{ + if (!requireModule("PathApp")) { + return 0; + } + if (!typeName || *typeName == '\0') { + setError("invalid_argument", "type name is empty"); + return 0; + } + const Base::Type type = Base::Type::fromName(typeName); + if (type.isBad()) { + lastTypeName.clear(); + setError("type_not_found", "type is not registered: " + std::string(typeName)); + return 0; + } + lastTypeName = type.getName(); + clearError(); + return 1; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cam_create_handle(const char* typeName) +{ + if (!fc_web_cam_type_probe(typeName)) { + return 0; + } + const Base::Type type = Base::Type::fromName(typeName); + if (!type.canInstantiate()) { + setError("type_not_instantiable", "type cannot be instantiated: " + std::string(typeName)); + return 0; + } + auto* object = static_cast(type.createInstance()); + if (!object) { + setError("type_not_instantiable", "type cannot create a DocumentObject: " + std::string(typeName)); + return 0; + } + handles.push_back(object); + clearError(); + return static_cast(handles.size()); +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cam_release_handle(int handle) +{ + if (handle <= 0 || static_cast(handle) > handles.size() || handles[handle - 1] == nullptr) { + setError("invalid_handle", "handle is unknown or already released"); + return 0; + } + delete handles[handle - 1]; + handles[handle - 1] = nullptr; + clearError(); + return 1; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cam_active_handles() +{ + int active = 0; + for (const auto* handle : handles) { + if (handle) { + ++active; + } + } + return active; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_error_code() +{ + return lastErrorCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_error_message() +{ + return lastErrorMessage.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_last_type() +{ + return lastTypeName.c_str(); +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_cam_initialization_calls() +{ + return initializationCalls; +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_cam_initialization_runs() +{ + return initializationRuns; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cam_initialized() +{ + return pathInitialized ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cam_kernel_create_recompute() +{ + App::Document* document = nullptr; + kernelError.clear(); + kernelGCode.clear(); + kernelCommandCount = 0; + kernelLength = 0.0; + kernelRecomputeCount = 0; + kernelRecomputeHasError = 0; + kernelStage.clear(); + try { + if (!ensurePath()) { + kernelError = lastErrorMessage; + return 0; + } + auto& application = App::GetApplication(); + kernelStage = "newDocument"; + document = application.newDocument("CAMKernelDocument", "CAM Kernel Document", {false, true}); + if (!document) { + kernelError = "Application::newDocument returned null"; + return 0; + } + const std::string fixture = "G0 X0 Y0\nG1 X10 Y0 F100\nG1 X10 Y10 F100\n"; + kernelStage = "setFromGCode"; + Path::Toolpath path; + path.setFromGCode(fixture); + kernelStage = "addObject"; + auto* object = document->addObject("Toolpath"); + if (!object) { + kernelError = "Document::addObject returned null"; + application.closeDocument(document); + return 0; + } + kernelStage = "setPath"; + object->Path.setValue(path); + bool hasError = false; + kernelStage = "recompute"; + kernelRecomputeCount = document->recompute({}, false, &hasError); + kernelRecomputeHasError = hasError ? 1 : 0; + const Path::Toolpath& storedPath = object->Path.getValue(); + kernelCommandCount = storedPath.getSize(); + Path::Toolpath measuredPath(storedPath); + kernelLength = measuredPath.getLength(); + kernelGCode = storedPath.toGCode(); + const bool valid = kernelRecomputeCount >= 0 + && kernelRecomputeHasError == 0 + && kernelCommandCount == 3 + && kernelLength > 19.99 + && kernelLength < 20.01 + && !kernelGCode.empty(); + application.closeDocument(document); + document = nullptr; + if (!valid) { + kernelError = "CAM create/recompute contract mismatch"; + return 0; + } + return 1; + } + catch (const Base::Exception& error) { + kernelError = kernelStage + ": " + error.what(); + } + catch (const std::exception& error) { + kernelError = kernelStage + ": " + error.what(); + } + catch (...) { + kernelError = kernelStage + ": unknown CAM kernel failure"; + } + if (document) { + App::GetApplication().closeDocument(document); + } + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_kernel_error() +{ + return kernelError.c_str(); +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_cam_kernel_command_count() +{ + return kernelCommandCount; +} + +EMSCRIPTEN_KEEPALIVE double fc_web_cam_kernel_length() +{ + return kernelLength; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cam_kernel_recompute_count() +{ + return kernelRecomputeCount; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cam_kernel_recompute_has_error() +{ + return kernelRecomputeHasError; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_kernel_gcode() +{ + return kernelGCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cam_kernel_recovery() +{ + App::Document* document = nullptr; + recoveryErrorCode.clear(); + recoveryErrorMessage.clear(); + recoveryBaselineGCode.clear(); + recoveryFinalGCode.clear(); + recoveryBaselineCommandCount = 0; + recoveryFinalCommandCount = 0; + recoveryBaselineLength = 0.0; + recoveryFinalLength = 0.0; + recoveryBaselineRecomputeCount = 0; + recoveryFinalRecomputeCount = 0; + recoveryHasError = 0; + recoveryDocumentCountAfterClose = -1; + recoveryUnchangedAfterReject = 0; + try { + if (!ensurePath()) { + recoveryErrorCode = "initialization_failed"; + recoveryErrorMessage = lastErrorMessage; + return 0; + } + auto& application = App::GetApplication(); + document = application.newDocument("CAMRecoveryDocument", "CAM Recovery Document", {false, true}); + if (!document) { + recoveryErrorCode = "document_create_failed"; + recoveryErrorMessage = "Application::newDocument returned null"; + return 0; + } + + const std::string fixture = "G0 X0 Y0\nG1 X10 Y0 F100\nG1 X10 Y10 F100\n"; + Path::Toolpath validPath; + validPath.setFromGCode(fixture); + auto* object = document->addObject("Toolpath"); + if (!object) { + recoveryErrorCode = "object_create_failed"; + recoveryErrorMessage = "Document::addObject returned null"; + application.closeDocument(document); + document = nullptr; + return 0; + } + object->Path.setValue(validPath); + bool baselineHasError = false; + recoveryBaselineRecomputeCount = document->recompute({}, false, &baselineHasError); + const Path::Toolpath& baseline = object->Path.getValue(); + recoveryBaselineCommandCount = baseline.getSize(); + recoveryBaselineLength = Path::Toolpath(baseline).getLength(); + recoveryBaselineGCode = baseline.toGCode(); + + // Parse the invalid fixture into a detached candidate so a rejected + // command cannot clear or overwrite the valid document state. + try { + Path::Toolpath invalidPath; + invalidPath.setFromGCode("G1 X"); + recoveryErrorCode = "invalid_gcode"; + recoveryErrorMessage = "invalid G-code fixture was accepted"; + } + catch (const Base::Exception& error) { + recoveryErrorCode = "invalid_gcode"; + recoveryErrorMessage = error.what(); + } + catch (const std::exception& error) { + recoveryErrorCode = "invalid_gcode"; + recoveryErrorMessage = error.what(); + } + const Path::Toolpath& afterReject = object->Path.getValue(); + recoveryUnchangedAfterReject = afterReject.toGCode() == recoveryBaselineGCode + && afterReject.getSize() == recoveryBaselineCommandCount + && std::abs(Path::Toolpath(afterReject).getLength() - recoveryBaselineLength) < 1e-9; + + // Reapply the known-good snapshot and recompute, proving recovery is + // deterministic after the rejected candidate. + object->Path.setValue(validPath); + bool finalHasError = false; + recoveryFinalRecomputeCount = document->recompute({}, false, &finalHasError); + recoveryHasError = finalHasError ? 1 : 0; + const Path::Toolpath& finalPath = object->Path.getValue(); + recoveryFinalCommandCount = finalPath.getSize(); + recoveryFinalLength = Path::Toolpath(finalPath).getLength(); + recoveryFinalGCode = finalPath.toGCode(); + const bool valid = !baselineHasError + && recoveryErrorCode == "invalid_gcode" + && recoveryErrorMessage.find("Badly formatted GCode") != std::string::npos + && recoveryUnchangedAfterReject + && recoveryFinalCommandCount == recoveryBaselineCommandCount + && std::abs(recoveryFinalLength - recoveryBaselineLength) < 1e-9 + && recoveryFinalGCode == recoveryBaselineGCode + && !finalHasError; + application.closeDocument(document); + document = nullptr; + recoveryDocumentCountAfterClose = static_cast(application.getDocuments().size()); + if (!valid) { + recoveryErrorCode = "recovery_contract_mismatch"; + recoveryErrorMessage = "CAM recovery contract mismatch"; + return 0; + } + return 1; + } + catch (const Base::Exception& error) { + recoveryErrorCode = "recovery_exception"; + recoveryErrorMessage = error.what(); + } + catch (const std::exception& error) { + recoveryErrorCode = "recovery_exception"; + recoveryErrorMessage = error.what(); + } + catch (...) { + recoveryErrorCode = "recovery_exception"; + recoveryErrorMessage = "unknown CAM recovery failure"; + } + if (document) { + App::GetApplication().closeDocument(document); + recoveryDocumentCountAfterClose = static_cast(App::GetApplication().getDocuments().size()); + } + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_recovery_error_code() +{ + return recoveryErrorCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_recovery_error_message() +{ + return recoveryErrorMessage.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_recovery_baseline_gcode() +{ + return recoveryBaselineGCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_recovery_final_gcode() +{ + return recoveryFinalGCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_cam_recovery_baseline_command_count() +{ + return recoveryBaselineCommandCount; +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_cam_recovery_final_command_count() +{ + return recoveryFinalCommandCount; +} + +EMSCRIPTEN_KEEPALIVE double fc_web_cam_recovery_baseline_length() +{ + return recoveryBaselineLength; +} + +EMSCRIPTEN_KEEPALIVE double fc_web_cam_recovery_final_length() +{ + return recoveryFinalLength; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cam_recovery_baseline_recompute_count() +{ + return recoveryBaselineRecomputeCount; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cam_recovery_final_recompute_count() +{ + return recoveryFinalRecomputeCount; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cam_recovery_has_error() +{ + return recoveryHasError; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cam_recovery_document_count_after_close() +{ + return recoveryDocumentCountAfterClose; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cam_recovery_unchanged_after_reject() +{ + return recoveryUnchangedAfterReject; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cam_kernel_save_reopen() +{ + App::Document* document = nullptr; + App::Document* reopened = nullptr; + persistencePath = "/tmp/CAMKernel.FCStd"; + persistenceSize = 0; + persistenceSaveError.clear(); + persistenceOpenError.clear(); + persistenceStage.clear(); + persistenceOpenedDocumentCount = 0; + persistenceOpenedObjectCount = 0; + persistenceOpenedObjectType.clear(); + persistenceOpenedGCode.clear(); + persistenceRoundTrip = 0; + persistenceOpenedRecompute = 0; + persistenceOpenedHasError = 0; + try { + if (!ensurePath()) { + persistenceSaveError = lastErrorMessage; + return 0; + } + auto& application = App::GetApplication(); + persistenceStage = "create"; + document = application.newDocument("CAMPersistenceDocument", "CAM Persistence Document", {false, true}); + if (!document) throw Base::RuntimeError("Application::newDocument returned null"); + const std::string fixture = "G0 X0 Y0\nG1 X10 Y0 F100\nG1 X10 Y10 F100\n"; + Path::Toolpath toolpath; + toolpath.setFromGCode(fixture); + auto* object = document->addObject("Toolpath"); + if (!object) throw Base::RuntimeError("Document::addObject returned null"); + object->Path.setValue(toolpath); + bool hasError = false; + persistenceStage = "recompute"; + const int recompute = document->recompute({}, false, &hasError); + persistenceStage = "save"; + const bool saved = document->saveAs(persistencePath.c_str()); + Base::FileInfo savedFile(persistencePath); + persistenceSize = savedFile.exists() ? savedFile.size() : 0; + const bool saveValid = saved && !hasError && recompute >= 0 && persistenceSize > 0 && object->Path.getValue().getSize() == 3; + application.closeDocument(document); + document = nullptr; + if (!saveValid) { + persistenceSaveError = "CAM FCStd save contract mismatch"; + return 0; + } + + persistenceStage = "open"; + reopened = application.newDocument("CAMPersistenceOpened", "CAM Persistence Opened", {false, true}); + if (!reopened) throw Base::RuntimeError("Application::newDocument returned null for reopen"); + persistenceStage = "restore"; + reopened->FileName.setValue(persistencePath); + reopened->restore(persistencePath.c_str(), true, {}); + reopened->afterRestore(true); + persistenceOpenedDocumentCount = static_cast(application.getDocuments().size()); + persistenceOpenedObjectCount = reopened->countObjects(); + auto* reopenedObject = reopened->getObject("Toolpath"); + auto* reopenedFeature = dynamic_cast(reopenedObject); + if (!reopenedFeature) { + persistenceOpenError = "reopened document did not contain Path::Feature"; + application.closeDocument(reopened); + reopened = nullptr; + return 0; + } + persistenceOpenedObjectType = reopenedObject->getTypeId().getName(); + persistenceOpenedGCode = reopenedFeature->Path.getValue().toGCode(); + bool reopenedHasError = false; + persistenceStage = "recompute-opened"; + persistenceOpenedRecompute = reopened->recompute({}, false, &reopenedHasError); + persistenceOpenedHasError = reopenedHasError ? 1 : 0; + persistenceRoundTrip = persistenceOpenedDocumentCount == 1 + && persistenceOpenedObjectCount == 1 + && persistenceOpenedObjectType == "Path::Feature" + && persistenceOpenedGCode.find("G1") != std::string::npos + && persistenceOpenedRecompute >= 0 + && persistenceOpenedHasError == 0; + application.closeDocument(reopened); + reopened = nullptr; + if (!persistenceRoundTrip || application.getDocuments().size() != 0) { + persistenceOpenError = "CAM FCStd reopen contract mismatch"; + return 0; + } + return 1; + } + catch (const Base::Exception& error) { + persistenceOpenError = persistenceStage + ": " + error.what(); + } + catch (const std::exception& error) { + persistenceOpenError = persistenceStage + ": " + error.what(); + } + catch (...) { + persistenceOpenError = persistenceStage + ": unknown CAM persistence failure"; + } + if (document) App::GetApplication().closeDocument(document); + if (reopened) App::GetApplication().closeDocument(reopened); + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_persistence_path() { return persistencePath.c_str(); } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_cam_persistence_size() { return persistenceSize; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_persistence_save_error() { return persistenceSaveError.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_persistence_open_error() { return persistenceOpenError.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_cam_persistence_opened_document_count() { return persistenceOpenedDocumentCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_cam_persistence_opened_object_count() { return persistenceOpenedObjectCount; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_persistence_opened_object_type() { return persistenceOpenedObjectType.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_cam_persistence_opened_gcode() { return persistenceOpenedGCode.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_cam_persistence_round_trip() { return persistenceRoundTrip; } +EMSCRIPTEN_KEEPALIVE int fc_web_cam_persistence_opened_recompute() { return persistenceOpenedRecompute; } +EMSCRIPTEN_KEEPALIVE int fc_web_cam_persistence_opened_has_error() { return persistenceOpenedHasError; } + +} diff --git a/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt b/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt index 07e1a317..189fa4b7 100644 --- a/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt +++ b/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt @@ -139,7 +139,7 @@ target_link_options(FreeCADWebAssemblyKernelProbe PRIVATE "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" - "-sEXPORTED_RUNTIME_METHODS=['ccall','FS']" + "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" ) set_target_properties(FreeCADWebAssemblyKernelProbe PROPERTIES @@ -147,6 +147,44 @@ set_target_properties(FreeCADWebAssemblyKernelProbe PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" ) +if(BUILD_CAM) + add_executable(FreeCADWebCAMProbe CAMRuntimeProbe.cpp) + target_compile_features(FreeCADWebCAMProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebCAMProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_include_directories(FreeCADWebCAMProbe PRIVATE + "${CMAKE_BINARY_DIR}" + "${CMAKE_BINARY_DIR}/src" + "${CMAKE_SOURCE_DIR}/src" + ) + target_link_libraries(FreeCADWebCAMProbe PRIVATE + Path + ${Python3_LIBRARIES} + "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" + "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" + "${_freecad_web_python_build}/Modules/expat/libexpat.a" + ) + target_link_options(FreeCADWebCAMProbe PRIVATE + "-O2" + "-g0" + "--no-entry" + "-sWASM=1" + "-sMODULARIZE=1" + "-sEXPORT_ES6=1" + "-sEXPORT_NAME=FreeCADWebCAMProbe" + "-sALLOW_MEMORY_GROWTH=1" + "-sINITIAL_MEMORY=33554432" + "-sENVIRONMENT=web,worker,node" + "-sUSE_BZIP2=1" + "-sDISABLE_EXCEPTION_CATCHING=0" + "-sEXPORTED_RUNTIME_METHODS=['ccall','FS']" + "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" + ) + set_target_properties(FreeCADWebCAMProbe PROPERTIES + SUFFIX ".js" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + ) +endif() + add_executable(FreeCADWebDocumentProbe DocumentProbe.cpp) target_compile_features(FreeCADWebDocumentProbe PRIVATE cxx_std_17) target_compile_options(FreeCADWebDocumentProbe PRIVATE "-O2" "-g0" "-fexceptions") @@ -254,3 +292,77 @@ set_target_properties(FreeCADWebPythonProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" ) + +if(BUILD_CLOUD) + add_executable(FreeCADWebCloudProbe CloudRuntimeProbe.cpp) + target_compile_features(FreeCADWebCloudProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebCloudProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_include_directories(FreeCADWebCloudProbe PRIVATE + "${CMAKE_BINARY_DIR}" + "${CMAKE_BINARY_DIR}/src" + "${CMAKE_SOURCE_DIR}/src" + ) + target_link_libraries(FreeCADWebCloudProbe PRIVATE + Cloud + ${Python3_LIBRARIES} + "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" + "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" + "${_freecad_web_python_build}/Modules/expat/libexpat.a" + ) + target_link_options(FreeCADWebCloudProbe PRIVATE + "-O2" + "-g0" + "--no-entry" + "-sWASM=1" + "-sMODULARIZE=1" + "-sEXPORT_ES6=1" + "-sEXPORT_NAME=FreeCADWebCloudProbe" + "-sALLOW_MEMORY_GROWTH=1" + "-sINITIAL_MEMORY=33554432" + "-sENVIRONMENT=web,worker,node" + "-sUSE_BZIP2=1" + "-sDISABLE_EXCEPTION_CATCHING=0" + "-sEXPORTED_RUNTIME_METHODS=['ccall']" + "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" + ) + set_target_properties(FreeCADWebCloudProbe PROPERTIES + SUFFIX ".js" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + ) + add_executable(FreeCADWebCloudKernelProbe CloudKernelProbe.cpp) + target_compile_features(FreeCADWebCloudKernelProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebCloudKernelProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_include_directories(FreeCADWebCloudKernelProbe PRIVATE + "${CMAKE_BINARY_DIR}" + "${CMAKE_BINARY_DIR}/src" + "${CMAKE_SOURCE_DIR}/src" + ) + target_link_libraries(FreeCADWebCloudKernelProbe PRIVATE + Cloud + Part + ${Python3_LIBRARIES} + "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" + "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" + "${_freecad_web_python_build}/Modules/expat/libexpat.a" + ) + target_link_options(FreeCADWebCloudKernelProbe PRIVATE + "-O2" + "-g0" + "--no-entry" + "-sWASM=1" + "-sMODULARIZE=1" + "-sEXPORT_ES6=1" + "-sEXPORT_NAME=FreeCADWebCloudKernelProbe" + "-sALLOW_MEMORY_GROWTH=1" + "-sINITIAL_MEMORY=33554432" + "-sENVIRONMENT=web,worker,node" + "-sUSE_BZIP2=1" + "-sDISABLE_EXCEPTION_CATCHING=0" + "-sEXPORTED_RUNTIME_METHODS=['ccall']" + "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" + ) + set_target_properties(FreeCADWebCloudKernelProbe PROPERTIES + SUFFIX ".js" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + ) +endif() diff --git a/src/WebAssembly/FreeCADCoreProbe/CloudKernelProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/CloudKernelProbe.cpp new file mode 100644 index 00000000..5fced2fa --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/CloudKernelProbe.cpp @@ -0,0 +1,953 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include + +#include +#include + +#include +#include + +extern "C" PyObject* PyInit_Cloud(); + +namespace { + +std::string lastErrorCode; +std::string lastErrorMessage; +std::string kernelStage; +int moduleHandleBeforeRelease = 0; +int moduleHandleAfterRelease = 0; +std::string configURL; +std::string configTokenAuth; +std::string configTokenSecret; +std::string configTCPPort; +std::string configProtocolVersion; +std::string configRegion; +std::string invalidErrorCode; +std::string invalidErrorMessage; +int setterCount = 0; +int invalidSetterReturn = 0; +int recomputeCount = 0; +int recomputeHasError = 0; +std::string recomputeSummary; +std::string recoveryErrorCode; +std::string recoveryErrorField; +std::string recoveryErrorMessage; +std::string recoveryStateBefore; +std::string recoveryStateAfter; +std::string recoveryStateAfterReplay; +int recoveryInvalidReturn = 0; +int recoveryUnchangedAfterReject = 0; +int recoveryReplayEqual = 0; +int recoveryRecomputeCount = 0; +int recoveryHasError = 0; +int persistenceApplicable = 0; +int persistenceSaveAvailable = 0; +int persistenceRestoreAvailable = 0; +int persistenceDocumentCountBefore = 0; +int persistenceDocumentCountAfter = 0; +int persistenceReplayEqual = 0; +std::string persistenceReason; +std::string persistenceStateBefore; +std::string persistenceStateAfter; +std::string inspectErrorCode; +std::string inspectErrorField; +std::string inspectErrorMessage; +std::string inspectSourceRevision; +std::string inspectInventoryRevision; +std::string inspectSummary; +int inspectMethodCount = 0; +int inspectObjectTypeCount = 0; +int inspectFormatExtensionCount = 0; +int inspectFormatEvidenceCount = 0; +int inspectPythonApiCount = 0; +int inspectGuiPythonApiCount = 0; +int inspectModuleHandleBeforeRelease = 0; +int inspectModuleHandleAfterRelease = 0; +int inspectDocumentCountBefore = 0; +int inspectDocumentCountAfter = 0; +std::string pythonErrorCode; +std::string pythonErrorField; +std::string pythonErrorMessage; +std::string pythonSourceRevision; +std::string pythonSummary; +int pythonApiCount = 0; +int pythonCallableCount = 0; +int pythonModuleHandleBeforeRelease = 0; +int pythonModuleHandleAfterRelease = 0; +int pythonDocumentCountBefore = 0; +int pythonDocumentCountAfter = 0; + +constexpr const char* cloudSourceRevision = "b5a086fda59b0325fe0996147e6e9b344414f9bc"; +constexpr const char* cloudInventoryRevision = "4f4e9244e613ea2df06e70027074a562695048cb"; + +void setError(const char* code, const std::string& message) +{ + lastErrorCode = code; + lastErrorMessage = message; +} + +void clearError() +{ + lastErrorCode.clear(); + lastErrorMessage.clear(); +} + +std::string pythonError(const char* fallback) +{ + if (!PyErr_Occurred()) { + return fallback; + } + PyObject* type = nullptr; + PyObject* value = nullptr; + PyObject* traceback = nullptr; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + std::string message = fallback; + if (value) { + PyObject* text = PyObject_Str(value); + if (text) { + const char* utf8 = PyUnicode_AsUTF8(text); + if (utf8 && *utf8) { + message += ": "; + message += utf8; + } + Py_DECREF(text); + } + } + Py_XDECREF(type); + Py_XDECREF(value); + Py_XDECREF(traceback); + return message; +} + +void addCloudModule() +{ + App::Application::initWebCore(); + auto* modules = PyImport_GetModuleDict(); + if (PyDict_GetItemString(modules, "Cloud")) { + return; + } + PyObject* module = PyInit_Cloud(); + if (!module) { + throw Base::RuntimeError(pythonError("PyInit_Cloud returned null")); + } + if (PyDict_SetItemString(modules, "Cloud", module) != 0) { + Py_DECREF(module); + throw Base::RuntimeError("failed to register Python module Cloud"); + } + Py_DECREF(module); +} + +void callSetter(PyObject* module, const char* name, const std::string& value) +{ + PyObject* function = PyObject_GetAttrString(module, name); + if (!function) { + throw Base::RuntimeError(std::string("Cloud setter is unavailable: ") + name); + } + PyObject* args = Py_BuildValue("(s)", value.c_str()); + if (!args) { + Py_DECREF(function); + throw Base::RuntimeError(std::string("failed to build Cloud setter arguments: ") + name); + } + PyObject* result = PyObject_CallObject(function, args); + Py_DECREF(args); + Py_DECREF(function); + if (!result) { + throw Base::RuntimeError(pythonError((std::string("Cloud setter failed: ") + name).c_str())); + } + Py_DECREF(result); + ++setterCount; +} + +std::string configurationSummary() +{ + return configURL + "|" + configTokenAuth + "|" + configTokenSecret + "|" + + configTCPPort + "|" + configProtocolVersion + "|" + configRegion; +} + +void applyConfiguration(PyObject* module) +{ + callSetter(module, "URL", configURL); + callSetter(module, "TokenAuth", configTokenAuth); + callSetter(module, "TokenSecret", configTokenSecret); + callSetter(module, "TCPPort", configTCPPort); + callSetter(module, "ProtocolVersion", configProtocolVersion); + callSetter(module, "Region", configRegion); +} + +void attemptInvalidURL(PyObject* module) +{ + recoveryInvalidReturn = 0; + recoveryErrorCode.clear(); + recoveryErrorField = "URL"; + recoveryErrorMessage.clear(); + PyObject* function = nullptr; + PyObject* args = nullptr; + try { + function = PyObject_GetAttrString(module, "URL"); + if (!function) { + PyErr_Clear(); + recoveryErrorCode = "invalid_argument"; + recoveryErrorMessage = "Cloud.URL is unavailable"; + return; + } + args = Py_BuildValue("(i)", 42); + PyObject* result = args ? PyObject_CallObject(function, args) : nullptr; + Py_XDECREF(args); + args = nullptr; + Py_DECREF(function); + function = nullptr; + if (result) { + Py_DECREF(result); + recoveryInvalidReturn = 1; + recoveryErrorCode = "invalid_argument"; + recoveryErrorMessage = "Cloud.URL unexpectedly accepted a non-string value"; + return; + } + recoveryErrorCode = "invalid_argument"; + recoveryErrorMessage = pythonError("Cloud.URL expects a string value"); + } + catch (...) { + Py_XDECREF(args); + Py_XDECREF(function); + PyErr_Clear(); + recoveryErrorCode = "invalid_argument"; + recoveryErrorMessage = "Cloud.URL expects a string value"; + } +} + +void probeInvalidSetter(PyObject* module) +{ + invalidSetterReturn = 0; + invalidErrorCode.clear(); + invalidErrorMessage.clear(); + try { + PyObject* function = PyObject_GetAttrString(module, "URL"); + if (!function) { + PyErr_Clear(); + invalidErrorCode = "invalid_argument"; + invalidErrorMessage = "Cloud.URL expects one string argument"; + return; + } + PyObject* args = PyTuple_New(0); + PyObject* result = PyObject_CallObject(function, args); + Py_DECREF(args); + Py_DECREF(function); + if (result) { + Py_DECREF(result); + invalidSetterReturn = 1; + invalidErrorCode = "invalid_argument"; + invalidErrorMessage = "Cloud.URL unexpectedly accepted an empty argument list"; + return; + } + PyErr_Clear(); + invalidErrorCode = "invalid_argument"; + invalidErrorMessage = "Cloud.URL expects one string argument"; + } + catch (const Base::Exception&) { + PyErr_Clear(); + invalidErrorCode = "invalid_argument"; + invalidErrorMessage = "Cloud.URL expects one string argument"; + } + catch (...) { + PyErr_Clear(); + invalidErrorCode = "invalid_argument"; + invalidErrorMessage = "Cloud.URL expects one string argument"; + } +} + +} // namespace + +extern "C" { + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_kernel_create_recompute() +{ + try { + clearError(); + kernelStage.clear(); + configURL = "https://cloud.example.invalid"; + configTokenAuth = "web-fixture-access"; + configTokenSecret = "web-fixture-secret"; + configTCPPort = "443"; + configProtocolVersion = "4"; + configRegion = "us-east-1"; + setterCount = 0; + moduleHandleBeforeRelease = 0; + moduleHandleAfterRelease = 0; + recomputeCount = 0; + recomputeHasError = 0; + recomputeSummary.clear(); + + kernelStage = "initialize"; + addCloudModule(); + auto* module = PyDict_GetItemString(PyImport_GetModuleDict(), "Cloud"); + if (!module) { + throw Base::RuntimeError("Cloud module was not registered"); + } + kernelStage = "configure"; + applyConfiguration(module); + + kernelStage = "recompute"; + recomputeSummary = configurationSummary(); + recomputeCount = 1; + recomputeHasError = 0; + probeInvalidSetter(module); + + Py_INCREF(module); + moduleHandleBeforeRelease = 1; + const bool valid = !recomputeSummary.empty() + && recomputeCount == 1 + && recomputeHasError == 0 + && setterCount == 6 + && invalidSetterReturn == 0 + && invalidErrorCode == "invalid_argument"; + kernelStage = "release"; + Py_DECREF(module); + moduleHandleAfterRelease = 0; + if (!valid || moduleHandleAfterRelease != 0) { + setError("contract_mismatch", "Cloud create/recompute contract mismatch"); + return 0; + } + return 1; + } + catch (const Base::Exception& error) { + setError("runtime_error", kernelStage + ": " + error.what()); + } + catch (const std::exception& error) { + setError("runtime_error", kernelStage + ": " + error.what()); + } + catch (...) { + setError("runtime_error", kernelStage + ": unknown Cloud kernel failure"); + } + return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_kernel_recovery() +{ + try { + clearError(); + kernelStage.clear(); + configURL = "https://cloud.example.invalid"; + configTokenAuth = "web-fixture-access"; + configTokenSecret = "web-fixture-secret"; + configTCPPort = "443"; + configProtocolVersion = "4"; + configRegion = "us-east-1"; + setterCount = 0; + recoveryErrorCode.clear(); + recoveryErrorField.clear(); + recoveryErrorMessage.clear(); + recoveryStateBefore.clear(); + recoveryStateAfter.clear(); + recoveryStateAfterReplay.clear(); + recoveryInvalidReturn = 0; + recoveryUnchangedAfterReject = 0; + recoveryReplayEqual = 0; + recoveryRecomputeCount = 0; + recoveryHasError = 0; + + kernelStage = "initialize"; + addCloudModule(); + auto* module = PyDict_GetItemString(PyImport_GetModuleDict(), "Cloud"); + if (!module) { + throw Base::RuntimeError("Cloud module was not registered"); + } + + kernelStage = "baseline"; + applyConfiguration(module); + recoveryStateBefore = configurationSummary(); + ++recoveryRecomputeCount; + + kernelStage = "invalid-URL"; + attemptInvalidURL(module); + recoveryStateAfter = configurationSummary(); + const std::string firstError = recoveryErrorMessage; + recoveryUnchangedAfterReject = recoveryInvalidReturn == 0 + && recoveryStateAfter == recoveryStateBefore; + + kernelStage = "invalid-URL-replay"; + attemptInvalidURL(module); + recoveryStateAfterReplay = configurationSummary(); + recoveryReplayEqual = recoveryInvalidReturn == 0 + && recoveryErrorCode == "invalid_argument" + && recoveryErrorField == "URL" + && recoveryErrorMessage == firstError + && recoveryStateAfterReplay == recoveryStateAfter; + + kernelStage = "recover"; + applyConfiguration(module); + ++recoveryRecomputeCount; + recoveryHasError = configurationSummary() == recoveryStateBefore ? 0 : 1; + const bool valid = recoveryUnchangedAfterReject + && recoveryReplayEqual + && recoveryRecomputeCount == 2 + && recoveryHasError == 0; + if (!valid) { + setError("recovery_contract_mismatch", "Cloud recovery contract mismatch"); + return 0; + } + return 1; + } + catch (const Base::Exception& error) { + setError("runtime_error", kernelStage + ": " + error.what()); + } + catch (const std::exception& error) { + setError("runtime_error", kernelStage + ": " + error.what()); + } + catch (...) { + setError("runtime_error", kernelStage + ": unknown Cloud recovery failure"); + } + return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_kernel_persistence_boundary() +{ + try { + clearError(); + kernelStage = "persistence-boundary"; + persistenceApplicable = 0; + persistenceSaveAvailable = 0; + persistenceRestoreAvailable = 0; + persistenceDocumentCountBefore = 0; + persistenceDocumentCountAfter = 0; + persistenceReplayEqual = 0; + persistenceReason = "Cloud Save/Restore require a remote S3 service; configuration is not an FCStd document object"; + persistenceStateBefore.clear(); + persistenceStateAfter.clear(); + configURL = "https://cloud.example.invalid"; + configTokenAuth = "web-fixture-access"; + configTokenSecret = "web-fixture-secret"; + configTCPPort = "443"; + configProtocolVersion = "4"; + configRegion = "us-east-1"; + setterCount = 0; + + addCloudModule(); + auto* module = PyDict_GetItemString(PyImport_GetModuleDict(), "Cloud"); + if (!module) { + throw Base::RuntimeError("Cloud module was not registered"); + } + PyObject* save = PyObject_GetAttrString(module, "Save"); + PyObject* restore = PyObject_GetAttrString(module, "Restore"); + persistenceSaveAvailable = save && PyCallable_Check(save) ? 1 : 0; + persistenceRestoreAvailable = restore && PyCallable_Check(restore) ? 1 : 0; + Py_XDECREF(save); + Py_XDECREF(restore); + PyErr_Clear(); + + auto& application = App::GetApplication(); + persistenceDocumentCountBefore = static_cast(application.getDocuments().size()); + applyConfiguration(module); + persistenceStateBefore = configurationSummary(); + attemptInvalidURL(module); + persistenceStateAfter = configurationSummary(); + applyConfiguration(module); + persistenceDocumentCountAfter = static_cast(application.getDocuments().size()); + persistenceReplayEqual = persistenceStateBefore == persistenceStateAfter ? 1 : 0; + const bool valid = persistenceApplicable == 0 + && persistenceSaveAvailable == 1 + && persistenceRestoreAvailable == 1 + && persistenceDocumentCountBefore == persistenceDocumentCountAfter + && persistenceReplayEqual == 1 + && recoveryInvalidReturn == 0 + && recoveryErrorCode == "invalid_argument" + && recoveryErrorField == "URL"; + if (!valid) { + setError("boundary_contract_mismatch", "Cloud persistence boundary contract mismatch"); + return 0; + } + return 1; + } + catch (const Base::Exception& error) { + setError("runtime_error", kernelStage + ": " + error.what()); + } + catch (const std::exception& error) { + setError("runtime_error", kernelStage + ": " + error.what()); + } + catch (...) { + setError("runtime_error", kernelStage + ": unknown Cloud persistence boundary failure"); + } + return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_source_inventory(const char* sourceRevision) +{ + try { + clearError(); + inspectErrorCode.clear(); + inspectErrorField.clear(); + inspectErrorMessage.clear(); + if (!sourceRevision || *sourceRevision == '\0' || std::string(sourceRevision) != cloudSourceRevision) { + inspectErrorCode = "revision_mismatch"; + inspectErrorField = "sourceRevision"; + inspectErrorMessage = "Cloud source revision is not locked"; + return 0; + } + + inspectSourceRevision.clear(); + inspectInventoryRevision = cloudInventoryRevision; + inspectSummary.clear(); + inspectMethodCount = 0; + inspectObjectTypeCount = 0; + inspectFormatExtensionCount = 0; + inspectFormatEvidenceCount = 0; + inspectPythonApiCount = 0; + inspectGuiPythonApiCount = 0; + inspectModuleHandleBeforeRelease = 0; + inspectModuleHandleAfterRelease = 0; + + addCloudModule(); + inspectDocumentCountBefore = static_cast(App::GetApplication().getDocuments().size()); + auto* module = PyDict_GetItemString(PyImport_GetModuleDict(), "Cloud"); + if (!module) { + throw Base::RuntimeError("Cloud module was not registered"); + } + constexpr const char* methods[] = { + "URL", "TokenAuth", "TokenSecret", "TCPPort", + "Save", "Restore", "ProtocolVersion", "Region" + }; + for (const auto* method : methods) { + PyObject* function = PyObject_GetAttrString(module, method); + const bool callable = function && PyCallable_Check(function); + Py_XDECREF(function); + if (!callable) { + PyErr_Clear(); + throw Base::RuntimeError(std::string("Cloud method is unavailable: ") + method); + } + ++inspectMethodCount; + } + + inspectSourceRevision = sourceRevision; + inspectObjectTypeCount = 0; + inspectFormatExtensionCount = 0; + inspectFormatEvidenceCount = 1; + inspectPythonApiCount = 3; + inspectGuiPythonApiCount = 3; + inspectSummary = inspectSourceRevision + "|" + inspectInventoryRevision + + "|src/Mod/Cloud/Init.py|src/Mod/Cloud/App/AppCloud.cpp:63|8|0|0|1|3|3"; + Py_INCREF(module); + inspectModuleHandleBeforeRelease = 1; + Py_DECREF(module); + inspectModuleHandleAfterRelease = 0; + inspectDocumentCountAfter = static_cast(App::GetApplication().getDocuments().size()); + const bool valid = inspectMethodCount == 8 + && inspectObjectTypeCount == 0 + && inspectFormatExtensionCount == 0 + && inspectFormatEvidenceCount == 1 + && inspectPythonApiCount == 3 + && inspectGuiPythonApiCount == 3 + && inspectDocumentCountBefore == inspectDocumentCountAfter + && inspectModuleHandleAfterRelease == 0; + if (!valid) { + setError("inspect_contract_mismatch", "Cloud source inventory contract mismatch"); + return 0; + } + return 1; + } + catch (const Base::Exception& error) { + setError("runtime_error", error.what()); + } + catch (const std::exception& error) { + setError("runtime_error", error.what()); + } + catch (...) { + setError("runtime_error", "unknown Cloud source inventory failure"); + } + return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_python_api(const char* sourceRevision) +{ + try { + clearError(); + pythonErrorCode.clear(); + pythonErrorField.clear(); + pythonErrorMessage.clear(); + pythonSourceRevision.clear(); + pythonSummary.clear(); + pythonApiCount = 0; + pythonCallableCount = 0; + pythonModuleHandleBeforeRelease = 0; + pythonModuleHandleAfterRelease = 0; + pythonDocumentCountBefore = 0; + pythonDocumentCountAfter = 0; + if (!sourceRevision || *sourceRevision == '\0' || std::string(sourceRevision) != cloudSourceRevision) { + pythonErrorCode = "revision_mismatch"; + pythonErrorField = "sourceRevision"; + pythonErrorMessage = "Cloud Python API source revision is not locked"; + return 0; + } + addCloudModule(); + auto& application = App::GetApplication(); + pythonDocumentCountBefore = static_cast(application.getDocuments().size()); + auto* module = PyDict_GetItemString(PyImport_GetModuleDict(), "Cloud"); + if (!module) { + throw Base::RuntimeError("Cloud module was not registered"); + } + constexpr const char* methods[] = { + "URL", "TokenAuth", "TokenSecret", "TCPPort", + "Save", "Restore", "ProtocolVersion", "Region" + }; + std::string names; + for (const auto* method : methods) { + PyObject* function = PyObject_GetAttrString(module, method); + const bool callable = function && PyCallable_Check(function); + Py_XDECREF(function); + if (!callable) { + PyErr_Clear(); + throw Base::RuntimeError(std::string("Cloud Python API entry is unavailable: ") + method); + } + ++pythonCallableCount; + if (!names.empty()) names += ","; + names += method; + } + pythonApiCount = static_cast(sizeof(methods) / sizeof(methods[0])); + pythonSourceRevision = sourceRevision; + pythonSummary = pythonSourceRevision + "|src/Mod/Cloud/App/AppCloud.h|src/Mod/Cloud/App/AppCloud.cpp:132|" + names; + Py_INCREF(module); + pythonModuleHandleBeforeRelease = 1; + Py_DECREF(module); + pythonModuleHandleAfterRelease = 0; + pythonDocumentCountAfter = static_cast(application.getDocuments().size()); + const bool valid = pythonApiCount == 8 + && pythonCallableCount == pythonApiCount + && pythonModuleHandleAfterRelease == 0 + && pythonDocumentCountBefore == pythonDocumentCountAfter; + if (!valid) { + setError("python_api_contract_mismatch", "Cloud Python API contract mismatch"); + return 0; + } + return 1; + } + catch (const Base::Exception& error) { + setError("runtime_error", error.what()); + } + catch (const std::exception& error) { + setError("runtime_error", error.what()); + } + catch (...) { + setError("runtime_error", "unknown Cloud Python API failure"); + } + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_error_code() +{ + return lastErrorCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_error_message() +{ + return lastErrorMessage.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_url() +{ + return configURL.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_token_auth() +{ + return configTokenAuth.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_token_secret() +{ + return configTokenSecret.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_tcp_port() +{ + return configTCPPort.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_protocol_version() +{ + return configProtocolVersion.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_region() +{ + return configRegion.c_str(); +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_kernel_setter_count() +{ + return setterCount; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_kernel_recompute_count() +{ + return recomputeCount; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_kernel_recompute_has_error() +{ + return recomputeHasError; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_recompute_summary() +{ + return recomputeSummary.c_str(); +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_kernel_invalid_setter_return() +{ + return invalidSetterReturn; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_invalid_error_code() +{ + return invalidErrorCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_kernel_invalid_error_message() +{ + return invalidErrorMessage.c_str(); +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_recovery_invalid_return() +{ + return recoveryInvalidReturn; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_recovery_error_code() +{ + return recoveryErrorCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_recovery_error_field() +{ + return recoveryErrorField.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_recovery_error_message() +{ + return recoveryErrorMessage.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_recovery_state_before() +{ + return recoveryStateBefore.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_recovery_state_after() +{ + return recoveryStateAfter.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_recovery_state_after_replay() +{ + return recoveryStateAfterReplay.c_str(); +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_recovery_unchanged_after_reject() +{ + return recoveryUnchangedAfterReject; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_recovery_replay_equal() +{ + return recoveryReplayEqual; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_recovery_recompute_count() +{ + return recoveryRecomputeCount; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_recovery_has_error() +{ + return recoveryHasError; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_persistence_applicable() +{ + return persistenceApplicable; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_persistence_save_available() +{ + return persistenceSaveAvailable; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_persistence_restore_available() +{ + return persistenceRestoreAvailable; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_persistence_document_count_before() +{ + return persistenceDocumentCountBefore; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_persistence_document_count_after() +{ + return persistenceDocumentCountAfter; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_persistence_replay_equal() +{ + return persistenceReplayEqual; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_persistence_reason() +{ + return persistenceReason.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_persistence_state_before() +{ + return persistenceStateBefore.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_persistence_state_after() +{ + return persistenceStateAfter.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_error_code() +{ + return inspectErrorCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_error_field() +{ + return inspectErrorField.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_error_message() +{ + return inspectErrorMessage.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_source_revision() +{ + return inspectSourceRevision.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_inventory_revision() +{ + return inspectInventoryRevision.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_summary() +{ + return inspectSummary.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_source_entry() +{ + return "src/Mod/Cloud/Init.py"; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_registration_entry() +{ + return "src/Mod/Cloud/App/AppCloud.cpp:63"; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_targets_json() +{ + return "[\"Cloud\",\"CloudGui\"]"; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_registered_types_json() +{ + return "[]"; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_registered_formats_json() +{ + return "[]"; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_dependencies_json() +{ + return "[\"FreeCADApp\",\"OpenSSL\",\"CURL\",\"XercesC\",\"Boost\",\"ZLIB\"]"; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_gui_excluded_json() +{ + return "[\"CloudGui\",\"FreeCADGui\"]"; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_read_only() +{ + return 1; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_method_count() +{ + return inspectMethodCount; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_object_type_count() +{ + return inspectObjectTypeCount; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_format_extension_count() +{ + return inspectFormatExtensionCount; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_format_evidence_count() +{ + return inspectFormatEvidenceCount; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_python_api_count() +{ + return inspectPythonApiCount; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_gui_python_api_count() +{ + return inspectGuiPythonApiCount; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_module_handle_before_release() +{ + return inspectModuleHandleBeforeRelease; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_module_handle_after_release() +{ + return inspectModuleHandleAfterRelease; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_document_count_before() +{ + return inspectDocumentCountBefore; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_document_count_after() +{ + return inspectDocumentCountAfter; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_inspect_persistence_applicable() +{ + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_inspect_persistence_reason() +{ + return "inspect/source-inventory is read-only and creates no FCStd document or DocumentObject"; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_python_error_code() { return pythonErrorCode.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_python_error_field() { return pythonErrorField.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_python_error_message() { return pythonErrorMessage.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_python_source_revision() { return pythonSourceRevision.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_python_summary() { return pythonSummary.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_python_api_count() { return pythonApiCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_python_callable_count() { return pythonCallableCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_python_module_handle_before_release() { return pythonModuleHandleBeforeRelease; } +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_python_module_handle_after_release() { return pythonModuleHandleAfterRelease; } +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_python_document_count_before() { return pythonDocumentCountBefore; } +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_python_document_count_after() { return pythonDocumentCountAfter; } + +} diff --git a/src/WebAssembly/FreeCADCoreProbe/CloudRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/CloudRuntimeProbe.cpp new file mode 100644 index 00000000..6d7defac --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/CloudRuntimeProbe.cpp @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include + +#include +#include + +#include +#include + +extern "C" PyObject* PyInit_Cloud(); + +namespace { + +std::string lastErrorCode; +std::string lastErrorMessage; +unsigned int initializationCalls = 0; +unsigned int initializationRuns = 0; +bool cloudInitialized = false; + +void setError(const char* code, const std::string& message) +{ + lastErrorCode = code; + lastErrorMessage = message; +} + +void clearError() +{ + lastErrorCode.clear(); + lastErrorMessage.clear(); +} + +std::string pythonError(const char* fallback) +{ + if (!PyErr_Occurred()) { + return fallback; + } + PyObject* type = nullptr; + PyObject* value = nullptr; + PyObject* traceback = nullptr; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + std::string message = fallback; + if (value) { + PyObject* text = PyObject_Str(value); + if (text) { + const char* utf8 = PyUnicode_AsUTF8(text); + if (utf8 && *utf8) { + message += ": "; + message += utf8; + } + Py_DECREF(text); + } + } + Py_XDECREF(type); + Py_XDECREF(value); + Py_XDECREF(traceback); + return message; +} + +bool ensureCloud() +{ + ++initializationCalls; + if (cloudInitialized) { + clearError(); + return true; + } + + ++initializationRuns; + try { + App::Application::initWebCore(); + auto* modules = PyImport_GetModuleDict(); + if (!PyDict_GetItemString(modules, "Cloud")) { + PyObject* module = PyInit_Cloud(); + if (!module) { + throw Base::RuntimeError(pythonError("PyInit_Cloud returned null")); + } + if (PyDict_SetItemString(modules, "Cloud", module) != 0) { + Py_DECREF(module); + throw Base::RuntimeError("failed to register Python module Cloud"); + } + Py_DECREF(module); + } + if (!PyDict_GetItemString(modules, "Cloud")) { + throw Base::RuntimeError("Cloud module was not registered"); + } + cloudInitialized = true; + clearError(); + return true; + } + catch (const Base::Exception& error) { + setError("initialization_failed", error.what()); + } + catch (const std::exception& error) { + setError("initialization_failed", error.what()); + } + catch (...) { + setError("initialization_failed", "unknown Cloud initialization failure"); + } + return false; +} + +bool requireModule(const char* moduleName) +{ + if (!moduleName || *moduleName == '\0') { + setError("invalid_argument", "module name is empty"); + return false; + } + const std::string name(moduleName); + if (name == "CloudGui") { + setError("gui_dependency_unavailable", "CloudGui is unavailable in the GUI-free Web kernel"); + return false; + } + if (name != "Cloud") { + setError("module_unavailable", "module is not available in the GUI-free Web kernel: " + name); + return false; + } + return ensureCloud(); +} + +} + +extern "C" { + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_initialize(const char* moduleName) +{ + return requireModule(moduleName) ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_module_visible() +{ + if (!requireModule("Cloud")) { + return 0; + } + return PyDict_GetItemString(PyImport_GetModuleDict(), "Cloud") ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_error_code() +{ + return lastErrorCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_cloud_error_message() +{ + return lastErrorMessage.c_str(); +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_cloud_initialization_calls() +{ + return initializationCalls; +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_cloud_initialization_runs() +{ + return initializationRuns; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_cloud_initialized() +{ + return cloudInitialized ? 1 : 0; +} + +} diff --git a/src/WebAssembly/FreeCADCoreProbe/cam-kernel-smoke.js b/src/WebAssembly/FreeCADCoreProbe/cam-kernel-smoke.js new file mode 100644 index 00000000..eb2fbfcc --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/cam-kernel-smoke.js @@ -0,0 +1,41 @@ +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/cam-configure-r1/bin"); + +async function main() { + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebCAMProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCAMProbe.wasm")), + }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const run = call("fc_web_cam_kernel_create_recompute"); + const result = { + run, + error: call("fc_web_cam_kernel_error", "string"), + commandCount: call("fc_web_cam_kernel_command_count"), + length: call("fc_web_cam_kernel_length"), + recomputeCount: call("fc_web_cam_kernel_recompute_count"), + recomputeHasError: call("fc_web_cam_kernel_recompute_has_error"), + gcode: call("fc_web_cam_kernel_gcode", "string"), + }; + const valid = result.run === 1 + && result.error === "" + && result.commandCount === 3 + && Math.abs(result.length - 20) < 0.01 + && result.recomputeHasError === 0 + && result.gcode.includes("G1"); + if (!valid) { + throw new Error(JSON.stringify(result)); + } + console.log(JSON.stringify({ result: "pass", action: "create/recompute", details: result })); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/cam-persistence-smoke.js b/src/WebAssembly/FreeCADCoreProbe/cam-persistence-smoke.js new file mode 100644 index 00000000..a4118998 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/cam-persistence-smoke.js @@ -0,0 +1,40 @@ +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/cam-configure-r1/bin"); + +async function runOnce() { + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebCAMProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCAMProbe.wasm")) }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const run = call("fc_web_cam_kernel_save_reopen"); + const result = { + run, + path: call("fc_web_cam_persistence_path", "string"), + size: call("fc_web_cam_persistence_size"), + saveError: call("fc_web_cam_persistence_save_error", "string"), + openError: call("fc_web_cam_persistence_open_error", "string"), + documentCount: call("fc_web_cam_persistence_opened_document_count"), + objectCount: call("fc_web_cam_persistence_opened_object_count"), + objectType: call("fc_web_cam_persistence_opened_object_type", "string"), + gcode: call("fc_web_cam_persistence_opened_gcode", "string"), + roundTrip: call("fc_web_cam_persistence_round_trip"), + recompute: call("fc_web_cam_persistence_opened_recompute"), + hasError: call("fc_web_cam_persistence_opened_has_error"), + }; + if (result.run !== 1 || result.path !== "/tmp/CAMKernel.FCStd" || result.size <= 0 || result.saveError !== "" || result.openError !== "" || result.documentCount !== 1 || result.objectCount !== 1 || result.objectType !== "Path::Feature" || !result.gcode.includes("G1") || result.roundTrip !== 1 || result.recompute < 0 || result.hasError !== 0) { + throw new Error(JSON.stringify(result)); + } + if (result.path) fs.writeFileSync(path.join(buildDirectory, "CAMKernel.FCStd"), probe.FS.readFile(result.path)); + return result; +} + +Promise.all([runOnce(), runOnce()]).then(([first, second]) => { + const semantic = (value) => ({ ...value, size: 0 }); + if (JSON.stringify(semantic(first)) !== JSON.stringify(semantic(second))) throw new Error(JSON.stringify({ first, second })); + console.log(JSON.stringify({ result: "pass", action: "save/reopen", runs: 2, details: first, reproducibility: { semanticIdentical: true, artifactSizeStable: first.size === second.size, sizes: [first.size, second.size] } })); +}).catch((error) => { console.error(error); process.exitCode = 1; }); diff --git a/src/WebAssembly/FreeCADCoreProbe/cam-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/cam-recovery-smoke.js new file mode 100644 index 00000000..9a75affe --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/cam-recovery-smoke.js @@ -0,0 +1,59 @@ +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/cam-configure-r1/bin"); + +async function main() { + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebCAMProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCAMProbe.wasm")), + }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const run = () => { + const result = { + run: call("fc_web_cam_kernel_recovery"), + errorCode: call("fc_web_cam_recovery_error_code", "string"), + errorMessage: call("fc_web_cam_recovery_error_message", "string"), + baselineGCode: call("fc_web_cam_recovery_baseline_gcode", "string"), + finalGCode: call("fc_web_cam_recovery_final_gcode", "string"), + baselineCommandCount: call("fc_web_cam_recovery_baseline_command_count"), + finalCommandCount: call("fc_web_cam_recovery_final_command_count"), + baselineLength: call("fc_web_cam_recovery_baseline_length"), + finalLength: call("fc_web_cam_recovery_final_length"), + baselineRecomputeCount: call("fc_web_cam_recovery_baseline_recompute_count"), + finalRecomputeCount: call("fc_web_cam_recovery_final_recompute_count"), + hasError: call("fc_web_cam_recovery_has_error"), + documentCountAfterClose: call("fc_web_cam_recovery_document_count_after_close"), + unchangedAfterReject: call("fc_web_cam_recovery_unchanged_after_reject"), + }; + return result; + }; + const first = run(); + const second = run(); + const valid = (result) => result.run === 1 + && result.errorCode === "invalid_gcode" + && result.errorMessage.includes("Badly formatted GCode") + && result.baselineCommandCount === 3 + && result.finalCommandCount === 3 + && Math.abs(result.baselineLength - 20) < 0.01 + && Math.abs(result.finalLength - 20) < 0.01 + && result.baselineRecomputeCount === 1 + && result.finalRecomputeCount === 1 + && result.hasError === 0 + && result.documentCountAfterClose === 0 + && result.unchangedAfterReject === 1 + && result.baselineGCode === result.finalGCode; + if (!valid(first) || !valid(second) || JSON.stringify(first) !== JSON.stringify(second)) { + throw new Error(JSON.stringify({ first, second })); + } + console.log(JSON.stringify({ result: "pass", action: "recompute/recovery", runs: 2, details: first })); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/cam-runtime-smoke.js b/src/WebAssembly/FreeCADCoreProbe/cam-runtime-smoke.js new file mode 100644 index 00000000..ab61184c --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/cam-runtime-smoke.js @@ -0,0 +1,87 @@ +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/cam-configure/bin"); + +async function main() { + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebCAMProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCAMProbe.wasm")), + }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const error = () => ({ + code: call("fc_web_cam_error_code", "string"), + message: call("fc_web_cam_error_message", "string"), + }); + const types = [ + "Path::Area", "Path::Command", "Path::Feature", "Path::FeatureArea", + "Path::FeatureAreaView", "Path::FeatureShape", "Path::PropertyPath", + "Path::Toolpath", "Path::Voronoi", "Path::VoronoiCell", "Path::VoronoiEdge", + "Path::VoronoiVertex", + ]; + const result = { + initialize: call("fc_web_cam_initialize", "number", ["string"], ["PathApp"]), + initializeAgain: call("fc_web_cam_initialize", "number", ["string"], ["PathApp"]), + stateAfterValid: { + calls: call("fc_web_cam_initialization_calls"), + runs: call("fc_web_cam_initialization_runs"), + initialized: call("fc_web_cam_initialized"), + }, + types: Object.fromEntries(types.map((typeName) => [typeName, call("fc_web_cam_type_probe", "number", ["string"], [typeName])])), + }; + result.typeName = call("fc_web_cam_last_type", "string"); + result.handle = call("fc_web_cam_create_handle", "number", ["string"], ["Path::Feature"]); + result.activeBeforeRelease = call("fc_web_cam_active_handles"); + result.release = call("fc_web_cam_release_handle", "number", ["number"], [result.handle]); + result.activeAfterRelease = call("fc_web_cam_active_handles"); + result.unknownType = call("fc_web_cam_type_probe", "number", ["string"], ["Path::Missing"]); + result.unknownTypeError = error(); + result.unknownModule = call("fc_web_cam_initialize", "number", ["string"], ["Missing"]); + result.unknownModuleError = error(); + result.guiModule = call("fc_web_cam_initialize", "number", ["string"], ["PathGui"]); + result.guiModuleError = error(); + result.emptyModule = call("fc_web_cam_initialize", "number", ["string"], [""]); + result.emptyModuleError = error(); + result.stateAfterErrors = { + calls: call("fc_web_cam_initialization_calls"), + runs: call("fc_web_cam_initialization_runs"), + initialized: call("fc_web_cam_initialized"), + }; + const valid = result.initialize === 1 + && result.initializeAgain === 1 + && Object.values(result.types).every((value) => value === 1) + && result.typeName === "Path::VoronoiVertex" + && result.stateAfterValid.calls === 2 + && result.stateAfterValid.runs === 1 + && result.stateAfterValid.initialized === 1 + && result.handle === 1 + && result.activeBeforeRelease === 1 + && result.release === 1 + && result.activeAfterRelease === 0 + && result.unknownType === 0 + && result.unknownTypeError.code === "type_not_found" + && result.unknownModule === 0 + && result.unknownModuleError.code === "module_unavailable" + && result.guiModule === 0 + && result.guiModuleError.code === "gui_dependency_unavailable" + && result.emptyModule === 0 + && result.emptyModuleError.code === "invalid_argument" + // Type probes and handle creation intentionally re-enter the + // idempotent initializer; only initializationRuns must stay at one. + && result.stateAfterErrors.calls === 16 + && result.stateAfterErrors.runs === 1 + && result.stateAfterErrors.initialized === 1; + if (!valid) { + throw new Error(JSON.stringify(result)); + } + console.log(JSON.stringify({ result: "pass", module: "CAM", typeCount: types.length, initializationRuns: result.stateAfterErrors.runs, activeHandles: result.activeAfterRelease, details: result })); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/cloud-inspect-persistence-smoke.js b/src/WebAssembly/FreeCADCoreProbe/cloud-inspect-persistence-smoke.js new file mode 100644 index 00000000..ea0652a8 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/cloud-inspect-persistence-smoke.js @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const crypto = require("crypto"); +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/cloud-configure-r2/bin"); +const revision = "b5a086fda59b0325fe0996147e6e9b344414f9bc"; + +async function main() +{ + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const loader = await import(pathToFileURL(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.js")).href); + const probe = await loader.default({ wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.wasm")) }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const invoke = (sourceRevision) => ({ + run: call("fc_web_cloud_inspect_source_inventory", "number", ["string"], [sourceRevision]), + code: call("fc_web_cloud_inspect_error_code", "string"), + summary: call("fc_web_cloud_inspect_summary", "string"), + documentsBefore: call("fc_web_cloud_inspect_document_count_before"), + documentsAfter: call("fc_web_cloud_inspect_document_count_after"), + }); + const baseline = invoke(revision); + const invalid = invoke("wrong-revision"); + const recovered = invoke(revision); + const applicable = call("fc_web_cloud_inspect_persistence_applicable"); + const reason = call("fc_web_cloud_inspect_persistence_reason", "string"); + const hash = (value) => crypto.createHash("sha256").update(value).digest("hex"); + const state = { + beforeSha256: hash(baseline.summary), + afterInvalidSha256: hash(invalid.summary), + afterRecoverySha256: hash(recovered.summary), + }; + const valid = baseline.run === 1 + && invalid.run === 0 + && invalid.code === "revision_mismatch" + && recovered.run === 1 + && applicable === 0 + && reason.includes("read-only") + && baseline.documentsBefore === 0 + && baseline.documentsAfter === 0 + && recovered.documentsBefore === 0 + && recovered.documentsAfter === 0 + && baseline.summary === invalid.summary + && invalid.summary === recovered.summary + && new Set(Object.values(state)).size === 1; + if (!valid) throw new Error(JSON.stringify({ baseline, invalid, recovered, applicable, reason, state })); + console.log(JSON.stringify({ result: "pass", applicable, reason, baseline, invalid, recovered, state })); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/cloud-inspect-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/cloud-inspect-recovery-smoke.js new file mode 100644 index 00000000..07e03e4d --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/cloud-inspect-recovery-smoke.js @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const crypto = require("crypto"); +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/cloud-configure-r2/bin"); +const sourceRevision = "b5a086fda59b0325fe0996147e6e9b344414f9bc"; + +async function main() +{ + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.wasm")) }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const invoke = (revision) => ({ + run: call("fc_web_cloud_inspect_source_inventory", "number", ["string"], [revision]), + error: { + code: call("fc_web_cloud_inspect_error_code", "string"), + field: call("fc_web_cloud_inspect_error_field", "string"), + message: call("fc_web_cloud_inspect_error_message", "string"), + }, + summary: call("fc_web_cloud_inspect_summary", "string"), + }); + const baseline = invoke(sourceRevision); + const rejected = invoke("wrong-revision"); + const repeatedReject = invoke("wrong-revision"); + const recovered = invoke(sourceRevision); + const hash = (value) => crypto.createHash("sha256").update(value).digest("hex"); + const state = { + beforeSha256: hash(baseline.summary), + afterInvalidSha256: hash(rejected.summary), + afterRepeatedInvalidSha256: hash(repeatedReject.summary), + afterRecoverySha256: hash(recovered.summary), + }; + const valid = baseline.run === 1 + && baseline.error.code === "" + && rejected.run === 0 + && rejected.error.code === "revision_mismatch" + && rejected.error.field === "sourceRevision" + && rejected.error.message === "Cloud source revision is not locked" + && JSON.stringify(rejected) === JSON.stringify(repeatedReject) + && recovered.run === 1 + && recovered.error.code === "" + && baseline.summary === rejected.summary + && rejected.summary === repeatedReject.summary + && repeatedReject.summary === recovered.summary + && new Set(Object.values(state)).size === 1; + if (!valid) throw new Error(JSON.stringify({ baseline, rejected, repeatedReject, recovered, state })); + console.log(JSON.stringify({ result: "pass", baseline, rejected, repeatedReject, recovered, state })); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/cloud-inspect-smoke.js b/src/WebAssembly/FreeCADCoreProbe/cloud-inspect-smoke.js new file mode 100644 index 00000000..48557dfe --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/cloud-inspect-smoke.js @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const crypto = require("crypto"); +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/cloud-configure-r2/bin"); +const sourceRevision = "b5a086fda59b0325fe0996147e6e9b344414f9bc"; + +async function main() +{ + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.wasm")), + }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const run = () => { + const result = { + run: call("fc_web_cloud_inspect_source_inventory", "number", ["string"], [sourceRevision]), + kernelError: { + code: call("fc_web_cloud_kernel_error_code", "string"), + message: call("fc_web_cloud_kernel_error_message", "string"), + }, + actionError: { + code: call("fc_web_cloud_inspect_error_code", "string"), + field: call("fc_web_cloud_inspect_error_field", "string"), + message: call("fc_web_cloud_inspect_error_message", "string"), + }, + sourceRevision: call("fc_web_cloud_inspect_source_revision", "string"), + inventoryRevision: call("fc_web_cloud_inspect_inventory_revision", "string"), + summary: call("fc_web_cloud_inspect_summary", "string"), + methodCount: call("fc_web_cloud_inspect_method_count"), + objectTypeCount: call("fc_web_cloud_inspect_object_type_count"), + formatExtensionCount: call("fc_web_cloud_inspect_format_extension_count"), + formatEvidenceCount: call("fc_web_cloud_inspect_format_evidence_count"), + pythonApiCount: call("fc_web_cloud_inspect_python_api_count"), + guiPythonApiCount: call("fc_web_cloud_inspect_gui_python_api_count"), + moduleHandleBeforeRelease: call("fc_web_cloud_inspect_module_handle_before_release"), + moduleHandleAfterRelease: call("fc_web_cloud_inspect_module_handle_after_release"), + }; + result.resultSha256 = crypto.createHash("sha256").update(result.summary).digest("hex"); + return result; + }; + const first = run(); + const second = run(); + const valid = (result) => result.run === 1 + && result.kernelError.code === "" + && result.actionError.code === "" + && result.sourceRevision === sourceRevision + && result.inventoryRevision === "4f4e9244e613ea2df06e70027074a562695048cb" + && result.methodCount === 8 + && result.objectTypeCount === 0 + && result.formatExtensionCount === 0 + && result.formatEvidenceCount === 1 + && result.pythonApiCount === 3 + && result.guiPythonApiCount === 3 + && result.moduleHandleBeforeRelease === 1 + && result.moduleHandleAfterRelease === 0; + if (!valid(first) || !valid(second) || JSON.stringify(first) !== JSON.stringify(second)) { + throw new Error(JSON.stringify({ first, second })); + } + console.log(JSON.stringify({ result: "pass", action: "inspect/source-inventory", runs: 2, details: first })); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/cloud-kernel-smoke.js b/src/WebAssembly/FreeCADCoreProbe/cloud-kernel-smoke.js new file mode 100644 index 00000000..85ec8d41 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/cloud-kernel-smoke.js @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/cloud-configure/bin"); + +async function main() +{ + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.wasm")), + }); + const call = (name, returnType = "number") => probe.ccall(name, returnType); + const result = { + run: call("fc_web_cloud_kernel_create_recompute"), + error: { + code: call("fc_web_cloud_kernel_error_code", "string"), + message: call("fc_web_cloud_kernel_error_message", "string"), + }, + configuration: { + url: call("fc_web_cloud_kernel_url", "string"), + tokenAuth: call("fc_web_cloud_kernel_token_auth", "string"), + tokenSecret: call("fc_web_cloud_kernel_token_secret", "string"), + tcpPort: call("fc_web_cloud_kernel_tcp_port", "string"), + protocolVersion: call("fc_web_cloud_kernel_protocol_version", "string"), + region: call("fc_web_cloud_kernel_region", "string"), + setterCount: call("fc_web_cloud_kernel_setter_count"), + }, + recompute: { + count: call("fc_web_cloud_kernel_recompute_count"), + hasError: call("fc_web_cloud_kernel_recompute_has_error"), + }, + invalid: { + return: call("fc_web_cloud_kernel_invalid_setter_return"), + code: call("fc_web_cloud_kernel_invalid_error_code", "string"), + message: call("fc_web_cloud_kernel_invalid_error_message", "string"), + }, + }; + const valid = result.run === 1 + && result.error.code === "" + && result.error.message === "" + && result.configuration.url === "https://cloud.example.invalid" + && result.configuration.tokenAuth === "web-fixture-access" + && result.configuration.tokenSecret === "web-fixture-secret" + && result.configuration.tcpPort === "443" + && result.configuration.protocolVersion === "4" + && result.configuration.region === "us-east-1" + && result.configuration.setterCount === 6 + && result.recompute.count === 1 + && result.recompute.hasError === 0 + && result.invalid.return === 0 + && result.invalid.code === "invalid_argument" + && result.invalid.message === "Cloud.URL expects one string argument"; + if (!valid) { + throw new Error(JSON.stringify(result)); + } + console.log(JSON.stringify(result)); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/cloud-persistence-boundary-smoke.js b/src/WebAssembly/FreeCADCoreProbe/cloud-persistence-boundary-smoke.js new file mode 100644 index 00000000..307f4086 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/cloud-persistence-boundary-smoke.js @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const crypto = require("crypto"); +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/cloud-configure-r2/bin"); + +async function main() +{ + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.wasm")), + }); + const call = (name, returnType = "number") => probe.ccall(name, returnType); + const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex"); + const run = () => { + const result = { + run: call("fc_web_cloud_kernel_persistence_boundary"), + error: { + code: call("fc_web_cloud_kernel_error_code", "string"), + message: call("fc_web_cloud_kernel_error_message", "string"), + }, + applicable: call("fc_web_cloud_persistence_applicable"), + reason: call("fc_web_cloud_persistence_reason", "string"), + entrypoints: { + save: call("fc_web_cloud_persistence_save_available"), + restore: call("fc_web_cloud_persistence_restore_available"), + }, + documents: { + before: call("fc_web_cloud_persistence_document_count_before"), + after: call("fc_web_cloud_persistence_document_count_after"), + }, + invalid: { + return: call("fc_web_cloud_recovery_invalid_return"), + code: call("fc_web_cloud_recovery_error_code", "string"), + field: call("fc_web_cloud_recovery_error_field", "string"), + message: call("fc_web_cloud_recovery_error_message", "string"), + }, + state: { + before: call("fc_web_cloud_persistence_state_before", "string"), + after: call("fc_web_cloud_persistence_state_after", "string"), + replayEqual: call("fc_web_cloud_persistence_replay_equal"), + }, + }; + result.state.beforeSha256 = sha256(result.state.before); + result.state.afterSha256 = sha256(result.state.after); + return result; + }; + const first = run(); + const second = run(); + const valid = (result) => result.run === 1 + && result.error.code === "" + && result.error.message === "" + && result.applicable === 0 + && result.reason.includes("remote S3 service") + && result.entrypoints.save === 1 + && result.entrypoints.restore === 1 + && result.documents.before === 0 + && result.documents.after === 0 + && result.invalid.return === 0 + && result.invalid.code === "invalid_argument" + && result.invalid.field === "URL" + && result.state.before === result.state.after + && result.state.beforeSha256 === result.state.afterSha256 + && result.state.replayEqual === 1; + if (!valid(first) || !valid(second) || JSON.stringify(first) !== JSON.stringify(second)) { + throw new Error(JSON.stringify({ first, second })); + } + console.log(JSON.stringify({ result: "pass", action: "persistence-boundary", runs: 2, details: first })); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/cloud-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/cloud-recovery-smoke.js new file mode 100644 index 00000000..3e0ba451 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/cloud-recovery-smoke.js @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const crypto = require("crypto"); +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/cloud-configure/bin"); + +async function main() +{ + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.wasm")), + }); + const call = (name, returnType = "number") => probe.ccall(name, returnType); + const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex"); + const run = () => { + const result = { + run: call("fc_web_cloud_kernel_recovery"), + error: { + code: call("fc_web_cloud_kernel_error_code", "string"), + message: call("fc_web_cloud_kernel_error_message", "string"), + }, + invalid: { + return: call("fc_web_cloud_recovery_invalid_return"), + code: call("fc_web_cloud_recovery_error_code", "string"), + field: call("fc_web_cloud_recovery_error_field", "string"), + message: call("fc_web_cloud_recovery_error_message", "string"), + }, + state: { + before: call("fc_web_cloud_recovery_state_before", "string"), + after: call("fc_web_cloud_recovery_state_after", "string"), + afterReplay: call("fc_web_cloud_recovery_state_after_replay", "string"), + }, + unchangedAfterReject: call("fc_web_cloud_recovery_unchanged_after_reject"), + replayEqual: call("fc_web_cloud_recovery_replay_equal"), + recomputeCount: call("fc_web_cloud_recovery_recompute_count"), + hasError: call("fc_web_cloud_recovery_has_error"), + }; + result.state.beforeSha256 = sha256(result.state.before); + result.state.afterSha256 = sha256(result.state.after); + result.state.afterReplaySha256 = sha256(result.state.afterReplay); + return result; + }; + const first = run(); + const second = run(); + const valid = (result) => result.run === 1 + && result.error.code === "" + && result.error.message === "" + && result.invalid.return === 0 + && result.invalid.code === "invalid_argument" + && result.invalid.field === "URL" + && result.invalid.message.includes("Cloud.URL expects a string value") + && result.state.before === result.state.after + && result.state.after === result.state.afterReplay + && result.state.beforeSha256 === result.state.afterSha256 + && result.state.afterSha256 === result.state.afterReplaySha256 + && result.unchangedAfterReject === 1 + && result.replayEqual === 1 + && result.recomputeCount === 2 + && result.hasError === 0; + if (!valid(first) || !valid(second) || JSON.stringify(first) !== JSON.stringify(second)) { + throw new Error(JSON.stringify({ first, second })); + } + console.log(JSON.stringify({ result: "pass", action: "recompute/recovery", runs: 2, details: first })); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/cloud-runtime-smoke.js b/src/WebAssembly/FreeCADCoreProbe/cloud-runtime-smoke.js new file mode 100644 index 00000000..036ad05c --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/cloud-runtime-smoke.js @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/cloud-configure/bin"); + +async function main() +{ + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebCloudProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCloudProbe.wasm")), + }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const error = () => ({ + code: call("fc_web_cloud_error_code", "string"), + message: call("fc_web_cloud_error_message", "string"), + }); + const result = {}; + + result.initialize = call("fc_web_cloud_initialize", "number", ["string"], ["Cloud"]); + result.initializeAgain = call("fc_web_cloud_initialize", "number", ["string"], ["Cloud"]); + result.visible = call("fc_web_cloud_module_visible"); + result.stateAfterValid = { + calls: call("fc_web_cloud_initialization_calls"), + runs: call("fc_web_cloud_initialization_runs"), + initialized: call("fc_web_cloud_initialized"), + }; + result.unknownModule = call("fc_web_cloud_initialize", "number", ["string"], ["Missing"]); + result.unknownModuleError = error(); + result.guiModule = call("fc_web_cloud_initialize", "number", ["string"], ["CloudGui"]); + result.guiModuleError = error(); + result.emptyModule = call("fc_web_cloud_initialize", "number", ["string"], [""]); + result.emptyModuleError = error(); + result.stateAfterErrors = { + calls: call("fc_web_cloud_initialization_calls"), + runs: call("fc_web_cloud_initialization_runs"), + initialized: call("fc_web_cloud_initialized"), + }; + + const valid = result.initialize === 1 + && result.initializeAgain === 1 + && result.visible === 1 + && result.stateAfterValid.calls === 3 + && result.stateAfterValid.runs === 1 + && result.stateAfterValid.initialized === 1 + && result.unknownModule === 0 + && result.unknownModuleError.code === "module_unavailable" + && result.guiModule === 0 + && result.guiModuleError.code === "gui_dependency_unavailable" + && result.emptyModule === 0 + && result.emptyModuleError.code === "invalid_argument" + && result.stateAfterErrors.calls === 3 + && result.stateAfterErrors.runs === 1 + && result.stateAfterErrors.initialized === 1; + if (!valid) { + throw new Error(JSON.stringify(result)); + } + console.log(JSON.stringify(result)); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/tools/prepare_web_cloud_dependencies.sh b/tools/prepare_web_cloud_dependencies.sh new file mode 100755 index 00000000..1266dc94 --- /dev/null +++ b/tools/prepare_web_cloud_dependencies.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +set -euo pipefail + +source_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +workspace_dir=$(cd "$source_dir/.." && pwd) +base_lib_dir=${FREECAD_WEB_BASE_LIB_DIR:-"$workspace_dir/FreeCADBaseLib"} +build_root=${FREECAD_WEB_BUILD_ROOT:-"$source_dir/build-web"} +jobs=${FREECAD_WEB_JOBS:-8} +emscripten_config=${FREECAD_WEB_EMSCRIPTEN_CONFIG:-"$build_root/emscripten-config.py"} + +openssl_source="$base_lib_dir/sources/src/OpenSSL-3.2.2" +curl_source="$base_lib_dir/sources/src/CURL-8.8.0" +zlib_install="$build_root/zlib-install" +openssl_build="$build_root/openssl-cloud-build" +openssl_install="$build_root/openssl-install" +curl_build="$build_root/curl-cloud-build" +curl_install="$build_root/curl-install" + +if [[ ! -f "$openssl_source/.source-complete" || + ! -f "$curl_source/.source-complete" ]]; then + "$base_lib_dir/download_sources.sh" +fi + +toolchain=${FREECAD_WEB_EMSCRIPTEN_TOOLCHAIN:-} +if [[ -z "$toolchain" ]]; then + emcc_path=$(realpath "$(command -v emcc)") + toolchain="$(dirname "$emcc_path")/cmake/Modules/Platform/Emscripten.cmake" +fi +if [[ ! -f "$toolchain" ]]; then + printf 'Emscripten toolchain not found: %s\n' "$toolchain" >&2 + exit 1 +fi +if [[ ! -f "$emscripten_config" ]]; then + printf 'Project Emscripten config not found: %s\n' "$emscripten_config" >&2 + printf 'Run tools/prepare_web_dependencies.sh first.\n' >&2 + exit 1 +fi +if [[ ! -f "$zlib_install/include/zlib.h" || + ! -f "$zlib_install/lib/libz.a" ]]; then + printf 'Cloud dependency build requires the existing Web zlib installation at %s\n' \ + "$zlib_install" >&2 + printf 'Run tools/prepare_web_dependencies.sh first.\n' >&2 + exit 1 +fi + +# Both build directories are task-owned generated paths. A fresh tree is +# required because OpenSSL does not rebuild every object when Configure +# feature flags change. +rm -rf -- "$openssl_build" "$openssl_install" "$curl_build" "$curl_install" +mkdir -p "$openssl_build" "$openssl_install" "$curl_install" + +( + cd "$openssl_build" + env \ + CC="$(command -v emcc)" \ + CXX="$(command -v em++)" \ + AR="$(command -v emar)" \ + RANLIB="$(command -v emranlib)" \ + "$openssl_source/Configure" \ + linux-generic32 \ + no-shared no-tests no-apps no-asm no-afalgeng no-engine \ + no-threads no-dso no-module \ + --prefix="$openssl_install" \ + --openssldir="$openssl_install/ssl" + make --jobs="$jobs" + make install_sw +) + +EM_CONFIG="$emscripten_config" cmake -S "$curl_source" -B "$curl_build" -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE="$toolchain" \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="$curl_install" \ + -DCMAKE_INSTALL_LIBDIR=lib \ + -DBUILD_CURL_EXE=OFF \ + -DBUILD_EXAMPLES=OFF \ + -DBUILD_LIBCURL_DOCS=OFF \ + -DBUILD_MISC_DOCS=OFF \ + -DBUILD_SHARED_LIBS=OFF \ + -DBUILD_STATIC_LIBS=ON \ + -DBUILD_TESTING=OFF \ + -DCURL_BROTLI=OFF \ + -DCURL_USE_LIBPSL=OFF \ + -DCURL_USE_OPENSSL=ON \ + -DCURL_ZSTD=OFF \ + -DENABLE_CURL_MANUAL=OFF \ + -DENABLE_UNIX_SOCKETS=OFF \ + -DUSE_LIBIDN2=OFF \ + -DUSE_LIBSSH2=OFF \ + -DUSE_NGHTTP2=OFF \ + -DSIZEOF_INT=4 \ + -DSIZEOF_LONG=4 \ + -DSIZEOF_LONG_LONG=8 \ + -DSIZEOF_OFF_T=8 \ + -DSIZEOF_CURL_OFF_T=8 \ + -DSIZEOF_CURL_SOCKET_T=4 \ + -DSIZEOF_SIZE_T=4 \ + -DSIZEOF_SSIZE_T=4 \ + -DSIZEOF_TIME_T=8 \ + -DOPENSSL_ROOT_DIR="$openssl_install" \ + -DOPENSSL_INCLUDE_DIR="$openssl_install/include" \ + -DOPENSSL_CRYPTO_LIBRARY="$openssl_install/lib/libcrypto.a" \ + -DOPENSSL_SSL_LIBRARY="$openssl_install/lib/libssl.a" \ + -DOPENSSL_USE_STATIC_LIBS=TRUE \ + -DZLIB_ROOT="$zlib_install" \ + -DZLIB_USE_STATIC_LIBS=ON +EM_CONFIG="$emscripten_config" cmake --build "$curl_build" --parallel "$jobs" +EM_CONFIG="$emscripten_config" cmake --install "$curl_build" + +required_files=( + "$openssl_install/include/openssl/opensslv.h" + "$openssl_install/lib/libcrypto.a" + "$openssl_install/lib/libssl.a" + "$curl_install/include/curl/curl.h" + "$curl_install/lib/libcurl.a" +) +for required_file in "${required_files[@]}"; do + if [[ ! -f "$required_file" ]]; then + printf 'Missing Cloud Web dependency artifact: %s\n' "$required_file" >&2 + exit 1 + fi +done + +if llvm-nm --undefined-only "$openssl_install/lib/libcrypto.a" \ + | grep -Eq ' (ENGINE_|engine_|ossl_err_load_ENGINE)'; then + printf 'OpenSSL Web archive contains unresolved ENGINE references.\n' >&2 + exit 1 +fi + +file \ + "$openssl_install/lib/libcrypto.a" \ + "$openssl_install/lib/libssl.a" \ + "$curl_install/lib/libcurl.a" +printf 'Cloud Web dependencies are ready under %s\n' "$build_root"