Skip to content

Commit 5c6d82d

Browse files
committed
Support emscripten/pygbag in the meson buildconfig
1 parent ef5e641 commit 5c6d82d

File tree

6 files changed

+203
-30
lines changed

6 files changed

+203
-30
lines changed

.github/workflows/build-emsdk.yml

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,43 +32,45 @@ concurrency:
3232
cancel-in-progress: true
3333

3434
jobs:
35-
build:
35+
build-pygbag:
3636
runs-on: ubuntu-22.04
3737
env:
3838
# pin SDK version to the latest, update manually
39-
SDK_VERSION: 3.1.32.0
40-
SDK_ARCHIVE: python3.11-wasm-sdk-Ubuntu-22.04.tar.lz4
39+
SDK_VERSION: 3.1.61.12bi
40+
SDK_ARCHIVE: python3.13-wasm-sdk-Ubuntu-22.04.tar.lz4
4141
SDKROOT: /opt/python-wasm-sdk
42+
PYBUILD: 3.13
4243

4344
steps:
4445
- uses: actions/checkout@v5.0.0
4546

46-
- name: Regen with latest cython (using system python3)
47-
run: |
48-
pip3 install cython==3.0.10
49-
python3 setup.py cython_only
50-
5147
- name: Install python-wasm-sdk
5248
run: |
5349
sudo apt-get install lz4
54-
echo https://github.com/pygame-web/python-wasm-sdk/releases/download/$SDK_VERSION/$SDK_ARCHIVE
5550
curl -sL --retry 5 https://github.com/pygame-web/python-wasm-sdk/releases/download/$SDK_VERSION/$SDK_ARCHIVE | tar xvP --use-compress-program=lz4
56-
# do not let SDL1 interfere
57-
rm -rf /opt/python-wasm-sdk/emsdk/upstream/emscripten/cache/sysroot/include/SDL
5851
working-directory: /opt
5952

6053
- name: Build WASM with emsdk
61-
run: |
62-
${SDKROOT}/python3-wasm setup.py build -j$(nproc)
63-
64-
- name: Generate libpygame.a static binaries archive
65-
run: |
66-
mkdir -p dist
67-
SYS_PYTHON=python3 /opt/python-wasm-sdk/emsdk/upstream/emscripten/emar rcs dist/libpygame.a $(find build/temp.wasm32-*/ | grep o$)
54+
run: ${SDKROOT}/python3-wasm dev.py build --wheel
6855

6956
# Upload the generated files under github actions assets section
7057
- name: Upload dist
7158
uses: actions/upload-artifact@v4
7259
with:
7360
name: pygame-wasm-dist
7461
path: ./dist/*
62+
63+
build-pyodide:
64+
name: Pyodide build
65+
runs-on: ubuntu-latest
66+
steps:
67+
- uses: actions/checkout@v5.0.0
68+
69+
- uses: pypa/cibuildwheel@v3.1.4
70+
env:
71+
CIBW_PLATFORM: pyodide
72+
73+
- uses: actions/upload-artifact@v4
74+
with:
75+
name: pyodide-wheels
76+
path: wheelhouse/*.whl

dev.py

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import re
1212
import subprocess
1313
import sys
14+
import sysconfig
1415
from enum import Enum
1516
from pathlib import Path
1617
from typing import Any, Union
@@ -35,6 +36,13 @@
3536
# We assume this script works with any pip version above this.
3637
PIP_MIN_VERSION = "23.1"
3738

39+
# we will assume dev.py wasm builds are made for pygbag.
40+
host_gnu_type = sysconfig.get_config_var("HOST_GNU_TYPE")
41+
if isinstance(host_gnu_type, str) and "wasm" in host_gnu_type:
42+
wasm = "wasi" if "wasi" in host_gnu_type else "emscripten"
43+
else:
44+
wasm = ""
45+
3846

3947
class Colors(Enum):
4048
RESET = "\033[0m"
@@ -187,9 +195,56 @@ def check_module_in_constraint(mod: str, constraint: str):
187195
return mod.lower().strip() == constraint_mod[0]
188196

189197

198+
def get_wasm_cross_file(sdkroot: Path):
199+
emsdk_dir = sdkroot / "emsdk"
200+
bin_dir = emsdk_dir / "upstream" / "emscripten"
201+
202+
node_matches = sorted(emsdk_dir.glob("node/*/bin/node"))
203+
node_path = node_matches[0] if node_matches else Path("node")
204+
205+
sysroot_dir = bin_dir / "cache" / "sysroot"
206+
inc_dir = sysroot_dir / "include"
207+
lib_dir = sysroot_dir / "lib" / "wasm32-emscripten" / "pic"
208+
209+
c_args = [
210+
f"-I{x}"
211+
for x in [
212+
inc_dir / "SDL2",
213+
inc_dir / "freetype2",
214+
sdkroot / "devices" / "emsdk" / "usr" / "include" / "SDL2",
215+
]
216+
]
217+
c_link_args = [f"-L{lib_dir}"]
218+
return f"""
219+
[host_machine]
220+
system = 'emscripten'
221+
cpu_family = 'wasm32'
222+
cpu = 'wasm'
223+
endian = 'little'
224+
225+
[binaries]
226+
c = {str(bin_dir / 'emcc')!r}
227+
cpp = {str(bin_dir / 'em++')!r}
228+
ar = {str(bin_dir / 'emar')!r}
229+
strip = {str(bin_dir / 'emstrip')!r}
230+
exe_wrapper = {str(node_path)!r}
231+
232+
[project options]
233+
emscripten_type = 'pygbag'
234+
235+
[built-in options]
236+
c_args = {c_args!r}
237+
c_link_args = {c_link_args!r}
238+
"""
239+
240+
190241
class Dev:
191242
def __init__(self) -> None:
192-
self.py: Path = Path(sys.executable)
243+
self.py: Path = (
244+
Path(os.environ["SDKROOT"]) / "python3-wasm"
245+
if wasm
246+
else Path(sys.executable)
247+
)
193248
self.args: dict[str, Any] = {}
194249

195250
self.deps: dict[str, set[str]] = {
@@ -227,12 +282,22 @@ def cmd_build(self):
227282
build_suffix += "-sdl3"
228283
if coverage:
229284
build_suffix += "-cov"
285+
286+
build_dir = Path(f".mesonpy-build{build_suffix}")
230287
install_args = [
231288
"--no-build-isolation",
232-
f"-Cbuild-dir=.mesonpy-build{build_suffix}",
289+
f"-Cbuild-dir={build_dir}",
233290
]
234291

235292
if not wheel_dir:
293+
if wasm:
294+
pprint(
295+
"Editable builds are not supported on WASM as of now. "
296+
"Pass --wheel to do a regular build",
297+
Colors.RED,
298+
)
299+
sys.exit(1)
300+
236301
# editable install
237302
if not quiet:
238303
install_args.append("-Ceditable-verbose=true")
@@ -259,6 +324,14 @@ def cmd_build(self):
259324
if sanitize:
260325
install_args.append(f"-Csetup-args=-Db_sanitize={sanitize}")
261326

327+
if wasm:
328+
wasm_cross_file = build_dir / "meson-cross-wasm.ini"
329+
build_dir.mkdir(exist_ok=True)
330+
wasm_cross_file.write_text(get_wasm_cross_file(self.py.parent))
331+
install_args.append(
332+
f"-Csetup-args=--cross-file={wasm_cross_file.resolve()}"
333+
)
334+
262335
info_str = (
263336
f"with {debug=}, {lax=}, {sdl3=}, {stripped=}, {coverage=} and {sanitize=}"
264337
)
@@ -497,6 +570,10 @@ def prep_env(self):
497570
pprint("pip version is too old or unknown, attempting pip upgrade")
498571
pip_install(self.py, ["-U", "pip"])
499572

573+
if wasm:
574+
# dont try to install any deps on WASM, exit early
575+
return
576+
500577
deps = self.deps.get(self.args["command"], set())
501578
ignored_deps = self.args["ignore_dep"]
502579
deps_filtered = deps.copy()

meson.build

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,7 @@ elif host_machine.system() == 'android'
3434
'However it may be added in the future',
3535
)
3636
elif host_machine.system() == 'emscripten'
37-
plat = 'emscripten'
38-
error(
39-
'The meson buildconfig of pygame-ce does not support emscripten for now. ',
40-
'However it may be added in the future',
41-
)
37+
plat = 'emscripten-@0@'.format(get_option('emscripten_type'))
4238
else
4339
# here it one of: cygwin, dragonfly, freebsd, gnu, haiku, netbsd, openbsd, sunos
4440
plat = 'unix'
@@ -90,6 +86,63 @@ endif
9086

9187
pg_inc_dirs = []
9288
pg_lib_dirs = []
89+
90+
if plat == 'emscripten-pygbag'
91+
sdl_dep = declare_dependency(
92+
link_args: ['-lSDL2'],
93+
)
94+
sdl_image_dep = declare_dependency(
95+
link_args: ['-lSDL2_image'],
96+
)
97+
sdl_mixer_dep = declare_dependency(
98+
link_args: ['-lSDL2_mixer_ogg', '-logg', '-lvorbis'],
99+
)
100+
freetype_dep = declare_dependency(
101+
link_args: ['-lfreetype', '-lharfbuzz']
102+
)
103+
sdl_ttf_dep = declare_dependency(
104+
link_args: ['-lSDL2_ttf'],
105+
dependencies: [freetype_dep]
106+
)
107+
elif plat == 'emscripten-pyodide'
108+
# Check out before-build attribute in [tool.cibuildwheel.pyodide] section
109+
# of pyproject.toml to see how these dependencies were installed.
110+
wasm_exceptions = ['-fwasm-exceptions', '-sSUPPORT_LONGJMP=wasm', '-sRELOCATABLE=1']
111+
add_global_arguments(wasm_exceptions, language: 'c')
112+
add_global_link_arguments(wasm_exceptions, language: 'c')
113+
114+
sdl_flags = ['-sUSE_SDL=2']
115+
freetype_flags = ['-sUSE_FREETYPE=1']
116+
sdl_dep = declare_dependency(
117+
compile_args: sdl_flags,
118+
link_args: sdl_flags + ['-lSDL2', '-lhtml5'],
119+
)
120+
sdl_image_dep = declare_dependency(
121+
link_args: [
122+
'-lSDL2_image-bmp-gif-jpg-lbm-pcx-png-pnm-qoi-svg-tga-xcf-xpm-xv-wasm-sjlj',
123+
'-ljpeg',
124+
'-lpng-legacysjlj',
125+
],
126+
)
127+
sdl_mixer_dep = declare_dependency(
128+
link_args: [
129+
'-lSDL2_mixer-mid-mod-mp3-ogg',
130+
'-lmodplug',
131+
'-lmpg123',
132+
'-logg',
133+
'-lvorbis'
134+
],
135+
)
136+
freetype_dep = declare_dependency(
137+
compile_args: freetype_flags,
138+
link_args: freetype_flags + ['-lfreetype-legacysjlj', '-lharfbuzz']
139+
)
140+
sdl_ttf_dep = declare_dependency(
141+
link_args: ['-lSDL2_ttf'],
142+
dependencies: [freetype_dep]
143+
)
144+
else
145+
93146
if plat == 'win' and host_machine.cpu_family().startswith('x86')
94147
# yes, this is a bit ugly and hardcoded but it is what it is
95148
# TODO (middle-term goal) - Should migrate away from this
@@ -311,8 +364,10 @@ if not freetype_dep.found()
311364
)
312365
endif
313366

367+
endif # emscripten
368+
314369
portmidi_dep = dependency('portmidi', required: false)
315-
if not portmidi_dep.found()
370+
if not portmidi_dep.found() and not plat.startswith('emscripten')
316371
portmidi_dep = declare_dependency(
317372
include_directories: pg_inc_dirs,
318373
dependencies: cc.find_library(
@@ -436,7 +491,7 @@ endif
436491
subdir('src_c')
437492
subdir('src_py')
438493

439-
if not get_option('stripped')
494+
if not get_option('stripped') and not plat.startswith('emscripten')
440495
# run make_docs and make docs
441496
if not fs.is_dir('docs/generated')
442497
make_docs = files('buildconfig/make_docs.py')

meson_options.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,6 @@ option('coverage', type: 'boolean', value: false)
4040

4141
# Controls whether to use SDL3 instead of SDL2. The default is to use SDL2
4242
option('sdl_api', type: 'integer', min: 2, max: 3, value: 2)
43+
44+
# Specify the type of emscripten build being done.
45+
option('emscripten_type', type: 'combo', choices: ['pyodide', 'pygbag'])

pyproject.toml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ pygame_ce = 'pygame.__briefcase.pygame_ce:PygameCEGuiBootstrap'
5555
[build-system]
5656
requires = [
5757
"meson-python<=0.18.0",
58-
"meson<=1.8.2",
59-
"ninja<=1.12.1",
60-
"cython<=3.1.2",
58+
"meson<=1.9.1",
59+
"ninja<=1.13.0",
60+
"cython<=3.1.4",
6161
"sphinx<=8.2.3",
6262
"sphinx-autoapi<=3.6.0",
6363
"pyproject-metadata!=0.9.1",
@@ -97,6 +97,18 @@ setup-args = [
9797
"-Derror_docs_missing=true",
9898
]
9999

100+
[tool.cibuildwheel.pyodide]
101+
build = "cp313-*" # build only for the latest python version.
102+
before-build = """
103+
sed -i 's/var SUPPORT_LONGJMP *= *[^;]*;/var SUPPORT_LONGJMP = "wasm";/' \
104+
$EMSDK/upstream/emscripten/src/settings.js &&
105+
embuilder --pic --force build \
106+
sdl2 libhtml5 sdl2_ttf 'sdl2_mixer:formats=ogg,mp3,mod,mid' \
107+
'sdl2_image:formats=bmp,gif,jpg,lbm,pcx,png,pnm,qoi,svg,tga,xcf,xpm,xv'
108+
"""
109+
test-command = "" # TODO: fix runtime issues and then figure out how to test
110+
111+
100112
[tool.ruff]
101113
exclude = [
102114
"buildconfig/*.py",

src_c/meson.build

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
if plat.startswith('emscripten') # single build
2+
3+
base_files = ['base.c', 'bitmask.c', 'rotozoom.c', 'SDL_gfx/SDL_gfxPrimitives.c']
4+
cython_files = [
5+
'cython/pygame/_sdl2/audio.pyx',
6+
'cython/pygame/_sdl2/mixer.pyx',
7+
'cython/pygame/_sdl2/sdl2.pyx',
8+
'cython/pygame/_sdl2/video.pyx',
9+
]
10+
11+
# make one big shared build on emscripten
12+
pygame = py.extension_module(
13+
'base',
14+
base_files + cython_files,
15+
c_args: ['-DBUILD_STATIC=1'],
16+
dependencies: pg_base_deps + [sdl_image_dep, sdl_mixer_dep, sdl_ttf_dep, freetype_dep],
17+
install: true,
18+
subdir: pg,
19+
)
20+
21+
else # regular build
22+
123
# first the "required" modules
224

325
base = py.extension_module(
@@ -452,3 +474,5 @@ if portmidi_dep.found()
452474
subdir: pg,
453475
)
454476
endif
477+
478+
endif # regular build

0 commit comments

Comments
 (0)