Skip to content

Conversation

ankith26
Copy link
Member

@ankith26 ankith26 commented Sep 14, 2025

This is my take on @pmp-p 's PR at pmp-p#12

  • pygbag path: dev.py updated to handle this. It uses python-wasm-sdk and makes a custom wheel with a single shared library build of src_c and the pure python contents of src_py.
  • pyodide path: pyodide build now works out of box (provided dependencies are installed as done in before-build attribute in [tool.cibuildwheel.pyodide] section of pyproject.toml). Added CI to test building, however running unit tests on CI is still out of scope of this PR, I may look at it in a future PR.

@pmp-p I would appreciate your review on this PR + help with testing it. If this PR works out, pygbag would need to be updated accordingly to handle the wheel file this PR can build.

@ankith26 ankith26 requested a review from a team as a code owner September 14, 2025 11:18
Copy link
Contributor

coderabbitai bot commented Sep 14, 2025

📝 Walkthrough

Walkthrough

Adds Pyodide CI alongside Pygbag, updates the WASM SDK and build commands, makes dev.py WASM-aware with a Meson cross-file generator and WASM path changes, adds Meson emscripten variants and option, consolidates emscripten C extensions, renames/init-symbols in C sources, updates freetype include, and bumps build-tool constraints.

Changes

Cohort / File(s) Summary
CI workflows
.github/workflows/build-emsdk.yml
Renames job buildbuild-pygbag; updates SDK vars (SDK_VERSION, SDK_ARCHIVE, adds PYBUILD); removes Cython regen and manual libpygame.a steps; replaces manual WASM build with ${SDKROOT}/python3-wasm dev.py build --wheel; adds new build-pyodide job that runs cibuildwheel for Pyodide and uploads pyodide-wheels.
dev tooling / WASM support
dev.py
Adds host_gnu_type and wasm detection via sysconfig; new get_wasm_cross_file(sdkroot: Path) to generate a Meson cross-file for WASM; switches Dev.python to ${SDKROOT}/python3-wasm when wasm; appends -wasm to build suffix; writes/uses generated Meson cross-file via setup args; skips dependency installation and errors on editable builds for WASM.
Meson options & platform branching
meson_options.txt, meson.build
Adds emscripten_type option (pyodide / pygbag); changes plat handling to support emscripten-<type>; adds emscripten-pygbag and emscripten-pyodide branches with SDL/freetype/link flags and wasm exception flags; gates portmidi/docs/stripped-build to skip on emscripten; integrates emscripten variants into top-level platform branching.
Emscripten extension build consolidation
src_c/meson.build
For emscripten builds, creates a single shared extension_module target base that combines multiple C and Cython sources with -DBUILD_STATIC=1 and SDL/freetype deps; retains per-module targets for non-emscripten builds.
C source changes — init symbols & glue
src_c/base.c, src_c/mixer.c
Renames module init symbols and call sites: PyInit_mixerPyInit_pg_mixer; PyInit_sdl2_audioPyInit_audio; PyInit_sdl2_videoPyInit_video; removes PyInit_sdl2_controller and PyInit_sdl2_mixer; in mixer.c conditionalizes init symbol behind BUILD_STATIC (pg_mixer vs mixer).
Freetype include
src_c/_freetype.c
Replaces #include "freetype.h" with #include "freetype/ft_freetype.h".
Build-system / cibuildwheel
pyproject.toml
Bumps build-tool upper bounds: meson<=1.9.1, ninja<=1.13.0, cython<=3.1.4; adds [tool.cibuildwheel.pyodide] section to build cp313-* with a multi-line before-build that runs embuilder and tweaks settings.js and an empty test-command.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Actions as GitHub Actions
  participant Repo as Repository
  participant SDK as Wasm SDK
  participant DevPy as dev.py
  participant Meson as Meson/Ninja
  participant Art as Artifacts

  rect rgb(236,248,255)
  note over Actions: build-pygbag (emscripten-pygbag)
  Actions->>Repo: checkout
  Actions->>SDK: setup SDK (python3.13-wasm)
  Actions->>DevPy: `${SDKROOT}/python3-wasm dev.py build --wheel`
  DevPy->>DevPy: detect wasm, write wasm.cross
  DevPy->>Meson: configure with --cross-file=wasm.cross -> build (emscripten-pygbag)
  Meson-->>DevPy: wheel produced
  Actions->>Art: upload emsdk-wheels
  end

  rect rgb(245,236,255)
  note over Actions: build-pyodide (cibuildwheel)
  Actions->>Repo: checkout
  Actions->>Actions: run cibuildwheel (pyodide cp313)
  Actions->>Actions: run before-build (embuilder + settings.js)
  Actions->>Art: upload pyodide-wheels
  end
Loading
sequenceDiagram
  autonumber
  participant DevPy as dev.py
  participant Sys as sysconfig
  participant FS as SDKROOT
  participant Meson as Meson

  DevPy->>Sys: read HOST_GNU_TYPE
  alt platform is emscripten / wasm
    DevPy->>DevPy: wasm set, host_gnu_type assigned
    DevPy->>FS: get_wasm_cross_file(SDKROOT) -> write wasm.cross
    DevPy->>Meson: configure with --cross-file=wasm.cross (skip deps)
    Meson-->>DevPy: build single extension module (emscripten path)
  else native build
    DevPy->>Meson: normal configure (install deps)
    Meson-->>DevPy: build per-module targets (native path)
  end
  note right of DevPy: editable builds abort early on wasm
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly captures the core objective by stating support for emscripten targets (pygbag and pyodide) in the meson build configuration, which aligns directly with the extensive changes to meson files, dev.py, and CI workflows in this PR. It is concise, specific, and immediately conveys the primary change to a reviewer scanning the history. There is no extraneous detail or ambiguity in the phrasing.
Description Check ✅ Passed The description clearly outlines how the PR implements support for both pygbag and pyodide build paths, referencing updates in dev.py, CI workflows, and cibuildwheel configuration as seen in the changeset. It stays on topic by describing the new build capabilities and known limitations without deviating into unrelated details. The level of detail is sufficient to understand the purpose of the changes.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ankith26-meson-wasm

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 62ed850 and 5d59620.

⛔ Files ignored due to path filters (2)
  • src_c/freetype/ft_freetype.h is excluded by !src_c/freetype/*
  • src_c/freetype/ft_wrap.h is excluded by !src_c/freetype/*
📒 Files selected for processing (9)
  • .github/workflows/build-emsdk.yml (1 hunks)
  • dev.py (6 hunks)
  • meson.build (4 hunks)
  • meson_options.txt (1 hunks)
  • pyproject.toml (2 hunks)
  • src_c/_freetype.c (1 hunks)
  • src_c/base.c (4 hunks)
  • src_c/meson.build (2 hunks)
  • src_c/mixer.c (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • .github/workflows/build-emsdk.yml
  • pyproject.toml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (19)
  • GitHub Check: x86
  • GitHub Check: AMD64
  • GitHub Check: i686
  • GitHub Check: aarch64
  • GitHub Check: Pyodide build
  • GitHub Check: build (ubuntu-22.04)
  • GitHub Check: msys2 (mingw64, x86_64)
  • GitHub Check: Debian (Bookworm - 12) [ppc64le]
  • GitHub Check: msys2 (clang64, clang-x86_64)
  • GitHub Check: Debian (Bookworm - 12) [armv6]
  • GitHub Check: Debian (Bookworm - 12) [armv7]
  • GitHub Check: msys2 (ucrt64, ucrt-x86_64)
  • GitHub Check: Debian (Bookworm - 12) [s390x]
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.14.0rc1)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.9.23)
  • GitHub Check: build (ubuntu-24.04)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.13.5)
  • GitHub Check: dev-check
  • GitHub Check: build (windows-latest)
🔇 Additional comments (8)
meson.build (6)

37-37: LGTM: Emscripten variant selection.

Using get_option('emscripten_type') to differentiate between pygbag and pyodide builds is a clean approach that aligns with the PR objectives.


90-107: LGTM: Pygbag dependencies are straightforward.

The dependency declarations for the pygbag variant are clean and use standard library names. The absence of PNG support in sdl_image_dep aligns with the documented gap in PR objectives (PNG/libpng skipped due to setjmp/longjmp issues).


144-367: LGTM: Generic fallbacks are correctly gated.

The else block starting at line 144 ensures that the generic SDL and FreeType dependency fallbacks (lines 309-365) only execute for non-emscripten platforms. This addresses the past review concern about emscripten-specific dependencies being overwritten by generic fallbacks.


370-370: LGTM: PortMIDI correctly excluded for emscripten.

The condition not plat.startswith('emscripten') appropriately prevents PortMIDI dependency resolution on emscripten targets, as MIDI is not relevant for web/wasm environments.


494-494: LGTM: Documentation and tests correctly excluded for emscripten.

The condition not plat.startswith('emscripten') appropriately skips documentation, test, and example subdirectories for emscripten builds, as these are not needed in wasm wheel artifacts.


110-143: Verify exception-handling compatibility of prebuilt dependencies
Line 110 adds -sSUPPORT_LONGJMP=wasm, but you’re linking against *-legacysjlj SDL2_image and freetype libraries. Ensure that embuilder --pic --force build generates WASM longjmp variants; otherwise switch to non-legacy builds or pass the appropriate flags to embuilder.

src_c/meson.build (2)

1-21: LGTM: Emscripten single-build consolidation is correct for SDL2.

The consolidated build approach (lines 1-21) correctly:

  • Uses plat.startswith('emscripten') to cover both pygbag and pyodide variants
  • Combines multiple C sources and Cython SDL2 modules into a single base extension
  • Sets -DBUILD_STATIC=1 flag as required for emscripten
  • Links all necessary SDL and freetype dependencies

The unconditional inclusion of _sdl2 Cython sources (lines 4-9) is appropriate for the current SDL2-only emscripten support. When SDL3 support is added to emscripten in the future, consider adding a guard similar to the regular build path (see lines 322, 334, 347).


21-478: LGTM: Regular build path preserved.

The else block (lines 21-478) correctly preserves the existing per-module build logic for non-emscripten platforms. The structure cleanly separates emscripten's consolidated build from the regular modular build approach.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ankith26 ankith26 force-pushed the ankith26-meson-wasm branch 2 times, most recently from eedb48b to febe9a1 Compare September 14, 2025 11:25
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (7)
dev.py (4)

39-51: Guard WASM cross-file/args and handle unsupported WASI explicitly

Computing wasm_cross_file and wasm_args unconditionally risks pointing to a non-existent file (e.g., meson-cross-.ini when not WASM, or meson-cross-wasi.ini which doesn’t exist here). Build will fail silently later.

  • Only compute/use these when wasm == 'emscripten'.
  • Fail fast for wasm == 'wasi' (or allow selecting type via a flag/env).

Apply this diff:

-host_gnu_type = sysconfig.get_config_var("HOST_GNU_TYPE")
-if isinstance(host_gnu_type, str) and "wasm" in host_gnu_type:
-    wasm = "wasi" if "wasi" in host_gnu_type else "emscripten"
-else:
-    wasm = ""
-
-wasm_cross_file = (source_tree / "buildconfig" / f"meson-cross-{wasm}.ini").resolve()
-wasm_args = [
-    f"-Csetup-args=--cross-file={wasm_cross_file}",
-    "-Csetup-args=-Demscripten_type=pygbag",
-]
+host_gnu_type = sysconfig.get_config_var("HOST_GNU_TYPE")
+if isinstance(host_gnu_type, str) and "wasm" in host_gnu_type:
+    wasm = "wasi" if "wasi" in host_gnu_type else "emscripten"
+else:
+    wasm = ""
+
+wasm_args: list[str] = []
+if wasm == "emscripten":
+    wasm_cross_file = (source_tree / "buildconfig" / "meson-cross-emscripten.ini").resolve()
+    wasm_args = [
+        f"-Csetup-args=--cross-file={wasm_cross_file}",
+        "-Csetup-args=-Demscripten_type=pygbag",
+    ]
+elif wasm == "wasi":
+    pprint("WASI is not supported by this buildconfig yet. Please use emscripten.", Colors.RED)
+    sys.exit(1)

206-211: Fail gracefully when SDKROOT is missing in WASM mode

Accessing os.environ["SDKROOT"] will KeyError if the environment isn’t set correctly, leading to a cryptic crash.

Apply this diff:

-        self.py: Path = (
-            Path(os.environ["SDKROOT"]) / "python3-wasm"
-            if wasm
-            else Path(sys.executable)
-        )
+        if wasm:
+            sdkroot = os.environ.get("SDKROOT")
+            if not sdkroot:
+                pprint("SDKROOT is not set but WASM was detected. Set SDKROOT to your python-wasm SDK root.", Colors.RED)
+                sys.exit(1)
+            self.py = Path(sdkroot) / "python3-wasm"
+        else:
+            self.py = Path(sys.executable)

516-522: PATH injection: skip non-existent dirs and preserve order

Prepending blindly may add junk paths on custom layouts. Also, prefer emscripten upstream dir before devices bin.

Apply this diff:

-        if wasm:
-            for wasm_path in [
-                self.py.parent / "devices" / "emsdk" / "usr" / "bin",
-                self.py.parent / "emsdk" / "upstream" / "emscripten",
-            ]:
-                os.environ["PATH"] = f"{wasm_path}{os.pathsep}{os.environ['PATH']}"
+        if wasm:
+            for wasm_path in [
+                self.py.parent / "emsdk" / "upstream" / "emscripten",
+                self.py.parent / "devices" / "emsdk" / "usr" / "bin",
+            ]:
+                if wasm_path.exists():
+                    os.environ["PATH"] = f"{wasm_path}{os.pathsep}{os.environ['PATH']}"

535-538: Skip pip version check/upgrade on WASM

In SDK environments this can be slow or fail; since you don’t install deps on WASM, you can skip the pip check/upgrade too.

Apply this diff:

-        pprint("Checking pip version")
-        pip_v = cmd_run([self.py, "-m", "pip", "-V"], capture_output=True)
+        if not wasm:
+            pprint("Checking pip version")
+            pip_v = cmd_run([self.py, "-m", "pip", "-V"], capture_output=True)

And guard the subsequent pip upgrade block under if not wasm:.

src_c/meson.build (3)

1-22: Emscripten static lib path: good direction; confirm .pyx in static_library works across Meson versions

Building a single static pygame is correct for pygbag. Two follow-ups:

  • Meson cython support with .pyx inside static_library() varies by Meson/Cython versions; please confirm CI uses a Meson that treats cython as a first-class language for non-py.extension_module targets.
  • Consider moving 'pgcompat.c' out of cython_files to base_files to keep typed lists clean.

Proposed tidy:

-base_files = ['base.c', 'bitmask.c', 'SDL_gfx/SDL_gfxPrimitives.c']
-cython_files = [
+base_files = ['base.c', 'bitmask.c', 'SDL_gfx/SDL_gfxPrimitives.c', 'pgcompat.c']
+cython_files = [
     'cython/pygame/_sdl2/audio.pyx',
     'cython/pygame/_sdl2/mixer.pyx',
     'cython/pygame/_sdl2/sdl2.pyx',
     'cython/pygame/_sdl2/video.pyx',
-    'pgcompat.c'
 ]

12-21: Optional deps: tie inclusion to options to avoid link-time surprises

You always add sdl_image_dep, sdl_mixer_dep, sdl_ttf_dep, freetype_dep. When options are disabled or libs absent, some toolchains produce empty/“disabler” deps that still end up in the list. Safer to filter by get_option(...) or *_dep.found().

Apply this diff:

-    dependencies: pg_base_deps + [sdl_image_dep, sdl_mixer_dep, sdl_ttf_dep, freetype_dep],
+    dependencies: pg_base_deps
+        + (get_option('image') ? [sdl_image_dep] : [])
+        + (get_option('mixer') ? [sdl_mixer_dep] : [])
+        + (get_option('font') ? [sdl_ttf_dep, freetype_dep] : []),

23-24: Style: place comment on its own line

Meson allows trailing comments, but else # regular build is brittle in diffs. Non-blocking nit.

-else  # regular build
+else
+  # regular build
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9a6e7fc and febe9a1.

📒 Files selected for processing (6)
  • .github/workflows/build-emsdk.yml (1 hunks)
  • buildconfig/meson-cross-emscripten.ini (1 hunks)
  • dev.py (7 hunks)
  • meson.build (6 hunks)
  • meson_options.txt (1 hunks)
  • src_c/meson.build (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • meson_options.txt
  • buildconfig/meson-cross-emscripten.ini
  • .github/workflows/build-emsdk.yml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: arm64
  • GitHub Check: x86_64
  • GitHub Check: i686
  • GitHub Check: x86_64
  • GitHub Check: aarch64
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.13.5)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.14.0rc1)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.9.23)
  • GitHub Check: x86
  • GitHub Check: AMD64
  • GitHub Check: Debian (Bookworm - 12) [armv6]
  • GitHub Check: Debian (Bookworm - 12) [armv7]
  • GitHub Check: Debian (Bookworm - 12) [s390x]
  • GitHub Check: Debian (Bookworm - 12) [ppc64le]
🔇 Additional comments (8)
dev.py (2)

253-261: Nice: clear error for editable installs on WASM

Good user-facing guard; avoids a confusing partial install.


288-290: Plumb WASM args only when set

This is safe now, but after guarding wasm_args above, ensure we only extend when non-empty.

Suggested tiny tweak:

-        if wasm:
+        if wasm_args:
             install_args.extend(wasm_args)
src_c/meson.build (1)

480-481: LGTM: explicit endif comment

Helpful for long files.

meson.build (5)

36-38: Good: make emscripten build selectable via option

plat = 'emscripten-@0@' sets the ground for multiple flavors. LGTM.


79-80: Mixer variant name for pygbag: confirm library name

Using SDL2_mixer_ogg is reasonable for pygbag SDKs; ensure the PC/lib name matches the shipped artifact to avoid fallback failures.

If the actual lib is SDL2_mixer with built-in OGG, consider using a cross property or option to switch names rather than hard-coding.


255-262: Nice: include wasm-specific lib dirs

Including lib/wasm32-emscripten and /pic improves resolution across SDK variants. LGTM.


323-333: Good: skip portmidi resolution on emscripten

Avoids unnecessary lookups and simplifies the wasm build.


448-464: Docs/tests/stubs gating for emscripten is appropriate

Prevents cross-environment build steps from running where they can’t execute. LGTM.

@WebReflection
Copy link

PyScript maintainer here, we were surprised by latest Pyodide inability to use pygame-ce so we do hope this can be merged sooner than later and see this wonderful package usable again as our users got confused (as much as ourselves) too, thank you!

@ankith26 ankith26 force-pushed the ankith26-meson-wasm branch 4 times, most recently from 08a7efb to bb249a2 Compare September 25, 2025 16:27
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (6)
buildconfig/meson-cross-emscripten.ini (1)

1-12: Add cross properties for more robust Meson behavior

Explicit properties improve portability and avoid Meson trying to run build-time code with the target. Consider:

  • needs_exe_wrapper = true (explicit)
  • cmake, strip fallbacks (optional)
 [host_machine]
 system = 'emscripten'
 cpu_family = 'wasm32'
 cpu = 'wasm'
 endian = 'little'
 
+[properties]
+needs_exe_wrapper = true
+
 [binaries]
 c = 'emcc'
 cpp = 'em++'
 ar = 'emar'
 strip = 'emstrip'
 exe_wrapper = 'node'

Do you want to also expose an sdkroot property here (to be consumed by meson.build) if you need to reference SDK paths in future?

meson.build (1)

37-37: Guard SDL API on emscripten

Emscripten ports are SDL2-first. Building with -Dsdl_api=3 likely breaks. Add a fast-fail until SDL3 support is verified.

 elif host_machine.system() == 'emscripten'
     plat = 'emscripten-@0@'.format(get_option('emscripten_type'))
+    if get_option('sdl_api') != 2
+        error('Emscripten builds currently require -Dsdl_api=2')
+    endif

If SDL3 on Emscripten is intended, please point to the tested Emscripten ports settings and I’ll adapt the guards accordingly.

pyproject.toml (1)

100-106: Pyodide block: good start; add minimal smoke test or skip rationale

An empty test-command can mask regressions. If tests aren’t feasible yet, add a short smoke (e.g., import pygame; print(version)) or document intentional skip.

Example:

-[tool.cibuildwheel.pyodide]
-build = "cp313-*"  # build only for the latest python version.
-# taken from pyodide recipe at https://github.com/pyodide/pyodide-recipes/blob/main/packages/pygame-ce/meta.yaml
-before-build = "embuilder build sdl2 sdl2_ttf sdl2_image sdl2_mixer sdl2_gfx libjpeg libpng giflib harfbuzz vorbis mpg123 libmodplug freetype libhtml5 --pic"
-test-command = ""  # TODO: fix runtime issues and then figure out how to test
+[tool.cibuildwheel.pyodide]
+build = "cp313-*"
+# taken from pyodide recipe at https://github.com/pyodide/pyodide-recipes/blob/main/packages/pygame-ce/meta.yaml
+before-build = "embuilder build sdl2 sdl2_ttf sdl2_image sdl2_mixer sdl2_gfx libjpeg libpng giflib harfbuzz vorbis mpg123 libmodplug freetype libhtml5 --pic"
+# Minimal smoke test; replace with real tests when possible.
+test-command = "python - <<'PY'\nimport pygame, sys; print(pygame.__version__); sys.exit(0)\nPY"

Confirm cibuildwheel version in CI supports the pyodide platform block you’re using.

dev.py (3)

39-51: Build WASM args lazily to avoid invalid cross-file when not on WASM

Avoid computing a bogus meson-cross-.ini path when wasm is false; initialize only when needed.

Apply:

-# we will assume dev.py wasm builds are made for pygbag.
-host_gnu_type = sysconfig.get_config_var("HOST_GNU_TYPE")
-if isinstance(host_gnu_type, str) and "wasm" in host_gnu_type:
-    wasm = "wasi" if "wasi" in host_gnu_type else "emscripten"
-else:
-    wasm = ""
-
-wasm_cross_file = (source_tree / "buildconfig" / f"meson-cross-{wasm}.ini").resolve()
-wasm_args = [
-    f"-Csetup-args=--cross-file={wasm_cross_file}",
-    "-Csetup-args=-Demscripten_type=pygbag",
-]
+# we will assume dev.py wasm builds are made for pygbag.
+wasm: str = ""
+host_gnu_type = sysconfig.get_config_var("HOST_GNU_TYPE")
+if isinstance(host_gnu_type, str) and "wasm" in host_gnu_type:
+    wasm = "wasi" if "wasi" in host_gnu_type else "emscripten"
+
+wasm_cross_file: Path | None = None
+wasm_args: list[str] = []
+if wasm:
+    wasm_cross_file = (source_tree / "buildconfig" / f"meson-cross-{wasm}.ini").resolve()
+    wasm_args = [
+        f"-Csetup-args=--cross-file={wasm_cross_file}",
+        "-Csetup-args=-Demscripten_type=pygbag",
+    ]

206-211: Avoid hard dependency on SDKROOT; use current interpreter path

Relying on SDKROOT can KeyError. Using sys.executable is robust (and equals python3-wasm in CI).

-        self.py: Path = (
-            Path(os.environ["SDKROOT"]) / "python3-wasm"
-            if wasm
-            else Path(sys.executable)
-        )
+        self.py: Path = Path(sys.executable)

516-522: Add existence checks before PATH injection

Guard against non-existent directories to keep PATH clean.

-        if wasm:
-            for wasm_path in [
-                self.py.parent / "emsdk" / "upstream" / "emscripten",
-                self.py.parent / "devices" / "emsdk" / "usr" / "bin",
-            ]:
-                os.environ["PATH"] = f"{wasm_path}{os.pathsep}{os.environ['PATH']}"
+        if wasm:
+            for wasm_path in [
+                self.py.parent / "emsdk" / "upstream" / "emscripten",
+                self.py.parent / "devices" / "emsdk" / "usr" / "bin",
+            ]:
+                if wasm_path.exists():
+                    os.environ["PATH"] = f"{wasm_path}{os.pathsep}{os.environ['PATH']}"
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between febe9a1 and bb249a2.

⛔ Files ignored due to path filters (2)
  • src_c/freetype/ft_freetype.h is excluded by !src_c/freetype/*
  • src_c/freetype/ft_wrap.h is excluded by !src_c/freetype/*
📒 Files selected for processing (10)
  • .github/workflows/build-emsdk.yml (1 hunks)
  • buildconfig/meson-cross-emscripten.ini (1 hunks)
  • dev.py (7 hunks)
  • meson.build (4 hunks)
  • meson_options.txt (1 hunks)
  • pyproject.toml (1 hunks)
  • src_c/_freetype.c (1 hunks)
  • src_c/meson.build (4 hunks)
  • src_c/mixer.c (1 hunks)
  • src_py/__init__.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src_c/_freetype.c
  • src_c/mixer.c
  • meson_options.txt
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-01T20:18:57.500Z
Learnt from: Starbuck5
PR: pygame-community/pygame-ce#3573
File: src_c/_pygame.h:108-133
Timestamp: 2025-09-01T20:18:57.500Z
Learning: In pygame-ce PR #3573, PG_SURF_BitsPerPixel was changed from a simple macro to a static inline function to handle SDL2/SDL3 differences. SDL2's BitsPerPixel includes padding bits (e.g., RGBX => 32) while SDL3's SDL_BITSPERPIXEL excludes padding (e.g., RGBX => 24). The new implementation bridges this gap rather than removing the helper entirely.

Applied to files:

  • src_c/meson.build
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: x86_64
  • GitHub Check: arm64
  • GitHub Check: msys2 (ucrt64, ucrt-x86_64)
  • GitHub Check: msys2 (clang64, clang-x86_64)
  • GitHub Check: msys2 (mingw64, x86_64)
  • GitHub Check: x86
  • GitHub Check: AMD64
  • GitHub Check: i686
  • GitHub Check: x86_64
  • GitHub Check: aarch64
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.14.0rc1)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.13.5)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.9.23)
  • GitHub Check: Debian (Bookworm - 12) [ppc64le]
  • GitHub Check: Debian (Bookworm - 12) [armv7]
  • GitHub Check: Debian (Bookworm - 12) [s390x]
  • GitHub Check: Debian (Bookworm - 12) [armv6]
  • GitHub Check: build (ubuntu-24.04)
  • GitHub Check: Pyodide build
  • GitHub Check: dev-check
🔇 Additional comments (12)
meson.build (2)

458-474: Nice: skip docs/tests on emscripten builds

Avoids long-running steps and native execution requirements under cross builds.


37-37: Option emscripten_type is defined — meson_options.txt declares this combo (choices ['pygbag','generic'], default 'generic'), so get_option('emscripten_type') is valid.

src_c/meson.build (2)

23-25: Keep else branch intact

No changes needed here; regular build path remains consistent.


354-385: Good: exclude camera on emscripten

Avoids platform-only code under WASM.

src_py/__init__.py (1)

118-131: Safer WASM guard looks good

Automated symbol and code scans didn’t locate import_cython; manually verify that your WASM static lib’s pygame.base actually exports import_cython.

.github/workflows/build-emsdk.yml (3)

54-54: LGTM: unified build entrypoint

Switching to ${SDKROOT}/python3-wasm dev.py build --wheel keeps the build logic single-sourced in dev.py.


63-76: Pyodide cibuildwheel configuration verified pyproject.toml includes a [tool.cibuildwheel.pyodide] section and the workflow uploads wheelhouse/*.whl.


39-42: Approve SDK bump

Asset is available (HTTP 200).

dev.py (4)

14-14: LGTM: sysconfig import

Needed for HOST_GNU_TYPE detection.


254-261: Good guard: block editable installs on WASM

Clear error path; avoids an unsupported mode.


288-290: LGTM: meson cross and emscripten args inclusion

Only applied under WASM; safe for native builds.


535-538: LGTM: skip Python deps on WASM

Prevents accidental native dependency resolution.

@ankith26
Copy link
Member Author

If things work out I would also like to get this in by our next release 2.5.6 which should be happening soon. My goal for this PR is to lay down the buildconfig support so that both pygbag and pyodide can benefit from this.

I just updated this PR and got compilation to work with pyodide locally. I am facing some runtime issues with the generated wheels on pyodide, and I don't know much about emscripten/pyodide to fix those errors. The current error is pygame.error: The video driver did not add any displays when pygame.display.set_mode runs as part of a simple test script.

I have also started work on adding this to our CI though there is some compilation error I don't see locally: wasm-ld: error: unable to find library -lfreetype-legacysjlj

@ryanking13 hello! I believe you maintain pygame-ce for pyodide. I would appreciate your help with moving this PR ahead.

@ankith26
Copy link
Member Author

Some more explanations about the changes in this PR:

All dev.py changes are for handling builds with pmp-p's pygame-web/python-wasm-sdk (pygbag), these changes should be irrelevant to the pyodide side of things. While for pygbag we do a static build of all our sources to generate one single static library, I'm thinking we should go with our usual shared library build strategy that we do on every other platform for pyodide.

@pmp-p
Copy link
Member

pmp-p commented Sep 26, 2025

Actually pygbag since 0.9.0 uses a shared library in a wheel, but the intermediate fully statically linked is important in CI to avoid any runtime link errors that were quite common in emscripten early days when enabling various size optimisations / DCE.

It is quite easy to make a shared lib from the full static one without recompiling anything.

Afaik the major difference beetween python-wasm / pyodide wheels is that pygbag link SDL2/3 and OpenGL/ES into the pygame dynamic library directly and force JS glue code of pymain to embed full support for SDL/GL/GLES/OpenAL/GLFW calls without any sort of code elimination. Pyodide main must be quite different since it cannot load eg panda3d / raylib wheels without missing some symbols.

@ankith26 I will test that PR coming week.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bb249a2 and b57cb89.

📒 Files selected for processing (1)
  • pyproject.toml (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: msys2 (ucrt64, ucrt-x86_64)
  • GitHub Check: msys2 (mingw64, x86_64)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.14.0rc1)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.13.5)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.9.23)
  • GitHub Check: build (macos-14)
  • GitHub Check: AMD64
  • GitHub Check: x86
  • GitHub Check: build (windows-latest)
  • GitHub Check: build (ubuntu-24.04)
  • GitHub Check: Debian (Bookworm - 12) [armv7]
  • GitHub Check: Debian (Bookworm - 12) [ppc64le]
  • GitHub Check: Debian (Bookworm - 12) [s390x]
  • GitHub Check: Pyodide build
  • GitHub Check: Debian (Bookworm - 12) [armv6]
  • GitHub Check: dev-check
  • GitHub Check: aarch64
  • GitHub Check: i686
  • GitHub Check: x86_64
  • GitHub Check: build (ubuntu-22.04)

@ryanking13
Copy link

Currently the pyodide wheel in the CI artifact has dynamic linking issue.

>>> pygame.init()
Pyodide has suffered a fatal error. Please report this to the Pyodide mai
ntainers.
The cause of the fatal error was:
Error: Dynamic linking error: cannot resolve symbol FT_Init_FreeType

The font.cpython-313-wasm32-emscripten.so requires libfreetype, but it doesn't seem linking the libfreetype static library during the linking step.

@ryanking13
Copy link

The same issue exists in the mixer.cpython-313-wasm32-emscripten.so and mixer_music.cpython-313-wasm32-emscripten.so. It requires SDL_mixer but it doesn't seem linked.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
meson_options.txt (1)

44-45: Consider documenting the emscripten_type options.

While the option is well-structured, consider adding a comment above it explaining the difference between 'pygbag' and 'generic' variants to help maintainers understand when to use each.

Add documentation like this:

+# Specify the type of emscripten build being done.
+# 'pygbag': For pygame-web/python-wasm-sdk builds (uses static linking)
+# 'generic': For generic emscripten/pyodide builds (uses WASM exceptions)
 option('emscripten_type', type: 'combo', choices: ['pygbag', 'generic'], value: 'generic')
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b57cb89 and 5e2eb3a.

📒 Files selected for processing (8)
  • .github/workflows/build-emsdk.yml (1 hunks)
  • dev.py (6 hunks)
  • meson.build (4 hunks)
  • meson_options.txt (1 hunks)
  • pyproject.toml (1 hunks)
  • src_c/meson.build (4 hunks)
  • src_c/mixer.c (1 hunks)
  • src_py/__init__.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src_c/mixer.c
  • pyproject.toml
  • src_c/meson.build
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: arm64
  • GitHub Check: i686
  • GitHub Check: x86_64
  • GitHub Check: aarch64
  • GitHub Check: Pyodide build
  • GitHub Check: msys2 (mingw64, x86_64)
  • GitHub Check: dev-check
  • GitHub Check: msys2 (clang64, clang-x86_64)
  • GitHub Check: msys2 (ucrt64, ucrt-x86_64)
  • GitHub Check: build (ubuntu-22.04)
  • GitHub Check: build (ubuntu-24.04)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.9.23)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.13.5)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.14.0rc1)
  • GitHub Check: Debian (Bookworm - 12) [ppc64le]
  • GitHub Check: Debian (Bookworm - 12) [s390x]
  • GitHub Check: Debian (Bookworm - 12) [armv7]
  • GitHub Check: Debian (Bookworm - 12) [armv6]
  • GitHub Check: x86
  • GitHub Check: AMD64
🔇 Additional comments (7)
src_py/__init__.py (1)

118-118: LGTM! Proper attribute check for static builds.

The addition of hasattr(pygame_static, 'import_cython') is a good defensive check that ensures the static module has the required method before attempting to call it. This prevents potential AttributeError exceptions.

meson.build (6)

37-37: LGTM! Dynamic platform variant for emscripten.

The dynamic platform value using emscripten_type option properly enables different emscripten build configurations.


90-107: Consider extracting hardcoded paths to configuration.

The /opt/python-wasm-sdk paths are hardcoded. While this works for the current CI setup, it reduces portability for other build environments.

Based on the previous review comment, consider making these paths configurable via cross properties:

 if plat == 'emscripten-pygbag'
+    # Get SDK root from cross file, with fallback to default location
+    sdkroot = meson.get_cross_property('sdkroot', '/opt/python-wasm-sdk')
     add_global_arguments('-DBUILD_STATIC=1', language: 'c')
     sdl_dep = declare_dependency(
         link_args: ['-lSDL2'],
     )

302-358: Generic dependency fallback overwrites emscripten-specific dependencies.

These generic SDL and freetype dependency lookups will execute even for emscripten builds, potentially overwriting the carefully configured emscripten-specific dependencies defined earlier.

Wrap this entire block in a guard:

+if not plat.startswith('emscripten')
 # TODO: add version constraints?
 sdl_dep = dependency(sdl, required: false)
 if not sdl_dep.found()
     sdl_dep = declare_dependency(
         include_directories: pg_inc_dirs,
         dependencies: cc.find_library(sdl, dirs: pg_lib_dirs),
     )
 endif

 # ... rest of the dependency declarations ...

 freetype_dep = dependency('freetype2', required: false)
 if not freetype_dep.found()
     freetype_dep = declare_dependency(
         include_directories: pg_inc_dirs,
         dependencies: cc.find_library(
             'freetype',
             dirs: pg_lib_dirs,
             required: get_option('freetype'),
         ),
     )
 endif
+endif # not emscripten

362-362: LGTM! Proper emscripten exclusion for portmidi.

The condition correctly excludes emscripten platforms from portmidi dependency resolution, which is appropriate since emscripten doesn't support MIDI.


486-486: LGTM! Documentation build correctly skipped for emscripten.

The condition properly excludes emscripten platforms from documentation and test builds, which is appropriate for WASM targets.


136-136: Critical: Missing dependency declarations for non-emscripten platforms.

The else block starting here doesn't contain any dependency declarations. This means that for non-emscripten platforms, the SDL and freetype dependencies won't be properly declared before the generic fallback block at lines 302-358.

The structure should be:

-else
+endif # emscripten
 
 if plat == 'win' and host_machine.cpu_family().startswith('x86')

Then move the endif # emscripten to after line 358 to properly guard the generic fallback block.

@ankith26
Copy link
Member Author

Can you retry with the latest CI artifact? I think those linker errors should be gone now.

@ryanking13
Copy link

@ankith26

Thanks. The linking errors are gone, but now I am experiencing The video driver did not add any displays that you mentioned. This also happened when I was building pygame-ce for Pyodide for the first time. But I didn't figure out the exact reason or the situation when it happens. I basically tried some random flags and someday the error was gone.

I'll try to debug with the wheel and the code changes in this PR more.

@ryanking13
Copy link

ryanking13 commented Sep 28, 2025

So, I did some debugging, and here is what I found so far.

The problem happens in resolving the global _this variable that SDL video use:

SDL uses a static global variable _this which is initialized and used by several functions. But I noticed that some functions read this _this variable from different addresses.

I think it happens because of three different reasons combined:

  1. Static global variables have a different memory address per binary.
  2. SDL2 is built as a static library and linked to each module separately.
  3. Emscripten's GOT (global offset table) overwrites a symbol when there are multiple symbols with the same name.

Because of this, the following situation happens.


  1. Inside pygame wheel, there are controller.so and video.so (of course, there are other .so files too) that are linked to SDL and expose Emscripten_bootstrap symbol.
  2. Emscripten_bootstrap is a function that is used inside SDL2 to initialize emscripten video driver (code pointer).
  3. When the pygame wheel is loaded, because there are multiple Emscripten_bootstrap symbols exported, Emscripten selects one of them to GOT and discard others. Let's say that the one in the controller.so is stored in GOT.
  4. pygame tries to initialize the video driver in video.so, it first initializes the global _this pointer, and calls Emscripten_bootstrap to do the rest of the bootstrap.
  5. However, since Emscripten_bootstrap is from controller.so, the _this memory address is different from video.so, and it dereferences the different value.
  6. Error!

... So, how to fix? Here are some possible options that I can think of.

  1. Patching SDL

1.a. removing static from _this

- static SDL_VideoDevice *_this = NULL;
+ SDL_VideoDevice *_this = NULL;

This seems to make the memory address shared by multiple modules.

1.b. hiding probablematic symbols

[Not verified] Hiding symbols such as Emscripten_bootstrap will probably resolve the GOT overwrite issue.

  1. Build libsdl as a shared library

Building SDL as a shared library will avoid exporting symbols from multiple binaries. (probably related issue: pyodide/pyodide#5887)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
.github/workflows/build-emsdk.yml (1)

53-61: CI step looks good; consider caching the SDK archive to speed up runs

Optional: cache /opt/python-wasm-sdk by keying on $SDK_VERSION/$SDK_ARCHIVE to avoid repeated downloads.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5e2eb3a and 30a4ca0.

📒 Files selected for processing (9)
  • .github/workflows/build-emsdk.yml (1 hunks)
  • dev.py (6 hunks)
  • meson.build (4 hunks)
  • meson_options.txt (1 hunks)
  • pyproject.toml (2 hunks)
  • src_c/base.c (4 hunks)
  • src_c/meson.build (4 hunks)
  • src_c/mixer.c (2 hunks)
  • src_py/__init__.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • meson_options.txt
  • src_c/mixer.c
  • src_py/init.py
  • pyproject.toml
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-01T20:18:57.500Z
Learnt from: Starbuck5
PR: pygame-community/pygame-ce#3573
File: src_c/_pygame.h:108-133
Timestamp: 2025-09-01T20:18:57.500Z
Learning: In pygame-ce PR #3573, PG_SURF_BitsPerPixel was changed from a simple macro to a static inline function to handle SDL2/SDL3 differences. SDL2's BitsPerPixel includes padding bits (e.g., RGBX => 32) while SDL3's SDL_BITSPERPIXEL excludes padding (e.g., RGBX => 24). The new implementation bridges this gap rather than removing the helper entirely.

Applied to files:

  • src_c/meson.build
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: Debian (Bookworm - 12) [ppc64le]
  • GitHub Check: Debian (Bookworm - 12) [armv6]
  • GitHub Check: Debian (Bookworm - 12) [s390x]
  • GitHub Check: Debian (Bookworm - 12) [armv7]
  • GitHub Check: dev-check
  • GitHub Check: build (ubuntu-24.04)
  • GitHub Check: build (macos-14)
  • GitHub Check: x86
  • GitHub Check: AMD64
  • GitHub Check: aarch64
  • GitHub Check: msys2 (ucrt64, ucrt-x86_64)
  • GitHub Check: msys2 (clang64, clang-x86_64)
  • GitHub Check: msys2 (mingw64, x86_64)
  • GitHub Check: build (ubuntu-22.04)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.9.23)
  • GitHub Check: x86_64
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.14.0rc1)
  • GitHub Check: Pyodide build
  • GitHub Check: i686
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.13.5)
🔇 Additional comments (9)
dev.py (4)

288-295: Good guard: editable install is not supported on WASM

Clear error and early exit LGTM.


322-329: Nice: write cross file into build dir and pass via --cross-file

This integrates cleanly with Meson’s setup-args flow.


568-571: Early-return from dependency install on WASM

Pragmatic choice to avoid accidental host installs. LGTM.


198-234: Cross file is missing link flags and has a brittle Node path

  • Without c_link_args/cpp_link_args, emcc won’t retain SDL/FreeType ports; only include paths are passed. Add the Emscripten link flags here so linkers pull the right archives.
  • Hard-coding node/22.16.0_64bit is fragile. Resolve the first node/*/bin dynamically or fall back to plain 'node' if present in PATH.
  • Optional: expose sdkroot via cross properties for future Meson use.

Apply:

 def get_wasm_cross_file(sdkroot: Path):
     emsdk_dir = sdkroot / "emsdk"
     bin_dir = emsdk_dir / "upstream" / "emscripten"
-    node_bin_dir = emsdk_dir / "node" / "22.16.0_64bit" / "bin"
+    # pick first available node/*/bin, else fallback to 'node'
+    node_candidates = sorted((emsdk_dir / "node").glob("*/bin/node"))
+    node_path = node_candidates[0] if node_candidates else "node"

     sysroot_dir = bin_dir / "cache" / "sysroot"
@@
     lib_dirs = [lib_dir]
     c_args = [f"-I{x}" for x in inc_dirs] + [f"-L{x}" for x in lib_dirs]
+    link_args = [
+        "-sUSE_SDL=2",
+        "-sUSE_SDL_TTF=2",
+        "-sUSE_SDL_MIXER=2",
+        "-sUSE_FREETYPE=1",
+        "-lSDL2",
+        "-lhtml5",
+    ]
     return f"""
 [host_machine]
@@
 [binaries]
 c = {str(bin_dir / 'emcc')!r}
 cpp = {str(bin_dir / 'em++')!r}
 ar = {str(bin_dir / 'emar')!r}
 strip = {str(bin_dir / 'emstrip')!r}
-exe_wrapper = {str(node_bin_dir / 'node')!r}
+exe_wrapper = {str(node_path)!r}
 
 [project options]
 emscripten_type = 'pygbag'
 
 [built-in options]
 c_args = {c_args!r}
+c_link_args = {link_args!r}
+cpp_link_args = {link_args!r}
+
+[properties]
+sdkroot = {str(sdkroot)!r}
 """
src_c/meson.build (1)

3-9: Skip pygame._sdl2 Cython modules when building against SDL3

Only include the _sdl2 cython modules if sdl_api != 3 to avoid mismatches under SDL3.

-cython_files = [
-    'cython/pygame/_sdl2/audio.pyx',
-    'cython/pygame/_sdl2/mixer.pyx',
-    'cython/pygame/_sdl2/sdl2.pyx',
-    'cython/pygame/_sdl2/video.pyx',
-]
+cython_files = ['pgcompat.c']
+if sdl_api != 3
+  cython_files += [
+    'cython/pygame/_sdl2/audio.pyx',
+    'cython/pygame/_sdl2/mixer.pyx',
+    'cython/pygame/_sdl2/sdl2.pyx',
+    'cython/pygame/_sdl2/video.pyx',
+  ]
+endif
meson.build (2)

300-356: Guard the generic SDL/FreeType fallback to avoid clobbering Emscripten settings

Currently this block runs for emscripten too and can override the emscripten-specific declare_dependency you set above.

-# TODO: add version constraints?
-sdl_dep = dependency(sdl, required: false)
+if not plat.startswith('emscripten')
+  # TODO: add version constraints?
+  sdl_dep = dependency(sdl, required: false)
   if not sdl_dep.found()
       sdl_dep = declare_dependency(
           include_directories: pg_inc_dirs,
           dependencies: cc.find_library(sdl, dirs: pg_lib_dirs),
       )
   endif
@@
-endif
+endif # not emscripten

Repeat the same wrap for sdl_image_dep, sdl_mixer_dep, sdl_ttf_dep, and freetype_dep in this block.


485-501: Docs/tests install correctly skipped on Emscripten

This avoids unnecessary work and reduces wheel size. LGTM.

.github/workflows/build-emsdk.yml (1)

63-76: Pyodide job: minimal and fine

Using cibuildwheel with CIBW_PLATFORM=pyodide is aligned with the new meson path. LGTM.

src_c/base.c (1)

2423-2468: Verify new PyInit symbols and module names

  • Confirm that mixer.c defines PyInit_pg_mixer and that importing pygame.mixer still resolves correctly.
  • Confirm that the SDL2 Cython extensions export PyInit_audio and PyInit_video matching their module filenames.

@ankith26
Copy link
Member Author

@ryanking13

Thanks for investigating and finding out the root cause of that issue. Well, I suppose the easiest solution is to go back to doing a single (shared library) build on our side. That way there is only one _this in the globals. I implemented that in our meson code and now finally its working!

I tested examples.stars and examples.aliens and both worked as intended (for aliens I had to tweak some resource loading code, in this PR the examples directory is no longer bundled into the wheel to save space and therefore reduce loading time for users)

I suppose the only thing left to do here is to fix the png loading issue. For now libpng is skipped: when I tried adding it there were some issues related to its setjmp/longjmp usage. This PR currently links against -lSDL2_image-bmp-gif-jpg-qoi-svg-tga but I figured I needed it to link against the -wasm-sjlj variant of sdl2_image. I got stuck on how to install this with embuilder, do you know how to do this?

@ryanking13
Copy link

Thanks for investigating and finding out the root cause of that issue. Well, I suppose the easiest solution is to go back to doing a single (shared library) build on our side. That way there is only one _this in the globals. I implemented that in our meson code and now finally its working!

Yes, that's also fine as there will not be the same symbols exported from multiplie modules. Thanks for handling it!

in this PR the examples directory is no longer bundled into the wheel to save space and therefore reduce loading time for users)

That's awesome. In pyodide, we've been removing the docs directory as well to reduce the size further.

@ryanking13
Copy link

I suppose the only thing left to do here is to fix the png loading issue. For now libpng is skipped: when I tried adding it there were some issues related to its setjmp/longjmp usage. This PR currently links against -lSDL2_image-bmp-gif-jpg-qoi-svg-tga but I figured I needed it to link against the -wasm-sjlj variant of sdl2_image. I got stuck on how to install this with embuilder, do you know how to do this?

Passing -sSUPPORT_LONGJMP=wasm when building sdl2_image with embuilder would work I think. The resulting library would be something like -lSDL2_image-bmp-gif-jpg-qoi-svg-tga-wasm-sjlj

@ankith26
Copy link
Member Author

ankith26 commented Sep 29, 2025

But embuilder doesn't accept -sSUPPORT_LONGJMP as an argument. It throws embuilder.py: error: unrecognized arguments: -sSUPPORT_LONGJMP=wasm. The port file does however check for settings.SUPPORT_LONGJMP attribute but there is no documentation on how to modify this attribute from embuilder commandline or any env var.

EDIT: I got a fix for it

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
dev.py (1)

198-238: Missing critical Emscripten link flags in the cross-file.

The cross-file only sets include paths via c_args and library search paths via c_link_args. However, it's missing the essential -sUSE_* Emscripten flags that instruct emcc to pull in the SDL/FreeType static ports. Without these, the linker will drop all SDL/FreeType symbols, causing import-time failures (as reported in PR comments).

Apply this diff to add the required link flags:

     c_args = [
         f"-I{x}"
         for x in [
             inc_dir / "SDL2",
             inc_dir / "freetype2",
             sdkroot / "devices" / "emsdk" / "usr" / "include" / "SDL2",
         ]
     ]
-    c_link_args = [f"-L{lib_dir}"]
+    c_link_args = [
+        f"-L{lib_dir}",
+        "-sUSE_SDL=2",
+        "-sUSE_FREETYPE=1",
+        "-sUSE_SDL_MIXER=2",
+        "-sUSE_SDL_TTF=2",
+    ]
     return f"""

Based on past review comments.

src_c/meson.build (1)

1-19: Emscripten single-build consolidation looks correct, but missing SDL3 guard.

The condition plat.startswith('emscripten') correctly covers both pygbag and pyodide variants. The single shared extension module approach aligns with the PR's goal to resolve SDL global state issues by consolidating all C extensions into one module.

However, the cython_files list includes SDL2-specific modules (_sdl2/*.pyx) without checking if sdl_api == 3. According to past review comments and the regular build path (lines 322-349), SDL2 modules should be skipped when building for SDL3.

Apply this diff to add the SDL3 guard:

 base_files = ['base.c', 'bitmask.c', 'rotozoom.c', 'SDL_gfx/SDL_gfxPrimitives.c']
-cython_files = [
-    'cython/pygame/_sdl2/audio.pyx',
-    'cython/pygame/_sdl2/mixer.pyx',
-    'cython/pygame/_sdl2/sdl2.pyx',
-    'cython/pygame/_sdl2/video.pyx',
-]
+cython_files = []
+if sdl_api != 3
+    cython_files += [
+        'cython/pygame/_sdl2/audio.pyx',
+        'cython/pygame/_sdl2/mixer.pyx',
+        'cython/pygame/_sdl2/sdl2.pyx',
+        'cython/pygame/_sdl2/video.pyx',
+    ]
+endif

Based on past review comments.

🧹 Nitpick comments (3)
dev.py (1)

202-203: Use a glob pattern for the node path to handle SDK version changes.

The current code assumes a single node version directory exists. As noted by pmp-p in past comments, emsdk may use different custom node builds for each SDK version.

The current sorted(...)[0] approach works but could fail silently if multiple node versions exist. Consider adding a fallback or validation:

     node_matches = sorted(emsdk_dir.glob("node/*/bin/node"))
-    node_path = node_matches[0] if node_matches else Path("node")
+    node_path = node_matches[-1] if node_matches else Path("node")  # use latest

Based on past review comments.

meson.build (1)

107-143: Pyodide dependencies include critical flags but use SDK-specific library names.

The pyodide variant correctly enables wasm exceptions and longjmp support (line 110), and uses -sUSE_SDL=2 and -sUSE_FREETYPE=1 flags to pull in Emscripten ports. However, line 124 uses -lpng-legacysjlj and line 138 uses -lfreetype-legacysjlj, which are SDK-specific library names that may not be portable across different pyodide/emscripten SDK versions.

As noted in past review comments, prefer the standard -lpng and -lfreetype names, relying on the -sUSE_FREETYPE=1 flag to handle the correct variant:

     sdl_image_dep = declare_dependency(
         link_args: [
             '-lSDL2_image-bmp-gif-jpg-lbm-pcx-png-pnm-qoi-svg-tga-xcf-xpm-xv-wasm-sjlj',
             '-ljpeg',
-            '-lpng-legacysjlj',
+            '-lpng',
         ],
     )
     freetype_dep = declare_dependency(
         compile_args: freetype_flags,
-        link_args: freetype_flags + ['-lfreetype-legacysjlj', '-lharfbuzz']
+        link_args: freetype_flags + ['-lfreetype', '-lharfbuzz']
     )

Based on past review comments.

.github/workflows/build-emsdk.yml (1)

35-42: Remove unused PYBUILD environment variable
The PYBUILD env var isn’t referenced anywhere in the build scripts or config and should be removed.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 30a4ca0 and 5c6d82d.

📒 Files selected for processing (6)
  • .github/workflows/build-emsdk.yml (1 hunks)
  • dev.py (6 hunks)
  • meson.build (4 hunks)
  • meson_options.txt (1 hunks)
  • pyproject.toml (2 hunks)
  • src_c/meson.build (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • pyproject.toml
  • meson_options.txt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: x86_64
  • GitHub Check: AMD64
  • GitHub Check: Debian (Bookworm - 12) [armv7]
  • GitHub Check: Debian (Bookworm - 12) [s390x]
  • GitHub Check: Debian (Bookworm - 12) [armv6]
  • GitHub Check: Debian (Bookworm - 12) [ppc64le]
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.14.0rc1)
  • GitHub Check: dev-check
  • GitHub Check: build (windows-latest)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.9.23)
  • GitHub Check: build (macos-14)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.13.5)
  • GitHub Check: build (ubuntu-24.04)
  • GitHub Check: msys2 (ucrt64, ucrt-x86_64)
  • GitHub Check: msys2 (mingw64, x86_64)
  • GitHub Check: msys2 (clang64, clang-x86_64)
  • GitHub Check: x86_64
  • GitHub Check: i686
  • GitHub Check: aarch64
  • GitHub Check: build (ubuntu-22.04)
🔇 Additional comments (13)
dev.py (5)

39-44: WASM detection looks correct.

The logic correctly detects both emscripten and wasi variants from HOST_GNU_TYPE and provides a fallback to an empty string for non-WASM builds.


243-247: Python path switching logic is sound.

When wasm is detected, the script correctly switches to the WASM-wrapped Python interpreter from SDKROOT. Otherwise, it uses the system Python.


293-299: Appropriate guard for editable WASM builds.

Editable installs are not currently supported on WASM, and the early exit with a clear error message guides users to use --wheel instead.


327-333: Cross-file generation and usage in build is correct.

The build directory is created, the cross-file is written, and the --cross-file argument is correctly passed to Meson via setup-args.


573-575: Early return for WASM dependency installation is appropriate.

Since WASM builds use pre-built SDK dependencies, skipping the normal pip dependency installation is correct.

src_c/meson.build (1)

21-478: Regular build structure is preserved correctly.

The explicit else branch and closing endif # regular build comment clearly separate the emscripten and regular build paths, maintaining all existing per-module builds for non-emscripten targets.

meson.build (5)

37-37: Dynamic platform assignment for emscripten is correct.

The plat variable correctly incorporates the emscripten_type option (pygbag/pyodide) to create distinct platform identifiers (emscripten-pygbag, emscripten-pyodide), enabling variant-specific logic downstream.


144-367: Non-emscripten fallback block is correctly guarded.

The else branch (line 144) and corresponding endif # emscripten (line 367) properly isolate the generic SDL/FreeType dependency resolution logic from the emscripten-specific declarations, preventing the overwrites flagged in past reviews.


370-370: Portmidi guard for emscripten is correct.

Adding and not plat.startswith('emscripten') ensures portmidi dependencies are not declared on emscripten builds, which don't support MIDI.


494-494: Docs and stripped build guard for emscripten is appropriate.

Skipping documentation and example installation on emscripten builds reduces wheel size and avoids unnecessary overhead, aligning with the stripped build strategy.


90-106: Please set SDKROOT and verify pygbag SDK libraries
Re-run the library-check script with SDKROOT pointing to your pygbag SDK root so that $SDKROOT/emsdk/upstream/emscripten/cache/sysroot/lib/wasm32-emscripten/pic is inspected for libSDL2.a, libSDL2_image.a, libSDL2_mixer_ogg.a (or libSDL2_mixer.a), libogg.a, libvorbis.a, libfreetype.a, libharfbuzz.a, and libSDL2_ttf.a. If any are missing or named differently, adjust the link_args or add explicit -L search paths.

.github/workflows/build-emsdk.yml (2)

54-54: Build command simplification is a good improvement.

The consolidated single-command build (${SDKROOT}/python3-wasm dev.py build --wheel) simplifies the workflow by moving build logic into dev.py. This aligns with the PR's goal to streamline emscripten/pygbag support.


63-76: Confirm runner selection for Pyodide job

We’ve verified the [tool.cibuildwheel.pyodide] section exists in pyproject.toml. The only remaining question is that the Pyodide job uses runs-on: ubuntu-latest while the Pygbag job uses ubuntu-22.04. Please confirm this difference is intentional and align the runners if not.

@ankith26 ankith26 force-pushed the ankith26-meson-wasm branch 2 times, most recently from 29b9a8f to 62ed850 Compare September 30, 2025 10:42
@ankith26 ankith26 changed the title Support emscripten/pygbag in the meson buildconfig Support emscripten (pygbag + pyodide) in the meson buildconfig Sep 30, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
dev.py (1)

209-217: Add Emscripten link flags to pull in SDL and FreeType static ports

The cross file currently only provides include and library search paths but omits the -sUSE_* link flags that tell Emscripten to pull in the static SDL/FreeType ports. Without these, the linker will drop SDL/FreeType dependencies at link time, causing runtime import failures (as noted in PR comments about FT_Init_FreeType and SDL_mixer missing symbols).

Apply this diff to add the required link flags:

     c_args = [
         f"-I{x}"
         for x in [
             inc_dir / "SDL2",
             inc_dir / "freetype2",
             sdkroot / "devices" / "emsdk" / "usr" / "include" / "SDL2",
         ]
     ]
-    c_link_args = [f"-L{lib_dir}"]
+    c_link_args = [
+        f"-L{lib_dir}",
+        "-sUSE_SDL=2",
+        "-sUSE_FREETYPE=1",
+        "-sUSE_SDL_MIXER=2",
+        "-sUSE_SDL_TTF=2",
+    ]

Also add cpp_link_args = {c_link_args!r} to the returned string (line 237) to ensure C++ link steps also receive these flags.

src_c/meson.build (1)

4-9: Gate SDL2 Cython modules when building for SDL3

The cython_files list unconditionally includes _sdl2 modules, but these should be excluded when sdl_api == 3 (as done in the regular build at lines 346-349). The emscripten path currently lacks this guard.

Apply this diff to conditionally include SDL2 modules:

 base_files = ['base.c', 'bitmask.c', 'rotozoom.c', 'SDL_gfx/SDL_gfxPrimitives.c']
-cython_files = [
-    'cython/pygame/_sdl2/audio.pyx',
-    'cython/pygame/_sdl2/mixer.pyx',
-    'cython/pygame/_sdl2/sdl2.pyx',
-    'cython/pygame/_sdl2/video.pyx',
-]
+cython_files = []
+if sdl_api != 3
+    cython_files += [
+        'cython/pygame/_sdl2/audio.pyx',
+        'cython/pygame/_sdl2/mixer.pyx',
+        'cython/pygame/_sdl2/sdl2.pyx',
+        'cython/pygame/_sdl2/video.pyx',
+    ]
+endif
🧹 Nitpick comments (1)
meson.build (1)

124-124: Document or replace Emscripten-specific –legacysjlj library variants

meson.build (lines 124, 138): the flags -lpng-legacysjlj and -lfreetype-legacysjlj are toolchain-specific variants generated for legacy SJLJ exception mode. For broader portability, switch to -lpng and -lfreetype with -sSUPPORT_LONGJMP=wasm, or add a comment explaining why these specific builds are required.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c6d82d and 62ed850.

⛔ Files ignored due to path filters (2)
  • src_c/freetype/ft_freetype.h is excluded by !src_c/freetype/*
  • src_c/freetype/ft_wrap.h is excluded by !src_c/freetype/*
📒 Files selected for processing (9)
  • .github/workflows/build-emsdk.yml (1 hunks)
  • dev.py (6 hunks)
  • meson.build (4 hunks)
  • meson_options.txt (1 hunks)
  • pyproject.toml (2 hunks)
  • src_c/_freetype.c (1 hunks)
  • src_c/base.c (4 hunks)
  • src_c/meson.build (2 hunks)
  • src_c/mixer.c (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • pyproject.toml
  • .github/workflows/build-emsdk.yml
  • meson_options.txt
  • src_c/_freetype.c
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: aarch64
  • GitHub Check: x86_64
  • GitHub Check: i686
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.9.23)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.13.5)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.14.0rc1)
  • GitHub Check: AMD64
  • GitHub Check: x86
  • GitHub Check: Debian (Bookworm - 12) [ppc64le]
  • GitHub Check: Debian (Bookworm - 12) [armv7]
  • GitHub Check: Debian (Bookworm - 12) [s390x]
  • GitHub Check: Debian (Bookworm - 12) [armv6]
  • GitHub Check: Pyodide build
  • GitHub Check: dev-check
  • GitHub Check: msys2 (ucrt64, ucrt-x86_64)
  • GitHub Check: build (ubuntu-22.04)
  • GitHub Check: msys2 (clang64, clang-x86_64)
  • GitHub Check: msys2 (mingw64, x86_64)
  • GitHub Check: build (windows-latest)
  • GitHub Check: build (ubuntu-24.04)
🔇 Additional comments (14)
dev.py (4)

39-44: LGTM! Clean WASM detection logic

The WASM detection using sysconfig.HOST_GNU_TYPE is appropriate and handles both WASI and Emscripten variants correctly.


295-301: LGTM! Appropriate guard for WASM editable installs

Blocking editable installs on WASM with a clear error message is the right approach, as WASM builds require full wheel packaging.


329-335: LGTM! Clean cross-file integration

The cross-file generation and integration with Meson setup args is well-structured. The path handling correctly derives the SDK root from self.py.parent.


575-577: LGTM! Correct dependency handling for WASM

Skipping standard dependency installation for WASM is correct, as WASM builds rely on SDK-provided dependencies rather than PyPI packages.

meson.build (5)

36-37: LGTM! Clean platform determination for Emscripten variants

The platform string construction correctly incorporates the emscripten_type option to create variant-specific identifiers (emscripten-pygbag, emscripten-pyodide).


90-106: LGTM! Clean pygbag dependency declarations

The pygbag variant correctly declares minimal static library dependencies with appropriate link arguments. The dependency chain (sdl_ttf_dep → freetype_dep) is properly expressed.


144-367: LGTM! Correct guard structure for dependency fallbacks

The dependency fallback block is properly guarded within the else branch (lines 144-367), ensuring it only executes for non-Emscripten builds. The Emscripten-specific dependencies (lines 90-143) are kept separate and won't be overwritten.


370-370: LGTM! Correct portmidi guard for Emscripten

Excluding portmidi dependency discovery for Emscripten builds is appropriate, as MIDI is not supported in the browser environment.


494-494: LGTM! Appropriate docs/tests exclusion for Emscripten

Skipping docs, tests, and examples for Emscripten builds is correct. This reduces wheel size (critical for WASM) and avoids potential issues with doc generation in the WASM environment.

src_c/mixer.c (1)

2052-2057: LGTM! Conditional module initialization symbol handles static build requirements.

The conditional definition of the module initialization symbol correctly prevents name collisions in static builds. When BUILD_STATIC is defined, pg_mixer is used as the entry point; otherwise, mixer is used. This aligns with the corresponding changes in src_c/base.c (line 2425) where PyInit_pg_mixer() is declared and referenced (line 2620).

src_c/base.c (4)

2425-2425: LGTM! Entry point renamed to avoid collision in static builds.

The renaming of PyInit_mixer to PyInit_pg_mixer is consistent with the conditional initialization in src_c/mixer.c (lines 2052-2057), where pg_mixer is used as the module init symbol when BUILD_STATIC is defined.


2467-2473: LGTM! Entry points renamed for _sdl2 submodules.

The new declarations reflect the reorganization of module initialization:

  • Line 2467: PyInit_mixer now refers to _sdl2.mixer (distinct from the main pg_mixer at line 2425).
  • Line 2470: PyInit_sdl2_audio renamed to PyInit_audio for _sdl2.audio.
  • Line 2473: PyInit_sdl2_video renamed to PyInit_video for _sdl2.video.

These changes prevent symbol collisions and align with the static build requirements for emscripten/pygbag.


2552-2554: LGTM! Submodule loading calls updated with renamed entry points.

The load_submodule_mphase calls now use the renamed initialization functions:

  • Line 2552: PyInit_mixer() for _sdl2.mixer.
  • Line 2553: PyInit_audio() for _sdl2.audio.
  • Line 2554: PyInit_video() for _sdl2.video.

These are consistent with the declarations at lines 2467, 2470, and 2473.


2620-2620: LGTM! Main mixer module loading updated to use PyInit_pg_mixer.

The call to load_submodule now uses PyInit_pg_mixer(), which matches the renamed declaration at line 2425 and the conditional initialization in src_c/mixer.c (lines 2052-2057).

@ryanking13
Copy link

But embuilder doesn't accept -sSUPPORT_LONGJMP as an argument. It throws embuilder.py: error: unrecognized arguments: -sSUPPORT_LONGJMP=wasm. The port file does however check for settings.SUPPORT_LONGJMP attribute but there is no documentation on how to modify this attribute from embuilder commandline or any env var.

EDIT: I got a fix for it

Oh yeah, that's a good point. Probably it should be reported to Emscripten so we can pass those flags to embuilder.

The current patch looks okay to me. Another workaround that I use in that situation is to build a dummy source file with all the ports enabled. It will trigger building all the ports.

# create dummy c file
touch temp.c 
# build dummy file, which ports enabled.
emcc temp.c -sSUPPORT_LONGJMP="wasm" -fpic --use-port=sdl2_image

@ankith26 ankith26 force-pushed the ankith26-meson-wasm branch from 62ed850 to 5d59620 Compare October 2, 2025 12:36
@ankith26 ankith26 added this to the 2.5.6 milestone Oct 2, 2025
@ankith26
Copy link
Member Author

ankith26 commented Oct 2, 2025

My fix is also pretty hacky but I guess it'll do for now...

Anyways, I should be done with this PR from my end. I have decided to push running tests on our CI to a future PR in the interest of getting this PR merged quicker (+ I see the infra to run tests is already available on pyodide recipe repo).

@ryanking13
Copy link

I can confirm that the current Pyodide wheel works well with Pyodide 0.28.3. Thanks! There seems like a little bit of performance issue in Chrome, but I think it is a problem in Pyodide side, not Pygame, let me handle that.

Thanks for fixing the Emscripten build!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants