17db96d56Sopenharmony_ci#!/usr/bin/env python
27db96d56Sopenharmony_ci"""Create a WASM asset bundle directory structure.
37db96d56Sopenharmony_ci
47db96d56Sopenharmony_ciThe WASM asset bundles are pre-loaded by the final WASM build. The bundle
57db96d56Sopenharmony_cicontains:
67db96d56Sopenharmony_ci
77db96d56Sopenharmony_ci- a stripped down, pyc-only stdlib zip file, e.g. {PREFIX}/lib/python311.zip
87db96d56Sopenharmony_ci- os.py as marker module {PREFIX}/lib/python3.11/os.py
97db96d56Sopenharmony_ci- empty lib-dynload directory, to make sure it is copied into the bundle {PREFIX}/lib/python3.11/lib-dynload/.empty
107db96d56Sopenharmony_ci"""
117db96d56Sopenharmony_ci
127db96d56Sopenharmony_ciimport argparse
137db96d56Sopenharmony_ciimport pathlib
147db96d56Sopenharmony_ciimport shutil
157db96d56Sopenharmony_ciimport sys
167db96d56Sopenharmony_ciimport sysconfig
177db96d56Sopenharmony_ciimport zipfile
187db96d56Sopenharmony_ci
197db96d56Sopenharmony_ci# source directory
207db96d56Sopenharmony_ciSRCDIR = pathlib.Path(__file__).parent.parent.parent.absolute()
217db96d56Sopenharmony_ciSRCDIR_LIB = SRCDIR / "Lib"
227db96d56Sopenharmony_ci
237db96d56Sopenharmony_ci
247db96d56Sopenharmony_ci# Library directory relative to $(prefix).
257db96d56Sopenharmony_ciWASM_LIB = pathlib.PurePath("lib")
267db96d56Sopenharmony_ciWASM_STDLIB_ZIP = (
277db96d56Sopenharmony_ci    WASM_LIB / f"python{sys.version_info.major}{sys.version_info.minor}.zip"
287db96d56Sopenharmony_ci)
297db96d56Sopenharmony_ciWASM_STDLIB = (
307db96d56Sopenharmony_ci    WASM_LIB / f"python{sys.version_info.major}.{sys.version_info.minor}"
317db96d56Sopenharmony_ci)
327db96d56Sopenharmony_ciWASM_DYNLOAD = WASM_STDLIB / "lib-dynload"
337db96d56Sopenharmony_ci
347db96d56Sopenharmony_ci
357db96d56Sopenharmony_ci# Don't ship large files / packages that are not particularly useful at
367db96d56Sopenharmony_ci# the moment.
377db96d56Sopenharmony_ciOMIT_FILES = (
387db96d56Sopenharmony_ci    # regression tests
397db96d56Sopenharmony_ci    "test/",
407db96d56Sopenharmony_ci    # package management
417db96d56Sopenharmony_ci    "ensurepip/",
427db96d56Sopenharmony_ci    "venv/",
437db96d56Sopenharmony_ci    # build system
447db96d56Sopenharmony_ci    "distutils/",
457db96d56Sopenharmony_ci    "lib2to3/",
467db96d56Sopenharmony_ci    # deprecated
477db96d56Sopenharmony_ci    "asyncore.py",
487db96d56Sopenharmony_ci    "asynchat.py",
497db96d56Sopenharmony_ci    "uu.py",
507db96d56Sopenharmony_ci    "xdrlib.py",
517db96d56Sopenharmony_ci    # other platforms
527db96d56Sopenharmony_ci    "_aix_support.py",
537db96d56Sopenharmony_ci    "_bootsubprocess.py",
547db96d56Sopenharmony_ci    "_osx_support.py",
557db96d56Sopenharmony_ci    # webbrowser
567db96d56Sopenharmony_ci    "antigravity.py",
577db96d56Sopenharmony_ci    "webbrowser.py",
587db96d56Sopenharmony_ci    # Pure Python implementations of C extensions
597db96d56Sopenharmony_ci    "_pydecimal.py",
607db96d56Sopenharmony_ci    "_pyio.py",
617db96d56Sopenharmony_ci    # concurrent threading
627db96d56Sopenharmony_ci    "concurrent/futures/thread.py",
637db96d56Sopenharmony_ci    # Misc unused or large files
647db96d56Sopenharmony_ci    "pydoc_data/",
657db96d56Sopenharmony_ci    "msilib/",
667db96d56Sopenharmony_ci)
677db96d56Sopenharmony_ci
687db96d56Sopenharmony_ci# Synchronous network I/O and protocols are not supported; for example,
697db96d56Sopenharmony_ci# socket.create_connection() raises an exception:
707db96d56Sopenharmony_ci# "BlockingIOError: [Errno 26] Operation in progress".
717db96d56Sopenharmony_ciOMIT_NETWORKING_FILES = (
727db96d56Sopenharmony_ci    "cgi.py",
737db96d56Sopenharmony_ci    "cgitb.py",
747db96d56Sopenharmony_ci    "email/",
757db96d56Sopenharmony_ci    "ftplib.py",
767db96d56Sopenharmony_ci    "http/",
777db96d56Sopenharmony_ci    "imaplib.py",
787db96d56Sopenharmony_ci    "mailbox.py",
797db96d56Sopenharmony_ci    "mailcap.py",
807db96d56Sopenharmony_ci    "nntplib.py",
817db96d56Sopenharmony_ci    "poplib.py",
827db96d56Sopenharmony_ci    "smtpd.py",
837db96d56Sopenharmony_ci    "smtplib.py",
847db96d56Sopenharmony_ci    "socketserver.py",
857db96d56Sopenharmony_ci    "telnetlib.py",
867db96d56Sopenharmony_ci    # keep urllib.parse for pydoc
877db96d56Sopenharmony_ci    "urllib/error.py",
887db96d56Sopenharmony_ci    "urllib/request.py",
897db96d56Sopenharmony_ci    "urllib/response.py",
907db96d56Sopenharmony_ci    "urllib/robotparser.py",
917db96d56Sopenharmony_ci    "wsgiref/",
927db96d56Sopenharmony_ci)
937db96d56Sopenharmony_ci
947db96d56Sopenharmony_ciOMIT_MODULE_FILES = {
957db96d56Sopenharmony_ci    "_asyncio": ["asyncio/"],
967db96d56Sopenharmony_ci    "audioop": ["aifc.py", "sunau.py", "wave.py"],
977db96d56Sopenharmony_ci    "_crypt": ["crypt.py"],
987db96d56Sopenharmony_ci    "_curses": ["curses/"],
997db96d56Sopenharmony_ci    "_ctypes": ["ctypes/"],
1007db96d56Sopenharmony_ci    "_decimal": ["decimal.py"],
1017db96d56Sopenharmony_ci    "_dbm": ["dbm/ndbm.py"],
1027db96d56Sopenharmony_ci    "_gdbm": ["dbm/gnu.py"],
1037db96d56Sopenharmony_ci    "_json": ["json/"],
1047db96d56Sopenharmony_ci    "_multiprocessing": ["concurrent/futures/process.py", "multiprocessing/"],
1057db96d56Sopenharmony_ci    "pyexpat": ["xml/", "xmlrpc/"],
1067db96d56Sopenharmony_ci    "readline": ["rlcompleter.py"],
1077db96d56Sopenharmony_ci    "_sqlite3": ["sqlite3/"],
1087db96d56Sopenharmony_ci    "_ssl": ["ssl.py"],
1097db96d56Sopenharmony_ci    "_tkinter": ["idlelib/", "tkinter/", "turtle.py", "turtledemo/"],
1107db96d56Sopenharmony_ci    "_zoneinfo": ["zoneinfo/"],
1117db96d56Sopenharmony_ci}
1127db96d56Sopenharmony_ci
1137db96d56Sopenharmony_ci# regression test sub directories
1147db96d56Sopenharmony_ciOMIT_SUBDIRS = (
1157db96d56Sopenharmony_ci    "ctypes/test/",
1167db96d56Sopenharmony_ci    "tkinter/test/",
1177db96d56Sopenharmony_ci    "unittest/test/",
1187db96d56Sopenharmony_ci)
1197db96d56Sopenharmony_ci
1207db96d56Sopenharmony_ciSYSCONFIG_NAMES = (
1217db96d56Sopenharmony_ci    "_sysconfigdata__emscripten_wasm32-emscripten",
1227db96d56Sopenharmony_ci    "_sysconfigdata__emscripten_wasm32-emscripten",
1237db96d56Sopenharmony_ci    "_sysconfigdata__wasi_wasm32-wasi",
1247db96d56Sopenharmony_ci    "_sysconfigdata__wasi_wasm64-wasi",
1257db96d56Sopenharmony_ci)
1267db96d56Sopenharmony_ci
1277db96d56Sopenharmony_ci
1287db96d56Sopenharmony_cidef get_builddir(args: argparse.Namespace) -> pathlib.Path:
1297db96d56Sopenharmony_ci    """Get builddir path from pybuilddir.txt"""
1307db96d56Sopenharmony_ci    with open("pybuilddir.txt", encoding="utf-8") as f:
1317db96d56Sopenharmony_ci        builddir = f.read()
1327db96d56Sopenharmony_ci    return pathlib.Path(builddir)
1337db96d56Sopenharmony_ci
1347db96d56Sopenharmony_ci
1357db96d56Sopenharmony_cidef get_sysconfigdata(args: argparse.Namespace) -> pathlib.Path:
1367db96d56Sopenharmony_ci    """Get path to sysconfigdata relative to build root"""
1377db96d56Sopenharmony_ci    data_name = sysconfig._get_sysconfigdata_name()
1387db96d56Sopenharmony_ci    if not data_name.startswith(SYSCONFIG_NAMES):
1397db96d56Sopenharmony_ci        raise ValueError(
1407db96d56Sopenharmony_ci            f"Invalid sysconfig data name '{data_name}'.", SYSCONFIG_NAMES
1417db96d56Sopenharmony_ci        )
1427db96d56Sopenharmony_ci    filename = data_name + ".py"
1437db96d56Sopenharmony_ci    return args.builddir / filename
1447db96d56Sopenharmony_ci
1457db96d56Sopenharmony_ci
1467db96d56Sopenharmony_cidef create_stdlib_zip(
1477db96d56Sopenharmony_ci    args: argparse.Namespace,
1487db96d56Sopenharmony_ci    *,
1497db96d56Sopenharmony_ci    optimize: int = 0,
1507db96d56Sopenharmony_ci) -> None:
1517db96d56Sopenharmony_ci    def filterfunc(filename: str) -> bool:
1527db96d56Sopenharmony_ci        pathname = pathlib.Path(filename).resolve()
1537db96d56Sopenharmony_ci        return pathname not in args.omit_files_absolute
1547db96d56Sopenharmony_ci
1557db96d56Sopenharmony_ci    with zipfile.PyZipFile(
1567db96d56Sopenharmony_ci        args.wasm_stdlib_zip,
1577db96d56Sopenharmony_ci        mode="w",
1587db96d56Sopenharmony_ci        compression=args.compression,
1597db96d56Sopenharmony_ci        optimize=optimize,
1607db96d56Sopenharmony_ci    ) as pzf:
1617db96d56Sopenharmony_ci        if args.compresslevel is not None:
1627db96d56Sopenharmony_ci            pzf.compresslevel = args.compresslevel
1637db96d56Sopenharmony_ci        pzf.writepy(args.sysconfig_data)
1647db96d56Sopenharmony_ci        for entry in sorted(args.srcdir_lib.iterdir()):
1657db96d56Sopenharmony_ci            entry = entry.resolve()
1667db96d56Sopenharmony_ci            if entry.name == "__pycache__":
1677db96d56Sopenharmony_ci                continue
1687db96d56Sopenharmony_ci            if entry.name.endswith(".py") or entry.is_dir():
1697db96d56Sopenharmony_ci                # writepy() writes .pyc files (bytecode).
1707db96d56Sopenharmony_ci                pzf.writepy(entry, filterfunc=filterfunc)
1717db96d56Sopenharmony_ci
1727db96d56Sopenharmony_ci
1737db96d56Sopenharmony_cidef detect_extension_modules(args: argparse.Namespace):
1747db96d56Sopenharmony_ci    modules = {}
1757db96d56Sopenharmony_ci
1767db96d56Sopenharmony_ci    # disabled by Modules/Setup.local ?
1777db96d56Sopenharmony_ci    with open(args.buildroot / "Makefile") as f:
1787db96d56Sopenharmony_ci        for line in f:
1797db96d56Sopenharmony_ci            if line.startswith("MODDISABLED_NAMES="):
1807db96d56Sopenharmony_ci                disabled = line.split("=", 1)[1].strip().split()
1817db96d56Sopenharmony_ci                for modname in disabled:
1827db96d56Sopenharmony_ci                    modules[modname] = False
1837db96d56Sopenharmony_ci                break
1847db96d56Sopenharmony_ci
1857db96d56Sopenharmony_ci    # disabled by configure?
1867db96d56Sopenharmony_ci    with open(args.sysconfig_data) as f:
1877db96d56Sopenharmony_ci        data = f.read()
1887db96d56Sopenharmony_ci    loc = {}
1897db96d56Sopenharmony_ci    exec(data, globals(), loc)
1907db96d56Sopenharmony_ci
1917db96d56Sopenharmony_ci    for key, value in loc["build_time_vars"].items():
1927db96d56Sopenharmony_ci        if not key.startswith("MODULE_") or not key.endswith("_STATE"):
1937db96d56Sopenharmony_ci            continue
1947db96d56Sopenharmony_ci        if value not in {"yes", "disabled", "missing", "n/a"}:
1957db96d56Sopenharmony_ci            raise ValueError(f"Unsupported value '{value}' for {key}")
1967db96d56Sopenharmony_ci
1977db96d56Sopenharmony_ci        modname = key[7:-6].lower()
1987db96d56Sopenharmony_ci        if modname not in modules:
1997db96d56Sopenharmony_ci            modules[modname] = value == "yes"
2007db96d56Sopenharmony_ci    return modules
2017db96d56Sopenharmony_ci
2027db96d56Sopenharmony_ci
2037db96d56Sopenharmony_cidef path(val: str) -> pathlib.Path:
2047db96d56Sopenharmony_ci    return pathlib.Path(val).absolute()
2057db96d56Sopenharmony_ci
2067db96d56Sopenharmony_ci
2077db96d56Sopenharmony_ciparser = argparse.ArgumentParser()
2087db96d56Sopenharmony_ciparser.add_argument(
2097db96d56Sopenharmony_ci    "--buildroot",
2107db96d56Sopenharmony_ci    help="absolute path to build root",
2117db96d56Sopenharmony_ci    default=pathlib.Path(".").absolute(),
2127db96d56Sopenharmony_ci    type=path,
2137db96d56Sopenharmony_ci)
2147db96d56Sopenharmony_ciparser.add_argument(
2157db96d56Sopenharmony_ci    "--prefix",
2167db96d56Sopenharmony_ci    help="install prefix",
2177db96d56Sopenharmony_ci    default=pathlib.Path("/usr/local"),
2187db96d56Sopenharmony_ci    type=path,
2197db96d56Sopenharmony_ci)
2207db96d56Sopenharmony_ci
2217db96d56Sopenharmony_ci
2227db96d56Sopenharmony_cidef main():
2237db96d56Sopenharmony_ci    args = parser.parse_args()
2247db96d56Sopenharmony_ci
2257db96d56Sopenharmony_ci    relative_prefix = args.prefix.relative_to(pathlib.Path("/"))
2267db96d56Sopenharmony_ci    args.srcdir = SRCDIR
2277db96d56Sopenharmony_ci    args.srcdir_lib = SRCDIR_LIB
2287db96d56Sopenharmony_ci    args.wasm_root = args.buildroot / relative_prefix
2297db96d56Sopenharmony_ci    args.wasm_stdlib_zip = args.wasm_root / WASM_STDLIB_ZIP
2307db96d56Sopenharmony_ci    args.wasm_stdlib = args.wasm_root / WASM_STDLIB
2317db96d56Sopenharmony_ci    args.wasm_dynload = args.wasm_root / WASM_DYNLOAD
2327db96d56Sopenharmony_ci
2337db96d56Sopenharmony_ci    # bpo-17004: zipimport supports only zlib compression.
2347db96d56Sopenharmony_ci    # Emscripten ZIP_STORED + -sLZ4=1 linker flags results in larger file.
2357db96d56Sopenharmony_ci    args.compression = zipfile.ZIP_DEFLATED
2367db96d56Sopenharmony_ci    args.compresslevel = 9
2377db96d56Sopenharmony_ci
2387db96d56Sopenharmony_ci    args.builddir = get_builddir(args)
2397db96d56Sopenharmony_ci    args.sysconfig_data = get_sysconfigdata(args)
2407db96d56Sopenharmony_ci    if not args.sysconfig_data.is_file():
2417db96d56Sopenharmony_ci        raise ValueError(f"sysconfigdata file {args.sysconfig_data} missing.")
2427db96d56Sopenharmony_ci
2437db96d56Sopenharmony_ci    extmods = detect_extension_modules(args)
2447db96d56Sopenharmony_ci    omit_files = list(OMIT_FILES)
2457db96d56Sopenharmony_ci    if sysconfig.get_platform().startswith("emscripten"):
2467db96d56Sopenharmony_ci        omit_files.extend(OMIT_NETWORKING_FILES)
2477db96d56Sopenharmony_ci    for modname, modfiles in OMIT_MODULE_FILES.items():
2487db96d56Sopenharmony_ci        if not extmods.get(modname):
2497db96d56Sopenharmony_ci            omit_files.extend(modfiles)
2507db96d56Sopenharmony_ci
2517db96d56Sopenharmony_ci    args.omit_files_absolute = {
2527db96d56Sopenharmony_ci        (args.srcdir_lib / name).resolve() for name in omit_files
2537db96d56Sopenharmony_ci    }
2547db96d56Sopenharmony_ci
2557db96d56Sopenharmony_ci    # Empty, unused directory for dynamic libs, but required for site initialization.
2567db96d56Sopenharmony_ci    args.wasm_dynload.mkdir(parents=True, exist_ok=True)
2577db96d56Sopenharmony_ci    marker = args.wasm_dynload / ".empty"
2587db96d56Sopenharmony_ci    marker.touch()
2597db96d56Sopenharmony_ci    # os.py is a marker for finding the correct lib directory.
2607db96d56Sopenharmony_ci    shutil.copy(args.srcdir_lib / "os.py", args.wasm_stdlib)
2617db96d56Sopenharmony_ci    # The rest of stdlib that's useful in a WASM context.
2627db96d56Sopenharmony_ci    create_stdlib_zip(args)
2637db96d56Sopenharmony_ci    size = round(args.wasm_stdlib_zip.stat().st_size / 1024**2, 2)
2647db96d56Sopenharmony_ci    parser.exit(0, f"Created {args.wasm_stdlib_zip} ({size} MiB)\n")
2657db96d56Sopenharmony_ci
2667db96d56Sopenharmony_ci
2677db96d56Sopenharmony_ciif __name__ == "__main__":
2687db96d56Sopenharmony_ci    main()
269