summaryrefslogtreecommitdiffstats
path: root/Tools
diff options
context:
space:
mode:
Diffstat (limited to 'Tools')
-rw-r--r--Tools/wasm/README.md52
-rwxr-xr-xTools/wasm/wasm_build.py273
2 files changed, 242 insertions, 83 deletions
diff --git a/Tools/wasm/README.md b/Tools/wasm/README.md
index c4c21b4..fe9a1dc 100644
--- a/Tools/wasm/README.md
+++ b/Tools/wasm/README.md
@@ -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
@@ -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
@@ -336,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
@@ -446,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
@@ -471,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.
diff --git a/Tools/wasm/wasm_build.py b/Tools/wasm/wasm_build.py
index 9054370..63812c6 100755
--- a/Tools/wasm/wasm_build.py
+++ b/Tools/wasm/wasm_build.py
@@ -1,9 +1,9 @@
#!/usr/bin/env python3
"""Build script for Python on WebAssembly platforms.
- $ ./Tools/wasm/wasm_builder.py emscripten-browser compile
- $ ./Tools/wasm/wasm_builder.py emscripten-node-dl test
- $ ./Tools/wasm/wasm_builder.py wasi test
+ $ ./Tools/wasm/wasm_builder.py emscripten-browser build repl
+ $ ./Tools/wasm/wasm_builder.py emscripten-node-dl build test
+ $ ./Tools/wasm/wasm_builder.py wasi build test
Primary build targets are "emscripten-node-dl" (NodeJS, dynamic linking),
"emscripten-browser", and "wasi".
@@ -14,23 +14,36 @@ activated EMSDK environment (". /path/to/emsdk_env.sh"). System packages
WASI builds require WASI SDK and wasmtime. The tool looks for 'WASI_SDK_PATH'
and falls back to /opt/wasi-sdk.
+
+The 'build' Python interpreter must be rebuilt every time Python's byte code
+changes.
+
+ ./Tools/wasm/wasm_builder.py --clean build build
+
"""
import argparse
import enum
import dataclasses
+import logging
import os
import pathlib
import re
import shlex
import shutil
+import socket
import subprocess
+import sys
import sysconfig
import tempfile
+import time
import warnings
+import webbrowser
# for Python 3.8
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
+logger = logging.getLogger("wasm_build")
+
SRCDIR = pathlib.Path(__file__).parent.parent.parent.absolute()
WASMTOOLS = SRCDIR / "Tools" / "wasm"
BUILDDIR = SRCDIR / "builddir"
@@ -45,8 +58,7 @@ WASI_SDK_PATH = pathlib.Path(os.environ.get("WASI_SDK_PATH", "/opt/wasi-sdk"))
# path to Emscripten SDK config file.
# auto-detect's EMSDK in /opt/emsdk without ". emsdk_env.sh".
EM_CONFIG = pathlib.Path(os.environ.setdefault("EM_CONFIG", "/opt/emsdk/.emscripten"))
-# 3.1.16 has broken utime()
-EMSDK_MIN_VERSION = (3, 1, 17)
+EMSDK_MIN_VERSION = (3, 1, 19)
EMSDK_BROKEN_VERSION = {
(3, 1, 14): "https://github.com/emscripten-core/emscripten/issues/17338",
(3, 1, 16): "https://github.com/emscripten-core/emscripten/issues/17393",
@@ -54,17 +66,25 @@ EMSDK_BROKEN_VERSION = {
}
_MISSING = pathlib.PurePath("MISSING")
-# WASM_WEBSERVER = WASMTOOLS / "wasmwebserver.py"
+WASM_WEBSERVER = WASMTOOLS / "wasm_webserver.py"
CLEAN_SRCDIR = f"""
Builds require a clean source directory. Please use a clean checkout or
run "make clean -C '{SRCDIR}'".
"""
+INSTALL_NATIVE = f"""
+Builds require a C compiler (gcc, clang), make, pkg-config, and development
+headers for dependencies like zlib.
+
+Debian/Ubuntu: sudo apt install build-essential git curl pkg-config zlib1g-dev
+Fedora/CentOS: sudo dnf install gcc make git-core curl pkgconfig zlib-devel
+"""
+
INSTALL_EMSDK = """
wasm32-emscripten builds need Emscripten SDK. Please follow instructions at
https://emscripten.org/docs/getting_started/downloads.html how to install
-Emscripten and how to activate the SDK with ". /path/to/emsdk/emsdk_env.sh".
+Emscripten and how to activate the SDK with "emsdk_env.sh".
git clone https://github.com/emscripten-core/emsdk.git /path/to/emsdk
cd /path/to/emsdk
@@ -182,6 +202,24 @@ def _check_clean_src():
raise DirtySourceDirectory(os.fspath(candidate), CLEAN_SRCDIR)
+def _check_native():
+ if not any(shutil.which(cc) for cc in ["cc", "gcc", "clang"]):
+ raise MissingDependency("cc", INSTALL_NATIVE)
+ if not shutil.which("make"):
+ raise MissingDependency("make", INSTALL_NATIVE)
+ if sys.platform == "linux":
+ # skip pkg-config check on macOS
+ if not shutil.which("pkg-config"):
+ raise MissingDependency("pkg-config", INSTALL_NATIVE)
+ # zlib is needed to create zip files
+ for devel in ["zlib"]:
+ try:
+ subprocess.check_call(["pkg-config", "--exists", devel])
+ except subprocess.CalledProcessError:
+ raise MissingDependency(devel, INSTALL_NATIVE) from None
+ _check_clean_src()
+
+
NATIVE = Platform(
"native",
# macOS has python.exe
@@ -192,7 +230,7 @@ NATIVE = Platform(
cc=None,
make_wrapper=None,
environ={},
- check=_check_clean_src,
+ check=_check_native,
)
@@ -362,9 +400,9 @@ class EmscriptenTarget(enum.Enum):
node_debug = "node-debug"
@property
- def can_execute(self) -> bool:
+ def is_browser(self):
cls = type(self)
- return self not in {cls.browser, cls.browser_debug}
+ return self in {cls.browser, cls.browser_debug}
@property
def emport_args(self) -> List[str]:
@@ -396,15 +434,12 @@ class BuildProfile:
target: Union[EmscriptenTarget, None] = None
dynamic_linking: Union[bool, None] = None
pthreads: Union[bool, None] = None
- testopts: str = "-j2"
+ default_testopts: str = "-j2"
@property
- def can_execute(self) -> bool:
- """Can target run pythoninfo and tests?
-
- Disabled for browser, enabled for all other targets
- """
- return self.target is None or self.target.can_execute
+ def is_browser(self) -> bool:
+ """Is this a browser build?"""
+ return self.target is not None and self.target.is_browser
@property
def builddir(self) -> pathlib.Path:
@@ -500,6 +535,7 @@ class BuildProfile:
cmd.extend(args)
if cwd is None:
cwd = self.builddir
+ logger.info('Running "%s" in "%s"', shlex.join(cmd), cwd)
return subprocess.check_call(
cmd,
cwd=os.fspath(cwd),
@@ -507,14 +543,15 @@ class BuildProfile:
)
def _check_execute(self):
- if not self.can_execute:
+ if self.is_browser:
raise ValueError(f"Cannot execute on {self.target}")
- def run_build(self, force_configure: bool = False):
+ def run_build(self, *args):
"""Run configure (if necessary) and make"""
- if force_configure or not self.makefile.exists():
- self.run_configure()
- self.run_make()
+ if not self.makefile.exists():
+ logger.info("Makefile not found, running configure")
+ self.run_configure(*args)
+ self.run_make("all", *args)
def run_configure(self, *args):
"""Run configure script to generate Makefile"""
@@ -525,15 +562,17 @@ class BuildProfile:
"""Run make (defaults to build all)"""
return self._run_cmd(self.make_cmd, args)
- def run_pythoninfo(self):
+ def run_pythoninfo(self, *args):
"""Run 'make pythoninfo'"""
self._check_execute()
- return self.run_make("pythoninfo")
+ return self.run_make("pythoninfo", *args)
- def run_test(self):
+ def run_test(self, target: str, testopts: Optional[str] = None):
"""Run buildbottests"""
self._check_execute()
- return self.run_make("buildbottest", f"TESTOPTS={self.testopts}")
+ if testopts is None:
+ testopts = self.default_testopts
+ return self.run_make(target, f"TESTOPTS={testopts}")
def run_py(self, *args):
"""Run Python with hostrunner"""
@@ -542,6 +581,37 @@ class BuildProfile:
"--eval", f"run: all; $(HOSTRUNNER) ./$(PYTHON) {shlex.join(args)}", "run"
)
+ def run_browser(self, bind="127.0.0.1", port=8000):
+ """Run WASM webserver and open build in browser"""
+ relbuilddir = self.builddir.relative_to(SRCDIR)
+ url = f"http://{bind}:{port}/{relbuilddir}/python.html"
+ args = [
+ sys.executable,
+ os.fspath(WASM_WEBSERVER),
+ "--bind",
+ bind,
+ "--port",
+ str(port),
+ ]
+ srv = subprocess.Popen(args, cwd=SRCDIR)
+ # wait for server
+ end = time.monotonic() + 3.0
+ while time.monotonic() < end and srv.returncode is None:
+ try:
+ with socket.create_connection((bind, port), timeout=0.1) as s:
+ pass
+ except OSError:
+ time.sleep(0.01)
+ else:
+ break
+
+ webbrowser.open(url)
+
+ try:
+ srv.wait()
+ except KeyboardInterrupt:
+ pass
+
def clean(self, all: bool = False):
"""Clean build directory"""
if all:
@@ -570,19 +640,19 @@ class BuildProfile:
# Trigger PIC build.
ports_cmd.append("-sMAIN_MODULE")
embuilder_cmd.append("--pic")
+
if self.pthreads:
# Trigger multi-threaded build.
ports_cmd.append("-sUSE_PTHREADS")
- # https://github.com/emscripten-core/emscripten/pull/17729
- # embuilder_cmd.append("--pthreads")
# Pre-build libbz2, libsqlite3, libz, and some system libs.
ports_cmd.extend(["-sUSE_ZLIB", "-sUSE_BZIP2", "-sUSE_SQLITE3"])
- embuilder_cmd.extend(["build", "bzip2", "sqlite3", "zlib"])
+ # Multi-threaded sqlite3 has different suffix
+ embuilder_cmd.extend(
+ ["build", "bzip2", "sqlite3-mt" if self.pthreads else "sqlite3", "zlib"]
+ )
- if not self.pthreads:
- # Emscripten <= 3.1.20 has no option to build multi-threaded ports.
- self._run_cmd(embuilder_cmd, cwd=SRCDIR)
+ self._run_cmd(embuilder_cmd, cwd=SRCDIR)
with tempfile.TemporaryDirectory(suffix="-py-emport") as tmpdir:
tmppath = pathlib.Path(tmpdir)
@@ -659,7 +729,7 @@ _profiles = [
dynamic_linking=True,
pthreads=True,
),
- # wasm64-emscripten (requires unreleased Emscripten >= 3.1.21)
+ # wasm64-emscripten (requires Emscripten >= 3.1.21)
BuildProfile(
"wasm64-emscripten-node-debug",
support_level=SupportLevel.experimental,
@@ -674,8 +744,6 @@ _profiles = [
"wasi",
support_level=SupportLevel.supported,
host=Host.wasm32_wasi,
- # skip sysconfig test_srcdir
- testopts="-i '*.test_srcdir' -j2",
),
# no SDK available yet
# BuildProfile(
@@ -690,10 +758,36 @@ PROFILES = {p.name: p for p in _profiles}
parser = argparse.ArgumentParser(
"wasm_build.py",
description=__doc__,
- formatter_class=argparse.RawDescriptionHelpFormatter,
+ formatter_class=argparse.RawTextHelpFormatter,
+)
+
+parser.add_argument(
+ "--clean",
+ "-c",
+ help="Clean build directories first",
+ action="store_true",
)
+
+parser.add_argument(
+ "--verbose",
+ "-v",
+ help="Verbose logging",
+ action="store_true",
+)
+
parser.add_argument(
- "--clean", "-c", help="Clean build directories first", action="store_true"
+ "--silent",
+ help="Run configure and make in silent mode",
+ action="store_true",
+)
+
+parser.add_argument(
+ "--testopts",
+ help=(
+ "Additional test options for 'test' and 'hostrunnertest', e.g. "
+ "--testopts='-v test_os'."
+ ),
+ default=None,
)
# Don't list broken and experimental variants in help
@@ -706,67 +800,104 @@ parser.add_argument(
choices=platforms_choices,
)
-ops = ["compile", "pythoninfo", "test", "repl", "clean", "cleanall", "emports"]
+ops = dict(
+ build="auto build (build 'build' Python, emports, configure, compile)",
+ configure="run ./configure",
+ compile="run 'make all'",
+ pythoninfo="run 'make pythoninfo'",
+ test="run 'make buildbottest TESTOPTS=...' (supports parallel tests)",
+ hostrunnertest="run 'make hostrunnertest TESTOPTS=...'",
+ repl="start interactive REPL / webserver + browser session",
+ clean="run 'make clean'",
+ cleanall="remove all build directories",
+ emports="build Emscripten port with embuilder (only Emscripten)",
+)
+ops_help = "\n".join(f"{op:16s} {help}" for op, help in ops.items())
parser.add_argument(
- "op",
+ "ops",
metavar="OP",
- help=f"operation: {', '.join(ops)}",
- choices=ops,
- default="compile",
- nargs="?",
+ help=f"operation (default: build)\n\n{ops_help}",
+ choices=tuple(ops),
+ default="build",
+ nargs="*",
)
def main():
args = parser.parse_args()
+ logging.basicConfig(
+ level=logging.INFO if args.verbose else logging.ERROR,
+ format="%(message)s",
+ )
+
if args.platform == "cleanall":
for builder in PROFILES.values():
builder.clean(all=True)
parser.exit(0)
+ # additional configure and make args
+ cm_args = ("--silent",) if args.silent else ()
+
+ # nargs=* with default quirk
+ if args.ops == "build":
+ args.ops = ["build"]
+
builder = PROFILES[args.platform]
try:
builder.host.platform.check()
except ConditionError as e:
parser.error(str(e))
+ if args.clean:
+ builder.clean(all=False)
+
# hack for WASI
if builder.host.is_wasi and not SETUP_LOCAL.exists():
SETUP_LOCAL.touch()
- if args.op in {"compile", "pythoninfo", "repl", "test"}:
- # all targets need a build Python
+ # auto-build
+ if "build" in args.ops:
+ # check and create build Python
if builder is not BUILD:
+ logger.info("Auto-building 'build' Python.")
+ try:
+ BUILD.host.platform.check()
+ except ConditionError as e:
+ parser.error(str(e))
if args.clean:
BUILD.clean(all=False)
- BUILD.run_build()
- elif not BUILD.python_cmd.exists():
- BUILD.run_build()
-
- if args.clean:
+ BUILD.run_build(*cm_args)
+ # build Emscripten ports with embuilder
+ if builder.host.is_emscripten and "emports" not in args.ops:
+ builder.build_emports()
+
+ for op in args.ops:
+ logger.info("\n*** %s %s", args.platform, op)
+ if op == "build":
+ builder.run_build(*cm_args)
+ elif op == "configure":
+ builder.run_configure(*cm_args)
+ elif op == "compile":
+ builder.run_make("all", *cm_args)
+ elif op == "pythoninfo":
+ builder.run_pythoninfo(*cm_args)
+ elif op == "repl":
+ if builder.is_browser:
+ builder.run_browser()
+ else:
+ builder.run_py()
+ elif op == "test":
+ builder.run_test("buildbottest", testopts=args.testopts)
+ elif op == "hostrunnertest":
+ builder.run_test("hostrunnertest", testopts=args.testopts)
+ elif op == "clean":
builder.clean(all=False)
-
- if args.op == "compile":
- if builder.host.is_emscripten:
- builder.build_emports()
- builder.run_build(force_configure=True)
+ elif op == "cleanall":
+ builder.clean(all=True)
+ elif op == "emports":
+ builder.build_emports(force=args.clean)
else:
- if not builder.makefile.exists():
- builder.run_configure()
- if args.op == "pythoninfo":
- builder.run_pythoninfo()
- elif args.op == "repl":
- builder.run_py()
- elif args.op == "test":
- builder.run_test()
- elif args.op == "clean":
- builder.clean(all=False)
- elif args.op == "cleanall":
- builder.clean(all=True)
- elif args.op == "emports":
- builder.build_emports(force=args.clean)
- else:
- raise ValueError(args.op)
+ raise ValueError(op)
print(builder.builddir)
parser.exit(0)