#!/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 zipfile # source directory SRCDIR = pathlib.Path(__file__).parent.parent.parent.absolute() SRCDIR_LIB = SRCDIR / "Lib" # sysconfig data relative to build dir. SYSCONFIGDATA = pathlib.PurePath( "build", f"lib.emscripten-wasm32-{sys.version_info.major}.{sys.version_info.minor}", "_sysconfigdata__emscripten_wasm32-emscripten.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 particularly useful at # the moment. OMIT_FILES = ( # regression tests "test/", # package management "ensurepip/", "venv/", # build system "distutils/", "lib2to3/", # deprecated "asyncore.py", "asynchat.py", "uu.py", "xdrlib.py", # other platforms "_aix_support.py", "_bootsubprocess.py", "_osx_support.py", # webbrowser "antigravity.py", "webbrowser.py", # Pure Python implementations of C extensions "_pydecimal.py", "_pyio.py", # Misc unused or large files "pydoc_data/", "msilib/", ) # 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 = ( "cgi.py", "cgitb.py", "email/", "ftplib.py", "http/", "imaplib.py", "mailbox.py", "mailcap.py", "nntplib.py", "poplib.py", "smtpd.py", "smtplib.py", "socketserver.py", "telnetlib.py", # keep urllib.parse for pydoc "urllib/error.py", "urllib/request.py", "urllib/response.py", "urllib/robotparser.py", "wsgiref/", ) OMIT_MODULE_FILES = { "_asyncio": ["asyncio/"], "audioop": ["aifc.py", "sunau.py", "wave.py"], "_crypt": ["crypt.py"], "_curses": ["curses/"], "_ctypes": ["ctypes/"], "_decimal": ["decimal.py"], "_dbm": ["dbm/ndbm.py"], "_gdbm": ["dbm/gnu.py"], "_json": ["json/"], "_multiprocessing": ["concurrent/", "multiprocessing/"], "pyexpat": ["xml/", "xmlrpc/"], "readline": ["rlcompleter.py"], "_sqlite3": ["sqlite3/"], "_ssl": ["ssl.py"], "_tkinter": ["idlelib/", "tkinter/", "turtle.py", "turtledemo/"], "_zoneinfo": ["zoneinfo/"], } # regression test sub directories OMIT_SUBDIRS = ( "ctypes/test/", "tkinter/test/", "unittest/test/", ) def create_stdlib_zip( args: argparse.Namespace, *, optimize: int = 0, ) -> None: def filterfunc(name: str) -> bool: return not name.startswith(args.omit_subdirs_absolute) with zipfile.PyZipFile( args.wasm_stdlib_zip, 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()): if entry.name == "__pycache__": continue if entry in args.omit_files_absolute: 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): modules = {} # disabled by Modules/Setup.local ? with open(args.builddir / "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 = {} exec(data, globals(), loc) for name, value in loc["build_time_vars"].items(): if value not in {"yes", "missing", "disabled", "n/a"}: continue if not name.startswith("MODULE_"): continue if name.endswith(("_CFLAGS", "_DEPS", "_LDFLAGS")): continue modname = name.removeprefix("MODULE_").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( "--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 # 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.sysconfig_data = args.builddir / SYSCONFIGDATA if not args.sysconfig_data.is_file(): raise ValueError(f"sysconfigdata file {SYSCONFIGDATA} missing.") extmods = detect_extension_modules(args) omit_files = list(OMIT_FILES) 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 for name in omit_files} args.omit_subdirs_absolute = tuple( str(args.srcdir_lib / name) for name in OMIT_SUBDIRS ) # 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. shutil.copy(args.srcdir_lib / "os.py", args.wasm_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") if __name__ == "__main__": main()