diff options
author | Hood Chatham <roberthoodchatham@gmail.com> | 2024-12-03 00:30:24 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-12-03 07:30:24 +0800 |
commit | bfb0788bfcaab7474c1be0605552744e15082ee9 (patch) | |
tree | 455a8e723f50b1f9e6782c4210c5d39b57f23b3b /Tools/wasm/emscripten/web_example/wasm_assets.py | |
parent | edefb8678a11a20bdcdcbb8bb6a62ae22101bb51 (diff) | |
download | cpython-bfb0788bfcaab7474c1be0605552744e15082ee9.tar.gz cpython-bfb0788bfcaab7474c1be0605552744e15082ee9.zip |
gh-127111: Emscripten Make web example work again (#127113)
Moves the Emscripten web example into a standalone folder, and updates
Makefile targets to build the web example. Instructions for usage have
also been added.
Diffstat (limited to 'Tools/wasm/emscripten/web_example/wasm_assets.py')
-rwxr-xr-x | Tools/wasm/emscripten/web_example/wasm_assets.py | 245 |
1 files changed, 245 insertions, 0 deletions
diff --git a/Tools/wasm/emscripten/web_example/wasm_assets.py b/Tools/wasm/emscripten/web_example/wasm_assets.py new file mode 100755 index 00000000000..7f0fa7ae7c1 --- /dev/null +++ b/Tools/wasm/emscripten/web_example/wasm_assets.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python +"""Create a WASM asset bundle directory structure. + +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, to make sure it is copied into the bundle: + {PREFIX}/lib/python3.11/lib-dynload/.empty +""" + +import argparse +import pathlib +import shutil +import sys +import sysconfig +import zipfile +from typing import Dict + +# source directory +SRCDIR = pathlib.Path(__file__).parents[4].absolute() +SRCDIR_LIB = SRCDIR / "Lib" + + +# 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 particularly useful at +# the moment. +OMIT_FILES = ( + # regression tests + "test/", + # package management + "ensurepip/", + "venv/", + # other platforms + "_aix_support.py", + "_osx_support.py", + # webbrowser + "antigravity.py", + "webbrowser.py", + # Pure Python implementations of C extensions + "_pydecimal.py", + "_pyio.py", + # concurrent threading + "concurrent/futures/thread.py", + # Misc unused or large files + "pydoc_data/", +) + +# Synchronous network I/O and protocols are not supported; for example, +# socket.create_connection() raises an exception: +# "BlockingIOError: [Errno 26] Operation in progress". +OMIT_NETWORKING_FILES = ( + "email/", + "ftplib.py", + "http/", + "imaplib.py", + "mailbox.py", + "poplib.py", + "smtplib.py", + "socketserver.py", + # keep urllib.parse for pydoc + "urllib/error.py", + "urllib/request.py", + "urllib/response.py", + "urllib/robotparser.py", + "wsgiref/", +) + +OMIT_MODULE_FILES = { + "_asyncio": ["asyncio/"], + "_curses": ["curses/"], + "_ctypes": ["ctypes/"], + "_decimal": ["decimal.py"], + "_dbm": ["dbm/ndbm.py"], + "_gdbm": ["dbm/gnu.py"], + "_json": ["json/"], + "_multiprocessing": ["concurrent/futures/process.py", "multiprocessing/"], + "pyexpat": ["xml/", "xmlrpc/"], + "readline": ["rlcompleter.py"], + "_sqlite3": ["sqlite3/"], + "_ssl": ["ssl.py"], + "_tkinter": ["idlelib/", "tkinter/", "turtle.py", "turtledemo/"], + "_zoneinfo": ["zoneinfo/"], +} + +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""" + with open("pybuilddir.txt", encoding="utf-8") as f: + builddir = f.read() + return pathlib.Path(builddir) + + +def get_sysconfigdata(args: argparse.Namespace) -> pathlib.Path: + """Get path to sysconfigdata relative to build root""" + assert isinstance(args.builddir, pathlib.Path) + data_name: str = sysconfig._get_sysconfigdata_name() # type: ignore[attr-defined] + 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 + + +def create_stdlib_zip( + args: argparse.Namespace, + *, + optimize: int = 0, +) -> None: + def filterfunc(filename: str) -> bool: + pathname = pathlib.Path(filename).resolve() + return pathname not in args.omit_files_absolute + + with zipfile.PyZipFile( + args.output, + mode="w", + compression=args.compression, + optimize=optimize, + ) as pzf: + if args.compresslevel is not None: + pzf.compresslevel = args.compresslevel + pzf.writepy(args.sysconfig_data) + for entry in sorted(args.srcdir_lib.iterdir()): + entry = entry.resolve() + if entry.name == "__pycache__": + continue + if entry.name.endswith(".py") or entry.is_dir(): + # writepy() writes .pyc files (bytecode). + pzf.writepy(entry, filterfunc=filterfunc) + + +def detect_extension_modules(args: argparse.Namespace) -> Dict[str, bool]: + modules = {} + + # disabled by Modules/Setup.local ? + with open(args.buildroot / "Makefile") as f: + for line in f: + if line.startswith("MODDISABLED_NAMES="): + disabled = line.split("=", 1)[1].strip().split() + for modname in disabled: + modules[modname] = False + break + + # disabled by configure? + with open(args.sysconfig_data) as f: + data = f.read() + loc: Dict[str, Dict[str, str]] = {} + exec(data, globals(), loc) + + for key, value in loc["build_time_vars"].items(): + if not key.startswith("MODULE_") or not key.endswith("_STATE"): + continue + if value not in {"yes", "disabled", "missing", "n/a"}: + raise ValueError(f"Unsupported value '{value}' for {key}") + + modname = key[7:-6].lower() + if modname not in modules: + modules[modname] = value == "yes" + return modules + + +def path(val: str) -> pathlib.Path: + return pathlib.Path(val).absolute() + + +parser = argparse.ArgumentParser() +parser.add_argument( + "--buildroot", + help="absolute path to build root", + default=pathlib.Path(".").absolute(), + type=path, +) +parser.add_argument( + "--prefix", + help="install prefix", + default=pathlib.Path("/usr/local"), + type=path, +) +parser.add_argument( + "-o", + "--output", + help="output file", + type=path, +) + + +def main() -> None: + args = parser.parse_args() + + relative_prefix = args.prefix.relative_to(pathlib.Path("/")) + args.srcdir = SRCDIR + args.srcdir_lib = SRCDIR_LIB + args.wasm_root = args.buildroot / relative_prefix + args.wasm_stdlib = args.wasm_root / WASM_STDLIB + args.wasm_dynload = args.wasm_root / WASM_DYNLOAD + + # bpo-17004: zipimport supports only zlib compression. + # Emscripten ZIP_STORED + -sLZ4=1 linker flags results in larger file. + args.compression = zipfile.ZIP_DEFLATED + args.compresslevel = 9 + + args.builddir = get_builddir(args) + args.sysconfig_data = get_sysconfigdata(args) + if not args.sysconfig_data.is_file(): + raise ValueError(f"sysconfigdata file {args.sysconfig_data} missing.") + + extmods = detect_extension_modules(args) + omit_files = list(OMIT_FILES) + if sysconfig.get_platform().startswith("emscripten"): + omit_files.extend(OMIT_NETWORKING_FILES) + for modname, modfiles in OMIT_MODULE_FILES.items(): + if not extmods.get(modname): + omit_files.extend(modfiles) + + args.omit_files_absolute = { + (args.srcdir_lib / name).resolve() for name in omit_files + } + + # 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() + # The rest of stdlib that's useful in a WASM context. + create_stdlib_zip(args) + size = round(args.output.stat().st_size / 1024**2, 2) + parser.exit(0, f"Created {args.output} ({size} MiB)\n") + + +if __name__ == "__main__": + main() |