From 1db84bd8143d6d9ba325f8c4ced8c8a6b0627149 Mon Sep 17 00:00:00 2001
From: Christian Heimes <christian@python.org>
Date: Tue, 7 Dec 2021 17:42:42 +0100
Subject: [PATCH 1/5] bpo-40280: Add Tools/wasm with helpers for cross building

---
 Makefile.pre.in                               |  18 ++
 .../2021-12-13-21-03-52.bpo-40280.b7NG4Y.rst  |   1 +
 Tools/wasm/README.md                          |  48 +++++
 Tools/wasm/config.site-wasm32-emscripten      |  70 +++++++
 Tools/wasm/wasm_assets.py                     | 175 ++++++++++++++++++
 configure                                     |  41 +++-
 configure.ac                                  |  31 +++-
 7 files changed, 374 insertions(+), 10 deletions(-)
 create mode 100644 Misc/NEWS.d/next/Build/2021-12-13-21-03-52.bpo-40280.b7NG4Y.rst
 create mode 100644 Tools/wasm/README.md
 create mode 100644 Tools/wasm/config.site-wasm32-emscripten
 create mode 100755 Tools/wasm/wasm_assets.py

diff --git a/Makefile.pre.in b/Makefile.pre.in
index 59c92a05680220..ed77bebfab901f 100644
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -830,6 +830,22 @@ $(DLLLIBRARY) libpython$(LDVERSION).dll.a: $(LIBRARY_OBJS)
 	else true; \
 	fi
 
+# wasm32-emscripten build
+# wasm assets directory is relative to current build dir, e.g. "./usr/local".
+# --preload-file turns a relative asset path into an absolute path.
+WASM_ASSETS_DIR=".$(prefix)"
+WASM_STDLIB="$(WASM_ASSETS_DIR)/local/lib/python$(VERSION)/os.py"
+
+$(WASM_STDLIB): $(srcdir)/Lib/*.py $(srcdir)/Lib/*/*.py \
+                pybuilddir.txt $(srcdir)/Tools/wasm/wasm_assets.py
+	$(PYTHON_FOR_BUILD) $(srcdir)/Tools/wasm/wasm_assets.py \
+	    --builddir . --prefix $(prefix)
+
+python.html: Programs/python.o $(LIBRARY_DEPS) $(WASM_STDLIB)
+	$(LINKCC) $(PY_CORE_LDFLAGS) $(LINKFORSHARED) -o $@ Programs/python.o \
+	    $(BLDLIBRARY) $(LIBS) $(MODLIBS) $(SYSLIBS) \
+	    -s ASSERTIONS=1 --preload-file $(WASM_ASSETS_DIR)
+
 ##########################################################################
 # Build static libmpdec.a
 LIBMPDEC_CFLAGS=$(PY_STDMODULE_CFLAGS) $(CCSHARED) @LIBMPDEC_CFLAGS@
@@ -938,6 +954,7 @@ Makefile Modules/config.c: Makefile.pre \
 	$(SHELL) $(MAKESETUP) -c $(srcdir)/Modules/config.c.in \
 				-s Modules \
 				Modules/Setup.local \
+				@MODULES_SETUP_STDLIB@ \
 				$(srcdir)/Modules/Setup.bootstrap \
 				$(srcdir)/Modules/Setup
 	@mv config.c Modules
@@ -2379,6 +2396,7 @@ clean-retain-profile: pycremoval
 	-rm -f pybuilddir.txt
 	-rm -f Lib/lib2to3/*Grammar*.pickle
 	-rm -f _bootstrap_python
+	-rm -f python.html python.js python.data
 	-rm -f Programs/_testembed Programs/_freeze_module
 	-rm -f Python/deepfreeze/*.[co]
 	-rm -f Python/frozen_modules/*.h
diff --git a/Misc/NEWS.d/next/Build/2021-12-13-21-03-52.bpo-40280.b7NG4Y.rst b/Misc/NEWS.d/next/Build/2021-12-13-21-03-52.bpo-40280.b7NG4Y.rst
new file mode 100644
index 00000000000000..905ee446802763
--- /dev/null
+++ b/Misc/NEWS.d/next/Build/2021-12-13-21-03-52.bpo-40280.b7NG4Y.rst
@@ -0,0 +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.
diff --git a/Tools/wasm/README.md b/Tools/wasm/README.md
new file mode 100644
index 00000000000000..fe62e842dba4e3
--- /dev/null
+++ b/Tools/wasm/README.md
@@ -0,0 +1,48 @@
+# Python WebAssembly (WASM) build
+
+This directory contains configuration and helpers to facilitate cross
+compile CPython to WebAssembly (WASM).
+
+## wasm32-emscripten build
+
+Cross compiling to wasm32-emscripten platform needs [Emscripten](https://emscripten.org/)
+tool chain and a build Python interpreter.
+
+### Compile a build Python interpreter
+
+```
+mkdir -p builddir/build
+pushd builddir/build
+../../configure -C
+make -j$(nproc)
+popd
+```
+
+### Fetch and build additional emscripten ports
+
+```
+embuilder build zlib
+```
+
+### Cross compile to wasm32-emscripten
+
+```
+mkdir -p builddir/emscripten
+pushd builddir/emscripten
+
+CONFIG_SITE=../../Tools/wasm/config.site-wasm32-emscripten \
+  emconfigure ../../configure -C \
+    --host=wasm32-unknown-emscripten \
+    --build=$(../../config.guess) \
+    --with-build-python=$(pwd)/../build/python
+
+emmake make -j$(nproc) python.html
+```
+
+### Test in browser
+
+Serve ``python.html`` with a local webserver and open the file in a browser.
+
+```
+python3 -m http.server
+```
diff --git a/Tools/wasm/config.site-wasm32-emscripten b/Tools/wasm/config.site-wasm32-emscripten
new file mode 100644
index 00000000000000..67304be060b52a
--- /dev/null
+++ b/Tools/wasm/config.site-wasm32-emscripten
@@ -0,0 +1,70 @@
+# config.site override for cross compiling to wasm32-emscripten platform
+#
+# CONFIG_SITE=Tools/wasm/config.site-wasm32-emscripten \
+#     emconfigure ./configure --host=wasm32-unknown-emscripten --build=...
+#
+# Written by Christian Heimes <christian@python.org>
+# Partly based on pyodide's pyconfig.undefs.h file.
+#
+
+# cannot be detected in cross builds
+ac_cv_buggy_getaddrinfo=no
+
+# Emscripten has no /dev/pt*
+ac_cv_file__dev_ptmx=no
+ac_cv_file__dev_ptc=no
+
+# dummy readelf, Emscripten build does not need readelf.
+ac_cv_prog_ac_ct_READELF=true
+
+# new undefined symbols / unsupported features
+ac_cv_func_posix_spawn=no
+ac_cv_func_posix_spawnp=no
+ac_cv_func_eventfd=no
+ac_cv_func_memfd_create=no
+ac_cv_func_prlimit=no
+
+# unsupported syscall, https://github.com/emscripten-core/emscripten/issues/13393
+ac_cv_func_shutdown=no
+
+# breaks build, see https://github.com/ethanhs/python-wasm/issues/16
+ac_cv_lib_bz2_BZ2_bzCompress=no
+
+# The rest is based on pyodide
+# https://github.com/pyodide/pyodide/blob/main/cpython/pyconfig.undefs.h
+
+ac_cv_func_epoll=no
+ac_cv_func_epoll_create1=no
+ac_cv_header_linux_vm_sockets_h=no
+ac_cv_func_socketpair=no
+ac_cv_func_utimensat=no
+ac_cv_func_sigaction=no
+
+# Untested syscalls in emscripten
+ac_cv_func_openat=no
+ac_cv_func_mkdirat=no
+ac_cv_func_fchownat=no
+ac_cv_func_renameat=no
+ac_cv_func_linkat=no
+ac_cv_func_symlinkat=no
+ac_cv_func_readlinkat=no
+ac_cv_func_fchmodat=no
+ac_cv_func_dup3=no
+
+# Syscalls not implemented in emscripten
+ac_cv_func_preadv2=no
+ac_cv_func_preadv=no
+ac_cv_func_pwritev2=no
+ac_cv_func_pwritev=no
+ac_cv_func_pipe2=no
+ac_cv_func_nice=no
+
+# Syscalls that resulted in a segfault
+ac_cv_func_utimensat=no
+ac_cv_header_sys_ioctl_h=no
+
+# sockets are supported, but only in non-blocking mode
+# ac_cv_header_sys_socket_h=no
+
+# Unsupported functionality
+#undef HAVE_PTHREAD_H
diff --git a/Tools/wasm/wasm_assets.py b/Tools/wasm/wasm_assets.py
new file mode 100755
index 00000000000000..2cddd8541413e5
--- /dev/null
+++ b/Tools/wasm/wasm_assets.py
@@ -0,0 +1,175 @@
+#!/usr/bin/env python
+"""Create a WASM asset bundle directory structure
+
+The WASM assett bundles are pre-loaded into the final WASM build. The bundle
+contains:
+
+- a stripped down, pyc-only stdlib zip file, e.g. {PREFIX}/lib/python311.zip
+- os.py as marker module {PREFIX}/lib/python3.11/os.py
+- empty lib-dynload directory {PREFIX}/lib/python3.11/lib-dynload/.empty
+"""
+
+import argparse
+import pathlib
+import shutil
+import sys
+import zipfile
+
+# source directory
+SRCDIR = pathlib.Path(__file__).parent.parent.parent.absolute()
+SRCDIR_LIB = SRCDIR / "Lib"
+
+# sysconfig data relative to build dir
+SYSCONFIGDATA_GLOB = "build/lib.*/_sysconfigdata_*.py"
+
+# library directory relative to $(prefix)
+WASM_LIB = pathlib.PurePath("lib")
+WASM_STDLIB_ZIP = (
+    WASM_LIB / f"python{sys.version_info.major}{sys.version_info.minor}.zip"
+)
+WASM_STDLIB = (
+    WASM_LIB / f"python{sys.version_info.major}.{sys.version_info.minor}"
+)
+WASM_DYNLOAD = WASM_STDLIB / "lib-dynload"
+
+
+# don't ship large files / packages that are not particular useful at
+# the moment.
+OMIT_FILES = (
+    # regression tests
+    "test/",
+    # user interfaces: TK, curses
+    "curses/",
+    "idlelib/",
+    "tkinter/",
+    "turtle.py",
+    "turtledemo/",
+    # venv / pip
+    "ensurepip/",
+    "venv/",
+    # build system
+    "distutils/",
+    "lib2to3/",
+    # concurrency
+    "concurrent/",
+    "multiprocessing/",
+    # deprecated
+    "asyncore.py",
+    "asynchat.py",
+    # synchronous network I/O and protocols are not supported, for example
+    # socket.create_connection() raises exception
+    # "BlockingIOError: [Errno 26] Operation in progress".
+    "cgi.py",
+    "cgitb.py",
+    "email/",
+    "ftplib.py",
+    "http/",
+    "imaplib.py",
+    "nntplib.py",
+    "poplib.py",
+    "smtpd.py",
+    "smtplib.py",
+    "socketserver.py",
+    "telnetlib.py",
+    "urllib/",
+    "wsgiref/",
+    "xmlrpc/",
+    # dbm / gdbm
+    "dbm/",
+    # other platforms
+    "_aix_support.py",
+    "_bootsubprocess.py",
+    "_osx_support.py",
+    # webbrowser
+    "antigravity.py",
+    "webbrowser.py",
+    # ctypes
+    "ctypes/",
+    # pure Python implementations of C extensions
+    "_pydecimal.py",
+    "_pyio.py",
+    # misc unused or large files
+    "pydoc_data/",
+    "msilib/",
+    # only pyc encoding files
+    # "encoding/*.py",
+)
+
+# regression test sub directories
+OMIT_SUBDIRS = (
+    "ctypes/test/",
+    "tkinter/test/",
+    "unittest/test/",
+)
+
+
+OMIT_ABSOLUTE = {SRCDIR_LIB / name for name in OMIT_FILES}
+OMIT_SUBDIRS_ABSOLUTE = tuple(str(SRCDIR_LIB / name) for name in OMIT_SUBDIRS)
+
+
+def filterfunc(name: str) -> bool:
+    return not name.startswith(OMIT_SUBDIRS_ABSOLUTE)
+
+
+def create_stdlib_zip(
+    args: argparse.Namespace, compression: int = zipfile.ZIP_DEFLATED, optimize: int = 0
+):
+    sysconfig_data = list(args.builddir.glob(SYSCONFIGDATA_GLOB))
+    if not sysconfig_data:
+        raise ValueError("No sysconfigdata file found")
+
+    with zipfile.PyZipFile(
+        args.wasm_stdlib_zip, mode="w", compression=compression, optimize=0
+    ) as pzf:
+        for entry in sorted(args.srcdir_lib.iterdir()):
+            if entry.name == "__pycache__":
+                continue
+            if entry in OMIT_ABSOLUTE:
+                continue
+            if entry.name.endswith(".py") or entry.is_dir():
+                pzf.writepy(entry, filterfunc=filterfunc)
+        for entry in sysconfig_data:
+            pzf.writepy(entry)
+
+
+def path(val: str) -> pathlib.Path:
+    return pathlib.Path(val).absolute()
+
+
+parser = argparse.ArgumentParser()
+parser.add_argument(
+    "--builddir",
+    help="absolute build directory",
+    default=pathlib.Path(".").absolute(),
+    type=path,
+)
+parser.add_argument(
+    "--prefix", help="install prefix", default=pathlib.Path("/usr/local"), type=path
+)
+
+
+def main():
+    args = parser.parse_args()
+
+    relative_prefix = args.prefix.relative_to(pathlib.Path("/"))
+    args.srcdir = SRCDIR
+    args.srcdir_lib = SRCDIR_LIB
+    args.wasm_root = args.builddir / relative_prefix
+    args.wasm_stdlib_zip = args.wasm_root / WASM_STDLIB_ZIP
+    args.wasm_stdlib = args.wasm_root / WASM_STDLIB
+    args.wasm_dynload = args.wasm_root / WASM_DYNLOAD
+
+    # empty, unused directory for dynamic libs
+    args.wasm_dynload.mkdir(parents=True, exist_ok=True)
+    marker = args.wasm_dynload / ".empty"
+    marker.touch()
+    # os.py is a marker for finding the correct lib directory
+    shutil.copy(args.srcdir_lib / "os.py", args.wasm_stdlib)
+    # useful rest of stdlib
+    create_stdlib_zip(args)
+    size = round(args.wasm_stdlib_zip.stat().st_size / 1024 ** 2, 2)
+    parser.exit(0, f"Created {args.wasm_stdlib_zip} ({size} MiB)\n")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/configure b/configure
index 1ede29989d9481..eca63518dbca31 100755
--- a/configure
+++ b/configure
@@ -772,6 +772,7 @@ MODULE_TIME_FALSE
 MODULE_TIME_TRUE
 MODULE__IO_FALSE
 MODULE__IO_TRUE
+MODULES_SETUP_STDLIB
 MODULE_BUILDTYPE
 TEST_MODULES
 LIBRARY_DEPS
@@ -13298,7 +13299,13 @@ fi
 
 if test -z "$with_pymalloc"
 then
+    case $ac_sys_system in #(
+  Emscripten) :
+    with_pymalloc="no" ;; #(
+  *) :
     with_pymalloc="yes"
+   ;;
+esac
 fi
 if test "$with_pymalloc" != "no"
 then
@@ -21165,12 +21172,22 @@ fi
 
 if test "$enable_test_modules" = no; then
     TEST_MODULES=no
-    { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5
-$as_echo "yes" >&6; }
 else
+    case $ac_sys_system in #(
+  Emscripten) :
+    TEST_MODULES=no ;; #(
+  *) :
     TEST_MODULES=yes
-    { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
+     ;;
+esac
+fi
+if test "x$TEST_MODULES" = xyes; then :
+  { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
 $as_echo "no" >&6; }
+else
+  { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+$as_echo "yes" >&6; }
+
 fi
 
 
@@ -21189,7 +21206,7 @@ case $ac_sys_system in #(
     py_stdlib_not_available="_scproxy spwd" ;; #(
   Emscripten) :
 
-    py_stdlib_not_available="_curses _curses_panel _dbm _gdbm _multiprocessing _posixshmem _posixsubprocess _scproxy _xxsubinterpreters fcntl grp nis ossaudiodev resource spwd syslog termios"
+    py_stdlib_not_available="_ctypes _curses _curses_panel _dbm _gdbm _multiprocessing _posixshmem _posixsubprocess _scproxy _tkinter _xxsubinterpreters fcntl grp nis ossaudiodev resource readline spwd syslog termios"
    ;; #(
   *) :
     py_stdlib_not_available="_scproxy"
@@ -21205,6 +21222,20 @@ case $host_cpu in #(
 esac
 
 
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for additional Modules/Setup files" >&5
+$as_echo_n "checking for additional Modules/Setup files... " >&6; }
+case $ac_sys_system in #(
+  Emscripten) :
+    MODULES_SETUP_STDLIB=Modules/Setup.stdlib ;; #(
+  *) :
+    MODULES_SETUP_STDLIB=
+ ;;
+esac
+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $MODULES_SETUP_STDLIB" >&5
+$as_echo "$MODULES_SETUP_STDLIB" >&6; }
+
+
+
 
 MODULE_BLOCK=
 
@@ -25100,7 +25131,7 @@ fi
 $as_echo "$as_me: creating Makefile" >&6;}
 $SHELL $srcdir/Modules/makesetup -c $srcdir/Modules/config.c.in \
 			-s Modules \
-			Modules/Setup.local $srcdir/Modules/Setup.bootstrap $srcdir/Modules/Setup
+			Modules/Setup.local $MODULES_SETUP_STDLIB $srcdir/Modules/Setup.bootstrap $srcdir/Modules/Setup
 mv config.c Modules
 
 if test -z "$PKG_CONFIG"; then
diff --git a/configure.ac b/configure.ac
index 86404bcadeaba1..050b907ac86248 100644
--- a/configure.ac
+++ b/configure.ac
@@ -3865,7 +3865,11 @@ AC_ARG_WITH(pymalloc,
 
 if test -z "$with_pymalloc"
 then
-    with_pymalloc="yes"
+  dnl default to yes except for wasm32-emscripten
+  AS_CASE([$ac_sys_system],
+    [Emscripten], [with_pymalloc="no"],
+    [with_pymalloc="yes"]
+  )
 fi
 if test "$with_pymalloc" != "no"
 then
@@ -6253,11 +6257,15 @@ AC_ARG_ENABLE(test-modules,
               AS_HELP_STRING([--disable-test-modules], [don't build nor install test modules]))
 if test "$enable_test_modules" = no; then
     TEST_MODULES=no
-    AC_MSG_RESULT(yes)
 else
-    TEST_MODULES=yes
-    AC_MSG_RESULT(no)
+    AS_CASE([$ac_sys_system],
+      [Emscripten], [TEST_MODULES=no],
+      [TEST_MODULES=yes]
+    )
 fi
+AS_VAR_IF([TEST_MODULES], [yes],
+  [AC_MSG_RESULT(no)], [AC_MSG_RESULT(yes)]
+)
 AC_SUBST(TEST_MODULES)
 
 dnl Modules that are not available on some platforms
@@ -6272,6 +6280,7 @@ AS_CASE([$ac_sys_system],
   [FreeBSD*], [py_stdlib_not_available="_scproxy spwd"],
   [Emscripten], [
     py_stdlib_not_available="m4_normalize([
+      _ctypes
       _curses
       _curses_panel
       _dbm
@@ -6280,12 +6289,14 @@ AS_CASE([$ac_sys_system],
       _posixshmem
       _posixsubprocess
       _scproxy
+      _tkinter
       _xxsubinterpreters
       fcntl
       grp
       nis
       ossaudiodev
       resource
+      readline
       spwd
       syslog
       termios
@@ -6301,6 +6312,16 @@ AS_CASE([$host_cpu],
 )
 AC_SUBST([MODULE_BUILDTYPE])
 
+dnl Use Modules/Setup.stdlib as additional provider?
+AC_MSG_CHECKING([for additional Modules/Setup files])
+AS_CASE([$ac_sys_system],
+    [Emscripten], [MODULES_SETUP_STDLIB=Modules/Setup.stdlib],
+    [MODULES_SETUP_STDLIB=]
+)
+AC_MSG_RESULT([$MODULES_SETUP_STDLIB])
+AC_SUBST([MODULES_SETUP_STDLIB])
+
+
 dnl _MODULE_BLOCK_ADD([VAR], [VALUE])
 dnl internal: adds $1=quote($2) to MODULE_BLOCK
 AC_DEFUN([_MODULE_BLOCK_ADD], [AS_VAR_APPEND([MODULE_BLOCK], ["$1=_AS_QUOTE([$2])$as_nl"])])
@@ -6515,7 +6536,7 @@ fi
 AC_MSG_NOTICE([creating Makefile])
 $SHELL $srcdir/Modules/makesetup -c $srcdir/Modules/config.c.in \
 			-s Modules \
-			Modules/Setup.local $srcdir/Modules/Setup.bootstrap $srcdir/Modules/Setup
+			Modules/Setup.local $MODULES_SETUP_STDLIB $srcdir/Modules/Setup.bootstrap $srcdir/Modules/Setup
 mv config.c Modules
 
 if test -z "$PKG_CONFIG"; then

From 30db8755bd5c88f3d807456405307d739ec9f860 Mon Sep 17 00:00:00 2001
From: Christian Heimes <christian@python.org>
Date: Tue, 14 Dec 2021 10:47:33 +0200
Subject: [PATCH 2/5] Apply suggestions from code review

Co-authored-by: Ethan Smith <ethan@ethanhs.me>
---
 Tools/wasm/wasm_assets.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/Tools/wasm/wasm_assets.py b/Tools/wasm/wasm_assets.py
index 2cddd8541413e5..212b8ab2fb1c38 100755
--- a/Tools/wasm/wasm_assets.py
+++ b/Tools/wasm/wasm_assets.py
@@ -1,12 +1,12 @@
 #!/usr/bin/env python
 """Create a WASM asset bundle directory structure
 
-The WASM assett bundles are pre-loaded into the final WASM build. The bundle
+The WASM asset bundles are pre-loaded by the final WASM build. The bundle
 contains:
 
 - a stripped down, pyc-only stdlib zip file, e.g. {PREFIX}/lib/python311.zip
 - os.py as marker module {PREFIX}/lib/python3.11/os.py
-- empty lib-dynload directory {PREFIX}/lib/python3.11/lib-dynload/.empty
+- empty lib-dynload directory, to make sure it is copied into the bundle {PREFIX}/lib/python3.11/lib-dynload/.empty
 """
 
 import argparse
@@ -159,7 +159,7 @@ def main():
     args.wasm_stdlib = args.wasm_root / WASM_STDLIB
     args.wasm_dynload = args.wasm_root / WASM_DYNLOAD
 
-    # empty, unused directory for dynamic libs
+    # empty, unused directory for dynamic libs, but required for site initialization.
     args.wasm_dynload.mkdir(parents=True, exist_ok=True)
     marker = args.wasm_dynload / ".empty"
     marker.touch()

From f933b91a4fdfeececd9d101389fd1a4969827284 Mon Sep 17 00:00:00 2001
From: Christian Heimes <christian@python.org>
Date: Tue, 14 Dec 2021 09:52:58 +0100
Subject: [PATCH 3/5] Suggest emrun, explain writepy()

---
 Tools/wasm/README.md      | 12 +++++++++---
 Tools/wasm/wasm_assets.py |  1 +
 2 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/Tools/wasm/README.md b/Tools/wasm/README.md
index fe62e842dba4e3..58878ee83dd910 100644
--- a/Tools/wasm/README.md
+++ b/Tools/wasm/README.md
@@ -10,7 +10,7 @@ tool chain and a build Python interpreter.
 
 ### Compile a build Python interpreter
 
-```
+```shell
 mkdir -p builddir/build
 pushd builddir/build
 ../../configure -C
@@ -20,13 +20,13 @@ popd
 
 ### Fetch and build additional emscripten ports
 
-```
+```shell
 embuilder build zlib
 ```
 
 ### Cross compile to wasm32-emscripten
 
-```
+```shell
 mkdir -p builddir/emscripten
 pushd builddir/emscripten
 
@@ -43,6 +43,12 @@ emmake make -j$(nproc) python.html
 
 Serve ``python.html`` with a local webserver and open the file in a browser.
 
+```shell
+emrun python.html
 ```
+
+or
+
+```shell
 python3 -m http.server
 ```
diff --git a/Tools/wasm/wasm_assets.py b/Tools/wasm/wasm_assets.py
index 212b8ab2fb1c38..dc233ad8365996 100755
--- a/Tools/wasm/wasm_assets.py
+++ b/Tools/wasm/wasm_assets.py
@@ -127,6 +127,7 @@ def create_stdlib_zip(
             if entry in OMIT_ABSOLUTE:
                 continue
             if entry.name.endswith(".py") or entry.is_dir():
+                # writepy() writes .pyc files (bytecode)
                 pzf.writepy(entry, filterfunc=filterfunc)
         for entry in sysconfig_data:
             pzf.writepy(entry)

From 6c46ab4af1f752681a24f1a74d2ee77232a191fa Mon Sep 17 00:00:00 2001
From: Christian Heimes <christian@python.org>
Date: Sat, 18 Dec 2021 11:45:31 +0200
Subject: [PATCH 4/5] Brett's code suggestions: upper cases and full stops

Co-authored-by: Brett Cannon <brett@python.org>
---
 Tools/wasm/wasm_assets.py | 32 +++++++++++++++-----------------
 1 file changed, 15 insertions(+), 17 deletions(-)

diff --git a/Tools/wasm/wasm_assets.py b/Tools/wasm/wasm_assets.py
index dc233ad8365996..6a4027184030f7 100755
--- a/Tools/wasm/wasm_assets.py
+++ b/Tools/wasm/wasm_assets.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-"""Create a WASM asset bundle directory structure
+"""Create a WASM asset bundle directory structure.
 
 The WASM asset bundles are pre-loaded by the final WASM build. The bundle
 contains:
@@ -19,10 +19,10 @@
 SRCDIR = pathlib.Path(__file__).parent.parent.parent.absolute()
 SRCDIR_LIB = SRCDIR / "Lib"
 
-# sysconfig data relative to build dir
+# sysconfig data relative to build dir.
 SYSCONFIGDATA_GLOB = "build/lib.*/_sysconfigdata_*.py"
 
-# library directory relative to $(prefix)
+# Library directory relative to $(prefix).
 WASM_LIB = pathlib.PurePath("lib")
 WASM_STDLIB_ZIP = (
     WASM_LIB / f"python{sys.version_info.major}{sys.version_info.minor}.zip"
@@ -33,7 +33,7 @@
 WASM_DYNLOAD = WASM_STDLIB / "lib-dynload"
 
 
-# don't ship large files / packages that are not particular useful at
+# Don't ship large files / packages that are not particularly useful at
 # the moment.
 OMIT_FILES = (
     # regression tests
@@ -44,7 +44,7 @@
     "tkinter/",
     "turtle.py",
     "turtledemo/",
-    # venv / pip
+    # package management
     "ensurepip/",
     "venv/",
     # build system
@@ -56,8 +56,8 @@
     # deprecated
     "asyncore.py",
     "asynchat.py",
-    # synchronous network I/O and protocols are not supported, for example
-    # socket.create_connection() raises exception
+    # Synchronous network I/O and protocols are not supported; for example,
+    # socket.create_connection() raises an exception:
     # "BlockingIOError: [Errno 26] Operation in progress".
     "cgi.py",
     "cgitb.py",
@@ -85,14 +85,12 @@
     "webbrowser.py",
     # ctypes
     "ctypes/",
-    # pure Python implementations of C extensions
+    # Pure Python implementations of C extensions
     "_pydecimal.py",
     "_pyio.py",
-    # misc unused or large files
+    # Misc unused or large files
     "pydoc_data/",
     "msilib/",
-    # only pyc encoding files
-    # "encoding/*.py",
 )
 
 # regression test sub directories
@@ -112,8 +110,8 @@ def filterfunc(name: str) -> bool:
 
 
 def create_stdlib_zip(
-    args: argparse.Namespace, compression: int = zipfile.ZIP_DEFLATED, optimize: int = 0
-):
+    args: argparse.Namespace, compression: int = zipfile.ZIP_DEFLATED, *, optimize: int = 0
+) -> None:
     sysconfig_data = list(args.builddir.glob(SYSCONFIGDATA_GLOB))
     if not sysconfig_data:
         raise ValueError("No sysconfigdata file found")
@@ -127,7 +125,7 @@ def create_stdlib_zip(
             if entry in OMIT_ABSOLUTE:
                 continue
             if entry.name.endswith(".py") or entry.is_dir():
-                # writepy() writes .pyc files (bytecode)
+                # writepy() writes .pyc files (bytecode).
                 pzf.writepy(entry, filterfunc=filterfunc)
         for entry in sysconfig_data:
             pzf.writepy(entry)
@@ -160,13 +158,13 @@ def main():
     args.wasm_stdlib = args.wasm_root / WASM_STDLIB
     args.wasm_dynload = args.wasm_root / WASM_DYNLOAD
 
-    # empty, unused directory for dynamic libs, but required for site initialization.
+    # Empty, unused directory for dynamic libs, but required for site initialization.
     args.wasm_dynload.mkdir(parents=True, exist_ok=True)
     marker = args.wasm_dynload / ".empty"
     marker.touch()
-    # os.py is a marker for finding the correct lib directory
+    # os.py is a marker for finding the correct lib directory.
     shutil.copy(args.srcdir_lib / "os.py", args.wasm_stdlib)
-    # useful rest of stdlib
+    # The rest of stdlib that's useful in a WASM context.
     create_stdlib_zip(args)
     size = round(args.wasm_stdlib_zip.stat().st_size / 1024 ** 2, 2)
     parser.exit(0, f"Created {args.wasm_stdlib_zip} ({size} MiB)\n")

From d0782cab243d2382abdece9ab5e83ad9e91069c3 Mon Sep 17 00:00:00 2001
From: Christian Heimes <christian@python.org>
Date: Sat, 18 Dec 2021 11:48:11 +0200
Subject: [PATCH 5/5] More grammar fixes from Brett

Co-authored-by: Brett Cannon <brett@python.org>
---
 Tools/wasm/README.md | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/Tools/wasm/README.md b/Tools/wasm/README.md
index 58878ee83dd910..93c76b225db79b 100644
--- a/Tools/wasm/README.md
+++ b/Tools/wasm/README.md
@@ -1,12 +1,13 @@
 # Python WebAssembly (WASM) build
 
 This directory contains configuration and helpers to facilitate cross
-compile CPython to WebAssembly (WASM).
+compilation of CPython to WebAssembly (WASM).
 
 ## wasm32-emscripten build
 
-Cross compiling to wasm32-emscripten platform needs [Emscripten](https://emscripten.org/)
+Cross compiling to wasm32-emscripten platform needs the [Emscripten](https://emscripten.org/)
 tool chain and a build Python interpreter.
+All commands below are relative to a repository checkout.
 
 ### Compile a build Python interpreter
 
@@ -41,7 +42,7 @@ emmake make -j$(nproc) python.html
 
 ### Test in browser
 
-Serve ``python.html`` with a local webserver and open the file in a browser.
+Serve `python.html` with a local webserver and open the file in a browser.
 
 ```shell
 emrun python.html