Skip to content

chore: update the Web Assembly CI job to Emscripten 6.0.13 - #16142

Draft
basvandijk wants to merge 14 commits into
leanprover:masterfrom
basvandijk:wasm-emsdk-6
Draft

basvandijk wants to merge 14 commits into
leanprover:masterfrom
basvandijk:wasm-emsdk-6

Conversation

@basvandijk

@basvandijk basvandijk commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

This PR updates the Web Assembly CI job from Emscripten 3.1.44, released in July 2023, to 6.0.13, the current release, so that the WebAssembly build of lean is checked against a maintained toolchain.

Three changes in newer Emscripten versions affect lean.js. emcc links the C++ runtime only when asked to since 6.0.6 and has always ignored -lstdc++, so on Emscripten the C++ standard library flag is now -sDEFAULT_TO_CXX=1; without it lean.js links but fails to start, with 109 unresolved C++ runtime imports. -shared builds a real side module since 6.0 instead of an object file, so lean.js now depends on the static archives it links instead of on the dummy libInit_shared.so, libleanshared_1.so, libleanshared_2.so and libleanshared.so, whose recipes no longer run; as the dummies are touched on every build, this also stops make install from relinking lean.js, which took nine minutes of the job. Since 4.0.11 Emscripten's compiler-rt bundles LLVM's profiling runtime, which a main module links whole, so every lean.js run wrote a default.profraw into its working directory (emscripten-core/emscripten#28123); lean_main now points that runtime at /dev/null.

The Emscripten -s settings are now passed only when linking, which removes the 147 linker setting ignored during compilation warnings, use the -sKEY=VALUE spelling, and no longer include LINKABLE, which MAIN_MODULE=1 implies and Emscripten deprecated in 4.0.15.

doc/make/emscripten.md, which still described the Lean 3 lean_js_js and lean_js_wasm targets, now describes the WebAssembly build as the CI job runs it and is linked from doc/make/index.md.

This PR is stacked on #16135 and carries its commits until that PR merges; it will be rebased onto master afterwards. Until then, the diff of this PR alone is basvandijk:wasm-ci...basvandijk:wasm-emsdk-6.

Claude Code, an AI coding assistant, helped with the investigation, the changes, the testing and this description. I reviewed and checked them.

🤖 Generated with Claude Code

@basvandijk

Copy link
Copy Markdown
Contributor Author

How this was tested:

  • The Web Assembly job runs only at check level 2 or higher (nightlies, release-ci), so the CI evidence is a run of this branch's job on my fork: https://github.com/basvandijk/lean4/actions/runs/41233382394 (emsdk 6.0.13 installed cold, Node.js 26.13.0, stage-0 ccache warm, stage-1 ccache cold): green in 1 h 5 min; Setup emsdk 45 s, Build Target Stage 40.1 min, Install 7 s (9.3 min before: lean.js is no longer relinked at install), Pack 8.4 min, Test 13.3 min, 6/6 tests, source tree clean. Warnings: 0 linker setting ignored during compilation (147 before), 0 -shared emulation warnings (4 before), 0 LINKABLE warnings, 1 dynamic linking + pthreads is experimental (2 before, one per link), the same 14 undefined symbols as before, once each (libuv internals, lean_uv_*, dl_iterate_phdr). Baselines on 3.1.44: run 41141309853 (warm caches: Build Target Stage 32.5 min, Install 9.3 min, Test 10.9 min, 1 h 1 min) and 41127897324 (cold: 54.3 min, 13.9 min, 13.4 min, 1 h 53 min). The run tested f662d3b, which differs from this branch only by the issue reference added to one comment afterwards.
  • Local build in a Docker container mirroring the runner (ubuntu:24.04, clang 18.1.3, cmake 3.31.6, i386 multilib, emsdk 6.0.13, Node.js 26.13.0) with the job's CMAKE_OPTIONS, the Install, Pack and Test steps and the six tests; lean --version, lean --run, lean --print-prefix (which now works; on 3.1.44 it crashed because __filename was undefined on the worker thread), exit codes of IO.Process.exit 3 and of a failing file, a kernel exception, IO.asTask, IO.getEnv via LEAN_PATH. lean.wasm: 247,834 exports (3.1.44: 251,027; the V8 limit is 1,000,000), 410 imports, 0 C++ runtime imports, the same 14 undefined imports as before. Peak RSS: 3.1 GB for the largest process of the lean.js link, 5.2 GB for elab/Format.lean with 4 threads (3.1.44: 10.7 GB).
  • Controls. Without -sDEFAULT_TO_CXX=1 on emsdk 6.0.13, lean.js links but lean --version prints nothing and fails in Emscripten's dynamic loader with TypeError: Cannot read properties of undefined (reading 'value') at reportUndefinedSymbols; lean.wasm imports 109 C++ runtime symbols (std::__2::cout, std::__2::cerr, the basic_streambuf and basic_stringbuf vtables, typeinfo for std::bad_alloc, ...) versus 0 with the flag. The old shared-library recipes, run by name on 6.0.13, build real side modules: libInit_shared.so becomes a 238-byte module with a dylink section, and the libleanshared.so recipe is a 76-second, 3.2 GB side-module LTO link of all of Lean producing an unused 140 MB file (0.1 s of emulation on 3.1.44); with the change neither runs and make install no longer relinks lean.js. Reverting the warning-cleanup commit and rebuilding brings the 147 compile-time warnings and the LINKABLE deprecation warning back; lean.js is byte-identical and lean.wasm has the same size, sections, imports, exports (247,834) and memory limits with and without it; the only difference inside lean.wasm is the respelled flag string that Lean.Compiler.FFI.getLinkerFlags embeds (src/util/ffi.cpp), which shifts later data addresses by two bytes. Without the default.profraw guard, every lean.js run on 6.0.13 left a 128-640 byte default.profraw in its working directory and the test step failed the source-tree check; with it, none (the file is LLVM's raw profile, written by compiler-rt's profiling runtime that Emscripten bundles into libclang_rt.builtins since 4.0.11 and that MAIN_MODULE=1 links whole; a two-line C program linked with -sMAIN_MODULE=1 reproduces it, see MAIN_MODULE programs write default.profraw at exit (profile runtime force-linked from libclang_rt.builtins) emscripten-core/emscripten#28123). The commits before the version bump were also built with emsdk 3.1.44 in the same harness: green, 6/6 tests, source tree clean, no -shared and no linker setting ignored warnings, one lean.js link per build and install.
  • Not tested: browser embedding (not a CI target); Emscripten versions between 3.1.44 and 6.0.13.

@basvandijk

Copy link
Copy Markdown
Contributor Author

changelog-no

@github-actions github-actions Bot added the changelog-no Do not include this PR in the release changelog label Oct 31, 2026
@github-actions github-actions Bot added the toolchain-available A toolchain is available for this PR, at leanprover/lean4-pr-releases:pr-release-NNNN label Oct 31, 2026
basvandijk and others added 14 commits November 2, 2026 11:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t `lean.js` links

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Emscripten the shared libraries are dummies: `lean.js` links the static archives (`LEAN_EXE_LINKER_FLAGS`), and `src/CMakeLists.txt` only touches `libInit_shared.so`, `libleanshared_1.so`, `libleanshared_2.so` and `libleanshared.so`. The `lean` rule in `stdlib.make` still depended on them, so once per build, after `lib/temp/empty.c` had been created, `make` ran their recipes, that is `emcc -shared`, which Emscripten before 6.0 emulated with a static object file and a warning and which Emscripten 6 turns into a real side-module link of all of Lean. And because the dummies are touched on every build, `make install` relinked `lean.js`, nine of the Web Assembly job's sixty minutes.

`lean.js` now depends on `libInit.a`, `libStd.a`, `libLean.a`, `libleancpp.a`, `libleanrt.a`, `libleanshell.a` and `libleanmain.a`, with the Lake `.trace` files of the first three like the other binaries, and the dummy `leanshared` target depends on `leanrt`, which nothing ordered before `lean.js` so far. The shared-library recipes are no longer reachable on Emscripten; the native rule is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Since Emscripten 6.0.6, `emcc` links libc++ and libc++abi only when run as `em++` or with `-sDEFAULT_TO_CXX=1`, and it has always ignored `-lstdc++` for compatibility with GNU toolchains. `lean.js` is linked by `leanc.sh`, that is by `emcc`, with `ERROR_ON_UNDEFINED_SYMBOLS=0` and `MAIN_MODULE=1`, so on Emscripten 6 the link succeeds and `lean.js` fails when Node.js starts it. On Emscripten the C++ standard library flag is therefore `-sDEFAULT_TO_CXX=1`; before 6.0.6 the setting is on by default, so this changes nothing there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`-sALLOW_MEMORY_GROWTH=1` takes effect only when linking, but `EMSCRIPTEN_SETTINGS` passed it to every compilation as well, through `LEAN_EXTRA_CXX_FLAGS` and libuv's `CMAKE_C_FLAGS`, which emcc reports 147 times per build as `linker setting ignored during compilation`. The flags for compiling and linking, `-fwasm-exceptions -pthread -flto`, are now `EMSCRIPTEN_FLAGS` and the settings `EMSCRIPTEN_LINK_SETTINGS`; both go into `LEAN_EXTRA_LINKER_FLAGS`, which `leanc.sh` adds to every link, so `LEAN_EXE_LINKER_FLAGS` does not repeat them. Settings use the `-sKEY=VALUE` spelling that Emscripten documents. `LINKABLE=1` is dropped: `MAIN_MODULE=1` implies it in every Emscripten version from 3.1.44 to 6.0.13, and Emscripten 4.0.15 deprecated it, so newer versions warn about setting it explicitly. The comment quoting Emscripten's old advice to pass all `-s` flags at compile time went with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Since Emscripten 4.0.11 the `libclang_rt.builtins` system library bundles LLVM's profiling runtime, and a main module (`-sMAIN_MODULE=1`) links its system libraries as whole archives, so the runtime's static initializer runs in every `lean.js` process and its `atexit` hook writes `default.profraw` into the current directory: 128 bytes after `lean --version`, 640 bytes after a test. That fails CI's check that the test suite leaves the source tree clean and would litter every user's working directory. `lean_main` now points the runtime at `/dev/null`. The symbol is weak, so Emscripten versions without the runtime, such as 3.1.44, link and behave as before. Reported upstream as emscripten-core/emscripten#28123.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Emscripten 3.1.44 is from July 2023; 6.0.13 is the current release. `setup-emsdk` installs it directly and keys its cache on the version. The previous commits cover the two changes in Emscripten 6 that affect this build: `-shared` builds real side modules, and `emcc` no longer links the C++ runtime by default. `lean.js` still runs on Node.js 26; emcc uses the Node.js bundled with the SDK for its own passes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The page described the Lean 3 build (`emconfigure`, the `lean_js_js` and `lean_js_wasm` targets, lean-web-editor) and was linked from nowhere. It now describes the build as the Web Assembly CI job runs it: a native 32-bit stage 0 compiles the `.olean` files, Emscripten cross-compiles stage 1 into `lean.js` and `lean.wasm`, Node.js 24 or newer runs them through the `bin/lean` launcher, and the tests run one at a time. `ci.yml` and `build-template.yml` remain the authoritative source of the flags and the SDK version. `doc/make/index.md` links the page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog-no Do not include this PR in the release changelog toolchain-available A toolchain is available for this PR, at leanprover/lean4-pr-releases:pr-release-NNNN

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant