Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
allendowney
GitHub Repository: allendowney/cpython
Path: blob/main/Tools/wasm/wasm_assets.py
12 views
1
#!/usr/bin/env python
2
"""Create a WASM asset bundle directory structure.
3
4
The WASM asset bundles are pre-loaded by the final WASM build. The bundle
5
contains:
6
7
- a stripped down, pyc-only stdlib zip file, e.g. {PREFIX}/lib/python311.zip
8
- os.py as marker module {PREFIX}/lib/python3.11/os.py
9
- empty lib-dynload directory, to make sure it is copied into the bundle:
10
{PREFIX}/lib/python3.11/lib-dynload/.empty
11
"""
12
13
import argparse
14
import pathlib
15
import shutil
16
import sys
17
import sysconfig
18
import zipfile
19
20
# source directory
21
SRCDIR = pathlib.Path(__file__).parent.parent.parent.absolute()
22
SRCDIR_LIB = SRCDIR / "Lib"
23
24
25
# Library directory relative to $(prefix).
26
WASM_LIB = pathlib.PurePath("lib")
27
WASM_STDLIB_ZIP = (
28
WASM_LIB / f"python{sys.version_info.major}{sys.version_info.minor}.zip"
29
)
30
WASM_STDLIB = (
31
WASM_LIB / f"python{sys.version_info.major}.{sys.version_info.minor}"
32
)
33
WASM_DYNLOAD = WASM_STDLIB / "lib-dynload"
34
35
36
# Don't ship large files / packages that are not particularly useful at
37
# the moment.
38
OMIT_FILES = (
39
# regression tests
40
"test/",
41
# package management
42
"ensurepip/",
43
"venv/",
44
# other platforms
45
"_aix_support.py",
46
"_osx_support.py",
47
# webbrowser
48
"antigravity.py",
49
"webbrowser.py",
50
# Pure Python implementations of C extensions
51
"_pydecimal.py",
52
"_pyio.py",
53
# concurrent threading
54
"concurrent/futures/thread.py",
55
# Misc unused or large files
56
"pydoc_data/",
57
)
58
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".
62
OMIT_NETWORKING_FILES = (
63
"email/",
64
"ftplib.py",
65
"http/",
66
"imaplib.py",
67
"mailbox.py",
68
"poplib.py",
69
"smtplib.py",
70
"socketserver.py",
71
# keep urllib.parse for pydoc
72
"urllib/error.py",
73
"urllib/request.py",
74
"urllib/response.py",
75
"urllib/robotparser.py",
76
"wsgiref/",
77
)
78
79
OMIT_MODULE_FILES = {
80
"_asyncio": ["asyncio/"],
81
"_curses": ["curses/"],
82
"_ctypes": ["ctypes/"],
83
"_decimal": ["decimal.py"],
84
"_dbm": ["dbm/ndbm.py"],
85
"_gdbm": ["dbm/gnu.py"],
86
"_json": ["json/"],
87
"_multiprocessing": ["concurrent/futures/process.py", "multiprocessing/"],
88
"pyexpat": ["xml/", "xmlrpc/"],
89
"readline": ["rlcompleter.py"],
90
"_sqlite3": ["sqlite3/"],
91
"_ssl": ["ssl.py"],
92
"_tkinter": ["idlelib/", "tkinter/", "turtle.py", "turtledemo/"],
93
"_zoneinfo": ["zoneinfo/"],
94
}
95
96
SYSCONFIG_NAMES = (
97
"_sysconfigdata__emscripten_wasm32-emscripten",
98
"_sysconfigdata__emscripten_wasm32-emscripten",
99
"_sysconfigdata__wasi_wasm32-wasi",
100
"_sysconfigdata__wasi_wasm64-wasi",
101
)
102
103
104
def get_builddir(args: argparse.Namespace) -> pathlib.Path:
105
"""Get builddir path from pybuilddir.txt"""
106
with open("pybuilddir.txt", encoding="utf-8") as f:
107
builddir = f.read()
108
return pathlib.Path(builddir)
109
110
111
def get_sysconfigdata(args: argparse.Namespace) -> pathlib.Path:
112
"""Get path to sysconfigdata relative to build root"""
113
data_name = sysconfig._get_sysconfigdata_name()
114
if not data_name.startswith(SYSCONFIG_NAMES):
115
raise ValueError(
116
f"Invalid sysconfig data name '{data_name}'.", SYSCONFIG_NAMES
117
)
118
filename = data_name + ".py"
119
return args.builddir / filename
120
121
122
def create_stdlib_zip(
123
args: argparse.Namespace,
124
*,
125
optimize: int = 0,
126
) -> None:
127
def filterfunc(filename: str) -> bool:
128
pathname = pathlib.Path(filename).resolve()
129
return pathname not in args.omit_files_absolute
130
131
with zipfile.PyZipFile(
132
args.wasm_stdlib_zip,
133
mode="w",
134
compression=args.compression,
135
optimize=optimize,
136
) as pzf:
137
if args.compresslevel is not None:
138
pzf.compresslevel = args.compresslevel
139
pzf.writepy(args.sysconfig_data)
140
for entry in sorted(args.srcdir_lib.iterdir()):
141
entry = entry.resolve()
142
if entry.name == "__pycache__":
143
continue
144
if entry.name.endswith(".py") or entry.is_dir():
145
# writepy() writes .pyc files (bytecode).
146
pzf.writepy(entry, filterfunc=filterfunc)
147
148
149
def detect_extension_modules(args: argparse.Namespace):
150
modules = {}
151
152
# disabled by Modules/Setup.local ?
153
with open(args.buildroot / "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 key, value in loc["build_time_vars"].items():
168
if not key.startswith("MODULE_") or not key.endswith("_STATE"):
169
continue
170
if value not in {"yes", "disabled", "missing", "n/a"}:
171
raise ValueError(f"Unsupported value '{value}' for {key}")
172
173
modname = key[7:-6].lower()
174
if modname not in modules:
175
modules[modname] = value == "yes"
176
return modules
177
178
179
def path(val: str) -> pathlib.Path:
180
return pathlib.Path(val).absolute()
181
182
183
parser = argparse.ArgumentParser()
184
parser.add_argument(
185
"--buildroot",
186
help="absolute path to build root",
187
default=pathlib.Path(".").absolute(),
188
type=path,
189
)
190
parser.add_argument(
191
"--prefix",
192
help="install prefix",
193
default=pathlib.Path("/usr/local"),
194
type=path,
195
)
196
197
198
def main():
199
args = parser.parse_args()
200
201
relative_prefix = args.prefix.relative_to(pathlib.Path("/"))
202
args.srcdir = SRCDIR
203
args.srcdir_lib = SRCDIR_LIB
204
args.wasm_root = args.buildroot / relative_prefix
205
args.wasm_stdlib_zip = args.wasm_root / WASM_STDLIB_ZIP
206
args.wasm_stdlib = args.wasm_root / WASM_STDLIB
207
args.wasm_dynload = args.wasm_root / WASM_DYNLOAD
208
209
# bpo-17004: zipimport supports only zlib compression.
210
# Emscripten ZIP_STORED + -sLZ4=1 linker flags results in larger file.
211
args.compression = zipfile.ZIP_DEFLATED
212
args.compresslevel = 9
213
214
args.builddir = get_builddir(args)
215
args.sysconfig_data = get_sysconfigdata(args)
216
if not args.sysconfig_data.is_file():
217
raise ValueError(f"sysconfigdata file {args.sysconfig_data} missing.")
218
219
extmods = detect_extension_modules(args)
220
omit_files = list(OMIT_FILES)
221
if sysconfig.get_platform().startswith("emscripten"):
222
omit_files.extend(OMIT_NETWORKING_FILES)
223
for modname, modfiles in OMIT_MODULE_FILES.items():
224
if not extmods.get(modname):
225
omit_files.extend(modfiles)
226
227
args.omit_files_absolute = {
228
(args.srcdir_lib / name).resolve() for name in omit_files
229
}
230
231
# Empty, unused directory for dynamic libs, but required for site initialization.
232
args.wasm_dynload.mkdir(parents=True, exist_ok=True)
233
marker = args.wasm_dynload / ".empty"
234
marker.touch()
235
# os.py is a marker for finding the correct lib directory.
236
shutil.copy(args.srcdir_lib / "os.py", args.wasm_stdlib)
237
# The rest of stdlib that's useful in a WASM context.
238
create_stdlib_zip(args)
239
size = round(args.wasm_stdlib_zip.stat().st_size / 1024**2, 2)
240
parser.exit(0, f"Created {args.wasm_stdlib_zip} ({size} MiB)\n")
241
242
243
if __name__ == "__main__":
244
main()
245
246