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.
=== YOUR SCOPE: The static / no-dlopen binding build strategy (THE crux) (slug: static-nodlopen) ===
PySide normally emits SHARED Python modules loaded via dlopen. wasm has no dlopen — modules must be STATIC libs whose PyInit_<Module> is registered via PyImport_AppendInittab (like FreeCAD's own Part/Sketcher). Investigate sources/shiboken6/libshiboken and sources/pyside6/libpyside + the generated-binding CMake to determine: (a) how the per-module PyInit is defined and whether it can be exposed for static linking (module name mangling, e.g. PyInit_QtCore); (b) what CMake/setup surgery makes them static archives instead of MODULE/SHARED; (c) how emscripten builds embed static Python C-extensions (reference FreeCAD's WasmInittab mechanism and how Part.a exposes PyInit_Part); (d) whether libshiboken/libpyside themselves link statically. Produce the concrete CMake changes. This is the highest-risk, highest-value item.
Create /home/magik6k/lcad-wasm/pyside-port/static-nodlopen/ and write plan.md (required) + any patches. Return the schema with deliverableFile=/home/magik6k/lcad-wasm/pyside-port/static-nodlopen/plan.md.
Fable
thinking
I'm diving into the PySide/shiboken static build setup for WebAssembly, starting with a parallel exploration of the relevant source directories to understand the current configuration.
Fable
I'll investigate the real source files before writing anything. Let me start exploring the PySide/shiboken build system and FreeCAD's static extension model.
Fable
⚙ Bash {"command": "ls /tmp/pyside-setup 2>/dev/null && echo \"---SOURCES---\" && ls /tmp/pyside-setup/sources 2>/dev/null && echo \"---SHIBOKEN6---\" && ls /tmp/pyside-setup/sources/shiboken6 2>/dev/null && echo \"---LIBSHIBOKEN---\" && ls /tmp/p…
Fable
⚙ Bash {"command": "cd /tmp/pyside-setup && git branch 2>/dev/null; git log --oneline -1 2>/dev/null; echo \"---pyside6---\"; ls sources/pyside6 2>/dev/null; echo \"---libpyside---\"; ls sources/pyside6/libpyside 2>/dev/null", "description": "pysi…
Fable
⚙ Bash {"command": "ls /home/magik6k/lcad-wasm/pyside-port/ 2>/dev/null; echo \"---create---\"; mkdir -p /home/magik6k/lcad-wasm/pyside-port/static-nodlopen && echo created", "description": "Check pyside-port dir and create target"}
Fable
thinking
I see there are other sibling scopes already in place, but I need to focus on the static/no-dlopen scope specifically. Let me trace through how the per-module PyInit functions are defined in the generated bindings and understand how CMake handles the module creation as either MODULE or SHARED libraries.
Fable
Let me examine the module init mechanism and how modules are built as SHARED/MODULE.
Fable
⚙ Bash {"command": "grep -rn \"PyInit_\" /tmp/pyside-setup/sources/shiboken6/generator/ 2>/dev/null | head -40", "description": "Find PyInit generation in shiboken generator"}
Fable
⚙ Bash {"command": "grep -rn \"PyInit\\|SBK_MODULE_INIT\\|MODINIT\" /tmp/pyside-setup/sources/shiboken6/generator/shiboken/*.cpp 2>/dev/null | head -40", "description": "Find module init function generation"}
Fable
⚙ Bash {"command": "ls /tmp/pyside-setup/sources/shiboken6/generator/shiboken/ 2>/dev/null", "description": "List shiboken generator files"}
Fable
⚙ Bash {"command": "find /tmp/pyside-setup/sources/shiboken6 -name \"*.cpp\" -path \"*generator*\" 2>/dev/null | head; echo \"---\"; find /tmp/pyside-setup/sources/shiboken6 -type d | head -40", "description": "Find generator source dirs"}
Fable
thinking
I need to check the shiboken6_generator directory to find what I'm looking for.
Fable
⚙ Bash {"command": "find /tmp/pyside-setup/sources/shiboken6_generator -type d 2>/dev/null; echo \"===FILES===\"; find /tmp/pyside-setup/sources/shiboken6_generator -name \"*.cpp\" | head -40", "description": "List generator dirs and cpp files"}
Fable
⚙ Bash {"command": "ls /tmp/pyside-setup/sources/shiboken6_generator/generator/shiboken/; echo \"===PyInit grep===\"; grep -rn \"PyInit_\\|PyInit\\|moduleInit\\|module init\\|initFunctionName\\|SBK_MODULE\" /tmp/pyside-setup/sources/shiboken6_gene…
Fable
thinking
I'm looking at the module initialization code generation around line 6981 to understand the context better.
Fable
⚙ Bash {"command": "sed -n '6900,7050p' /tmp/pyside-setup/sources/shiboken6_generator/generator/shiboken/cppgenerator.cpp", "description": "Read module init function generation"}
Fable
thinking
The generated initialization function follows the pattern `PyInit_<moduleName>`, so each module gets its own entry point like `PyInit_QtCore` and `PyInit_QtGui`. The symbol naming is already correct for the inittab, but I need to figure out how the module names map since they're actually qualified as `PySide6.QtCore` rather than just `QtCore`.
Fable
Excellent — the generated symbol is already `PyInit_QtCore` etc. Now let me find how CMake builds these as SHARED/MODULE targets, and the LIBSHIBOKEN_EXPORT macro.
Fable
⚙ Bash {"command": "grep -rn \"LIBSHIBOKEN_EXPORT\\|LIBSHIBOKEN_API\" /tmp/pyside-setup/sources/shiboken6/libshiboken/shibokenmacros.h", "description": "Find LIBSHIBOKEN_EXPORT definition"}
Fable
⚙ Bash {"command": "grep -rn \"add_library\\|MODULE\\|SHARED\\|create_generator_target\\|add_python_module\\|make_pyside_module\" /tmp/pyside-setup/sources/pyside6/PySide6/CMakeLists.txt 2>/dev/null | head; echo \"===dir===\"; ls /tmp/pyside-setup…
Fable
⚙ Bash {"command": "grep -rn \"add_library\\|MODULE\\|SHARED\\|create_pyside_module\\|PREFIX\\|SUFFIX\\|PyInit\" /tmp/pyside-setup/sources/pyside6/PySide6/QtCore/CMakeLists.txt 2>/dev/null; echo \"===global cmake macro===\"; grep -rln \"create_pys…
Fable
⚙ Bash {"command": "cat /tmp/pyside-setup/sources/pyside6/cmake/Macros/PySideModules.cmake", "description": "Read PySideModules.cmake macro"}
Fable
thinking
I found the issue—I need to switch the library type from `MODULE` to `STATIC` for WebAssembly builds. The `PyInit_QtCore` symbol is already being generated as an extern C function, so that part should work. Now I'm checking whether libshiboken and libpyside are configured as shared or static libraries to understand how they need to be built for this to work.
I'm also looking at how FreeCAD handles static linking with its Part.a and the WasmInittab mechanism, plus examining libshiboken's embed directory since that's relevant for embedding Python, and seeing how the CMake configuration determines library types across these components.
Fable
Now the crux is clear: `add_library(${module_NAME} MODULE ...)`. Let me examine libshiboken/libpyside library types, and FreeCAD's inittab mechanism.
Fable
⚙ Bash {"command": "grep -n \"add_library\\|SHARED\\|STATIC\\|LIBRARY_TYPE\\|libshiboken_TYPE\\|SHIBOKEN_BUILD_TYPE\\|POSITION_INDEP\\|SOVERSION\" /tmp/pyside-setup/sources/shiboken6/libshiboken/CMakeLists.txt", "description": "libshiboken library…
Fable
⚙ Bash {"command": "grep -n \"add_library\\|SHARED\\|STATIC\\|LIBRARY_TYPE\\|SOVERSION\" /tmp/pyside-setup/sources/pyside6/libpyside/CMakeLists.txt", "description": "libpyside library type"}
Fable
⚙ Bash {"command": "find /opt/toolchains/src/freecad-gui-build -name \"WasmInittab*\" 2>/dev/null; echo \"===src===\"; find /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Main -name \"*Inittab*\" -o -name \"*nittab*\" 2>/dev/null | head; echo \"…
Fable
⚙ Bash {"command": "sed -n '1,120p' /opt/toolchains/src/freecad-gui-build/src/Main/WasmInittab.cpp", "description": "Read generated WasmInittab.cpp"}
Fable
thinking
I need to examine how FreeCAD's CMakeLists.txt constructs the inittab in src/Main, and understand how Part.a exposes PyInit_Part as a static library module.
Fable
⚙ Bash {"command": "grep -n \"Inittab\\|AppendInittab\\|PyInit\\|WASM\\|EMSCRIPTEN\\|STATIC\\|MODULE\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Main/CMakeLists.txt | head -60", "description": "Find inittab generation in FreeCAD Main CMake"…
Fable
⚙ Bash {"command": "sed -n '1,130p' /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Main/CMakeLists.txt", "description": "Read Main CMakeLists inittab + link options"}
Fable
thinking
I need to understand how FreeCAD handles converting Python modules from dynamic to static libraries when building for Emscripten, so I can apply the same approach to PySide. Let me trace through FreeCAD's CMake configuration to find the macro that switches a MODULE library type to STATIC in the Emscripten build context.
Fable
FreeCAD's inittab model is clear. Now let me see how FreeCAD turns its own Python modules into static archives (the MODULE→STATIC pattern I need to mirror).
Fable
⚙ Bash {"command": "grep -rn \"add_library.*MODULE\\|EMSCRIPTEN\\|STATIC\\|PyMOD_ADD\\|python_module\\|SET_PYTHON_PREFIX_SUFFIX\\|fc_wrap_add\\|add_wasm\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/cMake/FreeCadMacros.cmake 2>/dev/null | head -4…
Fable
⚙ Bash {"command": "grep -rn \"add_library\\|MODULE\\|STATIC\\|EMSCRIPTEN\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/Part/App/CMakeLists.txt | head; echo \"===how add_library invoked - search generic macro===\"; grep -rln \"MODULE \\${…
Fable
thinking
I'm noticing that Part is defined as a shared library, but on WebAssembly it needs to be static instead. I'm trying to figure out how Emscripten handles library types globally through CMake variables or toolchain settings, and whether FreeCAD has specific handling for this scenario.
Fable
⚙ Bash {"command": "grep -rn \"EMSCRIPTEN\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/CMakeLists.txt | head; echo \"===search for SHARED->STATIC override===\"; grep -rln \"add_library.*SHARED.*STATIC\\|CMAKE_SHARED_LIBRARY\\|BUILD_SHARED_LIBS\\…
Fable
⚙ Bash {"command": "sed -n '85,180p' /home/magik6k/lcad-wasm/freecad-port/FreeCAD/CMakeLists.txt", "description": "Read EMSCRIPTEN block in top CMakeLists"}
Fable
thinking
FreeCAD's approach is elegant—it overrides `add_library` globally in the top-level CMakeLists.txt for Emscripten builds, automatically converting SHARED and MODULE libraries to STATIC. I could apply the same pattern to PySide by adding this override to its top-level CMakeLists.txt files, rather than patching individual library definitions throughout the codebase.
However, there are some complications to work through. The module targets use OUTPUT_NAME with SHIBOKEN_PYTHON_EXTENSION_SUFFIX and PREFIX "" to create the Python extension naming convention, which would produce archive filenames like `QtCore.cpython-...-wasm32.a` when converted to STATIC—but for inittab linking, the actual filenam …
Fable
FreeCAD's key mechanism found: a global `add_library` override (SHARED/MODULE→STATIC) in the top-level CMakeLists under EMSCRIPTEN. Now let me verify a critical correctness issue — how the module name flows (submodule `PySide6.QtCore` vs inittab which normally only serves top-level names). Let me check the generated moduleDef name and CPython's BuiltinImporter.
Fable
⚙ Bash {"command": "grep -n \"m_name\\|moduleName()\\|createOnly\\|PyModuleDef \" /tmp/pyside-setup/sources/shiboken6_generator/generator/shiboken/cppgenerator.cpp | head -30", "description": "Find moduleDef m_name value"}
Fable
⚙ Bash {"command": "grep -rn \"class BuiltinImporter\\|_bootstrap.py\\|path is not None\" /opt/toolchains/python-wasm/lib/python3.14/importlib/_bootstrap.py 2>/dev/null | head; echo \"===locate===\"; find /opt/toolchains -path \"*importlib/_bootst…
Fable
thinking
I'm checking what moduleName() actually returns—whether it's just the leaf name or the full qualified name—and looking into how PySide handles submodules through the writeSubModuleHandling function and its subModuleOf parameter, along with what BuiltinImporter.find_spec provides.
Fable
⚙ Bash {"command": "sed -n '974,1050p' /opt/toolchains/python-wasm/lib/python3.14/importlib/_bootstrap.py", "description": "Read BuiltinImporter class"}
Fable
⚙ Bash {"command": "grep -n \"QString CppGenerator::moduleName\\|::moduleName\\|moduleName() const\\|m_moduleName\\|subModuleOf\\|writeSubModuleHandling\" /tmp/pyside-setup/sources/shiboken6_generator/generator/shiboken/*.cpp /tmp/pyside-setup/sou…
Fable
thinking
I just realized that `BuiltinImporter.find_spec()` in CPython 3.14 doesn't have the path guard I thought it did—it only checks if the module is in the inittab using `_imp.is_builtin()`. This means builtin modules can actually be submodules as long as they're registered in the inittab, which eliminates my main concern about this approach.
Now I need to figure out the naming mismatch: when we register "PySide6.QtCore" in the inittab and call `_imp.create_builtin(spec)`, it creates the module with `__name__ = "PySide6.QtCore"`. But Shiboken's moduleDef has `m_name = "QtCore"` (just the leaf name). During multi-phase initialization, CPython uses the spec name for the module's `__name__`, so the …
Fable
⚙ Bash {"command": "sed -n '218,245p' /tmp/pyside-setup/sources/shiboken6_generator/generator/generator.cpp; echo \"===subModuleHandling===\"; sed -n '6531,6560p' /tmp/pyside-setup/sources/shiboken6_generator/generator/shiboken/cppgenerator.cpp", …
Fable
thinking
The `moduleName()` function extracts just the leaf portion after the last dot, so for QtCore it returns "QtCore". The `subModuleOf` attribute is a typesystem concept that's separate from the module hierarchy — it's used for nested namespaces like QtCore.Qt rather than top-level modules like PySide6.QtCore.
For the inittab registration approach, I need to verify that CPython's multi-phase init handles the module naming correctly: when registering "PySide6.QtCore" with an init function that returns a PyModuleDef with m_name "QtCore", the `PyModule_FromDefAndSpec` call uses the spec name for `__name__`, not the def's m_name, so the module ends up with the correct full name. The key constraint …
Fable
CPython 3.14's `BuiltinImporter.find_spec` has no `path is not None` guard — so submodules like `PySide6.QtCore` CAN be builtins. Now let me check shibokenmodule, the embed dir, and create_generator_target.
Fable
⚙ Bash {"command": "cat /tmp/pyside-setup/sources/shiboken6/shibokenmodule/CMakeLists.txt", "description": "Read shibokenmodule CMakeLists"}
Fable
thinking
The shibokenmodule builds a C-extension called `Shiboken` within the `shiboken6` package, so importing `shiboken6` loads its `__init__.py` which pulls everything from `shiboken6.Shiboken`. The C-extension module maps to "shiboken6.Shiboken" with the init function `PyInit_Shiboken` in the inittab. Now I need to look into `create_generator_target`, the embed directory, and the `SHIBOKEN_PYTHON_EXTENSION` setting.
I'm checking the `_SUFFIX` definition and `isCompiled()` function, plus investigating `libshiboken/embed` which handles embedding Python signature support. The embed mechanism works by having `Shiboken::init()` load the signature module from a frozen buffer using a generated header f …
Fable
⚙ Bash {"command": "grep -rn \"create_generator_target\\|PYTHON_EXTENSION_SUFFIX\\|SHIBOKEN_PYTHON_EXTENSION_SUFFIX\" /tmp/pyside-setup/sources/shiboken6/cmake/*.cmake /tmp/pyside-setup/sources/shiboken6/CMakeLists.txt 2>/dev/null | head; echo \"=…
Fable
⚙ Bash {"command": "sed -n '659,700p' /tmp/pyside-setup/sources/shiboken6/cmake/ShibokenHelpers.cmake", "description": "Read create_generator_target macro"}
Fable
thinking
So the key insight here is that shiboken runs as a generator on the host machine to produce the C++ binding sources, which means it needs its own host build with host Qt, host libclang, and host Python. Once those bindings are generated, the actual C++ sources get cross-compiled using emscripten for the target platform—it's a two-stage cross-build process.
The standard PySide approach handles this with a cross-build mechanism (PYSIDE_IS_CROSS_BUILD, used for Android and WebAssembly), so I should check whether Qt for Python already has existing WebAssembly support or experiments I can leverage. But my focus is narrower—I need to verify the static/no-dlopen binding strategy, so let me trace t …
Fable
Now let me verify the multi-phase vs single-phase init path (`Shiboken::isCompiled()`) and how `Shiboken::init()` works, since that determines inittab correctness.
Fable
⚙ Bash {"command": "grep -rn \"isCompiled\\|bool init(\\|void init(\\|createOnly\" /tmp/pyside-setup/sources/shiboken6/libshiboken/sbkmodule.cpp /tmp/pyside-setup/sources/shiboken6/libshiboken/basewrapper.cpp 2>/dev/null | head", "description": "F…
Fable
⚙ Bash {"command": "grep -rn \"isCompiled\" /tmp/pyside-setup/sources/shiboken6/libshiboken/*.cpp /tmp/pyside-setup/sources/shiboken6/libshiboken/*.h; echo \"===createOnly body===\"; sed -n '540,580p' /tmp/pyside-setup/sources/shiboken6/libshiboke…
Fable
⚙ Bash {"command": "sed -n '78,100p' /tmp/pyside-setup/sources/shiboken6/libshiboken/sbkbindingutils.cpp", "description": "isCompiledHelper body"}
Fable
thinking
The `isCompiled()` function checks for Nuitka-specific globals to determine if the code is running as a compiled binary, otherwise it falls back to multi-phase initialization using `PyModuleDef_Init`. I'm now tracing through how CPython actually loads builtin modules when multi-phase init is involved, since the standard dynamic loading path doesn't apply here.
When a builtin module is registered with a dotted name in the inittab, CPython's `create_builtin` calls the init function, which returns a `PyModuleDef` with slots defined (indicating multi-phase). CPython then uses `PyModule_FromDefAndSpec` to create the module from the spec, so the module's `__name__` correctly becomes the full dott …
Fable
Let me examine FreeCAD's PySide/shiboken integration point and confirm the SOABI suffix for the wasm target.
Fable
⚙ Bash {"command": "find /home/magik6k/lcad-wasm/freecad-port/FreeCAD -name \"SetupShibokenAndPyside.cmake\" 2>/dev/null -exec cat {} \\;", "description": "Read FreeCAD SetupShibokenAndPyside"}
Fable
thinking
FreeCAD expects to find PySide6 and Shiboken6 through CMake config files with specific targets, and needs the corresponding Python modules available at runtime. The build flags for these are currently enabled by default but disabled for WebAssembly builds, though my current focus is on statically building PySide itself—the FreeCAD integration is a separate piece I'll need to address afterward.
Now I need to figure out how the static archives get registered in Python's inittab, and I should check the WebAssembly Python configuration to determine the correct SOABI and extension suffix for naming the archives. I'm also going to search through the libshiboken and libpyside source code to see if …
Fable
Let me check for dlopen usage in the runtime libs and confirm the wasm Python SOABI suffix.
Fable
⚙ Bash {"command": "grep -rn \"dlopen\\|dlsym\\|LoadLibrary\\|SharedLibrary\\|import_module\\|PyImport_Import\" /tmp/pyside-setup/sources/shiboken6/libshiboken/*.cpp /tmp/pyside-setup/sources/pyside6/libpyside/*.cpp 2>/dev/null | grep -iv \"import…
Fable
⚙ Bash {"command": "cat /opt/toolchains/python-wasm/lib/python3.14/_sysconfigdata*.py 2>/dev/null | grep -i \"EXT_SUFFIX\\|SOABI\\|'MULTIARCH'\" | head; echo \"===or sysconfig===\"; ls /opt/toolchains/python-wasm/lib/python3.14/ | grep -i sysconfi…
Fable
thinking
The extension suffix is set to `.cpython-314-wasm32-emscripten.so` for runtime libraries, which will determine the Shiboken Python extension suffix. For static libraries, the output name becomes `QtCore.cpython-314-wasm32-emscripten` with an empty prefix, resulting in an archive file named `QtCore.cpython-314-wasm32-emscripten.a`.
I'm thinking through the linking strategy for multiple static modules — the main concern is symbol collisions. Each module has uniquely namespaced functions like `Sbk_QObject_Init` and module-level globals like `SbkPySide6_QtCoreTypes` that are prefixed by module name, so those are safe. The `cppApi` and `sbkConverters` arrays are file-local static variables in ea …
Fable
No dlopen in the runtime libs — clean. Let me verify the dependency-import names are the full dotted package names (they must match inittab keys).
Fable
⚙ Bash {"command": "grep -rn \"requiredTargetImports\\|m_requiredTargetImports\\|addRequiredTargetImport\" /tmp/pyside-setup/sources/shiboken6_generator/ApiExtractor/typedatabase.cpp | head; echo \"===Module::import===\"; grep -n \"PyObject *impor…
Fable
thinking
The `Module::import` function uses the full dotted package name when calling the import machinery, which means the inittab entries need to match that format for the dependency chain to resolve correctly. This confirms the approach is sound, and I have enough information to put together a detailed implementation plan.
Now I need to locate where to inject a global library override in the build system. I'm checking the CMakeLists files for libshiboken and pyside6 to understand their install sections and where the override should be applied at the top level.
Fable
The full chain is verified. Let me check the top-level CMakeLists of each sub-project (where to inject the global override) and the library install sections.
Fable
⚙ Bash {"command": "sed -n '1,40p' /tmp/pyside-setup/sources/shiboken6/CMakeLists.txt; echo \"=====PYSIDE6 TOP=====\"; sed -n '1,45p' /tmp/pyside-setup/sources/pyside6/CMakeLists.txt", "description": "Read tops of shiboken6 and pyside6 CMakeLists"…
Fable
thinking
I've identified the exact injection points in both CMakeLists files—right after the project declarations in shiboken6 and pyside6—where I'll add the EMSCRIPTEN overrides to convert the library types from SHARED and MODULE to STATIC, following FreeCAD's pattern.
Now I'm checking whether the install targets need ARCHIVE DESTINATION configuration for static libraries, and verifying that the SHIBOKEN_PYTHON_EXTENSION_SUFFIX variable is properly defined across the build system.
Fable
I have thorough ground truth. Let me do two final confirmations: the module OUTPUT_NAME suffix variable and whether there's an existing wasm/cross flag in the build.
Fable
⚙ Bash {"command": "grep -rn \"SHIBOKEN_PYTHON_EXTENSION_SUFFIX\\|PYSIDE_IS_CROSS_BUILD\\|EMSCRIPTEN\\|WebAssembly\\|wasm\" /tmp/pyside-setup/sources/pyside6/cmake/PySideSetup.cmake /tmp/pyside-setup/sources/shiboken6/cmake/ShibokenSetup.cmake /tm…
Fable
⚙ Bash {"command": "grep -rn \"SHIBOKEN_PYTHON_EXTENSION_SUFFIX\\|set(SHIBOKEN_PYTHON_EXTENSION\" /tmp/pyside-setup/sources/pyside6/cmake/*.cmake | head; echo \"===cross build setup===\"; sed -n '105,135p' /tmp/pyside-setup/sources/shiboken6/cmake…
Fable
thinking
I'm looking for where SHIBOKEN_PYTHON_EXTENSION_SUFFIX is defined in the PySide setup, but it's not showing up in the initial search—let me check the broader PySide6 codebase to track it down.
Fable
⚙ Bash {"command": "grep -rn \"SHIBOKEN_PYTHON_EXTENSION_SUFFIX\" /tmp/pyside-setup/sources/pyside6/ | grep -v \"OUTPUT_NAME\" | head; echo \"---PYSIDE_IS_CROSS_BUILD set---\"; grep -rn \"PYSIDE_IS_CROSS_BUILD\" /tmp/pyside-setup/sources/pyside6/c…
Fable
⚙ Bash {"command": "grep -rln \"SHIBOKEN_PYTHON_EXTENSION_SUFFIX\\|PYSIDE_IS_CROSS_BUILD\" /tmp/pyside-setup/sources/ /tmp/pyside-setup/build_scripts/ 2>/dev/null", "description": "Find files defining these vars"}
Fable
⚙ Bash {"command": "grep -n \"SHIBOKEN_PYTHON_EXTENSION_SUFFIX\\|PYSIDE_IS_CROSS_BUILD\" /tmp/pyside-setup/sources/shiboken6/cmake/ShibokenHelpers.cmake /tmp/pyside-setup/sources/pyside6/cmake/PySideHelpers.cmake /tmp/pyside-setup/sources/shiboken…
Fable
⚙ Bash {"command": "sed -n '1,70p' /tmp/pyside-setup/sources/pyside6/cmake/PySideHelpers.cmake", "description": "Read PySideHelpers cross build + suffix logic"}
Fable
thinking
So PySide already has the infrastructure for cross-compilation built in—it uses `QFP_SHIBOKEN_HOST_PATH` to point to a host-built shiboken generator, and the emscripten toolchain triggers cross-building by setting `CMAKE_CROSSCOMPILING`. The process is straightforward: first build the shiboken6 generator natively for the host system to get the executable and CMake tools, then use that to cross-compile pyside6.
But there's a complication—shiboken6 itself builds both the generator (a host tool) and libshiboken (the runtime library for the target). In a cross-build scenario, I need libshiboken compiled for WebAssembly, not the host. This means shiboken6 gets built twice: once natively to produ …
Fable
Let me confirm the exact install lines for libshiboken and libpyside to make the ARCHIVE-install fix precise.
Fable
⚙ Bash {"command": "sed -n '150,225p' /tmp/pyside-setup/sources/shiboken6/libshiboken/CMakeLists.txt", "description": "libshiboken properties + install"}
Fable
thinking
Libshiboken's install configuration already includes the ARCHIVE DESTINATION for static libraries, so that's handled. But the module install in PySideModules.cmake only specifies LIBRARY DESTINATION without ARCHIVE, which means I need to either add that or pull the static library from the build tree instead. Let me check how libpyside handles its installation.
Fable
⚙ Bash {"command": "grep -n \"install(TARGETS\\|ARCHIVE\\|LIBRARY DESTINATION\\|RUNTIME\" /tmp/pyside-setup/sources/pyside6/libpyside/CMakeLists.txt", "description": "libpyside install lines"}
Fable
thinking
I'm noticing that the install configuration is inconsistent across the modules—libshiboken and libpyside both specify ARCHIVE DESTINATION, but the per-module installation in PySideModules.cmake is missing the ARCHIVE keyword, while shibokenmodule uses a bare DESTINATION that applies to all types including archives.
So I just need to add the ARCHIVE keyword to the pyside module install line to make it consistent. Now I'll write out the complete plan and patch files with all the details.
Fable
I have complete ground truth. Let me write the deliverables.
Fable
⚙ Write {"file_path": "/home/magik6k/lcad-wasm/pyside-port/static-nodlopen/plan.md", "content": "# PySide6 / shiboken6 — static, no-dlopen binding build strategy (wasm)\n\nScope slug: `static-nodlopen`. This is the crux item: turn PySide's normally…
Fable
Now the patch files.
Fable
⚙ Write {"file_path": "/home/magik6k/lcad-wasm/pyside-port/static-nodlopen/0001-shiboken6-emscripten-static.patch", "content": "--- a/sources/shiboken6/CMakeLists.txt\n+++ b/sources/shiboken6/CMakeLists.txt\n@@ -9,6 +9,25 @@ set(CMAKE_BUILD_TYPE Re…
Fable
⚙ Write {"file_path": "/home/magik6k/lcad-wasm/pyside-port/static-nodlopen/0002-pyside6-emscripten-static.patch", "content": "--- a/sources/pyside6/CMakeLists.txt\n+++ b/sources/pyside6/CMakeLists.txt\n@@ -7,6 +7,25 @@ cmake_minimum_required(VERSIO…
Fable
thinking
Working on patch 0003 for PySideModules.cmake, specifically guarding the `-fno-exceptions` flag in the GNU|CLANG compiler branch with an EMSCRIPTEN condition to prevent it from being applied in that build environment.
Fable
⚙ Write {"file_path": "/home/magik6k/lcad-wasm/pyside-port/static-nodlopen/0003-pyside-module-static.patch", "content": "--- a/sources/pyside6/cmake/Macros/PySideModules.cmake\n+++ b/sources/pyside6/cmake/Macros/PySideModules.cmake\n@@ -40,7 +40,14…
Fable
thinking
I'm checking the exact context around the install target line in the CMake file to make sure the patch applies correctly, looking at the surrounding install commands for headers and other files.
Fable
⚙ Write {"file_path": "/home/magik6k/lcad-wasm/pyside-port/static-nodlopen/freecad-pyside-inittab.snippet.cmake", "content": "# Drop-in for FreeCAD/src/Main/CMakeLists.txt (inside the existing `if(EMSCRIPTEN)` block,\n# next to freecad_wasm_generat…
Fable
thinking
I should verify that the Emscripten toolchain actually sets the EMSCRIPTEN variable in CMake by checking the emsdk toolchain file directly.
Fable
Let me verify a load-bearing assumption: that the emscripten CMake toolchain actually sets the `EMSCRIPTEN` variable the shim keys on.
Fable
⚙ Bash {"command": "grep -rn \"set(EMSCRIPTEN\\|EMSCRIPTEN 1\\|EMSCRIPTEN TRUE\" /opt/toolchains/emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake 2>/dev/null | head", "description": "Confirm EMSCRIPTEN var is set by toolchain"}
Fable
thinking
I've confirmed the Emscripten cache variable is properly set in the CMake configuration, and FreeCAD already uses this same approach, so the shim is solid. I have all the pieces I need now—the deliverable is the plan.md file, and I'm confident in this since every claim checks out against the actual source code, though I haven't run a full end-to-end build test. There are no hard blockers within this scope, though there are dependencies on related work areas to keep in mind.
Now I'm cataloging the key files involved.
Fable
All assumptions verified against real files. The strategy is complete and grounded.
## Summary
The static/no-dlopen crux resolves cleanly because the hard part is already done for us:
**Entry points already exist.** shiboken's generator (`cppgenerator.cpp:6981`) emits `extern "C" LIBSHIBOKEN_EXPORT PyObject *PyInit_<leaf>()` per module — `PyInit_QtCore`, `PyInit_QtGui`, `PyInit_Shiboken` — exactly the symbol form FreeCAD's `WasmInittab.cpp` already consumes for `Part`/`Sketcher`. No generator change needed.
**One mechanism makes everything static.** The only thing forcing SHARED is `add_library(${module_NAME} MODULE …)` in `PySideModules.cmake` plus `SHARED` on `libshiboken`/`libpyside6`. FreeCAD already solves this with a global `add_library` shim (`FreeCAD/CMakeLists.txt:91-106`) that rewrites `SHARED|MODULE`→`STATIC` under `if(EMSCRIPTEN)`. Injecting the identical shim at the top of `sources/shiboken6/CMakeLists.txt` and `sources/pyside6/CMakeLists.txt` converts libshiboken, shibokenmodule, libpyside6 and every Qt binding module in one stroke, and is inert on the host generator build. Verified `EMSCRIPTEN` is set by the toolchain (`Emscripten.cmake:298`).
**Import semantics verified for submodules.** CPython 3.14's `BuiltinImporter.find_spec` (`_bootstrap.py:974`) has **no** `path is not None` guard, so `PySide6.QtCore` can be a builtin. Multi-phase init (`isCompiled()`==false in embedded CPython) takes `__name__` from the spec, and cross-module deps use `PyImport_ImportModule("PySide6.QtCore")` — so inittab keys must be the **full dotted names** → `PyInit_<leaf>`. No dlopen anywhere in the runtime libs.
**One real trap:** `append_size_optimization_flags` forces `-fno-exceptions`, which would break FreeCAD's JS-EH/ASYNCIFY build — patch `0003` suppresses it on wasm.
Deliverables written: `plan.md` plus three apply-ready patches and a FreeCAD inittab snippet. Not end-to-end built (needs the host-generator + cross-build stages and sibling scopes' typesystem thread-exclusion), hence confidence high rather than verified-by-test.
Fable
⚙ StructuredOutput {"__unparsedToolInput": {"raw": "{\"slug\": \"static-nodlopen\", \"title\": \"PySide6/shiboken6 static, no-dlopen binding build strategy for wasm\", \"verdict\": \"Feasible with low-risk, well-precedented changes. Each binding already emits…
Fable
⚙ StructuredOutput {"slug": "static-nodlopen", "title": "PySide6/shiboken6 static, no-dlopen binding build strategy for wasm", "verdict": "Feasible with low-risk, well-precedented changes. Each binding module already emits extern \"C\" PyInit_<leaf>() (PyInit…