Skip to content

Commit 0339434

Browse files
tiranemmatypingbrettcannon
authored
bpo-40280: Add Tools/wasm with helpers for cross building (GH-29984)
Co-authored-by: Ethan Smith <[email protected]> Co-authored-by: Brett Cannon <[email protected]>
1 parent ae36cd1 commit 0339434

File tree

7 files changed

+380
-10
lines changed

7 files changed

+380
-10
lines changed

Makefile.pre.in

+18
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,22 @@ $(DLLLIBRARY) libpython$(LDVERSION).dll.a: $(LIBRARY_OBJS)
830830
else true; \
831831
fi
832832

833+
# wasm32-emscripten build
834+
# wasm assets directory is relative to current build dir, e.g. "./usr/local".
835+
# --preload-file turns a relative asset path into an absolute path.
836+
WASM_ASSETS_DIR=".$(prefix)"
837+
WASM_STDLIB="$(WASM_ASSETS_DIR)/local/lib/python$(VERSION)/os.py"
838+
839+
$(WASM_STDLIB): $(srcdir)/Lib/*.py $(srcdir)/Lib/*/*.py \
840+
pybuilddir.txt $(srcdir)/Tools/wasm/wasm_assets.py
841+
$(PYTHON_FOR_BUILD) $(srcdir)/Tools/wasm/wasm_assets.py \
842+
--builddir . --prefix $(prefix)
843+
844+
python.html: Programs/python.o $(LIBRARY_DEPS) $(WASM_STDLIB)
845+
$(LINKCC) $(PY_CORE_LDFLAGS) $(LINKFORSHARED) -o $@ Programs/python.o \
846+
$(BLDLIBRARY) $(LIBS) $(MODLIBS) $(SYSLIBS) \
847+
-s ASSERTIONS=1 --preload-file $(WASM_ASSETS_DIR)
848+
833849
##########################################################################
834850
# Build static libmpdec.a
835851
LIBMPDEC_CFLAGS=$(PY_STDMODULE_CFLAGS) $(CCSHARED) @LIBMPDEC_CFLAGS@
@@ -938,6 +954,7 @@ Makefile Modules/config.c: Makefile.pre \
938954
$(SHELL) $(MAKESETUP) -c $(srcdir)/Modules/config.c.in \
939955
-s Modules \
940956
Modules/Setup.local \
957+
@MODULES_SETUP_STDLIB@ \
941958
$(srcdir)/Modules/Setup.bootstrap \
942959
$(srcdir)/Modules/Setup
943960
@mv config.c Modules
@@ -2379,6 +2396,7 @@ clean-retain-profile: pycremoval
23792396
-rm -f pybuilddir.txt
23802397
-rm -f Lib/lib2to3/*Grammar*.pickle
23812398
-rm -f _bootstrap_python
2399+
-rm -f python.html python.js python.data
23822400
-rm -f Programs/_testembed Programs/_freeze_module
23832401
-rm -f Python/deepfreeze/*.[co]
23842402
-rm -f Python/frozen_modules/*.h
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
A new directory ``Tools/wasm`` contains WebAssembly-related helpers like ``config.site`` override for wasm32-emscripten, wasm assets generator to bundle the stdlib, and a README.

Tools/wasm/README.md

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Python WebAssembly (WASM) build
2+
3+
This directory contains configuration and helpers to facilitate cross
4+
compilation of CPython to WebAssembly (WASM).
5+
6+
## wasm32-emscripten build
7+
8+
Cross compiling to wasm32-emscripten platform needs the [Emscripten](https://emscripten.org/)
9+
tool chain and a build Python interpreter.
10+
All commands below are relative to a repository checkout.
11+
12+
### Compile a build Python interpreter
13+
14+
```shell
15+
mkdir -p builddir/build
16+
pushd builddir/build
17+
../../configure -C
18+
make -j$(nproc)
19+
popd
20+
```
21+
22+
### Fetch and build additional emscripten ports
23+
24+
```shell
25+
embuilder build zlib
26+
```
27+
28+
### Cross compile to wasm32-emscripten
29+
30+
```shell
31+
mkdir -p builddir/emscripten
32+
pushd builddir/emscripten
33+
34+
CONFIG_SITE=../../Tools/wasm/config.site-wasm32-emscripten \
35+
emconfigure ../../configure -C \
36+
--host=wasm32-unknown-emscripten \
37+
--build=$(../../config.guess) \
38+
--with-build-python=$(pwd)/../build/python
39+
40+
emmake make -j$(nproc) python.html
41+
```
42+
43+
### Test in browser
44+
45+
Serve `python.html` with a local webserver and open the file in a browser.
46+
47+
```shell
48+
emrun python.html
49+
```
50+
51+
or
52+
53+
```shell
54+
python3 -m http.server
55+
```
+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# config.site override for cross compiling to wasm32-emscripten platform
2+
#
3+
# CONFIG_SITE=Tools/wasm/config.site-wasm32-emscripten \
4+
# emconfigure ./configure --host=wasm32-unknown-emscripten --build=...
5+
#
6+
# Written by Christian Heimes <[email protected]>
7+
# Partly based on pyodide's pyconfig.undefs.h file.
8+
#
9+
10+
# cannot be detected in cross builds
11+
ac_cv_buggy_getaddrinfo=no
12+
13+
# Emscripten has no /dev/pt*
14+
ac_cv_file__dev_ptmx=no
15+
ac_cv_file__dev_ptc=no
16+
17+
# dummy readelf, Emscripten build does not need readelf.
18+
ac_cv_prog_ac_ct_READELF=true
19+
20+
# new undefined symbols / unsupported features
21+
ac_cv_func_posix_spawn=no
22+
ac_cv_func_posix_spawnp=no
23+
ac_cv_func_eventfd=no
24+
ac_cv_func_memfd_create=no
25+
ac_cv_func_prlimit=no
26+
27+
# unsupported syscall, https://github.com/emscripten-core/emscripten/issues/13393
28+
ac_cv_func_shutdown=no
29+
30+
# breaks build, see https://github.com/ethanhs/python-wasm/issues/16
31+
ac_cv_lib_bz2_BZ2_bzCompress=no
32+
33+
# The rest is based on pyodide
34+
# https://github.com/pyodide/pyodide/blob/main/cpython/pyconfig.undefs.h
35+
36+
ac_cv_func_epoll=no
37+
ac_cv_func_epoll_create1=no
38+
ac_cv_header_linux_vm_sockets_h=no
39+
ac_cv_func_socketpair=no
40+
ac_cv_func_utimensat=no
41+
ac_cv_func_sigaction=no
42+
43+
# Untested syscalls in emscripten
44+
ac_cv_func_openat=no
45+
ac_cv_func_mkdirat=no
46+
ac_cv_func_fchownat=no
47+
ac_cv_func_renameat=no
48+
ac_cv_func_linkat=no
49+
ac_cv_func_symlinkat=no
50+
ac_cv_func_readlinkat=no
51+
ac_cv_func_fchmodat=no
52+
ac_cv_func_dup3=no
53+
54+
# Syscalls not implemented in emscripten
55+
ac_cv_func_preadv2=no
56+
ac_cv_func_preadv=no
57+
ac_cv_func_pwritev2=no
58+
ac_cv_func_pwritev=no
59+
ac_cv_func_pipe2=no
60+
ac_cv_func_nice=no
61+
62+
# Syscalls that resulted in a segfault
63+
ac_cv_func_utimensat=no
64+
ac_cv_header_sys_ioctl_h=no
65+
66+
# sockets are supported, but only in non-blocking mode
67+
# ac_cv_header_sys_socket_h=no
68+
69+
# Unsupported functionality
70+
#undef HAVE_PTHREAD_H

Tools/wasm/wasm_assets.py

+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
#!/usr/bin/env python
2+
"""Create a WASM asset bundle directory structure.
3+
4+
The WASM asset bundles are pre-loaded by the final WASM build. The bundle
5+
contains:
6+
7+
- a stripped down, pyc-only stdlib zip file, e.g. {PREFIX}/lib/python311.zip
8+
- os.py as marker module {PREFIX}/lib/python3.11/os.py
9+
- empty lib-dynload directory, to make sure it is copied into the bundle {PREFIX}/lib/python3.11/lib-dynload/.empty
10+
"""
11+
12+
import argparse
13+
import pathlib
14+
import shutil
15+
import sys
16+
import zipfile
17+
18+
# source directory
19+
SRCDIR = pathlib.Path(__file__).parent.parent.parent.absolute()
20+
SRCDIR_LIB = SRCDIR / "Lib"
21+
22+
# sysconfig data relative to build dir.
23+
SYSCONFIGDATA_GLOB = "build/lib.*/_sysconfigdata_*.py"
24+
25+
# Library directory relative to $(prefix).
26+
WASM_LIB = pathlib.PurePath("lib")
27+
WASM_STDLIB_ZIP = (
28+
WASM_LIB / f"python{sys.version_info.major}{sys.version_info.minor}.zip"
29+
)
30+
WASM_STDLIB = (
31+
WASM_LIB / f"python{sys.version_info.major}.{sys.version_info.minor}"
32+
)
33+
WASM_DYNLOAD = WASM_STDLIB / "lib-dynload"
34+
35+
36+
# Don't ship large files / packages that are not particularly useful at
37+
# the moment.
38+
OMIT_FILES = (
39+
# regression tests
40+
"test/",
41+
# user interfaces: TK, curses
42+
"curses/",
43+
"idlelib/",
44+
"tkinter/",
45+
"turtle.py",
46+
"turtledemo/",
47+
# package management
48+
"ensurepip/",
49+
"venv/",
50+
# build system
51+
"distutils/",
52+
"lib2to3/",
53+
# concurrency
54+
"concurrent/",
55+
"multiprocessing/",
56+
# deprecated
57+
"asyncore.py",
58+
"asynchat.py",
59+
# Synchronous network I/O and protocols are not supported; for example,
60+
# socket.create_connection() raises an exception:
61+
# "BlockingIOError: [Errno 26] Operation in progress".
62+
"cgi.py",
63+
"cgitb.py",
64+
"email/",
65+
"ftplib.py",
66+
"http/",
67+
"imaplib.py",
68+
"nntplib.py",
69+
"poplib.py",
70+
"smtpd.py",
71+
"smtplib.py",
72+
"socketserver.py",
73+
"telnetlib.py",
74+
"urllib/",
75+
"wsgiref/",
76+
"xmlrpc/",
77+
# dbm / gdbm
78+
"dbm/",
79+
# other platforms
80+
"_aix_support.py",
81+
"_bootsubprocess.py",
82+
"_osx_support.py",
83+
# webbrowser
84+
"antigravity.py",
85+
"webbrowser.py",
86+
# ctypes
87+
"ctypes/",
88+
# Pure Python implementations of C extensions
89+
"_pydecimal.py",
90+
"_pyio.py",
91+
# Misc unused or large files
92+
"pydoc_data/",
93+
"msilib/",
94+
)
95+
96+
# regression test sub directories
97+
OMIT_SUBDIRS = (
98+
"ctypes/test/",
99+
"tkinter/test/",
100+
"unittest/test/",
101+
)
102+
103+
104+
OMIT_ABSOLUTE = {SRCDIR_LIB / name for name in OMIT_FILES}
105+
OMIT_SUBDIRS_ABSOLUTE = tuple(str(SRCDIR_LIB / name) for name in OMIT_SUBDIRS)
106+
107+
108+
def filterfunc(name: str) -> bool:
109+
return not name.startswith(OMIT_SUBDIRS_ABSOLUTE)
110+
111+
112+
def create_stdlib_zip(
113+
args: argparse.Namespace, compression: int = zipfile.ZIP_DEFLATED, *, optimize: int = 0
114+
) -> None:
115+
sysconfig_data = list(args.builddir.glob(SYSCONFIGDATA_GLOB))
116+
if not sysconfig_data:
117+
raise ValueError("No sysconfigdata file found")
118+
119+
with zipfile.PyZipFile(
120+
args.wasm_stdlib_zip, mode="w", compression=compression, optimize=0
121+
) as pzf:
122+
for entry in sorted(args.srcdir_lib.iterdir()):
123+
if entry.name == "__pycache__":
124+
continue
125+
if entry in OMIT_ABSOLUTE:
126+
continue
127+
if entry.name.endswith(".py") or entry.is_dir():
128+
# writepy() writes .pyc files (bytecode).
129+
pzf.writepy(entry, filterfunc=filterfunc)
130+
for entry in sysconfig_data:
131+
pzf.writepy(entry)
132+
133+
134+
def path(val: str) -> pathlib.Path:
135+
return pathlib.Path(val).absolute()
136+
137+
138+
parser = argparse.ArgumentParser()
139+
parser.add_argument(
140+
"--builddir",
141+
help="absolute build directory",
142+
default=pathlib.Path(".").absolute(),
143+
type=path,
144+
)
145+
parser.add_argument(
146+
"--prefix", help="install prefix", default=pathlib.Path("/usr/local"), type=path
147+
)
148+
149+
150+
def main():
151+
args = parser.parse_args()
152+
153+
relative_prefix = args.prefix.relative_to(pathlib.Path("/"))
154+
args.srcdir = SRCDIR
155+
args.srcdir_lib = SRCDIR_LIB
156+
args.wasm_root = args.builddir / relative_prefix
157+
args.wasm_stdlib_zip = args.wasm_root / WASM_STDLIB_ZIP
158+
args.wasm_stdlib = args.wasm_root / WASM_STDLIB
159+
args.wasm_dynload = args.wasm_root / WASM_DYNLOAD
160+
161+
# Empty, unused directory for dynamic libs, but required for site initialization.
162+
args.wasm_dynload.mkdir(parents=True, exist_ok=True)
163+
marker = args.wasm_dynload / ".empty"
164+
marker.touch()
165+
# os.py is a marker for finding the correct lib directory.
166+
shutil.copy(args.srcdir_lib / "os.py", args.wasm_stdlib)
167+
# The rest of stdlib that's useful in a WASM context.
168+
create_stdlib_zip(args)
169+
size = round(args.wasm_stdlib_zip.stat().st_size / 1024 ** 2, 2)
170+
parser.exit(0, f"Created {args.wasm_stdlib_zip} ({size} MiB)\n")
171+
172+
173+
if __name__ == "__main__":
174+
main()

0 commit comments

Comments
 (0)