| 
									
										
										
										
											2021-12-18 16:54:02 +02:00
										 |  |  | #!/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 | 
					
						
							| 
									
										
										
										
											2023-04-24 19:42:02 -03:00
										 |  |  | - empty lib-dynload directory, to make sure it is copied into the bundle: | 
					
						
							|  |  |  |     {PREFIX}/lib/python3.11/lib-dynload/.empty | 
					
						
							| 
									
										
										
										
											2021-12-18 16:54:02 +02:00
										 |  |  | """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import argparse | 
					
						
							|  |  |  | import pathlib | 
					
						
							|  |  |  | import shutil | 
					
						
							|  |  |  | import sys | 
					
						
							| 
									
										
										
										
											2022-06-18 14:51:50 +02:00
										 |  |  | import sysconfig | 
					
						
							| 
									
										
										
										
											2021-12-18 16:54:02 +02:00
										 |  |  | import zipfile | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # source directory | 
					
						
							|  |  |  | SRCDIR = pathlib.Path(__file__).parent.parent.parent.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/", | 
					
						
							|  |  |  |     # build system | 
					
						
							|  |  |  |     "lib2to3/", | 
					
						
							|  |  |  |     # deprecated | 
					
						
							| 
									
										
										
										
											2022-04-10 10:29:51 +03:00
										 |  |  |     "uu.py", | 
					
						
							|  |  |  |     "xdrlib.py", | 
					
						
							|  |  |  |     # other platforms | 
					
						
							|  |  |  |     "_aix_support.py", | 
					
						
							|  |  |  |     "_osx_support.py", | 
					
						
							|  |  |  |     # webbrowser | 
					
						
							|  |  |  |     "antigravity.py", | 
					
						
							|  |  |  |     "webbrowser.py", | 
					
						
							|  |  |  |     # Pure Python implementations of C extensions | 
					
						
							|  |  |  |     "_pydecimal.py", | 
					
						
							|  |  |  |     "_pyio.py", | 
					
						
							| 
									
										
										
										
											2022-09-17 13:23:39 +02:00
										 |  |  |     # concurrent threading | 
					
						
							|  |  |  |     "concurrent/futures/thread.py", | 
					
						
							| 
									
										
										
										
											2022-04-10 10:29:51 +03:00
										 |  |  |     # 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 = ( | 
					
						
							| 
									
										
										
										
											2021-12-18 16:54:02 +02:00
										 |  |  |     "cgi.py", | 
					
						
							|  |  |  |     "cgitb.py", | 
					
						
							|  |  |  |     "email/", | 
					
						
							|  |  |  |     "ftplib.py", | 
					
						
							|  |  |  |     "http/", | 
					
						
							|  |  |  |     "imaplib.py", | 
					
						
							| 
									
										
										
										
											2022-04-10 10:29:51 +03:00
										 |  |  |     "mailbox.py", | 
					
						
							|  |  |  |     "mailcap.py", | 
					
						
							| 
									
										
										
										
											2021-12-18 16:54:02 +02:00
										 |  |  |     "nntplib.py", | 
					
						
							|  |  |  |     "poplib.py", | 
					
						
							|  |  |  |     "smtplib.py", | 
					
						
							|  |  |  |     "socketserver.py", | 
					
						
							| 
									
										
										
										
											2022-01-24 23:02:01 +02:00
										 |  |  |     # keep urllib.parse for pydoc | 
					
						
							|  |  |  |     "urllib/error.py", | 
					
						
							|  |  |  |     "urllib/request.py", | 
					
						
							|  |  |  |     "urllib/response.py", | 
					
						
							|  |  |  |     "urllib/robotparser.py", | 
					
						
							| 
									
										
										
										
											2021-12-18 16:54:02 +02:00
										 |  |  |     "wsgiref/", | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-10 10:29:51 +03:00
										 |  |  | 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/"], | 
					
						
							| 
									
										
										
										
											2022-09-17 13:23:39 +02:00
										 |  |  |     "_multiprocessing": ["concurrent/futures/process.py", "multiprocessing/"], | 
					
						
							| 
									
										
										
										
											2022-04-10 10:29:51 +03:00
										 |  |  |     "pyexpat": ["xml/", "xmlrpc/"], | 
					
						
							|  |  |  |     "readline": ["rlcompleter.py"], | 
					
						
							|  |  |  |     "_sqlite3": ["sqlite3/"], | 
					
						
							|  |  |  |     "_ssl": ["ssl.py"], | 
					
						
							|  |  |  |     "_tkinter": ["idlelib/", "tkinter/", "turtle.py", "turtledemo/"], | 
					
						
							|  |  |  |     "_zoneinfo": ["zoneinfo/"], | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-19 08:36:12 +02:00
										 |  |  | SYSCONFIG_NAMES = ( | 
					
						
							|  |  |  |     "_sysconfigdata__emscripten_wasm32-emscripten", | 
					
						
							|  |  |  |     "_sysconfigdata__emscripten_wasm32-emscripten", | 
					
						
							|  |  |  |     "_sysconfigdata__wasi_wasm32-wasi", | 
					
						
							|  |  |  |     "_sysconfigdata__wasi_wasm64-wasi", | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-18 14:51:50 +02:00
										 |  |  | def get_builddir(args: argparse.Namespace) -> pathlib.Path: | 
					
						
							| 
									
										
										
										
											2022-09-17 13:23:39 +02:00
										 |  |  |     """Get builddir path from pybuilddir.txt""" | 
					
						
							| 
									
										
										
										
											2022-06-18 14:51:50 +02:00
										 |  |  |     with open("pybuilddir.txt", encoding="utf-8") as f: | 
					
						
							|  |  |  |         builddir = f.read() | 
					
						
							|  |  |  |     return pathlib.Path(builddir) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_sysconfigdata(args: argparse.Namespace) -> pathlib.Path: | 
					
						
							| 
									
										
										
										
											2022-09-17 13:23:39 +02:00
										 |  |  |     """Get path to sysconfigdata relative to build root""" | 
					
						
							| 
									
										
										
										
											2022-06-18 14:51:50 +02:00
										 |  |  |     data_name = sysconfig._get_sysconfigdata_name() | 
					
						
							| 
									
										
										
										
											2022-08-19 08:36:12 +02:00
										 |  |  |     if not data_name.startswith(SYSCONFIG_NAMES): | 
					
						
							|  |  |  |         raise ValueError( | 
					
						
							| 
									
										
										
										
											2022-09-17 13:23:39 +02:00
										 |  |  |             f"Invalid sysconfig data name '{data_name}'.", SYSCONFIG_NAMES | 
					
						
							| 
									
										
										
										
											2022-08-19 08:36:12 +02:00
										 |  |  |         ) | 
					
						
							| 
									
										
										
										
											2022-06-18 14:51:50 +02:00
										 |  |  |     filename = data_name + ".py" | 
					
						
							|  |  |  |     return args.builddir / filename | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-18 16:54:02 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | def create_stdlib_zip( | 
					
						
							| 
									
										
										
										
											2022-04-10 10:29:51 +03:00
										 |  |  |     args: argparse.Namespace, | 
					
						
							|  |  |  |     *, | 
					
						
							|  |  |  |     optimize: int = 0, | 
					
						
							| 
									
										
										
										
											2021-12-18 16:54:02 +02:00
										 |  |  | ) -> None: | 
					
						
							| 
									
										
										
										
											2022-09-17 13:23:39 +02:00
										 |  |  |     def filterfunc(filename: str) -> bool: | 
					
						
							|  |  |  |         pathname = pathlib.Path(filename).resolve() | 
					
						
							|  |  |  |         return pathname not in args.omit_files_absolute | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-18 16:54:02 +02:00
										 |  |  |     with zipfile.PyZipFile( | 
					
						
							| 
									
										
										
										
											2022-09-17 13:23:39 +02:00
										 |  |  |         args.wasm_stdlib_zip, | 
					
						
							|  |  |  |         mode="w", | 
					
						
							|  |  |  |         compression=args.compression, | 
					
						
							|  |  |  |         optimize=optimize, | 
					
						
							| 
									
										
										
										
											2021-12-18 16:54:02 +02:00
										 |  |  |     ) as pzf: | 
					
						
							| 
									
										
										
										
											2022-04-10 10:29:51 +03:00
										 |  |  |         if args.compresslevel is not None: | 
					
						
							|  |  |  |             pzf.compresslevel = args.compresslevel | 
					
						
							|  |  |  |         pzf.writepy(args.sysconfig_data) | 
					
						
							| 
									
										
										
										
											2021-12-18 16:54:02 +02:00
										 |  |  |         for entry in sorted(args.srcdir_lib.iterdir()): | 
					
						
							| 
									
										
										
										
											2022-09-17 13:23:39 +02:00
										 |  |  |             entry = entry.resolve() | 
					
						
							| 
									
										
										
										
											2021-12-18 16:54:02 +02:00
										 |  |  |             if entry.name == "__pycache__": | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             if entry.name.endswith(".py") or entry.is_dir(): | 
					
						
							|  |  |  |                 # writepy() writes .pyc files (bytecode). | 
					
						
							| 
									
										
										
										
											2022-09-17 13:23:39 +02:00
										 |  |  |                 pzf.writepy(entry, filterfunc=filterfunc) | 
					
						
							| 
									
										
										
										
											2022-04-10 10:29:51 +03:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def detect_extension_modules(args: argparse.Namespace): | 
					
						
							|  |  |  |     modules = {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # disabled by Modules/Setup.local ? | 
					
						
							| 
									
										
										
										
											2022-06-18 14:51:50 +02:00
										 |  |  |     with open(args.buildroot / "Makefile") as f: | 
					
						
							| 
									
										
										
										
											2022-04-10 10:29:51 +03:00
										 |  |  |         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) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-20 08:53:26 +02:00
										 |  |  |     for key, value in loc["build_time_vars"].items(): | 
					
						
							|  |  |  |         if not key.startswith("MODULE_") or not key.endswith("_STATE"): | 
					
						
							| 
									
										
										
										
											2022-04-10 10:29:51 +03:00
										 |  |  |             continue | 
					
						
							| 
									
										
										
										
											2022-07-20 08:53:26 +02:00
										 |  |  |         if value not in {"yes", "disabled", "missing", "n/a"}: | 
					
						
							|  |  |  |             raise ValueError(f"Unsupported value '{value}' for {key}") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         modname = key[7:-6].lower() | 
					
						
							| 
									
										
										
										
											2022-04-10 10:29:51 +03:00
										 |  |  |         if modname not in modules: | 
					
						
							|  |  |  |             modules[modname] = value == "yes" | 
					
						
							|  |  |  |     return modules | 
					
						
							| 
									
										
										
										
											2021-12-18 16:54:02 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def path(val: str) -> pathlib.Path: | 
					
						
							|  |  |  |     return pathlib.Path(val).absolute() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | parser = argparse.ArgumentParser() | 
					
						
							|  |  |  | parser.add_argument( | 
					
						
							| 
									
										
										
										
											2022-06-18 14:51:50 +02:00
										 |  |  |     "--buildroot", | 
					
						
							|  |  |  |     help="absolute path to build root", | 
					
						
							| 
									
										
										
										
											2021-12-18 16:54:02 +02:00
										 |  |  |     default=pathlib.Path(".").absolute(), | 
					
						
							|  |  |  |     type=path, | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | parser.add_argument( | 
					
						
							| 
									
										
										
										
											2022-04-10 10:29:51 +03:00
										 |  |  |     "--prefix", | 
					
						
							|  |  |  |     help="install prefix", | 
					
						
							|  |  |  |     default=pathlib.Path("/usr/local"), | 
					
						
							|  |  |  |     type=path, | 
					
						
							| 
									
										
										
										
											2021-12-18 16:54:02 +02:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def main(): | 
					
						
							|  |  |  |     args = parser.parse_args() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     relative_prefix = args.prefix.relative_to(pathlib.Path("/")) | 
					
						
							|  |  |  |     args.srcdir = SRCDIR | 
					
						
							|  |  |  |     args.srcdir_lib = SRCDIR_LIB | 
					
						
							| 
									
										
										
										
											2022-06-18 14:51:50 +02:00
										 |  |  |     args.wasm_root = args.buildroot / relative_prefix | 
					
						
							| 
									
										
										
										
											2021-12-18 16:54:02 +02:00
										 |  |  |     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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-10 10:29:51 +03:00
										 |  |  |     # 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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-18 14:51:50 +02:00
										 |  |  |     args.builddir = get_builddir(args) | 
					
						
							|  |  |  |     args.sysconfig_data = get_sysconfigdata(args) | 
					
						
							| 
									
										
										
										
											2022-04-10 10:29:51 +03:00
										 |  |  |     if not args.sysconfig_data.is_file(): | 
					
						
							| 
									
										
										
										
											2022-06-18 14:51:50 +02:00
										 |  |  |         raise ValueError(f"sysconfigdata file {args.sysconfig_data} missing.") | 
					
						
							| 
									
										
										
										
											2022-04-10 10:29:51 +03:00
										 |  |  | 
 | 
					
						
							|  |  |  |     extmods = detect_extension_modules(args) | 
					
						
							|  |  |  |     omit_files = list(OMIT_FILES) | 
					
						
							| 
									
										
										
										
											2022-08-30 06:36:11 +02:00
										 |  |  |     if sysconfig.get_platform().startswith("emscripten"): | 
					
						
							|  |  |  |         omit_files.extend(OMIT_NETWORKING_FILES) | 
					
						
							| 
									
										
										
										
											2022-04-10 10:29:51 +03:00
										 |  |  |     for modname, modfiles in OMIT_MODULE_FILES.items(): | 
					
						
							|  |  |  |         if not extmods.get(modname): | 
					
						
							|  |  |  |             omit_files.extend(modfiles) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-17 13:23:39 +02:00
										 |  |  |     args.omit_files_absolute = { | 
					
						
							|  |  |  |         (args.srcdir_lib / name).resolve() for name in omit_files | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2022-04-10 10:29:51 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-18 16:54:02 +02:00
										 |  |  |     # 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) | 
					
						
							| 
									
										
										
										
											2022-04-10 10:29:51 +03:00
										 |  |  |     size = round(args.wasm_stdlib_zip.stat().st_size / 1024**2, 2) | 
					
						
							| 
									
										
										
										
											2021-12-18 16:54:02 +02:00
										 |  |  |     parser.exit(0, f"Created {args.wasm_stdlib_zip} ({size} MiB)\n") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if __name__ == "__main__": | 
					
						
							|  |  |  |     main() |