Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 20 additions & 18 deletions .github/workflows/build-emsdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,43 +32,45 @@ concurrency:
cancel-in-progress: true

jobs:
build:
build-pygbag:
runs-on: ubuntu-22.04
env:
# pin SDK version to the latest, update manually
SDK_VERSION: 3.1.32.0
SDK_ARCHIVE: python3.11-wasm-sdk-Ubuntu-22.04.tar.lz4
SDK_VERSION: 3.1.61.12bi
SDK_ARCHIVE: python3.13-wasm-sdk-Ubuntu-22.04.tar.lz4
SDKROOT: /opt/python-wasm-sdk
PYBUILD: 3.13

steps:
- uses: actions/checkout@v5.0.0

- name: Regen with latest cython (using system python3)
run: |
pip3 install cython==3.0.10
python3 setup.py cython_only

- name: Install python-wasm-sdk
run: |
sudo apt-get install lz4
echo https://github.com/pygame-web/python-wasm-sdk/releases/download/$SDK_VERSION/$SDK_ARCHIVE
curl -sL --retry 5 https://github.com/pygame-web/python-wasm-sdk/releases/download/$SDK_VERSION/$SDK_ARCHIVE | tar xvP --use-compress-program=lz4
# do not let SDL1 interfere
rm -rf /opt/python-wasm-sdk/emsdk/upstream/emscripten/cache/sysroot/include/SDL
working-directory: /opt

- name: Build WASM with emsdk
run: |
${SDKROOT}/python3-wasm setup.py build -j$(nproc)

- name: Generate libpygame.a static binaries archive
run: |
mkdir -p dist
SYS_PYTHON=python3 /opt/python-wasm-sdk/emsdk/upstream/emscripten/emar rcs dist/libpygame.a $(find build/temp.wasm32-*/ | grep o$)
run: ${SDKROOT}/python3-wasm dev.py build --wheel

# Upload the generated files under github actions assets section
- name: Upload dist
uses: actions/upload-artifact@v4
with:
name: pygame-wasm-dist
path: ./dist/*

build-pyodide:
name: Pyodide build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5.0.0

- uses: pypa/cibuildwheel@v3.1.4
env:
CIBW_PLATFORM: pyodide

- uses: actions/upload-artifact@v4
with:
name: pyodide-wheels
path: wheelhouse/*.whl
85 changes: 83 additions & 2 deletions dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import re
import subprocess
import sys
import sysconfig
from enum import Enum
from pathlib import Path
from typing import Any, Union
Expand All @@ -35,6 +36,13 @@
# We assume this script works with any pip version above this.
PIP_MIN_VERSION = "23.1"

# 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 = ""


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


def get_wasm_cross_file(sdkroot: Path):
emsdk_dir = sdkroot / "emsdk"
bin_dir = emsdk_dir / "upstream" / "emscripten"

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

sysroot_dir = bin_dir / "cache" / "sysroot"
inc_dir = sysroot_dir / "include"
lib_dir = sysroot_dir / "lib" / "wasm32-emscripten" / "pic"

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}"]
return f"""
[host_machine]
system = 'emscripten'
cpu_family = 'wasm32'
cpu = 'wasm'
endian = 'little'

[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_path)!r}

[project options]
emscripten_type = 'pygbag'

[built-in options]
c_args = {c_args!r}
c_link_args = {c_link_args!r}
"""


class Dev:
def __init__(self) -> None:
self.py: Path = Path(sys.executable)
self.py: Path = (
Path(os.environ["SDKROOT"]) / "python3-wasm"
if wasm
else Path(sys.executable)
)
self.args: dict[str, Any] = {}

self.deps: dict[str, set[str]] = {
Expand Down Expand Up @@ -227,12 +282,24 @@ def cmd_build(self):
build_suffix += "-sdl3"
if coverage:
build_suffix += "-cov"
if wasm:
build_suffix += "-wasm"

build_dir = Path(f".mesonpy-build{build_suffix}")
install_args = [
"--no-build-isolation",
f"-Cbuild-dir=.mesonpy-build{build_suffix}",
f"-Cbuild-dir={build_dir}",
]

if not wheel_dir:
if wasm:
pprint(
"Editable builds are not supported on WASM as of now. "
"Pass --wheel to do a regular build",
Colors.RED,
)
sys.exit(1)

# editable install
if not quiet:
install_args.append("-Ceditable-verbose=true")
Expand All @@ -259,6 +326,16 @@ def cmd_build(self):
if sanitize:
install_args.append(f"-Csetup-args=-Db_sanitize={sanitize}")

if wasm:
wasm_cross_file = build_dir / "meson-cross-wasm.ini"
build_dir.mkdir(exist_ok=True)
wasm_cross_file.write_text(get_wasm_cross_file(self.py.parent))
install_args.append(
f"-Csetup-args=--cross-file={wasm_cross_file.resolve()}"
)
if not debug:
os.environ["COPTS"] = "-Os -g0"

info_str = (
f"with {debug=}, {lax=}, {sdl3=}, {stripped=}, {coverage=} and {sanitize=}"
)
Expand Down Expand Up @@ -497,6 +574,10 @@ def prep_env(self):
pprint("pip version is too old or unknown, attempting pip upgrade")
pip_install(self.py, ["-U", "pip"])

if wasm:
# dont try to install any deps on WASM, exit early
return

deps = self.deps.get(self.args["command"], set())
ignored_deps = self.args["ignore_dep"]
deps_filtered = deps.copy()
Expand Down
69 changes: 62 additions & 7 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,7 @@ elif host_machine.system() == 'android'
'However it may be added in the future',
)
elif host_machine.system() == 'emscripten'
plat = 'emscripten'
error(
'The meson buildconfig of pygame-ce does not support emscripten for now. ',
'However it may be added in the future',
)
plat = 'emscripten-@0@'.format(get_option('emscripten_type'))
else
# here it one of: cygwin, dragonfly, freebsd, gnu, haiku, netbsd, openbsd, sunos
plat = 'unix'
Expand Down Expand Up @@ -90,6 +86,63 @@ endif

pg_inc_dirs = []
pg_lib_dirs = []

if plat == 'emscripten-pygbag'
sdl_dep = declare_dependency(
link_args: ['-lSDL2'],
)
sdl_image_dep = declare_dependency(
link_args: ['-lSDL2_image'],
)
sdl_mixer_dep = declare_dependency(
link_args: ['-lSDL2_mixer_ogg', '-logg', '-lvorbis'],
)
freetype_dep = declare_dependency(
link_args: ['-lfreetype', '-lharfbuzz']
)
sdl_ttf_dep = declare_dependency(
link_args: ['-lSDL2_ttf'],
dependencies: [freetype_dep]
)
elif plat == 'emscripten-pyodide'
# Check out before-build attribute in [tool.cibuildwheel.pyodide] section
# of pyproject.toml to see how these dependencies were installed.
wasm_exceptions = ['-fwasm-exceptions', '-sSUPPORT_LONGJMP=wasm', '-sRELOCATABLE=1']
add_global_arguments(wasm_exceptions, language: 'c')
add_global_link_arguments(wasm_exceptions, language: 'c')

sdl_flags = ['-sUSE_SDL=2']
freetype_flags = ['-sUSE_FREETYPE=1']
sdl_dep = declare_dependency(
compile_args: sdl_flags,
link_args: sdl_flags + ['-lSDL2', '-lhtml5'],
)
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',
],
)
sdl_mixer_dep = declare_dependency(
link_args: [
'-lSDL2_mixer-mid-mod-mp3-ogg',
'-lmodplug',
'-lmpg123',
'-logg',
'-lvorbis'
],
)
freetype_dep = declare_dependency(
compile_args: freetype_flags,
link_args: freetype_flags + ['-lfreetype-legacysjlj', '-lharfbuzz']
)
sdl_ttf_dep = declare_dependency(
link_args: ['-lSDL2_ttf'],
dependencies: [freetype_dep]
)
else

if plat == 'win' and host_machine.cpu_family().startswith('x86')
# yes, this is a bit ugly and hardcoded but it is what it is
# TODO (middle-term goal) - Should migrate away from this
Expand Down Expand Up @@ -311,8 +364,10 @@ if not freetype_dep.found()
)
endif

endif # emscripten

portmidi_dep = dependency('portmidi', required: false)
if not portmidi_dep.found()
if not portmidi_dep.found() and not plat.startswith('emscripten')
portmidi_dep = declare_dependency(
include_directories: pg_inc_dirs,
dependencies: cc.find_library(
Expand Down Expand Up @@ -436,7 +491,7 @@ endif
subdir('src_c')
subdir('src_py')

if not get_option('stripped')
if not get_option('stripped') and not plat.startswith('emscripten')
# run make_docs and make docs
if not fs.is_dir('docs/generated')
make_docs = files('buildconfig/make_docs.py')
Expand Down
3 changes: 3 additions & 0 deletions meson_options.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,6 @@ option('coverage', type: 'boolean', value: false)

# Controls whether to use SDL3 instead of SDL2. The default is to use SDL2
option('sdl_api', type: 'integer', min: 2, max: 3, value: 2)

# Specify the type of emscripten build being done.
option('emscripten_type', type: 'combo', choices: ['pyodide', 'pygbag'])
21 changes: 18 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ pygame_ce = 'pygame.__briefcase.pygame_ce:PygameCEGuiBootstrap'
[build-system]
requires = [
"meson-python<=0.18.0",
"meson<=1.8.2",
"ninja<=1.12.1",
"cython<=3.1.2",
"meson<=1.9.1",
"ninja<=1.13.0",
"cython<=3.1.4",
"sphinx<=8.2.3",
"sphinx-autoapi<=3.6.0",
"pyproject-metadata!=0.9.1",
Expand Down Expand Up @@ -97,6 +97,21 @@ setup-args = [
"-Derror_docs_missing=true",
]

[tool.cibuildwheel.pyodide]
build = "cp313-*" # build only for the latest python version.

# EMSDK path is hardcoded here, and has to be manually kept updated with updates
# to cibuildwheel and/or pyodide.
before-build = """
sed -i 's/var SUPPORT_LONGJMP *= *[^;]*;/var SUPPORT_LONGJMP = "wasm";/' \
/home/runner/.cache/cibuildwheel/emsdk-4.0.9/emsdk-4.0.9/upstream/emscripten/src/settings.js &&
embuilder --pic --force build \
sdl2 libhtml5 sdl2_ttf 'sdl2_mixer:formats=ogg,mp3,mod,mid' \
'sdl2_image:formats=bmp,gif,jpg,lbm,pcx,png,pnm,qoi,svg,tga,xcf,xpm,xv'
"""
test-command = "" # TODO: figure out how to test


[tool.ruff]
exclude = [
"buildconfig/*.py",
Expand Down
2 changes: 1 addition & 1 deletion src_c/_freetype.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
#define PYGAME_FREETYPE_INTERNAL
#define PYGAME_FREETYPE_FONT_INTERNAL

#include "freetype.h"
#include "freetype/ft_freetype.h"

#include "freetype/ft_wrap.h"

Expand Down
20 changes: 8 additions & 12 deletions src_c/base.c
Original file line number Diff line number Diff line change
Expand Up @@ -2422,7 +2422,7 @@ PyMODINIT_FUNC
PyInit_mixer_music(void);

PyMODINIT_FUNC
PyInit_mixer(void);
PyInit_pg_mixer(void);

PyMODINIT_FUNC
PyInit_pg_math(void);
Expand Down Expand Up @@ -2464,16 +2464,13 @@ PyMODINIT_FUNC
PyInit_sdl2(void);

PyMODINIT_FUNC
PyInit_sdl2_controller(void);

PyMODINIT_FUNC
PyInit_sdl2_mixer(void);
PyInit_mixer(void);

PyMODINIT_FUNC
PyInit_sdl2_audio(void);
PyInit_audio(void);

PyMODINIT_FUNC
PyInit_sdl2_video(void);
PyInit_video(void);

#endif

Expand Down Expand Up @@ -2552,10 +2549,9 @@ mod_pygame_import_cython(PyObject *self, PyObject *spec)
#pragma message "WARNING: pygame._sdl2.* are disabled"
#else
load_submodule_mphase("pygame._sdl2", PyInit_sdl2(), spec, "sdl2");
load_submodule_mphase("pygame._sdl2", PyInit_sdl2_mixer(), spec, "mixer");
load_submodule("pygame._sdl2", PyInit_sdl2_controller(), "controller");
load_submodule_mphase("pygame._sdl2", PyInit_sdl2_audio(), spec, "audio");
load_submodule_mphase("pygame._sdl2", PyInit_sdl2_video(), spec, "video");
load_submodule_mphase("pygame._sdl2", PyInit_mixer(), spec, "mixer");
load_submodule_mphase("pygame._sdl2", PyInit_audio(), spec, "audio");
load_submodule_mphase("pygame._sdl2", PyInit_video(), spec, "video");
#endif

Py_RETURN_NONE;
Expand Down Expand Up @@ -2621,7 +2617,7 @@ PyInit_pygame_static()
load_submodule("pygame", PyInit_mask(), "mask");
load_submodule("pygame", PyInit_mouse(), "mouse");

load_submodule("pygame", PyInit_mixer(), "mixer");
load_submodule("pygame", PyInit_pg_mixer(), "mixer");
load_submodule("pygame.mixer", PyInit_mixer_music(), "music");

// base, color, rect, bufferproxy, surflock, surface
Expand Down
File renamed without changes.
Loading
Loading