1111import re
1212import subprocess
1313import sys
14+ import sysconfig
1415from enum import Enum
1516from pathlib import Path
1617from typing import Any , Union
3536# We assume this script works with any pip version above this.
3637PIP_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
3947class 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+
190247class 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 ()
0 commit comments