Skip to content
File

Blob: src/pyodide/helpers.bzl

12.9 KB
1load("@aspect_rules_esbuild//esbuild:defs.bzl", "esbuild")
2load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
3load("@bazel_skylib//rules:expand_template.bzl", "expand_template")
4load("@capnp-cpp//src/capnp:cc_capnp_library.bzl", "cc_capnp_library")
5load("//:build/capnp_embed.bzl", "capnp_embed")
6load("//:build/js_file.bzl", "js_file")
7load("//:build/python_metadata.bzl", "BUNDLE_VERSION_INFO", "PYODIDE_VERSIONS", "PYTHON_LOCKFILES")
8load("//:build/wd_js_bundle.bzl", "wd_js_bundle")
9load("//:build/wd_ts_bundle.bzl", "wd_ts_bundle")
10 
11def _out_name(src):
12 src = src.removesuffix("//file")
13 src = src.removeprefix("@")
14 return src.rsplit(":", 2)[-1].rsplit("/", 2)[-1]
15 
16def _out_path(name, version):
17 res = "generated/" + name
18 if version:
19 res = version + "/" + res
20 return res
21 
22def _ts_bundle_out(prefix, name, version):
23 return ":" + _out_path(prefix + name.removeprefix("internal/").replace("/", "_"), version)
24 
25def _copy_to_generated(src, version = None, out_name = None, name = None):
26 out_name = out_name or _out_name(src)
27 if name == None:
28 name = out_name + "@copy"
29 if version:
30 name += "@" + version
31 copy_file(name = name, src = src, out = _out_path(out_name, version))
32 
33def _copy_and_capnp_embed(src):
34 out_name = _out_name(src)
35 _copy_to_generated(src)
36 capnp_embed(
37 name = out_name + "@capnp",
38 src = _out_path(out_name, None),
39 deps = [out_name + "@copy"],
40 )
41 
42def _fmt_python_snapshot_release(
43 pyodide_version,
44 pyodide_date,
45 packages,
46 backport,
47 baseline_snapshot_hash,
48 flag,
49 real_pyodide_version,
50 **_kwds):
51 content = ", ".join(
52 [
53 'pyodide = "%s"' % pyodide_version,
54 'realPyodideVersion = "%s"' % real_pyodide_version,
55 'pyodideRevision = "%s"' % pyodide_date,
56 'packages = "%s"' % packages,
57 "backport = %s" % backport,
58 'baselineSnapshotHash = "%s"' % baseline_snapshot_hash,
59 'flagName = "%s"' % flag,
60 ],
61 )
62 return "(%s)" % content
63 
64def pyodide_extra():
65 _copy_to_generated(
66 "pyodide_extra.capnp",
67 out_name = "pyodide_extra_tmpl.capnp",
68 )
69 
70 package_tags = [info["tag"] for info in PYTHON_LOCKFILES]
71 
72 expand_template(
73 name = "pyodide_extra_expand_template@rule",
74 out = "generated/pyodide_extra.capnp",
75 substitutions = {
76 "%PACKAGE_LOCKS": ",".join(
77 [
78 '(packageDate = "%s", lock = embed "pyodide-lock_%s.json")' %
79 (tag, tag)
80 for tag in package_tags
81 ],
82 ),
83 "%PYTHON_RELEASES": ", ".join(
84 [_fmt_python_snapshot_release(**info) for info in BUNDLE_VERSION_INFO.values()],
85 ),
86 },
87 template = "generated/pyodide_extra_tmpl.capnp",
88 )
89 
90 for tag in package_tags:
91 _copy_and_capnp_embed("@pyodide-lock_" + tag + ".json//file")
92 
93 cc_capnp_library(
94 name = "pyodide_extra_capnp",
95 srcs = ["generated/pyodide_extra.capnp"],
96 visibility = ["//visibility:public"],
97 deps = [
98 ] + [
99 ":pyodide-lock_%s.json@capnp" % tag
100 for tag in package_tags
101 ],
102 target_compatible_with = select({
103 "@//build/config:no_build": ["@platforms//:incompatible"],
104 "//conditions:default": [],
105 }),
106 )
107 
108def python_bundles(overrides = {}):
109 srcs = [_python_bundle_helper(info, overrides) for info in PYODIDE_VERSIONS]
110 native.filegroup(
111 name = "python_bundles",
112 srcs = srcs + [":bundle_version_info"],
113 )
114 
115def _python_bundle_helper(info, overrides):
116 version = info["version"]
117 override = overrides.get(version, {})
118 return _python_bundle(version, **override)
119 
120def pyodide_static():
121 internal_data_modules = native.glob([
122 "internal/*.py",
123 "internal/workers-api/src/*.py",
124 "internal/workers-api/src/workers/*.py",
125 "internal/patches/*.py",
126 "internal/topLevelEntropy/*.py",
127 ])
128 internal_modules = native.glob(
129 [
130 "internal/*.ts",
131 "internal/topLevelEntropy/*.ts",
132 "types/*.ts",
133 "types/*/*.ts",
134 ],
135 )
136 modules = ["python-entrypoint-helper.ts"]
137 
138 wd_ts_bundle(
139 name = "pyodide_static",
140 eslintrc_json = "eslint.config.mjs",
141 import_name = "pyodide",
142 internal_data_modules = internal_data_modules,
143 internal_modules = internal_modules,
144 lint = True,
145 modules = modules,
146 schema_id = "0xdc8d02dfbdf14025",
147 tsconfig_json = "tsconfig.json",
148 )
149 
150_PRELUDE = """
151import {
152 addEventListener,
153 getRandomValues,
154 location,
155 monotonicDateNow,
156 newWasmModule,
157 patchedApplyFunc,
158 patchedLoadLibData,
159 reportUndefinedSymbolsPatched,
160 wasmInstantiate,
161 patched_PyEM_CountFuncParams,
162} from "pyodide-internal:pool/builtin_wrappers";
163"""
164 
165# pyodide.asm.js patches
166# TODO: all of these should be fixed by linking our own Pyodide or by upstreaming.
167_REPLACEMENTS = [
168 [
169 # Convert pyodide.asm.js into an es6 module.
170 # When we link our own we can pass `-sES6_MODULE` to the linker and it will do this for us
171 # automatically.
172 "var _createPyodideModule",
173 _PRELUDE + "export const _createPyodideModule",
174 ],
175 [
176 "globalThis._createPyodideModule = _createPyodideModule;",
177 "",
178 ],
179 [
180 "new WebAssembly.Module",
181 "newWasmModule",
182 ],
183 [
184 "WebAssembly.instantiate",
185 "wasmInstantiate",
186 ],
187 [
188 "Date.now",
189 "monotonicDateNow",
190 ],
191 [
192 "reportUndefinedSymbols()",
193 "reportUndefinedSymbolsPatched(Module)",
194 ],
195 [
196 "crypto.getRandomValues(",
197 "getRandomValues(Module, ",
198 ],
199 [
200 # Direct eval disallowed in esbuild, see:
201 # https://esbuild.github.io/content-types/#direct-eval
202 "eval(func)",
203 "(() => {throw new Error('Internal Emscripten code tried to eval, this should not happen, please file a bug report with your requirements.txt file\\'s contents')})()",
204 ],
205 [
206 "eval(data)",
207 "(() => {throw new Error('Internal Emscripten code tried to eval, this should not happen, please file a bug report with your requirements.txt file\\'s contents')})()",
208 ],
209 [
210 "eval(UTF8ToString(ptr))",
211 "(() => {throw new Error('Internal Emscripten code tried to eval, this should not happen, please file a bug report with your requirements.txt file\\'s contents')})()",
212 ],
213 # Dynamic linking patches:
214 # library lookup
215 [
216 "function loadLibData(){",
217 """
218 function loadLibData(){
219 var libData = patchedLoadLibData(Module, libName, flags.rpath);
220 return flags.loadAsync ? Promise.resolve(libData) : libData;
221 }
222 function dummiedOutOrigLoadLibData(){
223 """,
224 ],
225 # for ensuring memory base of dynlib is stable when restoring snapshots
226 [
227 "getMemory(",
228 "Module.getMemoryPatched(Module, libName, ",
229 ],
230 # to fix RPC, applies https://github.com/pyodide/pyodide/commit/8da1f38f7
231 [
232 "nullToUndefined(func.apply(",
233 "nullToUndefined(patchedApplyFunc(API, func, ",
234 ],
235 [
236 "nullToUndefined(Function.prototype.apply.apply",
237 "nullToUndefined(API.config.jsglobals.Function.prototype.apply.apply",
238 ],
239 [
240 "function _PyEM_CountFuncParams(func){",
241 "function _PyEM_CountFuncParams(func){ return patched_PyEM_CountFuncParams(Module, func);",
242 ],
243 [
244 "var tableBase=metadata.tableSize?wasmTable.length:0;",
245 "var tableBase=metadata.tableSize?wasmTable.length:0;" +
246 "Module.snapshotDebug && console.log('loadWebAssemblyModule', libName, memoryBase, tableBase);",
247 ],
248 # to ensure we report every fatal error, not just the first one
249 [
250 'console.error("Recursive call to fatal_error. Inner error was:");',
251 'console.error("Recursive call to fatal_error. Inner error was:");\n' +
252 "try { API.on_fatal?.(e); } catch(e2) { console.error(e2); }\n",
253 ],
254]
255 
256def _python_bundle(version, *, pyodide_asm_wasm = None, pyodide_asm_js = None, python_stdlib_zip = None, emscripten_setup_override = None):
257 pyodide_package = "@pyodide-%s//" % version
258 if not pyodide_asm_wasm:
259 pyodide_asm_wasm = pyodide_package + ":pyodide/pyodide.asm.wasm"
260 
261 if not pyodide_asm_js:
262 pyodide_asm_js = pyodide_package + ":pyodide/pyodide.asm.js"
263 
264 if not python_stdlib_zip:
265 python_stdlib_zip = pyodide_package + ":pyodide/python_stdlib.zip"
266 
267 _copy_to_generated(pyodide_asm_wasm, version, out_name = "pyodide.asm.wasm")
268 
269 _copy_to_generated(python_stdlib_zip, version, out_name = "python_stdlib.zip")
270 
271 expand_template(
272 name = "pyodide.asm.js@rule@" + version,
273 out = _out_path("pyodide.asm.js", version),
274 substitutions = dict(_REPLACEMENTS),
275 template = pyodide_asm_js,
276 )
277 
278 js_file(
279 name = "pyodide.asm.js@rule_js@" + version,
280 srcs = [_out_path("pyodide.asm.js", version)],
281 deps = ["pyodide.asm.js@rule@" + version],
282 )
283 
284 if emscripten_setup_override:
285 _copy_to_generated(out_name = "emscriptenSetup.js", name = "emscriptenSetup", src = emscripten_setup_override, version = version)
286 else:
287 expand_template(
288 name = "esbuild.config.mjs@" + version,
289 out = _out_path("esbuild.config.mjs", version),
290 substitutions = {
291 "%PYODIDE_VERSION%": version,
292 },
293 template = "internal/pool/esbuild.config.mjs",
294 )
295 
296 esbuild(
297 name = "emscriptenSetup@" + version,
298 # exclude emscriptenSetup from source set so that rules_ts won't also try to create a JS output
299 # for it. The file is provided in entry_point instead.
300 srcs = native.glob([
301 "internal/pool/*.ts",
302 ], exclude = ["internal/pool/emscriptenSetup.ts"]) + [
303 _out_path("pyodide.asm.js", version),
304 "internal/util.ts",
305 "internal/const.ts",
306 ],
307 config = _out_path("esbuild.config.mjs", version),
308 entry_point = "internal/pool/emscriptenSetup.ts",
309 external = [
310 "child_process",
311 "crypto",
312 "fs",
313 "path",
314 "url",
315 "vm",
316 "ws",
317 "node:child_process",
318 "node:crypto",
319 "node:fs",
320 "node:path",
321 "node:url",
322 "node:vm",
323 "internal:unsafe-eval",
324 ],
325 format = "esm",
326 output = _out_path("emscriptenSetup.js", version),
327 target = "esnext",
328 deps = ["pyodide.asm.js@rule_js@" + version],
329 )
330 
331 import_name = "pyodideRuntime"
332 wd_js_bundle(
333 name = "pyodide@" + version,
334 import_name = import_name,
335 builtin_modules = [],
336 schema_id = "0xbcc8f57c63814005",
337 internal_modules = [
338 _out_path("emscriptenSetup.js", version),
339 ],
340 internal_wasm_modules = [
341 _out_path("pyodide.asm.wasm", version),
342 ],
343 internal_data_modules = [
344 _out_path("python_stdlib.zip", version),
345 ],
346 deps = [
347 "emscriptenSetup@" + version,
348 "pyodide.asm.wasm@copy@" + version,
349 "python_stdlib.zip@copy@" + version,
350 ],
351 out_dir = _out_path("", version),
352 )
353 
354 pyodide_cappn_bin_rule = "pyodide.capnp.bin@rule@" + version
355 native.genrule(
356 name = pyodide_cappn_bin_rule,
357 srcs = [
358 ":pyodide@%s.capnp" % version,
359 "//src/workerd/jsg:modules.capnp",
360 _ts_bundle_out(import_name + "-internal_", "emscriptenSetup", version),
361 _ts_bundle_out(import_name + "-internal_", "pyodide.asm.wasm", version),
362 _ts_bundle_out(import_name + "-internal_", "python_stdlib.zip", version),
363 ],
364 outs = [_out_path("pyodide.capnp.bin", version)],
365 cmd = " ".join([
366 # Annoying logic to deal with different paths in workerd vs downstream.
367 # Either need "-I src" in workerd or -I external/+local_repository+workerd/src downstream
368 "INCLUDE=$$(stat src > /dev/null 2>&1 && echo src || echo external/+local_repository+workerd/src);",
369 "$(execpath @capnp-cpp//src/capnp:capnp_tool)",
370 "eval",
371 "$(location :pyodide@%s.capnp)" % version,
372 import_name + "Bundle",
373 "-I $$INCLUDE",
374 "-o binary",
375 "> $@",
376 ]),
377 tools = ["@capnp-cpp//src/capnp:capnp_tool"],
378 visibility = ["//visibility:public"],
379 target_compatible_with = select({
380 "@//build/config:no_build": ["@platforms//:incompatible"],
381 "//conditions:default": [],
382 }),
383 )
384 return pyodide_cappn_bin_rule