subagent transcript
session 18e25685 · workflow wf_23df5b1c
delegated task
Review the toolchain for reproducibility and correctness. List and read the build scripts: `ls -la /opt/toolchains/*.sh /home/magik6k/lcad-wasm/freeca
▸ Magik
Review the toolchain for reproducibility and correctness. List and read the build scripts: `ls -la /opt/toolchains/*.sh /home/magik6k/lcad-wasm/freecad-port/toolchain/` then read /home/magik6k/lcad-wasm/freecad-port/toolchain/configure-stage2.sh, /opt/toolchains/rebuild-deps-wasmeh.sh, /opt/toolchains/rebuild-deps-jseh.sh, /opt/toolchains/rebuild-qt-asyncify-412.sh, /opt/toolchains/fix-freetype-sjlj.sh, and /home/magik6k/lcad-wasm/freecad-port/toolchain/versions.lock if present. Assess: is the CURRENT shipping config (ASYNCIFY + JS-EH, emsdk 4.0.12, qt-asyncify, JS-EH deps) captured reproducibly? Are there hardcoded paths, missing steps, or scripts left in an inconsistent state from the many pivots? Is the getPyEMTrampolinePtr->0 patch baked into the build or only applied manually to a deployed FreeCAD.js (fragile)? Report findings + reproducibility gaps.
Fable
thinking
I'll start by examining the build scripts to understand what's available.
Fable
I'll start by listing the build scripts and reading them.
Fable
⚙ Bash {"command": "ls -la /opt/toolchains/*.sh /home/magik6k/lcad-wasm/freecad-port/toolchain/ 2>&1", "description": "List toolchain build scripts"}
Fable
Let me read all the required files.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/toolchain/configure-stage2.sh"}
Fable
⚙ Read {"file_path": "/opt/toolchains/rebuild-deps-wasmeh.sh"}
Fable
⚙ Read {"file_path": "/opt/toolchains/rebuild-deps-jseh.sh"}
Fable
⚙ Read {"file_path": "/opt/toolchains/rebuild-qt-asyncify-412.sh"}
Fable
⚙ Read {"file_path": "/opt/toolchains/fix-freetype-sjlj.sh"}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/toolchain/versions.lock"}
Fable
thinking
I need to dig into the patches directory, check what's actually deployed in FreeCAD.js, verify which emsdk version is running, and trace through how the final build gets linked together—particularly whether that getPyEMTrampolinePtr patch is baked into the build or applied manually.
Fable
Now let me investigate the emsdk version, patches, the getPyEMTrampolinePtr patch, and the link step.
Fable
⚙ Bash {"command": "cat /opt/toolchains/emsdk/.emscripten 2>/dev/null; echo \"---EMSDK VERSION---\"; cat /opt/toolchains/emsdk/upstream/emscripten/emscripten-version.txt 2>/dev/null; echo \"---ACTIVE---\"; cat /opt/toolchains/emsdk/emsdk_env.sh >/…
Fable
⚙ Bash {"command": "ls -la /home/magik6k/lcad-wasm/freecad-port/toolchain/patches/; echo \"===\"; ls -la /home/magik6k/lcad-wasm/freecad-port/; echo \"=== search getPyEMTrampolinePtr ===\"; grep -rl \"getPyEMTrampolinePtr\" /home/magik6k/lcad-wasm…
Fable
thinking
Looking into the trampoline patch implementation—I need to examine how getPyEMTrampolinePtr is being handled across the CMakeLists configuration, the emscripten_trampoline.c file, and compare the deployed FreeCAD.js against the built version to understand what's different. The STATUS.md and research documentation should clarify the key details around this patch.
Fable
⚙ Bash {"command": "echo \"=== Main/CMakeLists.txt getPyEMTrampolinePtr context ===\"; grep -n \"getPyEMTrampolinePtr\\|Trampoline\\|trampoline\\|EM_JS\\|--post-js\\|post-js\\|jseh\\|asyncify\\|ASYNCIFY\\|JSPI\\|jspi\" /home/magik6k/lcad-wasm/free…
Fable
⚙ Bash {"command": "echo \"=== emscripten_trampoline.c getPyEMTrampolinePtr ===\"; grep -n \"getPyEMTrampolinePtr\\|EMSCRIPTEN_SUPPORTS_JSPI\\|__EMSCRIPTEN_major\\|return 0\\|WebAssembly.Function\" /opt/toolchains/src/Python-3.14.4/Python/emscript…
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Main/CMakeLists.txt", "offset": 60, "limit": 180}
Fable
thinking
The patch gets applied after linking rather than being baked into the build itself, which is what creates the fragility here. I need to check whether there's an automated script handling this or if it's a manual step, and compare the deployed FreeCAD.js against the built version to understand the actual state.
Fable
⚙ Bash {"command": "echo \"=== getPyEMTrampolinePtr in built vs deployed ===\"; for f in /opt/toolchains/src/freecad-gui-build/bin/FreeCAD.js /opt/toolchains/freecad-gui-web/FreeCAD.js /home/magik6k/lcad-wasm/freecad-port/deploy-gui/FreeCAD.js /ho…
Fable
⚙ Bash {"command": "echo \"=== deploy-gui contents ===\"; ls -la /home/magik6k/lcad-wasm/freecad-port/deploy-gui/ /opt/toolchains/freecad-gui-web/ 2>/dev/null; echo \"=== any deploy/build/patch scripts referencing trampoline ===\"; grep -rn \"getP…
Fable
thinking
I'm noticing that the built FreeCAD.js file at the build output path already contains the `return 0;` statement in getPyEMTrampolinePtr, which is puzzling since the comment indicates the patch should be applied post-link. I need to figure out whether this got baked in during the build process itself or if someone manually added it.
The key is checking the source—getPyEMTrampolinePtr is a JavaScript function that comes from Python's emscripten_trampoline.c file, which uses EM_JS to define it. If the source C file already has `return 0;` compiled into it, then it's baked in at the source level rather than patched afterward.
Fable
⚙ Read {"file_path": "/opt/toolchains/src/Python-3.14.4/Python/emscripten_trampoline.c", "offset": 45, "limit": 55}
Fable
thinking
The source file `emscripten_trampoline.c` hasn't been patched—`getPyEMTrampolinePtr()` still has its original implementation starting with `let isIOS = ...` at line 61. But the built FreeCAD.js file contains `return 0;` injected at the start of that function, which means the modification was added during the build process rather than in the source.
This is a post-link patch applied after emscripten generated the JavaScript, not baked into the build itself. If I re-run ninja, it would regenerate FreeCAD.js without the patch since the source `.c` file doesn't contain it. I need to verify whether any build automation actually applies this patch, check the timestamps to confirm the build-tree o …
Fable
Key finding emerging: the source `emscripten_trampoline.c` (line 57-83) is UNPATCHED, yet the built `FreeCAD.js` has `return 0;` injected. Let me confirm no script applies it and check STATUS/R10 docs and installed deps.
Fable
⚙ Bash {"command": "echo \"=== any script/sed that injects return 0 into getPyEMTrampolinePtr anywhere ===\"; grep -rn \"getPyEMTrampolinePtr\\|TrampolinePtr(){return 0\\|Trampoline.*return 0\" /opt/toolchains/*.sh /home/magik6k/lcad-wasm/freecad-…
Fable
thinking
I'm realizing there's a critical reproducibility issue: the getPyEMTrampolinePtr→0 patch that's manually baked into FreeCAD.js has no automation behind it. When the build regenerates FreeCAD.js from scratch via `ninja FreeCADMain`, the patch disappears, which breaks the asyncify trampoline. The build artifacts show the patch is present, but there's no script or sed command actually applying it—it was just hand-edited into the generated file.
Let me check STATUS.md and the R10 configuration to see if there's a documented patch step I'm missing, and verify the dependency build flags are consistent across configure-stage2 and rebuild-deps-jseh.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/STATUS.md"}
Fable
thinking
I'm noticing a discrepancy between what STATUS.md claims are in the reproducibility section and where those files actually live—specifically, `rebuild-deps-407.sh` and `rebuild-qt-asyncify.sh` are listed as being in `toolchain/` but they're actually located in `/opt/toolchains/`.
The bigger issue is that `versions.lock` is stale and doesn't reflect the current shipping configuration: it pins emsdk to 4.0.7 based on an old rationale about prebuilt Qt compatibility, but the system is now running emsdk 4.0.12 with source-built Qt via qt-asyncify, making that original concern irrelevant. The lock file also doesn't capture the JS-EH dependencies, the trampoline patch, or asyncify itself—it's doc …
Fable
STATUS.md gives me the intended config. Let me verify a few concrete correctness points: the freetype variant currently in the sysroot, and whether the deps-jseh ICU inconsistency and DEPS-WASMEH-OK tag are real.
Fable
⚙ Bash {"command": "C=/opt/toolchains/emsdk/upstream/emscripten/cache/sysroot/lib/wasm32-emscripten\necho \"=== current sysroot libfreetype.a longjmp variant ===\"; cd /tmp && rm -f ftbase.c.o; emar x \"$C/libfreetype.a\" ftbase.c.o 2>/dev/null &&…
Fable
thinking
The sysroot's libfreetype.a is using the legacy JS longjmp variant, which is correct for the current asyncify and exception handling setup. The base library differs from the legacy SJLJ version, confirming that the fix script hasn't overwritten it yet.
However, fix-freetype-sjlj.sh is a destructive leftover from the JSPI pivot that would break the current build if run—it would replace the correct variant with the wasm-longjmp one, causing undefined symbol errors. Since the base freetype was regenerated at 06:31 via embuilder with the right variant, the script is now stale and dangerous.
Both dependency scripts echo the same `DEPS-WASMEH-OK` string, so a build orchestrator can't distinguish …
Fable
I have enough to assess. Let me compile the findings.
Confirmed facts:
- Active emsdk is 4.0.12 (`emscripten-version.txt` = "4.0.12"), but `versions.lock` pins `emsdk = 4.0.7`.
- Source `emscripten_trampoline.c:57` is UNPATCHED (`function getPyEMTrampolinePtr() {` with original body), yet built/deployed `FreeCAD.js` have `return 0;` injected as first statement — no script does this.
- Current sysroot `libfreetype.a` = emscripten-longjmp variant (correct for JS-EH); differs from the `-legacysjlj` variant, so `fix-freetype-sjlj.sh` is NOT currently applied.
- Both `rebuild-deps-jseh.sh` and `rebuild-deps-wasmeh.sh` echo `DEPS-WASMEH-OK`; jseh's ICU uses `--with-cross-build=$TC/src/icu/source` (source dir) with no native build, which wasmeh's own comment documents as wrong.
Fable
⚙ StructuredOutput {"findings": [{"severity": "high", "category": "reproducibility-trampoline-patch", "summary": "The load-bearing getPyEMTrampolinePtr->0 fix is a manual hand-edit of the GENERATED FreeCAD.js, not baked into the build; a clean relink silently…