subagent transcript
session 17f09f89 · workflow wf_b0622fdd
delegated task
CONTEXT — porting PySide6 6.11 QtCore bindings to WebAssembly (emscripten 4.0.12, static, no-dlopen, ASYNCIFY, JS-exceptions) for a FreeCAD-wasm build
▸ Magik
CONTEXT — porting PySide6 6.11 QtCore bindings to WebAssembly (emscripten 4.0.12, static, no-dlopen, ASYNCIFY, JS-exceptions) for a FreeCAD-wasm build.
Toolchain: run `source /opt/toolchains/emsdk/emsdk_env.sh` first, then use emcc/emar/emnm (emscripten's llvm-nm) / emcc -c.
Paths:
- PySide6 source: /tmp/pyside-setup/sources/pyside6 (PySide6 module dir: .../PySide6). shiboken6 source: /tmp/pyside-setup/sources/shiboken6
- Generated QtCore wrappers (already produced): /home/magik6k/lcad-wasm/pyside-port/build/m2-qtcore/gen/PySide6/QtCore/*.cpp (module header: pyside6_qtcore_python.h)
- Built archive under test: /home/magik6k/lcad-wasm/pyside-port/build/m2-qtcore/libQtCore_wasm.a
- Build script that produced it: /home/magik6k/lcad-wasm/pyside-port/m2-qtcore/build.sh (IMPORTANT: it compiles ONLY the generated wrapper *.cpp into the archive; it compiles NONE of the glue TUs. It excludes files matching *numpy*.)
- Sibling archives present at final link: libpyside_wasm.a (pyside-port/build/libpyside/), libShiboken_wasm.a (pyside-port/build/shiboken-mod/), libshiboken_wasm.a (pyside-port/build/m0-minimal/)
- Qt (asyncify wasm): /opt/toolchains/qt-asyncify/6.11.1/wasm_singlethread (static libs in lib/libQt6*.a, headers in include/). Built single-thread (QT_FEATURE_thread=-1).
- Python wasm: /opt/toolchains/python-wasm (headers include/python3.14, static libs in lib/). CPython 3.14.4.
- Host moc for glue that needs it: /opt/toolchains/qt/6.11.1/gcc_64/libexec/moc
- shiboken generated-inc dir: /home/magik6k/lcad-wasm/pyside-port/build/shiboken-wasm-inc (has sbkversion.h, embed/*)
KNOWN (but INCOMPLETE — wasm-ld stopped at "too many errors") undefined symbols when linking FreeCAD against libQtCore_wasm.a:
- QtCoreHelper::QGenericArgumentHolder::* and QtCoreHelper::QGenericReturnArgumentHolder::* (ctor/dtor/data/metaType/toGenericReturnArgument/operator=)
- PyTime_ImportAndCheck(_object*)
- QLocale::qt_check_for_QGADGET_macro(), and same for QMimeType, QNativeIpcKey, QStandardPaths, QLocationPermission
AUTHORITATIVE FACT: sources/pyside6/PySide6/QtCore/CMakeLists.txt compiles 4 glue translation units into the QtCore module that build.sh is currently MISSING:
glue/qeasingcurve_glue.cpp, glue/core_snippets.cpp, glue/qtcorehelper.cpp, glue/qiopipe.cpp
qiopipe.cpp contains Q_OBJECT and #include "qiopipe.moc" (needs moc). qt_check_for_QGADGET_macro is a Q_GADGET moc-marker method that is DECLARED but NEVER defined anywhere (confirmed: not in libQt6Core.a) — shiboken wrongly generated a Python binding that CALLS it.
The build.sh compile flags for wrappers are:
EMFLAGS="-fexceptions -O2 -DNDEBUG -std=c++17 -DSHIBOKEN_NO_EMBEDDING_PYC=1 -DAVOID_PROTECTED_HACK=1"
INCS="-I<gen> -I<shiboken-wasm-inc> -I<inc>/embed -I<python3.14> -I<shiboken6/libshiboken> -I<pyside6/libpyside> -I<PySide6/QtCore> -I<PySide6> -I<qtinc> -I<qtinc/QtCore> -I<qtinc/QtCore/6.11.1> -I<qtinc/QtCore/6.11.1/QtCore> -I<qt/mkspecs/wasm-emscripten>"
GOAL: produce the COMPLETE set of changes to build.sh so that ONE rebuild yields an archive that links into FreeCAD with ZERO undefined symbols. Be concrete and evidence-based: cite file:line, and RUN emnm/emcc/grep to verify claims. Do not speculate — compile things and check.
YOUR DIMENSION — build completeness vs upstream. Read sources/pyside6/PySide6/QtCore/CMakeLists.txt (and the top-level sources/pyside6/CMakeLists.txt / cmake helpers it uses) and compare against build.sh. Enumerate EVERY item the upstream QtCore module build has that build.sh lacks:
- every non-generated .cpp translation unit compiled into the module (the 4 glue TUs — confirm exactly these and no others; check for per-file properties),
- every preprocessor -D define upstream sets for the module (e.g. Py_LIMITED_API, QT_NO_*, PYSIDE6_* , module version macros, feature flags),
- every include dir upstream adds that build.sh omits,
- whether excluding *numpy* is correct/complete (what enables/disables numpy? is there a define needed so non-numpy code compiles?),
- any generated source besides *_wrapper.cpp that must be compiled (module_wrapper.cpp / the module init TU — verify build.sh's glob catches it).
Return concrete requiredActions with exact snippets. Verify defines by grepping the actual cmake.
Fable
thinking
I need to examine the build configuration files to understand the project structure.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/pyside-port/m2-qtcore/build.sh"}
Fable
⚙ Read {"file_path": "/tmp/pyside-setup/sources/pyside6/PySide6/QtCore/CMakeLists.txt"}
Fable
thinking
I need to trace where the defines and include directories are coming from by looking at the `create_pyside_module` helper function and the glue files, then check what's being set at the top level.
Fable
⚙ Bash {"command": "grep -rn \"create_pyside_module\\|macro(create_pyside_module\\|function(create_pyside_module\" /tmp/pyside-setup/sources/pyside6/cmake /tmp/pyside-setup/sources/pyside6/*.cmake /tmp/pyside-setup/sources/pyside6/PySide6/CMakeLis…
Fable
⚙ Bash {"command": "grep -rln \"create_pyside_module\" /tmp/pyside-setup/sources/ 2>/dev/null"}
Fable
⚙ Read {"file_path": "/tmp/pyside-setup/sources/pyside6/cmake/Macros/PySideModules.cmake"}
Fable
thinking
I'm noticing a critical compilation flag difference: the create_pyside_module uses `-DQT_LEAN_HEADERS=1` when compiling the wrappers, but build.sh doesn't set this define. The shiboken generation also differs—create_pyside_module passes `--lean-headers` while build.sh's generate step doesn't, though that's a generation-time difference rather than a build completeness issue. The key problem is that the wrapper compilation is missing the QT_LEAN_HEADERS define that should be set.
Now I'm checking what other defines might be missing by looking at the include directories—QtCore_BINARY_DIR in the upstream build contains generated files like QtCore_global.post.h, and I need to verify whether buil …
Fable
Let me investigate the glue files, the missing symbols, and defines.
Fable
⚙ Bash {"command": "cd /tmp/pyside-setup/sources/pyside6/PySide6/QtCore/glue\necho \"=== glue dir listing ===\"; ls -la\necho; echo \"=== qiopipe.h location ===\"; find /tmp/pyside-setup/sources/pyside6 -name qiopipe.h\necho; echo \"=== qtcorehelp…
Fable
⚙ Bash {"command": "cd /tmp/pyside-setup/sources/pyside6\necho \"=== qt_check_for_QGADGET_macro references in generated QtCore wrappers ===\"\ngrep -rn \"qt_check_for_QGADGET_macro\" /home/magik6k/lcad-wasm/pyside-port/build/m2-qtcore/gen/PySide6/…
Fable
thinking
I'm confirming the locations of several key components: QGenericArgumentHolder and QGenericReturnArgumentHolder are defined in the qtcorehelper.cpp glue translation unit, PyTime_ImportAndCheck is in core_snippets.cpp, and qt_check_for_QGADGET_macro gets invoked in wrappers for various Q_GADGET types like QCalendarPermission, QBluetoothPermission, QCalendar, and others.
The core issue is that qt_check_for_QGADGET_macro is declared as a static method by the Q_GADGET macro but never actually defined anywhere—it's not in libQt6Core.a. The bindings are calling it as a non-static method, which shouldn't work. The real fix upstream would be to use `--drop-type-entries` or have the typesystem rejec …
Fable
Let me check the qt_check_for_QGADGET_macro mechanism, numpy gating, and top-level defines.
Fable
⚙ Bash {"command": "QTINC=/opt/toolchains/qt-asyncify/6.11.1/wasm_singlethread/include\necho \"=== Q_GADGET / qt_check_for_QGADGET_macro definition in Qt headers ===\"\ngrep -rn \"qt_check_for_QGADGET_macro\" $QTINC/QtCore/ 2>/dev/null | head\nech…
Fable
⚙ Bash {"command": "cd /tmp/pyside-setup/sources/pyside6\necho \"=== numpy gating: how is numpy enabled/disabled? ===\"\ngrep -rn \"numpy\\|NUMPY\\|HAVE_NUMPY\\|Numpy\" PySide6/QtCore/ cmake/ 2>/dev/null | grep -iv \"\\.log\" | head -30"}
Fable
thinking
So `qt_check_for_QGADGET_macro` is purely a compile-time marker that exists only as a declaration in the macro expansion—it's never actually defined anywhere because it only needs to be referenced in an unevaluated context where the compiler just checks that it can be found. Now I'm wondering how this pattern works upstream.
The generated wrapper actually calls `cppSelf->qt_check_for_QGADGET_macro()` as a real function, which would normally be an undefined symbol at link time. But upstream doesn't hit this problem because their bindings are built as shared libraries where undefined symbols are tolerated by the linker—they get resolved at runtime or left unresolved without causing a build fa …
Fable
⚙ Bash {"command": "QTINC=/opt/toolchains/qt-asyncify/6.11.1/wasm_singlethread/include\nsed -n '145,175p' $QTINC/QtCore/qtmetamacros.h"}
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/pyside-port/build/m2-qtcore/gen/PySide6/QtCore\necho \"=== all classes whose wrapper calls qt_check_for_QGADGET_macro ===\"\ngrep -l \"qt_check_for_QGADGET_macro\" *.cpp\necho; echo \"=== exact call f…
Fable
thinking
The issue is that `qt_check_for_QGADGET_macro()` is a public member function declared by Q_GADGET but never actually defined—it only exists in unevaluated contexts like sizeof and decltype. Shiboken treats it as a normal public function and generates bindings that try to call it, leading to undefined symbol errors at link time. This affects 17 Qt classes including QCalendarPermission, QBluetoothPermission, QContactsPermission, QCalendar, and QEasingCurve.
The fix is to patch the generated wrapper code with sed to remove the actual C++ call to `cppSelf->qt_check_for_QGADGET_macro()` while keeping the function stub that returns Py_None—this eliminates the undefined symbol reference without br …
Fable
`qt_check_for_QGADGET_macro()` is a public member declared by the Q_GADGET macro (qtmetamacros.h:157) and only ever used in unevaluated `sizeof(&T::...)` context (qmetatype.h) — so it is never defined. Upstream tolerates it because the module is a shared lib (undefined symbols allowed); the static wasm link cannot. 17 wrappers call it. Let me verify numpy gating, the module TU, and whether the glue compiles.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/pyside-port/build/m2-qtcore/gen/PySide6/QtCore\necho \"=== generated files matching numpy ===\"; ls *numpy* 2>/dev/null || echo \"NONE\"\necho; echo \"=== module init TU present? ===\"; ls -la qtcore_…
Fable
⚙ Bash {"command": "cd /tmp/pyside-setup/sources/pyside6\necho \"=== sbknumpy in shiboken libshiboken ===\"; ls /tmp/pyside-setup/sources/shiboken6/libshiboken/ | grep -i numpy\necho; echo \"=== how sbknumpycheck decides (guard define) ===\"; grep…
Fable
thinking
The numpy dependency isn't from any generated files—the grep exclusion in build.sh is harmless since there's nothing to filter out. Instead, numpy support comes through Shiboken::Numpy::View in the qrangemodel_wrapper.cpp source file, which depends on whether libShiboken_wasm.a includes the sbknumpyview object file.
Since Shiboken::Numpy::View has a stub fallback when built without numpy, no module define is needed. The exclusion pattern is essentially vestigial—it doesn't actually disable anything because the numpy calls are baked into the wrapper source itself, not a separate file.
The qtcore_module_wrapper.cpp is already present and will be caught by the glob, so the module initializati …
Fable
Let me actually compile the 4 glue TUs with build.sh's exact flags to prove they work (including moc for qiopipe).
Fable
⚙ Bash {"command": "source /opt/toolchains/emsdk/emsdk_env.sh 2>/dev/null\nSHIB=/tmp/pyside-setup/sources/shiboken6\nPS=/tmp/pyside-setup/sources/pyside6\nPSDIR=$PS/PySide6\nLIBPYSIDE=$PS/libpyside\nINC=/home/magik6k/lcad-wasm/pyside-port/build/sh…
Fable
thinking
The issue is that zsh handles unquoted variable expansion differently than bash—it doesn't word-split by default. Since the build script expects word-splitting behavior, I need to run it through bash explicitly or adjust how the variables are handled.
Fable
zsh didn't word-split the flags. Let me rerun under bash.
Fable
⚙ Bash {"command": "bash -c '\nsource /opt/toolchains/emsdk/emsdk_env.sh 2>/dev/null\nSHIB=/tmp/pyside-setup/sources/shiboken6\nPS=/tmp/pyside-setup/sources/pyside6\nPSDIR=$PS/PySide6\nLIBPYSIDE=$PS/libpyside\nINC=/home/magik6k/lcad-wasm/pyside-po…
Fable
thinking
The four glue translation units compile without issues using the existing build flags, and they provide the exact symbols that were missing—QtCoreHelper's constructors and destructors from qtcorehelper.o, plus the PyTime import check function from core_snippets.o.
For the qiopipe moc file, I placed it in the glue directory next to the cpp file, and since the include uses quotes, it searches the including file's directory first, so the compilation finds it correctly. The qt_check_for_QGADGET_macro call can be safely removed or replaced with a comment without breaking anything—I've confirmed there are no other references to it in the wrappers beyond that single call. As for Py_LIMITED_API, th …
Fable
All 4 glue TUs compile cleanly with the existing flags and emit exactly the missing symbols. Let me verify the sed neutralization for the Q_GADGET marker leaves valid code.
Fable
⚙ Bash {"command": "sed -n '1004,1022p' /home/magik6k/lcad-wasm/pyside-port/build/m2-qtcore/gen/PySide6/QtCore/qcalendar_wrapper.cpp\necho \"=== any non-call references (address-of) to the marker in wrappers? ===\"\ngrep -rn \"qt_check_for_QGADGET…
Fable
thinking
The sed replacement is targeting only the actual function call `cppSelf->qt_check_for_QGADGET_macro();`, not the wrapper function name or documentation strings, so it's precise. Removing that line leaves an empty block that returns None, which is valid.
I've confirmed the permissions and shared memory wrappers exist and compile since the build includes all generated .cpp files. The permission classes like QLocationPermission are among the callers of qt_check_for_QGADGET_macro and are handled by the sed. QNativeIpcKey is also a caller and gets handled the same way.
Looking at the full picture, the known undefined symbols break down into three categories: QtCoreHelper classes from qtcorehelp …
Fable
I have full evidence. All 4 glue TUs compile cleanly with the existing flags and emit exactly the missing symbols; the Q_GADGET marker is a never-defined moc marker that must be neutralized; the module init TU is already globbed; and the `*numpy*` exclusion is a no-op.
Fable
⚙ StructuredOutput {"requiredActions": [{"action": "Compile the 4 upstream QtCore_static_sources glue TUs into the archive (build.sh currently compiles NONE). Add a step after the wrapper compile loop and before '# ---- 3. ARCHIVE ----' (build.sh:82). qiopipe…