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"}
tool result
total 0
drwxr-xr-x 1 agent agent  246 Jul  5 12:44 .
drwxr-xr-x 1 agent agent 2268 Jul  5 12:41 ..
drwxr-xr-x 1 agent agent  104 Jul  5 12:47 build
drwxr-xr-x 1 agent agent  230 Jul  5 12:45 emscripten-crossbuild
drwxr-xr-x 1 agent agent  154 Jul  5 12:43 fc-pyside-usage
drwxr-xr-x 1 agent agent   24 Jul  5 12:49 poc-minimal
drwxr-xr-x 1 agent agent   86 Jul  5 12:41 py314-compat
drwxr-xr-x 1 agent agent  134 Jul  5 12:45 qt-nothread-typesystem
drwxr-xr-x 1 agent agent  160 Jul  5 12:43 shiboken  …[truncated]
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"}
tool result
1	# PySide6 / shiboken6 — Emscripten cross-compile toolchain for the FreeCAD wasm target
2	
3	**Scope (slug `emscripten-crossbuild`):** the emscripten toolchain setup to cross-compile
4	`libshiboken` + `libpyside` + the generated Qt bindings for wasm — the emcmake/toolchain-file
5	invocation, pointing at wasm CPython and wasm Qt, with the EXACT ABI flags FreeCAD uses. This is a
6	**split build**: the shiboken *generator* is a host (x86-64) tool; the runtime libs and bindings are
7	wasm. This doc  …[truncated]
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/pyside-port/static-nodlopen/plan.md"}
tool result
1	# PySide6 / shiboken6 — static, no-dlopen binding build strategy (wasm)
2	
3	Scope slug: `static-nodlopen`. This is the crux item: turn PySide's normally-SHARED
4	Python extension modules into **STATIC archives** whose `PyInit_<Module>` symbols are
5	registered via `PyImport_AppendInittab` before `Py_Initialize()`, exactly like FreeCAD
6	already does for its own `Part`/`Sketcher`/… modules on wasm.
7	
8	Everything below is grounded in the real trees:
9	- PySide/shiboken source: `/tmp/pyside-se  …[truncated]
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/pyside-port/poc-minimal/plan.md"}
tool result
1	# PySide6/shiboken6 → FreeCAD-wasm: Minimal Proof-of-Concept & Validation Plan
2	
3	**Scope (this doc):** the smallest end-to-end pipeline that proves the whole PySide-on-wasm
4	approach, plus the two follow-on milestones. Everything below is grounded in the real trees and,
5	where noted, in commands actually run on 2026-07-05. See `files/CAPTURED-OUTPUT.txt` for raw output.
6	
7	**Slug:** `poc-minimal`. Sibling plans own the details this one only references:
8	`../qt-nothread-typesystem` (QtC  …[truncated]
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"}
tool result
1	# QtCore shiboken typesystem vs. Qt `-no-feature-thread` (wasm) — analysis & fix
2	
3	**Scope slug:** `qt-nothread-typesystem`
4	**Goal:** Make shiboken's QtCore typesystem bind cleanly against the wasm Qt, which is built
5	`-no-feature-thread` (`QT_FEATURE_thread == -1`, i.e. `QT_CONFIG(thread) == 0`), so that
6	`QThread/QMutex/QWaitCondition/QReadWriteLock/…` do not break generation or compilation.
7	
8	**Verdict (bottom line):** PySide 6.11 was **already designed** to build against a
9	`-no  …[truncated]
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/pyside-port/shiboken-hostgen-build/plan.md"}
tool result
1	# Build the shiboken6 generator on the host — RESULT: SUCCESS (built + verified end-to-end)
2	
3	Scope: build the shiboken6 code generator (the HOST tool that parses Qt headers with
4	libclang and emits CPython C-extension source) with clang/llvm 21, host CPython 3.14.4,
5	host Qt 6.11.1 (gcc_64). This is the critical-path host tool for the whole PySide-wasm port.
6	
7	**Status: the generator builds cleanly and works end-to-end.** It produced real binding
8	source (with a `PyInit_*` entry poin  …[truncated]
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/pyside-port/fc-pyside-usage/plan.md"}
tool result
1	# FreeCAD's exact PySide/shiboken API surface (minimal binding scope)
2	
3	Scope owner: `fc-pyside-usage`.
4	Goal: determine the MINIMAL set of Qt binding modules + shiboken functions FreeCAD-wasm
5	actually needs, so the PySide/shiboken wasm port binds only those (size is the constraint —
6	each Qt binding module is tens of MB, wasm is already ~196 MB).
7	
8	Everything below is grounded in the real tree:
9	- FreeCAD source: `/home/magik6k/lcad-wasm/freecad-port/FreeCAD` (branch `wasm-port`)
1  …[truncated]
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"}
tool result
1	# CPython 3.14 C-API compatibility for shiboken6 / PySide6 6.11
2	
3	**Scope:** Does shiboken6/PySide6 6.11 build against CPython 3.14? What C-API breakages
4	exist and what patches are needed?
5	
6	**Verdict: NO PATCHES NEEDED.** PySide/shiboken 6.11 (git branch `6.11`) already officially
7	targets and supports CPython 3.14. Every internals-heavy libshiboken translation unit
8	compiles clean against both host Python 3.14.4 headers (g++/llvm) and the wasm CPython 3.14
9	headers (emscripten em+  …[truncated]
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/pyside-port/static-nodlopen/0003-pyside-module-static.patch"}
tool result
1	--- a/sources/pyside6/cmake/Macros/PySideModules.cmake
2	+++ b/sources/pyside6/cmake/Macros/PySideModules.cmake
3	@@ -40,7 +40,14 @@ macro(append_size_optimization_flags _module_name)
4	         if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")
5	             target_compile_options(${_module_name} PRIVATE /Gy /Gw /EHsc)
6	             target_link_options(${_module_name} PRIVATE LINKER:/OPT:REF)
7	         elseif ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU|CLANG")
8	-            target_compile_optio  …[truncated]
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/pyside-port/static-nodlopen/freecad-pyside-inittab.snippet.cmake"}
tool result
1	# Drop-in for FreeCAD/src/Main/CMakeLists.txt (inside the existing `if(EMSCRIPTEN)` block,
2	# next to freecad_wasm_generate_inittab). Registers PySide/shiboken static binding archives
3	# on the interpreter inittab using their FULL DOTTED module names.
4	#
5	# The binding archives already export `extern "C" PyObject *PyInit_<leaf>()`
6	# (PyInit_QtCore, PyInit_QtGui, PyInit_Shiboken, ...). We must register them under the
7	# dotted import name so that `import PySide6.QtCore` and shiboken's in  …[truncated]
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/pyside-port/emscripten-crossbuild/pyside-wasm-toolchain.cmake"}
tool result
1	# pyside-wasm-toolchain.cmake
2	# -----------------------------------------------------------------------------
3	# CMake toolchain file for cross-compiling the PySide6/shiboken6 *runtime* libs
4	# (libshiboken, libpyside) and the generated Qt bindings for the FreeCAD
5	# WebAssembly target.
6	#
7	# It CHAIN-LOADS the stock Emscripten toolchain (which sets CMAKE_SYSTEM_NAME=
8	# Emscripten, CMAKE_CROSSCOMPILING=TRUE, and the emcc/em++ compilers) and then
9	# pins the exact ABI-defining flags t  …[truncated]
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 …
tool result
=== 0001-shiboken6 ===
--- a/sources/shiboken6/CMakeLists.txt
+++ b/sources/shiboken6/CMakeLists.txt
@@ -9,6 +9,25 @@ set(CMAKE_BUILD_TYPE Release CACHE STRING "Build Type")

 include(".cmake.conf")
 project(shiboken6)
+
+# WebAssembly port: no dynamic linking / no dlopen. Every SHARED/MODULE target
+# (libshiboken runtime + the shibokenmodule "Shiboken" C-extension) becomes a
+# static archive linked into the final FreeCAD executable and registered via
+# PyImport_AppendInittab. Mirrors FreeCAD  …[truncated]
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…
tool result
File created successfully at: /home/magik6k/lcad-wasm/pyside-port/BUILD-PLAN.md (file state is current in your context — no need to Read it back)
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.