| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1183,7 +1183,9 @@ always available. | |||
| 1183 | 1183 | System ``platform`` value | |
| 1184 | 1184 | ================ =========================== | |
| 1185 | 1185 | AIX ``'aix'`` | |
| 1186 | + Emscripten ``'emscripten'`` | ||
| 1186 | 1187 | Linux ``'linux'`` | |
| 1188 | + WASI ``'wasi'`` | ||
| 1187 | 1189 | Windows ``'win32'`` | |
| 1188 | 1190 | Windows/Cygwin ``'cygwin'`` | |
| 1189 | 1191 | macOS ``'darwin'`` | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -812,8 +812,9 @@ $(DLLLIBRARY) libpython$(LDVERSION).dll.a: $(LIBRARY_OBJS) | |||
| 812 | 812 | # --preload-file turns a relative asset path into an absolute path. | |
| 813 | 813 | ||
| 814 | 814 | $(WASM_STDLIB): $(srcdir)/Lib/*.py $(srcdir)/Lib/*/*.py \ | |
| 815 | - pybuilddir.txt $(srcdir)/Tools/wasm/wasm_assets.py \ | ||
| 816 | - python.html python.worker.js | ||
| 815 | + $(srcdir)/Tools/wasm/wasm_assets.py \ | ||
| 816 | + Makefile pybuilddir.txt Modules/Setup.local \ | ||
| 817 | + python.html python.worker.js | ||
| 817 | 818 | $(PYTHON_FOR_BUILD) $(srcdir)/Tools/wasm/wasm_assets.py \ | |
| 818 | 819 | --builddir . --prefix $(prefix) | |
| 819 | 820 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -9,9 +9,21 @@ possible to build for *wasm32-wasi* out-of-the-box yet. | |||
| 9 | 9 | ||
| 10 | 10 | ## wasm32-emscripten build | |
| 11 | 11 | ||
| 12 | - Cross compiling to wasm32-emscripten platform needs the [Emscripten](https://emscripten.org/) | ||
| 13 | - tool chain and a build Python interpreter. | ||
| 14 | - All commands below are relative to a repository checkout. | ||
| 12 | + Cross compiling to the wasm32-emscripten platform needs the | ||
| 13 | + [Emscripten](https://emscripten.org/) SDK and a build Python interpreter. | ||
| 14 | + Emscripten 3.1.8 or newer are recommended. All commands below are relative | ||
| 15 | + to a repository checkout. | ||
| 16 | + | ||
| 17 | + Christian Heimes maintains a container image with Emscripten SDK, Python | ||
| 18 | + build dependencies, WASI-SDK, wasmtime, and several additional tools. | ||
| 19 | + | ||
| 20 | + ``` | ||
| 21 | + # Fedora, RHEL, CentOS | ||
| 22 | + podman run --rm -ti -v $(pwd):/python-wasm/cpython:Z quay.io/tiran/cpythonbuild:emsdk3 | ||
| 23 | + | ||
| 24 | + # other | ||
| 25 | + docker run --rm -ti -v $(pwd):/python-wasm/cpython quay.io/tiran/cpythonbuild:emsdk3 | ||
| 26 | + ``` | ||
| 15 | 27 | ||
| 16 | 28 | ### Compile a build Python interpreter | |
| 17 | 29 | ||
@@ -167,3 +179,77 @@ linker options. | |||
| 167 | 179 | - pthread support requires WASM threads and SharedArrayBuffer (bulk memory). | |
| 168 | 180 | The runtime keeps a pool of web workers around. Each web worker uses | |
| 169 | 181 | several file descriptors (eventfd, epoll, pipe). | |
| 182 | + | ||
| 183 | + # Hosting Python WASM builds | ||
| 184 | + | ||
| 185 | + The simple REPL terminal uses SharedArrayBuffer. For security reasons | ||
| 186 | + browsers only provide the feature in secure environents with cross-origin | ||
| 187 | + isolation. The webserver must send cross-origin headers and correct MIME types | ||
| 188 | + for the JavaScript and WASM files. Otherwise the terminal will fail to load | ||
| 189 | + with an error message like ``Browsers disable shared array buffer``. | ||
| 190 | + | ||
| 191 | + ## Apache HTTP .htaccess | ||
| 192 | + | ||
| 193 | + Place a ``.htaccess`` file in the same directory as ``python.wasm``. | ||
| 194 | + | ||
| 195 | + ``` | ||
| 196 | + # .htaccess | ||
| 197 | + Header set Cross-Origin-Opener-Policy same-origin | ||
| 198 | + Header set Cross-Origin-Embedder-Policy require-corp | ||
| 199 | + | ||
| 200 | + AddType application/javascript js | ||
| 201 | + AddType application/wasm wasm | ||
| 202 | + | ||
| 203 | + <IfModule mod_deflate.c> | ||
| 204 | + AddOutputFilterByType DEFLATE text/html application/javascript application/wasm | ||
| 205 | + </IfModule> | ||
| 206 | + ``` | ||
| 207 | + | ||
| 208 | + # Detect WebAssembly builds | ||
| 209 | + | ||
| 210 | + ## Python code | ||
| 211 | + | ||
| 212 | + ```# python | ||
| 213 | + import os, sys | ||
| 214 | + | ||
| 215 | + if sys.platform == "emscripten": | ||
| 216 | + # Python on Emscripten | ||
| 217 | + if sys.platform == "wasi": | ||
| 218 | + # Python on WASI | ||
| 219 | + | ||
| 220 | + if os.name == "posix": | ||
| 221 | + # WASM platforms identify as POSIX-like. | ||
| 222 | + # Windows does not provide os.uname(). | ||
| 223 | + machine = os.uname().machine | ||
| 224 | + if machine.startswith("wasm"): | ||
| 225 | + # WebAssembly (wasm32 or wasm64) | ||
| 226 | + ``` | ||
| 227 | + | ||
| 228 | + ## C code | ||
| 229 | + | ||
| 230 | + Emscripten SDK and WASI SDK define several built-in macros. You can dump a | ||
| 231 | + full list of built-ins with ``emcc -dM -E - < /dev/null`` and | ||
| 232 | + ``/path/to/wasi-sdk/bin/clang -dM -E - < /dev/null``. | ||
| 233 | + | ||
| 234 | + ```# C | ||
| 235 | + #ifdef __EMSCRIPTEN__ | ||
| 236 | + // Python on Emscripten | ||
| 237 | + #endif | ||
| 238 | + ``` | ||
| 239 | + | ||
| 240 | + * WebAssembly ``__wasm__`` (also ``__wasm``) | ||
| 241 | + * wasm32 ``__wasm32__`` (also ``__wasm32``) | ||
| 242 | + * wasm64 ``__wasm64__`` | ||
| 243 | + * Emscripten ``__EMSCRIPTEN__`` (also ``EMSCRIPTEN``) | ||
| 244 | + * Emscripten version ``__EMSCRIPTEN_major__``, ``__EMSCRIPTEN_minor__``, ``__EMSCRIPTEN_tiny__`` | ||
| 245 | + * WASI ``__wasi__`` | ||
| 246 | + | ||
| 247 | + Feature detection flags: | ||
| 248 | + | ||
| 249 | + * ``__EMSCRIPTEN_PTHREADS__`` | ||
| 250 | + * ``__EMSCRIPTEN_SHARED_MEMORY__`` | ||
| 251 | + * ``__wasm_simd128__`` | ||
| 252 | + * ``__wasm_sign_ext__`` | ||
| 253 | + * ``__wasm_bulk_memory__`` | ||
| 254 | + * ``__wasm_atomics__`` | ||
| 255 | + * ``__wasm_mutable_globals__`` | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,15 @@ | |||
| 1 | + # Module/Setup.local with reduced stdlib | ||
| 2 | + *disabled* | ||
| 3 | + _asyncio | ||
| 4 | + audioop | ||
| 5 | + _bz2 | ||
| 6 | + _crypt | ||
| 7 | + _decimal | ||
| 8 | + _pickle | ||
| 9 | + pyexpat _elementtree | ||
| 10 | + _sha3 _blake2 | ||
| 11 | + _zoneinfo | ||
| 12 | + xxsubtype | ||
| 13 | + | ||
| 14 | + # cjk codecs | ||
| 15 | + #_multibytecodec _codecs_cn _codecs_hk _codecs_iso2022 _codecs_jp _codecs_kr _codecs_tw | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -20,7 +20,11 @@ | |||
| 20 | 20 | SRCDIR_LIB = SRCDIR / "Lib" | |
| 21 | 21 | ||
| 22 | 22 | # sysconfig data relative to build dir. | |
| 23 | - SYSCONFIGDATA_GLOB = "build/lib.*/_sysconfigdata_*.py" | ||
| 23 | + SYSCONFIGDATA = pathlib.PurePath( | ||
| 24 | + "build", | ||
| 25 | + f"lib.emscripten-wasm32-{sys.version_info.major}.{sys.version_info.minor}", | ||
| 26 | + "_sysconfigdata__emscripten_wasm32-emscripten.py", | ||
| 27 | + ) | ||
| 24 | 28 | ||
| 25 | 29 | # Library directory relative to $(prefix). | |
| 26 | 30 | WASM_LIB = pathlib.PurePath("lib") | |
@@ -38,33 +42,44 @@ | |||
| 38 | 42 | OMIT_FILES = ( | |
| 39 | 43 | # regression tests | |
| 40 | 44 | "test/", | |
| 41 | - # user interfaces: TK, curses | ||
| 42 | - "curses/", | ||
| 43 | - "idlelib/", | ||
| 44 | - "tkinter/", | ||
| 45 | - "turtle.py", | ||
| 46 | - "turtledemo/", | ||
| 47 | 45 | # package management | |
| 48 | 46 | "ensurepip/", | |
| 49 | 47 | "venv/", | |
| 50 | 48 | # build system | |
| 51 | 49 | "distutils/", | |
| 52 | 50 | "lib2to3/", | |
| 53 | - # concurrency | ||
| 54 | - "concurrent/", | ||
| 55 | - "multiprocessing/", | ||
| 56 | 51 | # deprecated | |
| 57 | 52 | "asyncore.py", | |
| 58 | 53 | "asynchat.py", | |
| 59 | - # Synchronous network I/O and protocols are not supported; for example, | ||
| 60 | - # socket.create_connection() raises an exception: | ||
| 61 | - # "BlockingIOError: [Errno 26] Operation in progress". | ||
| 54 | + "uu.py", | ||
| 55 | + "xdrlib.py", | ||
| 56 | + # other platforms | ||
| 57 | + "_aix_support.py", | ||
| 58 | + "_bootsubprocess.py", | ||
| 59 | + "_osx_support.py", | ||
| 60 | + # webbrowser | ||
| 61 | + "antigravity.py", | ||
| 62 | + "webbrowser.py", | ||
| 63 | + # Pure Python implementations of C extensions | ||
| 64 | + "_pydecimal.py", | ||
| 65 | + "_pyio.py", | ||
| 66 | + # Misc unused or large files | ||
| 67 | + "pydoc_data/", | ||
| 68 | + "msilib/", | ||
| 69 | + ) | ||
| 70 | + | ||
| 71 | + # Synchronous network I/O and protocols are not supported; for example, | ||
| 72 | + # socket.create_connection() raises an exception: | ||
| 73 | + # "BlockingIOError: [Errno 26] Operation in progress". | ||
| 74 | + OMIT_NETWORKING_FILES = ( | ||
| 62 | 75 | "cgi.py", | |
| 63 | 76 | "cgitb.py", | |
| 64 | 77 | "email/", | |
| 65 | 78 | "ftplib.py", | |
| 66 | 79 | "http/", | |
| 67 | 80 | "imaplib.py", | |
| 81 | + "mailbox.py", | ||
| 82 | + "mailcap.py", | ||
| 68 | 83 | "nntplib.py", | |
| 69 | 84 | "poplib.py", | |
| 70 | 85 | "smtpd.py", | |
@@ -77,26 +92,28 @@ | |||
| 77 | 92 | "urllib/response.py", | |
| 78 | 93 | "urllib/robotparser.py", | |
| 79 | 94 | "wsgiref/", | |
| 80 | - "xmlrpc/", | ||
| 81 | - # dbm / gdbm | ||
| 82 | - "dbm/", | ||
| 83 | - # other platforms | ||
| 84 | - "_aix_support.py", | ||
| 85 | - "_bootsubprocess.py", | ||
| 86 | - "_osx_support.py", | ||
| 87 | - # webbrowser | ||
| 88 | - "antigravity.py", | ||
| 89 | - "webbrowser.py", | ||
| 90 | - # ctypes | ||
| 91 | - "ctypes/", | ||
| 92 | - # Pure Python implementations of C extensions | ||
| 93 | - "_pydecimal.py", | ||
| 94 | - "_pyio.py", | ||
| 95 | - # Misc unused or large files | ||
| 96 | - "pydoc_data/", | ||
| 97 | - "msilib/", | ||
| 98 | 95 | ) | |
| 99 | 96 | ||
| 97 | + OMIT_MODULE_FILES = { | ||
| 98 | + "_asyncio": ["asyncio/"], | ||
| 99 | + "audioop": ["aifc.py", "sunau.py", "wave.py"], | ||
| 100 | + "_crypt": ["crypt.py"], | ||
| 101 | + "_curses": ["curses/"], | ||
| 102 | + "_ctypes": ["ctypes/"], | ||
| 103 | + "_decimal": ["decimal.py"], | ||
| 104 | + "_dbm": ["dbm/ndbm.py"], | ||
| 105 | + "_gdbm": ["dbm/gnu.py"], | ||
| 106 | + "_json": ["json/"], | ||
| 107 | + "_multiprocessing": ["concurrent/", "multiprocessing/"], | ||
| 108 | + "pyexpat": ["xml/", "xmlrpc/"], | ||
| 109 | + "readline": ["rlcompleter.py"], | ||
| 110 | + "_sqlite3": ["sqlite3/"], | ||
| 111 | + "_ssl": ["ssl.py"], | ||
| 112 | + "_tkinter": ["idlelib/", "tkinter/", "turtle.py", "turtledemo/"], | ||
| 113 | + | ||
| 114 | + "_zoneinfo": ["zoneinfo/"], | ||
| 115 | + } | ||
| 116 | + | ||
| 100 | 117 | # regression test sub directories | |
| 101 | 118 | OMIT_SUBDIRS = ( | |
| 102 | 119 | "ctypes/test/", | |
@@ -105,34 +122,59 @@ | |||
| 105 | 122 | ) | |
| 106 | 123 | ||
| 107 | 124 | ||
| 108 | - OMIT_ABSOLUTE = {SRCDIR_LIB / name for name in OMIT_FILES} | ||
| 109 | - OMIT_SUBDIRS_ABSOLUTE = tuple(str(SRCDIR_LIB / name) for name in OMIT_SUBDIRS) | ||
| 110 | - | ||
| 111 | - | ||
| 112 | - def filterfunc(name: str) -> bool: | ||
| 113 | - return not name.startswith(OMIT_SUBDIRS_ABSOLUTE) | ||
| 114 | - | ||
| 115 | - | ||
| 116 | 125 | def create_stdlib_zip( | |
| 117 | - args: argparse.Namespace, compression: int = zipfile.ZIP_DEFLATED, *, optimize: int = 0 | ||
| 126 | + args: argparse.Namespace, | ||
| 127 | + *, | ||
| 128 | + optimize: int = 0, | ||
| 118 | 129 | ) -> None: | |
| 119 | - sysconfig_data = list(args.builddir.glob(SYSCONFIGDATA_GLOB)) | ||
| 120 | - if not sysconfig_data: | ||
| 121 | - raise ValueError("No sysconfigdata file found") | ||
| 130 | + def filterfunc(name: str) -> bool: | ||
| 131 | + return not name.startswith(args.omit_subdirs_absolute) | ||
| 122 | 132 | ||
| 123 | 133 | with zipfile.PyZipFile( | |
| 124 | - args.wasm_stdlib_zip, mode="w", compression=compression, optimize=0 | ||
| 134 | + args.wasm_stdlib_zip, mode="w", compression=args.compression, optimize=optimize | ||
| 125 | 135 | ) as pzf: | |
| 136 | + if args.compresslevel is not None: | ||
| 137 | + pzf.compresslevel = args.compresslevel | ||
| 138 | + pzf.writepy(args.sysconfig_data) | ||
| 126 | 139 | for entry in sorted(args.srcdir_lib.iterdir()): | |
| 127 | 140 | if entry.name == "__pycache__": | |
| 128 | 141 | continue | |
| 129 | - if entry in OMIT_ABSOLUTE: | ||
| 142 | + if entry in args.omit_files_absolute: | ||
| 130 | 143 | continue | |
| 131 | 144 | if entry.name.endswith(".py") or entry.is_dir(): | |
| 132 | 145 | # writepy() writes .pyc files (bytecode). | |
| 133 | 146 | pzf.writepy(entry, filterfunc=filterfunc) | |
| 134 | - for entry in sysconfig_data: | ||
| 135 | - pzf.writepy(entry) | ||
| 147 | + | ||
| 148 | + | ||
| 149 | + def detect_extension_modules(args: argparse.Namespace): | ||
| 150 | + modules = {} | ||
| 151 | + | ||
| 152 | + # disabled by Modules/Setup.local ? | ||
| 153 | + with open(args.builddir / "Makefile") as f: | ||
| 154 | + for line in f: | ||
| 155 | + if line.startswith("MODDISABLED_NAMES="): | ||
| 156 | + disabled = line.split("=", 1)[1].strip().split() | ||
| 157 | + for modname in disabled: | ||
| 158 | + modules[modname] = False | ||
| 159 | + break | ||
| 160 | + | ||
| 161 | + # disabled by configure? | ||
| 162 | + with open(args.sysconfig_data) as f: | ||
| 163 | + data = f.read() | ||
| 164 | + loc = {} | ||
| 165 | + exec(data, globals(), loc) | ||
| 166 | + | ||
| 167 | + for name, value in loc["build_time_vars"].items(): | ||
| 168 | + if value not in {"yes", "missing", "disabled", "n/a"}: | ||
| 169 | + continue | ||
| 170 | + if not name.startswith("MODULE_"): | ||
| 171 | + continue | ||
| 172 | + if name.endswith(("_CFLAGS", "_DEPS", "_LDFLAGS")): | ||
| 173 | + continue | ||
| 174 | + modname = name.removeprefix("MODULE_").lower() | ||
| 175 | + if modname not in modules: | ||
| 176 | + modules[modname] = value == "yes" | ||
| 177 | + return modules | ||
| 136 | 178 | ||
| 137 | 179 | ||
| 138 | 180 | def path(val: str) -> pathlib.Path: | |
@@ -147,7 +189,10 @@ def path(val: str) -> pathlib.Path: | |||
| 147 | 189 | type=path, | |
| 148 | 190 | ) | |
| 149 | 191 | parser.add_argument( | |
| 150 | - "--prefix", help="install prefix", default=pathlib.Path("/usr/local"), type=path | ||
| 192 | + "--prefix", | ||
| 193 | + help="install prefix", | ||
| 194 | + default=pathlib.Path("/usr/local"), | ||
| 195 | + type=path, | ||
| 151 | 196 | ) | |
| 152 | 197 | ||
| 153 | 198 | ||
@@ -162,6 +207,27 @@ def main(): | |||
| 162 | 207 | args.wasm_stdlib = args.wasm_root / WASM_STDLIB | |
| 163 | 208 | args.wasm_dynload = args.wasm_root / WASM_DYNLOAD | |
| 164 | 209 | ||
| 210 | + # bpo-17004: zipimport supports only zlib compression. | ||
| 211 | + # Emscripten ZIP_STORED + -sLZ4=1 linker flags results in larger file. | ||
| 212 | + args.compression = zipfile.ZIP_DEFLATED | ||
| 213 | + args.compresslevel = 9 | ||
| 214 | + | ||
| 215 | + args.sysconfig_data = args.builddir / SYSCONFIGDATA | ||
| 216 | + if not args.sysconfig_data.is_file(): | ||
| 217 | + raise ValueError(f"sysconfigdata file {SYSCONFIGDATA} missing.") | ||
| 218 | + | ||
| 219 | + extmods = detect_extension_modules(args) | ||
| 220 | + omit_files = list(OMIT_FILES) | ||
| 221 | + omit_files.extend(OMIT_NETWORKING_FILES) | ||
| 222 | + for modname, modfiles in OMIT_MODULE_FILES.items(): | ||
| 223 | + if not extmods.get(modname): | ||
| 224 | + omit_files.extend(modfiles) | ||
| 225 | + | ||
| 226 | + args.omit_files_absolute = {args.srcdir_lib / name for name in omit_files} | ||
| 227 | + args.omit_subdirs_absolute = tuple( | ||
| 228 | + str(args.srcdir_lib / name) for name in OMIT_SUBDIRS | ||
| 229 | + ) | ||
| 230 | + | ||
| 165 | 231 | # Empty, unused directory for dynamic libs, but required for site initialization. | |
| 166 | 232 | args.wasm_dynload.mkdir(parents=True, exist_ok=True) | |
| 167 | 233 | marker = args.wasm_dynload / ".empty" | |
@@ -170,7 +236,7 @@ def main(): | |||
| 170 | 236 | shutil.copy(args.srcdir_lib / "os.py", args.wasm_stdlib) | |
| 171 | 237 | # The rest of stdlib that's useful in a WASM context. | |
| 172 | 238 | create_stdlib_zip(args) | |
| 173 | - size = round(args.wasm_stdlib_zip.stat().st_size / 1024 ** 2, 2) | ||
| 239 | + size = round(args.wasm_stdlib_zip.stat().st_size / 1024**2, 2) | ||
| 174 | 240 | parser.exit(0, f"Created {args.wasm_stdlib_zip} ({size} MiB)\n") | |
| 175 | 241 | ||
| 176 | 242 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -14,6 +14,13 @@ | |||
| 14 | 14 | ||
| 15 | 15 | ||
| 16 | 16 | class MyHTTPRequestHandler(server.SimpleHTTPRequestHandler): | |
| 17 | + extensions_map = server.SimpleHTTPRequestHandler.extensions_map.copy() | ||
| 18 | + extensions_map.update( | ||
| 19 | + { | ||
| 20 | + ".wasm": "application/wasm", | ||
| 21 | + } | ||
| 22 | + ) | ||
| 23 | + | ||
| 17 | 24 | def end_headers(self): | |
| 18 | 25 | self.send_my_headers() | |
| 19 | 26 | super().end_headers() | |
| Back | FazBrowse Home | New Git URL |
0 commit comments