usrbinenv python create a wasm asset bundle directory structure the wasm asset bundles are preloaded by the final wasm build the bundle contains a stripped down pyconly stdlib zip file e g prefixlibpython311 zip os py as marker module prefixlibpython3 11os py empty libdynload directory to make sure it is copied into the bundle prefixlibpython3 11libdynload empty source directory library directory relative to prefix don t ship large files packages that are not particularly useful at the moment regression tests package management other platforms webbrowser pure python implementations of c extensions concurrent threading misc unused or large files synchronous network io and protocols are not supported for example socket createconnection raises an exception blockingioerror errno 26 operation in progress keep urllib parse for pydoc get builddir path from pybuilddir txt with openpybuilddir txt encodingutf8 as f builddir f read return pathlib pathbuilddir def getsysconfigdataargs argparse namespace pathlib path writepy writes pyc files bytecode disabled by modulessetup local disabled by configure bpo17004 zipimport supports only zlib compression emscripten zipstored slz41 linker flags results in larger file empty unused directory for dynamic libs but required for site initialization os py is a marker for finding the correct lib directory the rest of stdlib that s useful in a wasm context 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 source directory library directory relative to prefix don t ship large files packages that are not particularly useful at the moment regression tests package management other platforms webbrowser pure python implementations of c extensions concurrent threading misc unused or large files synchronous network i o and protocols are not supported for example socket create_connection raises an exception blockingioerror errno 26 operation in progress keep urllib parse for pydoc get builddir path from pybuilddir txt get path to sysconfigdata relative to build root type ignore attr defined writepy writes pyc files bytecode disabled by modules setup local disabled by configure bpo 17004 zipimport supports only zlib compression emscripten zip_stored slz4 1 linker flags results in larger file empty unused directory for dynamic libs but required for site initialization os py is a marker for finding the correct lib directory the rest of stdlib that s useful in a wasm context
import argparse
import pathlib
import shutil
import sys
import sysconfig
import zipfile
from typing import Dict


SRCDIR = pathlib.Path(__file__).parent.parent.parent.absolute()
SRCDIR_LIB = SRCDIR / "Lib"



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"




OMIT_FILES = (
    
    "test/",
    
    "ensurepip/",
    "venv/",
    
    "_aix_support.py",
    "_osx_support.py",
    
    "antigravity.py",
    "webbrowser.py",
    
    "_pydecimal.py",
    "_pyio.py",
    
    "concurrent/futures/thread.py",
    
    "pydoc_data/",
)




OMIT_NETWORKING_FILES = (
    "email/",
    "ftplib.py",
    "http/",
    "imaplib.py",
    "mailbox.py",
    "poplib.py",
    "smtplib.py",
    "socketserver.py",
    
    "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:
    
    with open("pybuilddir.txt", encoding="utf-8") as f:
        builddir = f.read()
    return pathlib.Path(builddir)


def get_sysconfigdata(args: argparse.Namespace) -> pathlib.Path:
    
    assert isinstance(args.builddir, pathlib.Path)
    data_name: str = sysconfig._get_sysconfigdata_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


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.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()):
            entry = entry.resolve()
            if entry.name == "__pycache__":
                continue
            if entry.name.endswith(".py") or entry.is_dir():
                
                pzf.writepy(entry, filterfunc=filterfunc)


def detect_extension_modules(args: argparse.Namespace) -> Dict[str, bool]:
    modules = {}

    
    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

    
    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,
)


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_zip = args.wasm_root / WASM_STDLIB_ZIP
    args.wasm_stdlib = args.wasm_root / WASM_STDLIB
    args.wasm_dynload = args.wasm_root / WASM_DYNLOAD

    
    
    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
    }

    
    args.wasm_dynload.mkdir(parents=True, exist_ok=True)
    marker = args.wasm_dynload / ".empty"
    marker.touch()
    
    shutil.copy(args.srcdir_lib / "os.py", args.wasm_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()