Skip to content

Commit e197e35

Browse files
committed
Support emscripten/pygbag in the meson buildconfig
1 parent 6ccb17c commit e197e35

File tree

6 files changed

+219
-30
lines changed

6 files changed

+219
-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: 92 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,62 @@ 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+
"""
200+
This returns a meson cross file for pygbag wasm sdk (pygame-web/python-wasm-sdk)
201+
as a string.
202+
Here we set paths to the compiler tooling and include/library paths to ensure that
203+
meson can pick up the compiler and build dependencies from the sdk.
204+
"""
205+
emsdk_dir = sdkroot / "emsdk"
206+
bin_dir = emsdk_dir / "upstream" / "emscripten"
207+
208+
node_matches = sorted(emsdk_dir.glob("node/*/bin/node"))
209+
node_path = node_matches[-1] if node_matches else Path("node")
210+
211+
sysroot_dir = bin_dir / "cache" / "sysroot"
212+
inc_dir = sysroot_dir / "include"
213+
lib_dir = sysroot_dir / "lib" / "wasm32-emscripten" / "pic"
214+
215+
c_args = [
216+
f"-I{x}"
217+
for x in [
218+
inc_dir / "SDL2",
219+
inc_dir / "freetype2",
220+
sdkroot / "devices" / "emsdk" / "usr" / "include" / "SDL2",
221+
]
222+
]
223+
c_link_args = [f"-L{lib_dir}"]
224+
return f"""
225+
[host_machine]
226+
system = 'emscripten'
227+
cpu_family = 'wasm32'
228+
cpu = 'wasm'
229+
endian = 'little'
230+
231+
[binaries]
232+
c = {str(bin_dir / 'emcc')!r}
233+
cpp = {str(bin_dir / 'em++')!r}
234+
ar = {str(bin_dir / 'emar')!r}
235+
strip = {str(bin_dir / 'emstrip')!r}
236+
exe_wrapper = {str(node_path)!r}
237+
238+
[project options]
239+
emscripten_type = 'pygbag'
240+
241+
[built-in options]
242+
c_args = {c_args!r}
243+
c_link_args = {c_link_args!r}
244+
"""
245+
246+
190247
class Dev:
191248
def __init__(self) -> None:
192-
self.py: Path = Path(sys.executable)
249+
self.py: Path = (
250+
Path(os.environ["SDKROOT"]) / "python3-wasm"
251+
if wasm
252+
else Path(sys.executable)
253+
)
193254
self.args: dict[str, Any] = {}
194255

195256
self.deps: dict[str, set[str]] = {
@@ -227,12 +288,24 @@ def cmd_build(self):
227288
build_suffix += "-sdl3"
228289
if coverage:
229290
build_suffix += "-cov"
291+
if wasm:
292+
build_suffix += "-wasm"
293+
294+
build_dir = Path(f".mesonpy-build{build_suffix}")
230295
install_args = [
231296
"--no-build-isolation",
232-
f"-Cbuild-dir=.mesonpy-build{build_suffix}",
297+
f"-Cbuild-dir={build_dir}",
233298
]
234299

235300
if not wheel_dir:
301+
if wasm:
302+
pprint(
303+
"Editable builds are not supported on WASM as of now. "
304+
"Pass --wheel to do a regular build",
305+
Colors.RED,
306+
)
307+
sys.exit(1)
308+
236309
# editable install
237310
if not quiet:
238311
install_args.append("-Ceditable-verbose=true")
@@ -259,6 +332,19 @@ def cmd_build(self):
259332
if sanitize:
260333
install_args.append(f"-Csetup-args=-Db_sanitize={sanitize}")
261334

335+
if wasm:
336+
wasm_cross_file = build_dir / "meson-cross-wasm.ini"
337+
build_dir.mkdir(exist_ok=True)
338+
wasm_cross_file.write_text(get_wasm_cross_file(self.py.parent))
339+
install_args.append(
340+
f"-Csetup-args=--cross-file={wasm_cross_file.resolve()}"
341+
)
342+
if not debug:
343+
# sdk uses this environment variable for extra compiler arguments.
344+
# So here we pass optimization flags. If this isn't set, sdk will
345+
# build for debug by default and we don't want that for release builds.
346+
os.environ["COPTS"] = "-Os -g0"
347+
262348
info_str = (
263349
f"with {debug=}, {lax=}, {sdl3=}, {stripped=}, {coverage=} and {sanitize=}"
264350
)
@@ -497,6 +583,10 @@ def prep_env(self):
497583
pprint("pip version is too old or unknown, attempting pip upgrade")
498584
pip_install(self.py, ["-U", "pip"])
499585

586+
if wasm:
587+
# dont try to install any deps on WASM, exit early
588+
return
589+
500590
deps = self.deps.get(self.args["command"], set())
501591
ignored_deps = self.args["ignore_dep"]
502592
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: 18 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,21 @@ setup-args = [
9797
"-Derror_docs_missing=true",
9898
]
9999

100+
[tool.cibuildwheel.pyodide]
101+
build = "cp313-*" # build only for the latest python version.
102+
103+
# EMSDK path is hardcoded here, and has to be manually kept updated with updates
104+
# to cibuildwheel and/or pyodide.
105+
before-build = """
106+
sed -i 's/var SUPPORT_LONGJMP *= *[^;]*;/var SUPPORT_LONGJMP = "wasm";/' \
107+
/home/runner/.cache/cibuildwheel/emsdk-4.0.9/emsdk-4.0.9/upstream/emscripten/src/settings.js &&
108+
embuilder --pic --force build \
109+
sdl2 libhtml5 sdl2_ttf 'sdl2_mixer:formats=ogg,mp3,mod,mid' \
110+
'sdl2_image:formats=bmp,gif,jpg,lbm,pcx,png,pnm,qoi,svg,tga,xcf,xpm,xv'
111+
"""
112+
test-command = "" # TODO: figure out how to test
113+
114+
100115
[tool.ruff]
101116
exclude = [
102117
"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)