Skip to content

Commit

Permalink
[3.11] gh-95853: Add script to automate WASM build (GH-95828, GH-95985,
Browse files Browse the repository at this point in the history
GH-96045, GH-96389, GH-96744) (GH-96749)

Automate WASM build with a new Python script. The script provides
several build profiles with configure flags for Emscripten flavors
and WASI. The script can detect and use Emscripten SDK and WASI SDK from
default locations or env vars.

``configure`` now detects Node arguments and creates HOSTRUNNER
arguments for Node 16. It also sets correct arguments for
``wasm64-emscripten``.
  • Loading branch information
tiran authored Sep 13, 2022
1 parent 390123b commit 4958820
Show file tree
Hide file tree
Showing 16 changed files with 1,236 additions and 40 deletions.
1 change: 1 addition & 0 deletions Lib/distutils/tests/test_sysconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def test_get_config_vars(self):
self.assertIsInstance(cvars, dict)
self.assertTrue(cvars)

@unittest.skipIf(is_wasi, "Incompatible with WASI mapdir and OOT builds")
def test_srcdir(self):
# See Issues #15322, #15364.
srcdir = sysconfig.get_config_var('srcdir')
Expand Down
3 changes: 2 additions & 1 deletion Lib/test/test_decimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
requires_legacy_unicode_capi, check_sanitizer)
from test.support import (TestFailed,
run_with_locale, cpython_only,
darwin_malloc_err_warning)
darwin_malloc_err_warning, is_emscripten)
from test.support.import_helper import import_fresh_module
from test.support import threading_helper
from test.support import warnings_helper
Expand Down Expand Up @@ -5623,6 +5623,7 @@ def __abs__(self):
# Issue 41540:
@unittest.skipIf(sys.platform.startswith("aix"),
"AIX: default ulimit: test is flaky because of extreme over-allocation")
@unittest.skipIf(is_emscripten, "Test is unstable on Emscripten")
@unittest.skipIf(check_sanitizer(address=True, memory=True),
"ASAN/MSAN sanitizer defaults to crashing "
"instead of returning NULL for malloc failure.")
Expand Down
1 change: 1 addition & 0 deletions Lib/test/test_sysconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ def test_platform_in_subprocess(self):
self.assertEqual(status, 0)
self.assertEqual(my_platform, test_platform)

@unittest.skipIf(is_wasi, "Incompatible with WASI mapdir and OOT builds")
def test_srcdir(self):
# See Issues #15322, #15364.
srcdir = sysconfig.get_config_var('srcdir')
Expand Down
5 changes: 5 additions & 0 deletions Lib/test/test_unicode_file_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import warnings
from unicodedata import normalize
from test.support import os_helper
from test import support


filenames = [
Expand Down Expand Up @@ -123,6 +124,10 @@ def test_open(self):
# NFKD in Python is useless, because darwin will normalize it later and so
# open(), os.stat(), etc. don't raise any exception.
@unittest.skipIf(sys.platform == 'darwin', 'irrelevant test on Mac OS X')
@unittest.skipIf(
support.is_emscripten or support.is_wasi,
"test fails on Emscripten/WASI when host platform is macOS."
)
def test_normalize(self):
files = set(self.files)
others = set()
Expand Down
9 changes: 8 additions & 1 deletion Lib/test/test_warnings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,14 @@ def test_warn_explicit_non_ascii_filename(self):
module=self.module) as w:
self.module.resetwarnings()
self.module.filterwarnings("always", category=UserWarning)
for filename in ("nonascii\xe9\u20ac", "surrogate\udc80"):
filenames = ["nonascii\xe9\u20ac"]
if not support.is_emscripten:
# JavaScript does not like surrogates.
# Invalid UTF-8 leading byte 0x80 encountered when
# deserializing a UTF-8 string in wasm memory to a JS
# string!
filenames.append("surrogate\udc80")
for filename in filenames:
try:
os.fsencode(filename)
except UnicodeEncodeError:
Expand Down
9 changes: 7 additions & 2 deletions Makefile.pre.in
Original file line number Diff line number Diff line change
Expand Up @@ -817,10 +817,11 @@ $(DLLLIBRARY) libpython$(LDVERSION).dll.a: $(LIBRARY_OBJS)
# wasm assets directory is relative to current build dir, e.g. "./usr/local".
# --preload-file turns a relative asset path into an absolute path.

.PHONY: wasm_stdlib
wasm_stdlib: $(WASM_STDLIB)
$(WASM_STDLIB): $(srcdir)/Lib/*.py $(srcdir)/Lib/*/*.py \
$(srcdir)/Tools/wasm/wasm_assets.py \
Makefile pybuilddir.txt Modules/Setup.local \
python.html python.worker.js
Makefile pybuilddir.txt Modules/Setup.local
$(PYTHON_FOR_BUILD) $(srcdir)/Tools/wasm/wasm_assets.py \
--buildroot . --prefix $(prefix)

Expand Down Expand Up @@ -1713,6 +1714,10 @@ buildbottest: all
fi
$(TESTRUNNER) -j 1 -u all -W --slowest --fail-env-changed --timeout=$(TESTTIMEOUT) $(TESTOPTS)

# Like testall, but run Python tests with HOSTRUNNER directly.
hostrunnertest: all
$(RUNSHARED) $(HOSTRUNNER) ./$(BUILDPYTHON) -m test -u all $(TESTOPTS)

pythoninfo: all
$(RUNSHARED) $(HOSTRUNNER) ./$(BUILDPYTHON) -m test.pythoninfo

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The new tool ``Tools/wasm/wasm_builder.py`` automates configure, compile, and
test steps for building CPython on WebAssembly platforms.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The ``wasm_build.py`` script now pre-builds Emscripten ports, checks for
broken EMSDK versions, and warns about pkg-config env vars.
2 changes: 1 addition & 1 deletion Modules/pyexpat.c
Original file line number Diff line number Diff line change
Expand Up @@ -775,7 +775,7 @@ readinst(char *buf, int buf_size, PyObject *meth)
Py_ssize_t len;
const char *ptr;

str = PyObject_CallFunction(meth, "n", buf_size);
str = PyObject_CallFunction(meth, "i", buf_size);
if (str == NULL)
goto error;

Expand Down
10 changes: 7 additions & 3 deletions Python/sysmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2775,14 +2775,18 @@ EM_JS(char *, _Py_emscripten_runtime, (void), {
if (typeof navigator == 'object') {
info = navigator.userAgent;
} else if (typeof process == 'object') {
info = "Node.js ".concat(process.version)
info = "Node.js ".concat(process.version);
} else {
info = "UNKNOWN"
info = "UNKNOWN";
}
var len = lengthBytesUTF8(info) + 1;
var res = _malloc(len);
stringToUTF8(info, res, len);
if (res) stringToUTF8(info, res, len);
#if __wasm64__
return BigInt(res);
#else
return res;
#endif
});

static PyObject *
Expand Down
94 changes: 75 additions & 19 deletions Tools/wasm/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
# Python WebAssembly (WASM) build

**WARNING: WASM support is highly experimental! Lots of features are not working yet.**
**WARNING: WASM support is work-in-progress! Lots of features are not working yet.**

This directory contains configuration and helpers to facilitate cross
compilation of CPython to WebAssembly (WASM). For now we support
*wasm32-emscripten* builds for modern browser and for *Node.js*. WASI
(*wasm32-wasi*) is work-in-progress
compilation of CPython to WebAssembly (WASM). Python supports Emscripten
(*wasm32-emscripten*) and WASI (*wasm32-wasi*) targets. Emscripten builds
run in modern browsers and JavaScript runtimes like *Node.js*. WASI builds
use WASM runtimes such as *wasmtime*.

Users and developers are encouraged to use the script
`Tools/wasm/wasm_build.py`. The tool automates the build process and provides
assistance with installation of SDKs.

## wasm32-emscripten build

Expand All @@ -17,7 +22,7 @@ access the file system directly.

Cross compiling to the wasm32-emscripten platform needs the
[Emscripten](https://emscripten.org/) SDK and a build Python interpreter.
Emscripten 3.1.8 or newer are recommended. All commands below are relative
Emscripten 3.1.19 or newer are recommended. All commands below are relative
to a repository checkout.

Christian Heimes maintains a container image with Emscripten SDK, Python
Expand All @@ -35,7 +40,13 @@ docker run --rm -ti -v $(pwd):/python-wasm/cpython -w /python-wasm/cpython quay.

### Compile a build Python interpreter

From within the container, run the following commands:
From within the container, run the following command:

```shell
./Tools/wasm/wasm_build.py build
```

The command is roughly equivalent to:

```shell
mkdir -p builddir/build
Expand All @@ -45,13 +56,13 @@ make -j$(nproc)
popd
```

### Fetch and build additional emscripten ports
### Cross-compile to wasm32-emscripten for browser

```shell
embuilder build zlib bzip2
./Tools/wasm/wasm_build.py emscripten-browser
```

### Cross compile to wasm32-emscripten for browser
The command is roughly equivalent to:

```shell
mkdir -p builddir/emscripten-browser
Expand Down Expand Up @@ -85,22 +96,29 @@ and header files with debug builds.
### Cross compile to wasm32-emscripten for node

```shell
mkdir -p builddir/emscripten-node
pushd builddir/emscripten-node
./Tools/wasm/wasm_build.py emscripten-browser-dl
```

The command is roughly equivalent to:

```shell
mkdir -p builddir/emscripten-node-dl
pushd builddir/emscripten-node-dl

CONFIG_SITE=../../Tools/wasm/config.site-wasm32-emscripten \
emconfigure ../../configure -C \
--host=wasm32-unknown-emscripten \
--build=$(../../config.guess) \
--with-emscripten-target=node \
--enable-wasm-dynamic-linking \
--with-build-python=$(pwd)/../build/python

emmake make -j$(nproc)
popd
```

```shell
node --experimental-wasm-threads --experimental-wasm-bulk-memory --experimental-wasm-bigint builddir/emscripten-node/python.js
node --experimental-wasm-threads --experimental-wasm-bulk-memory --experimental-wasm-bigint builddir/emscripten-node-dl/python.js
```

(``--experimental-wasm-bigint`` is not needed with recent NodeJS versions)
Expand Down Expand Up @@ -199,6 +217,15 @@ Node builds use ``NODERAWFS``.
- Node RawFS allows direct access to the host file system without need to
perform ``FS.mount()`` call.

## wasm64-emscripten

- wasm64 requires recent NodeJS and ``--experimental-wasm-memory64``.
- ``EM_JS`` functions must return ``BigInt()``.
- ``Py_BuildValue()`` format strings must match size of types. Confusing 32
and 64 bits types leads to memory corruption, see
[gh-95876](/~https://github.com/python/cpython/issues/95876) and
[gh-95878](/~https://github.com/python/cpython/issues/95878).

# Hosting Python WASM builds

The simple REPL terminal uses SharedArrayBuffer. For security reasons
Expand Down Expand Up @@ -234,6 +261,12 @@ The script ``wasi-env`` sets necessary compiler and linker flags as well as
``pkg-config`` overrides. The script assumes that WASI-SDK is installed in
``/opt/wasi-sdk`` or ``$WASI_SDK_PATH``.

```shell
./Tools/wasm/wasm_build.py wasi
```

The command is roughly equivalent to:

```shell
mkdir -p builddir/wasi
pushd builddir/wasi
Expand Down Expand Up @@ -308,26 +341,46 @@ if os.name == "posix":
```python
>>> import os, sys
>>> os.uname()
posix.uname_result(sysname='Emscripten', nodename='emscripten', release='1.0', version='#1', machine='wasm32')
posix.uname_result(
sysname='Emscripten',
nodename='emscripten',
release='3.1.19',
version='#1',
machine='wasm32'
)
>>> os.name
'posix'
>>> sys.platform
'emscripten'
>>> sys._emscripten_info
sys._emscripten_info(
emscripten_version=(3, 1, 8),
runtime='Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:99.0) Gecko/20100101 Firefox/99.0',
emscripten_version=(3, 1, 10),
runtime='Mozilla/5.0 (X11; Linux x86_64; rv:104.0) Gecko/20100101 Firefox/104.0',
pthreads=False,
shared_memory=False
)
```

```python
>>> sys._emscripten_info
sys._emscripten_info(emscripten_version=(3, 1, 8), runtime='Node.js v14.18.2', pthreads=True, shared_memory=True)
sys._emscripten_info(
emscripten_version=(3, 1, 19),
runtime='Node.js v14.18.2',
pthreads=True,
shared_memory=True
)
```

```python
>>> import os, sys
>>> os.uname()
posix.uname_result(sysname='wasi', nodename='(none)', release='0.0.0', version='0.0.0', machine='wasm32')
posix.uname_result(
sysname='wasi',
nodename='(none)',
release='0.0.0',
version='0.0.0',
machine='wasm32'
)
>>> os.name
'posix'
>>> sys.platform
Expand Down Expand Up @@ -418,7 +471,8 @@ embuilder build --pic zlib bzip2 MINIMAL_PIC

**NOTE**: WASI-SDK's clang may show a warning on Fedora:
``/lib64/libtinfo.so.6: no version information available``,
[RHBZ#1875587](https://bugzilla.redhat.com/show_bug.cgi?id=1875587).
[RHBZ#1875587](https://bugzilla.redhat.com/show_bug.cgi?id=1875587). The
warning can be ignored.

```shell
export WASI_VERSION=16
Expand All @@ -443,6 +497,8 @@ ln -srf -t /usr/local/bin/ ~/.wasmtime/bin/wasmtime

### WASI debugging

* ``wasmtime run -g`` generates debugging symbols for gdb and lldb.
* ``wasmtime run -g`` generates debugging symbols for gdb and lldb. The
feature is currently broken, see
/~https://github.com/bytecodealliance/wasmtime/issues/4669 .
* The environment variable ``RUST_LOG=wasi_common`` enables debug and
trace logging.
3 changes: 2 additions & 1 deletion Tools/wasm/wasi-env
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,5 @@ export CFLAGS LDFLAGS
export PKG_CONFIG_PATH PKG_CONFIG_LIBDIR PKG_CONFIG_SYSROOT_DIR
export PATH

exec "$@"
# no exec, it makes arvg[0] path absolute.
"$@"
14 changes: 13 additions & 1 deletion Tools/wasm/wasm_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@
"unittest/test/",
)

SYSCONFIG_NAMES = (
"_sysconfigdata__emscripten_wasm32-emscripten",
"_sysconfigdata__emscripten_wasm32-emscripten",
"_sysconfigdata__wasi_wasm32-wasi",
"_sysconfigdata__wasi_wasm64-wasi",
)


def get_builddir(args: argparse.Namespace) -> pathlib.Path:
"""Get builddir path from pybuilddir.txt
"""
Expand All @@ -128,7 +136,11 @@ def get_sysconfigdata(args: argparse.Namespace) -> pathlib.Path:
"""Get path to sysconfigdata relative to build root
"""
data_name = sysconfig._get_sysconfigdata_name()
assert "emscripten_wasm32" in data_name
if not data_name.startswith(SYSCONFIG_NAMES):
raise ValueError(
f"Invalid sysconfig data name '{data_name}'.",
SYSCONFIG_NAMES
)
filename = data_name + ".py"
return args.builddir / filename

Expand Down
Loading

0 comments on commit 4958820

Please sign in to comment.