subagent transcript
session 17f09f89 · workflow wf_6c234df9
delegated task
GOAL: port PySide6 + shiboken6 to the FreeCAD WebAssembly toolchain (static, no-dlopen, asyncify, no-pthreads) so FreeCAD's Python/PySide workbenches
▸ Magik
GOAL: port PySide6 + shiboken6 to the FreeCAD WebAssembly toolchain (static, no-dlopen, asyncify,
no-pthreads) so FreeCAD's Python/PySide workbenches and macros work in the browser. This is the
keystone dependency. Ground EVERYTHING in the real files — investigate, do not speculate.
ESTABLISHED GROUND TRUTH (verified this session):
- PySide+shiboken SOURCE: /tmp/pyside-setup (git branch 6.11, matches Qt). sources/shiboken6,
sources/shiboken6_generator, sources/pyside6 (check exact dir names). Built via setup.py +
coin_build_instructions.py OR raw CMake per sources/*/CMakeLists.txt + README.*.md at repo root.
- HOST toolchain (x86-64 Linux): clang/llvm 21 with libclang at /usr/lib/llvm-21/lib/libclang.so
(llvm-config = 21.1.8); host CPython 3.14.4 (/usr/bin/python3, headers
/opt/toolchains/src/Python-3.14.4/Include); host Qt 6.11.1 at /opt/toolchains/qt/6.11.1/gcc_64.
- TARGET (wasm): emscripten 4.0.12 (/opt/toolchains/emsdk); Qt 6.11.1 asyncify single-thread at
/opt/toolchains/qt-asyncify/6.11.1/wasm_singlethread (QT_FEATURE_thread == -1, i.e. -no-feature-thread);
wasm CPython 3.14 at /opt/toolchains/python-wasm (headers include/python3.14, lib/python3.14.zip).
FreeCAD-wasm build dir: /opt/toolchains/src/freecad-gui-build (reads Qt6_DIR=qt-asyncify/.../wasm_singlethread).
- FreeCAD build links Python C-extensions STATICALLY and registers them via PyImport_AppendInittab in
a generated src/Main/WasmInittab*.cpp (NO dlopen on wasm). PySide bindings must follow this model:
build as STATIC libs, expose PyInit_* for each module (PySide6.QtCore etc.), register in the inittab.
- FreeCAD source: /home/magik6k/lcad-wasm/freecad-port/FreeCAD (branch wasm-port). It ships a PySide
shim at src/Ext/PySide (a compat layer) and uses shiboken (getCppPointer/wrapInstance) e.g. in
Gui/PythonWrapper.cpp. FREECAD_USE_PYSIDE/FREECAD_USE_SHIBOKEN currently OFF.
- KNOWN HARD ISSUES to design around: (1) static + NO dlopen — PySide's normal build emits shared
modules; must emit static + inittab. (2) Qt is -no-feature-thread — shiboken typesystem for QtCore
references QThread/QMutex/etc.; those must be excluded/guarded. (3) CPython 3.14 C-API changes vs
what PySide 6.11 targets. (4) asyncify + JS-exceptions (-fexceptions) flags must match FreeCAD's;
libshiboken/libpyside signal dispatch must be single-thread/asyncify safe. (5) binary size (wasm is
already 196 MB; each Qt binding module is tens of MB — target ONLY the modules FreeCAD needs).
DELIVERABLES — do BOTH:
1. Write a thorough, concrete, execution-ready plan to /home/magik6k/lcad-wasm/pyside-port/<SLUG>/plan.md with exact commands,
cmake args, file paths, patch snippets (precise enough to apply), and any test outputs you gathered.
Put patch/new-file contents as separate files in that dir. If you build/test something, capture the
real output.
2. Return the structured summary (schema). Depth goes in plan.md; keep summary fields tight.
Token cost is not a concern — be exhaustive and correct. Prefer running real commands (grep, ls,
cmake --help, small test builds) over speculation.
=== SYNTHESIS: master PySide-wasm build plan ===
You have 7 investigation reports (summaries below; READ the full plan.md files for detail):
- [fc-pyside-usage] confidence=high | Minimal must-have set = shiboken6 (Shiboken) + PySide6.QtCore + PySide6.QtWidgets + PySide6.QtGui; everything else is optional or provided natively. Verified facts: (1) The generated Ext/PySide compat shim MERGES QtGui+QtWidgets, so the ubiquitous 'from PySide import QtGui' (169 files) pulls in BOTH bindings — inseparable, both must-have. (2) QtUiTools binding NOT needed: HAVE_QT_UI_TOOLS is defined on wasm (non-MinGW), so FreeCAD's C++ UiLoader uses native libQt6UiTools.a (present in asyncify Qt); PySideUic.loadUi / FreeCADGui.UiLoader run through native QUiLoader + shiboken wrap, avoiding the ui-parser typesystem. (3) Enabled wasm Mods (Part, PartDesign incl. gear/sprocket dialogs, Sketcher, Spreadsheet, Surface, Material, Measure) reach only QtCore/QtGui/QtWidgets, plus QtSvgWidgets in the Material editor only. (4) Zero thread classes used in enabled Python, so QtCore typesystem can drop QThread/QMutex/etc, matching the -no-feature-thread Qt. (5) Signal system (.connect/.emit/Signal/Slot, 55 connects) pervasive, so libpyside SignalManager required and must be asyncify/single-thread safe. (6) shiboken function surface is tiny: C++ mode ~15 Shiboken:: calls; current FREECAD_USE_SHIBOKEN=OFF mode needs only getCppPointer + wrapInstance + importable QtCore/QtGui/QtWidgets. Deferred: QtSvg+QtSvgWidgets (Material editor), QtPrintSupport (C++ print path). Exclude: QtUiTools, QtWebEngine*, QtNetwork, QtOpenGL*, QtXml, QtTest, QtConcurrent, QtSql. | blockers: (plan: /home/magik6k/lcad-wasm/pyside-port/fc-pyside-usage/plan.md)
- [shiboken-hostgen-build] confidence=verified-by-test | SUCCESS. The shiboken6 generator builds cleanly on the host and works end-to-end. It is the standalone CMake project sources/shiboken6_generator, built with clang/llvm 21.1.8 (libclang), host CPython 3.14.4, host Qt 6.11.1 gcc_64. Binary: build-gen/generator/shiboken6 reports "shiboken v6.11.0" and links libclang-21.so.21 + libQt6Core.so.6. Verified with --version, --help, and TWO real generation runs: a plain C++ class (emitted PyInit_foo, exactly the inittab symbol the wasm static/no-dlopen model needs) and a real Qt header (QtCore/qsize.h: libclang parsed 7 classes / 140 typedefs), proving libclang 21 parses Qt 6.11 headers with this toolchain. The generator is a HOST tool reused unchanged for the wasm port; only its output is cross-compiled (feed it wasm-Qt headers + a thread-dropped typesystem). | blockers: Default image was missing ClangConfig.cmake (find_package(Clang CONFIG REQUIRED) needs it; NOT in libclang-21-dev). Fixed via apt: libclang-cpp21-dev + clang-21, which install /usr/lib/llvm-21/lib/cmake/clang/ClangConfig.cmake.; Default image was missing Python 3.14 dev headers (/usr …
Fable
thinking
I need to synthesize the master build plan from the investigation reports I've gathered, so I'm starting by reviewing the existing plan files to ground my work in concrete details before writing the BUILD-PLAN.md and executive summary.
Fable
I'll ground this synthesis in the actual plan files. Let me read them all.
Fable
⚙ Bash {"command": "ls -la /home/magik6k/lcad-wasm/pyside-port/ && echo \"---\" && for d in /home/magik6k/lcad-wasm/pyside-port/*/; do echo \"=== $d ===\"; ls -la \"$d\"; done", "description": "List pyside-port investigation dirs and contents"}
Fable
I'll read all the plan files and the concrete patch/script artifacts in parallel.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/pyside-port/emscripten-crossbuild/plan.md"}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/pyside-port/static-nodlopen/plan.md"}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/pyside-port/poc-minimal/plan.md"}
Fable
thinking
Now I'm reviewing the remaining plan files—fc-pyside-usage, qt-nothread-typesystem, py314-compat, and shiboken-hostgen-build—along with their corresponding patch files to get the full picture.
Fable
Let me read the remaining plans and key patch artifacts.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/pyside-port/qt-nothread-typesystem/plan.md"}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/pyside-port/shiboken-hostgen-build/plan.md"}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/pyside-port/fc-pyside-usage/plan.md"}
Fable
thinking
I'm reviewing the py314-compat plan and examining the actual patch files to get the specific details and exact filenames I need to reference.
Fable
Let me read the py314-compat plan and the concrete patch/snippet files to cite exact content.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/pyside-port/py314-compat/plan.md"}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/pyside-port/static-nodlopen/0003-pyside-module-static.patch"}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/pyside-port/static-nodlopen/freecad-pyside-inittab.snippet.cmake"}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/pyside-port/emscripten-crossbuild/pyside-wasm-toolchain.cmake"}
Fable
thinking
I'm reviewing the patches from the static-nodlopen and poc-minimal directories, checking the CMakeLists and inittab configurations. I've spotted a contradiction in the emscripten-crossbuild documentation—it says to run plain `cmake` rather than `emcmake cmake` because the toolchain file loads Emscripten, but static-nodlopen and poc-minimal both use `emcmake cmake`. There's also a critical error in static-nodlopen's Stage 1 command: it tries to build the shiboken6 generator from `sources/shiboken6`, but the generator is actually in a separate directory `sources/shiboken6_generator`. Additionally, the STATIC mechanism differs across approaches—emscripten-crossbuild uses placeholder flags like …
Fable
I have enough to synthesize. Let me quickly check the two remaining static patches and the poc inittab patch to cite exact contents.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/pyside-port; echo \"=== 0001-shiboken6 ===\"; cat static-nodlopen/0001-shiboken6-emscripten-static.patch; echo \"=== 0002-pyside6 ===\"; cat static-nodlopen/0002-pyside6-emscripten-static.patch; echo …
Fable
thinking
I'm seeing two different approaches to handling inittab generation: one uses a patch file that extends the CMake function with a specific naming convention for modules, while the other defines a separate CMake snippet specifically for PySide initialization.
Both achieve the same dotted-name registration, but the first approach is simpler since it reuses the existing generator and a cache variable—which is what the M0 and M1 scripts already use. I'll recommend that as the canonical approach for those milestones and note the alternative as equivalent for larger module lists.
Now I'm structuring the BUILD-PLAN.md document to be comprehensive and execution-ready: starting with an overview and …
Fable
I now have full grounding. Writing the master build plan.
Fable
⚙ Write {"file_path": "/home/magik6k/lcad-wasm/pyside-port/BUILD-PLAN.md", "content": "# Master build plan — PySide6 + shiboken6 for the FreeCAD WebAssembly toolchain\n\nSynthesis of the 7 investigation reports in `pyside-port/*/plan.md`. This is t…
Fable
thinking
I need to prepare the executive summary with the three concrete first steps and identify the biggest remaining risk to wrap this up.
Fable
BUILD-PLAN.md written to /home/magik6k/lcad-wasm/pyside-port/BUILD-PLAN.md — ordered host-generator build, resolved static/no-dlopen strategy, the 5 patches + build knobs, the emcmake/toolchain resolution, milestones M0-M3 with pass/fail tests, a 9-item ranked risk register, and 6 flagged (all resolved) inter-report contradictions.
EXECUTIVE SUMMARY
Recommended first 3 concrete steps:
1. Build the host shiboken generator (Stage A, already verified this session): apt-install python3.14-dev + libclang-cpp21-dev + clang-21 + libxslt1-dev, then `cmake -S /tmp/pyside-setup/sources/shiboken6_generator` (NOT sources/shiboken6) with LLVM_INSTALL_DIR=/usr/lib/llvm-21 and host Qt 6.11.1 → installs Shiboken6Tools to /opt/toolchains/pyside-host. It builds and runs today (`shiboken v6.11.0`); it is a host tool reused unchanged, only its output is cross-compiled.
2. Apply the 5 ready patches to /tmp/pyside-setup: P1/P2 (add_library SHARED/MODULE→STATIC shim in shiboken6 + pyside6 CMakeLists — this IS the static/no-dlopen mechanism; the `-DShiboken6_BUILD_STATIC` flags in one report are non-existent placeholders, drop them), P3 (keep -fexceptions + ARCHIVE install), P4 (guard QBasicReadWriteLock + QReadWriteLock for -no-feature-thread — only 2 typesystem entries need fixing), P5 (RegName=CInitSuffix inittab extension). Python 3.14 needs NO patch (verified: 24/24 non-numpy libshiboken TUs compile under em++).
3. Land Milestone 0 then Milestone 1: run the PoC scripts (M0 = static `minimal` shiboken module imports via inittab under asyncify; M1 = `from PySide6 import QtCore; QtCore.QObject()` in the browser via a QObject-only reduced typesystem), relink FreeCAD with FREECAD_WASM_EXTRA_PY_MODULES + EXTRA_LINK_LIBS, and gate on `M0_PASS`/`M1_PASS` from `node pytest.mjs`. M1 green de-risks the whole port (host-generate → cross-compile → static-link → inittab → asyncify end-to-end).
Single biggest remaining risk: exception-model mismatch (R1). FreeCAD's wasm build is legacy/JS-EH (-fexceptions) + ASYNCIFY, but PySide's stock `append_size_optimization_flags` forces -fno-exceptions on the binding modules. If any binding TU is compiled with a different EH model it will link but be silently invalid (throws don't propagate). P3 suppresses -fno-exceptions on EMSCRIPTEN, but this must be verified across every actually-produced binding object — and the compounding unknown is that no full Stage B/C wasm compile (especially libpyside under -no-feature-thread) has been run yet; only configure + isolated TU compiles are proven.