diff --git a/.gitignore b/.gitignore index 53b5ca808..5e39ec54c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.o *.pyc src/js/*.gen.* +src/js/*.out.* *.egg-info/ diff --git a/Makefile b/Makefile index 97587a3a2..bea103b0c 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,7 @@ dist/pyodide.asm.js: \ src/core/pyproxy.o \ src/core/python2js_buffer.o \ src/core/python2js.o \ + src/js/_pyodide.out.js \ $(wildcard src/py/lib/*.py) \ $(CPYTHONLIB)/tzdata \ $(CPYTHONLIB) @@ -52,12 +53,7 @@ dist/pyodide.asm.js: \ # To show some stats on the symbols you can use the following: # cat dist/pyodide.asm.js | grep -ohE 'var _{0,5}.' | sort | uniq -c | sort -nr | head -n 20 sed -i -E 's/var __Z[^;]*;//g' dist/pyodide.asm.js - sed -i '1i\ - "use strict";\ - let setImmediate = globalThis.setImmediate;\ - let clearImmediate = globalThis.clearImmediate;\ - let baseName, fpcGOT, dyncallGOT, fpVal, dcVal;\ - ' dist/pyodide.asm.js + sed -i '1i "use strict";' dist/pyodide.asm.js # Remove last 6 lines of pyodide.asm.js, see issue #2282 # Hopefully we will remove this after emscripten fixes it, upstream issue # emscripten-core/emscripten#16518 @@ -76,7 +72,7 @@ node_modules/.installed : src/js/package.json src/js/package-lock.json ln -sfn src/js/node_modules/ node_modules touch node_modules/.installed -dist/pyodide.js: src/js/*.ts src/js/pyproxy.gen.ts src/js/error_handling.gen.ts node_modules/.installed +dist/pyodide.js src/js/_pyodide.out.js: src/js/*.ts src/js/pyproxy.gen.ts src/js/error_handling.gen.ts node_modules/.installed npx rollup -c src/js/rollup.config.js dist/pyodide.d.ts: src/js/*.ts src/js/pyproxy.gen.ts src/js/error_handling.gen.ts diff --git a/Makefile.envs b/Makefile.envs index 9ddeeb934..85c914494 100644 --- a/Makefile.envs +++ b/Makefile.envs @@ -110,7 +110,8 @@ export MAIN_MODULE_LDFLAGS= $(LDFLAGS_BASE) \ --exclude-file "*/test/*" \ --exclude-file "*/tests/*" \ --exclude-file "*/distutils/*" \ - --pre-js src/core/pre.js + --pre-js src/core/pre.js \ + --pre-js src/js/_pyodide.out.js export SIDE_MODULE_CXXFLAGS = $(CXXFLAGS_BASE) diff --git a/docs/project/changelog.md b/docs/project/changelog.md index 67331df00..4660502de 100644 --- a/docs/project/changelog.md +++ b/docs/project/changelog.md @@ -12,6 +12,20 @@ substitutions: # Change Log +## Unreleased + +- {{ Enhancement }} We now put our built files into the `dist` directory rather + than the `build` directory. {pr}`2387` + +- {{ Enhancement }} `loadPyodide` no longer uses any global state, so it can be + used more than once in the same thread. This is recommended if a network + request causes a loading failure, if there is a fatal error, if you damage the + state of the runtime so badly that it is no longer usable, or for certain + testing purposes. It is not recommended for creating multiple execution + environments, for which you should use + `pyodide.runPython(code, { globals : some_dict})`; + {pr}`2391` + ## Version 0.20.0 [See the release notes for a summary.](https://blog.pyodide.org/posts/0.20-release/) diff --git a/src/core/error_handling.ts b/src/core/error_handling.ts index 60e0b43f1..845c9d418 100644 --- a/src/core/error_handling.ts +++ b/src/core/error_handling.ts @@ -1,5 +1,8 @@ import ErrorStackParser from "error-stack-parser"; -import { Module, API, Hiwire, Tests } from "./module.js"; +declare var Module: any; +declare var Hiwire: any; +declare var API: any; +declare var Tests: any; /** * Dump the Python traceback to the browser console. @@ -192,13 +195,10 @@ Tests.convertCppException = convertCppException; function isPyodideFrame(frame: ErrorStackParser.StackFrame): boolean { const fileName = frame.fileName || ""; - if (fileName.includes("pyodide.asm")) { - return true; - } if (fileName.includes("wasm-function")) { return true; } - if (!fileName.includes("pyodide.js")) { + if (!fileName.includes("pyodide.asm.js")) { return false; } let funcName = frame.functionName || ""; diff --git a/src/core/pre.js b/src/core/pre.js index 8c44a90b9..9a8b6e21a 100644 --- a/src/core/pre.js +++ b/src/core/pre.js @@ -1,3 +1,5 @@ const API = Module.API; -const Hiwire = Module.hiwire; -const Tests = API.tests; +const Hiwire = {}; +const Tests = {}; +API.tests = Tests; +Module.hiwire = Hiwire; diff --git a/src/core/pyproxy.ts b/src/core/pyproxy.ts index 724a79232..b18a00d41 100644 --- a/src/core/pyproxy.ts +++ b/src/core/pyproxy.ts @@ -14,7 +14,9 @@ * See Makefile recipe for src/js/pyproxy.gen.ts */ -import { Module, API, Hiwire } from "./module.js"; +declare var Module: any; +declare var Hiwire: any; +declare var API: any; // pyodide-skip diff --git a/src/js/api.ts b/src/js/api.ts index 66bbd7b9e..e48de32b3 100644 --- a/src/js/api.ts +++ b/src/js/api.ts @@ -1,8 +1,13 @@ -import { Module, API, Hiwire } from "./module"; +declare var Module: any; +declare var Hiwire: any; +declare var API: any; +import "./module.ts"; + import { loadPackage, loadedPackages } from "./load-package"; import { isPyProxy, PyBuffer, PyProxy, TypedArray } from "./pyproxy.gen"; import { PythonError } from "./error_handling.gen"; export { loadPackage, loadedPackages, isPyProxy }; +import "./error_handling.gen.js"; /** * An alias to the Python :py:mod:`pyodide` package. @@ -31,6 +36,16 @@ export let globals: PyProxy; // actually defined in loadPyodide (see pyodide.js) */ export let version: string = ""; // actually defined in loadPyodide (see pyodide.js) +/** + * Just like `runPython` except uses a different globals dict and gets + * `eval_code` from `_pyodide` so that it can work before `pyodide` is imported. + * @private + */ +API.runPythonInternal = function (code: string): any { + // API.runPythonInternal_dict is initialized in finalizeBootstrap + return API._pyodide._base.eval_code(code, API.runPythonInternal_dict); +}; + let runPythonPositionalGlobalsDeprecationWarned = false; /** * Runs a string of Python code from JavaScript, using :any:`pyodide.eval_code` @@ -463,7 +478,7 @@ export let FS: any; /** * @private */ -export function makePublicAPI(): PyodideInterface { +API.makePublicAPI = function (): PyodideInterface { FS = Module.FS; let namespace = { globals, @@ -492,4 +507,4 @@ export function makePublicAPI(): PyodideInterface { API.public_api = namespace; return namespace; -} +}; diff --git a/src/js/load-package.ts b/src/js/load-package.ts index 1dd69f10d..12014f159 100644 --- a/src/js/load-package.ts +++ b/src/js/load-package.ts @@ -1,5 +1,13 @@ -import { Module, API, Tests } from "./module.js"; -import { IN_NODE, nodeFsPromisesMod, _loadBinaryFile } from "./compat.js"; +declare var Module: any; +declare var Tests: any; +declare var API: any; + +import { + IN_NODE, + nodeFsPromisesMod, + _loadBinaryFile, + initNodeModules, +} from "./compat.js"; import { PyProxy, isPyProxy } from "./pyproxy.gen"; /** @private */ @@ -15,6 +23,7 @@ export async function initializePackageIndex(indexURL: string) { baseURL = indexURL; let package_json; if (IN_NODE) { + await initNodeModules(); const package_string = await nodeFsPromisesMod.readFile( `${indexURL}packages.json` ); @@ -412,3 +421,5 @@ export async function loadPackage( * install location for a particular ``package_name``. */ export let loadedPackages: { [key: string]: string } = {}; + +API.packageIndexReady = initializePackageIndex(API.config.indexURL); diff --git a/src/js/module.ts b/src/js/module.ts index 1a649b3d8..d3e705c75 100644 --- a/src/js/module.ts +++ b/src/js/module.ts @@ -3,21 +3,15 @@ * * @private */ -export let Module: any = {}; -Module.noImageDecoding = true; -Module.noAudioDecoding = true; -Module.noWasmDecoding = false; // we preload wasm using the built in plugin now -Module.preloadedWasm = {}; -Module.preRun = []; - -export let API: any = {}; -Module.API = API; -export let Hiwire: any = {}; -Module.hiwire = Hiwire; - -// Put things that are exposed only for testing purposes here. -export let Tests: any = {}; -API.tests = Tests; +export function createModule(): any { + let Module: any = {}; + Module.noImageDecoding = true; + Module.noAudioDecoding = true; + Module.noWasmDecoding = false; // we preload wasm using the built in plugin now + Module.preloadedWasm = {}; + Module.preRun = []; + return Module; +} /** * @@ -27,6 +21,7 @@ API.tests = Tests; * @private */ export function setStandardStreams( + Module: any, stdin?: () => string, stdout?: (a: string) => void, stderr?: (a: string) => void @@ -100,7 +95,7 @@ function createStdinWrapper(stdin: () => string) { * @param path * @private */ -export function setHomeDirectory(path: string) { +export function setHomeDirectory(Module: any, path: string) { Module.preRun.push(function () { const fallbackPath = "/"; try { diff --git a/src/js/pyodide.ts b/src/js/pyodide.ts index 208b7a15e..b0fba8457 100644 --- a/src/js/pyodide.ts +++ b/src/js/pyodide.ts @@ -2,15 +2,14 @@ * The main bootstrap code for loading pyodide. */ import ErrorStackParser from "error-stack-parser"; -import { Module, setStandardStreams, setHomeDirectory, API } from "./module.js"; import { loadScript, _loadBinaryFile, initNodeModules } from "./compat.js"; -import { initializePackageIndex, loadPackage } from "./load-package.js"; -import { makePublicAPI, PyodideInterface } from "./api.js"; -import "./error_handling.gen.js"; -import { PyProxy, PyProxyDict } from "./pyproxy.gen"; +import { createModule, setStandardStreams, setHomeDirectory } from "./module"; -export { +import type { PyodideInterface } from "./api.js"; +import type { PyProxy, PyProxyDict } from "./pyproxy.gen"; + +export type { PyProxy, PyProxyWithLength, PyProxyDict, @@ -28,16 +27,6 @@ export { export type Py2JsResult = any; -let runPythonInternal_dict: PyProxy; // Initialized in finalizeBootstrap -/** - * Just like `runPython` except uses a different globals dict and gets - * `eval_code` from `_pyodide` so that it can work before `pyodide` is imported. - * @private - */ -API.runPythonInternal = function (code: string): any { - return API._pyodide._base.eval_code(code, runPythonInternal_dict); -}; - /** * A proxy around globals that falls back to checking for a builtin if has or * get fails to find a global with the given key. Note that this proxy is @@ -68,7 +57,7 @@ function wrapPythonGlobals( }); } -function unpackPyodidePy(pyodide_py_tar: Uint8Array) { +function unpackPyodidePy(Module: any, pyodide_py_tar: Uint8Array) { const fileName = "/pyodide_py.tar"; let stream = Module.FS.open(fileName, "w"); Module.FS.write( @@ -105,11 +94,11 @@ del importlib * the core `pyodide` apis. (But package loading is not ready quite yet.) * @private */ -function finalizeBootstrap(config: ConfigType) { +function finalizeBootstrap(API: any, config: ConfigType) { // First make internal dict so that we can use runPythonInternal. // runPythonInternal uses a separate namespace, so we don't pollute the main // environment with variables from our setup. - runPythonInternal_dict = API._pyodide._base.eval_code("{}") as PyProxy; + API.runPythonInternal_dict = API._pyodide._base.eval_code("{}") as PyProxy; API.importlib = API.runPythonInternal("import importlib; importlib"); let import_module = API.importlib.import_module; @@ -130,7 +119,7 @@ function finalizeBootstrap(config: ConfigType) { importhook.register_js_finder(); importhook.register_js_module("js", config.jsglobals); - let pyodide = makePublicAPI(); + let pyodide = API.makePublicAPI(); importhook.register_js_module("pyodide_js", pyodide); // import pyodide_py. We want to ensure that as much stuff as possible is @@ -183,7 +172,7 @@ function calculateIndexURL(): string { * See documentation for loadPyodide. * @private */ -type ConfigType = { +export type ConfigType = { indexURL: string; homedir: string; fullStdLib?: boolean; @@ -244,13 +233,9 @@ export async function loadPyodide( jsglobals?: object; } = {} ): Promise { - if ((loadPyodide as any).inProgress) { - throw new Error("Pyodide is already loading."); - } if (!options.indexURL) { options.indexURL = calculateIndexURL(); } - (loadPyodide as any).inProgress = true; const default_config = { fullStdLib: true, @@ -258,21 +243,24 @@ export async function loadPyodide( stdin: globalThis.prompt ? globalThis.prompt : undefined, homedir: "/home/pyodide", }; - let config = Object.assign(default_config, options) as ConfigType; + const config = Object.assign(default_config, options) as ConfigType; if (!config.indexURL.endsWith("/")) { config.indexURL += "/"; } await initNodeModules(); - let packageIndexReady = initializePackageIndex(config.indexURL); - let pyodide_py_tar_promise = _loadBinaryFile( + const pyodide_py_tar_promise = _loadBinaryFile( config.indexURL, "pyodide_py.tar" ); - setStandardStreams(config.stdin, config.stdout, config.stderr); - setHomeDirectory(config.homedir); + const Module = createModule(); + const API: any = { config }; + Module.API = API; - let moduleLoaded = new Promise((r) => (Module.postRun = r)); + setStandardStreams(Module, config.stdin, config.stdout, config.stderr); + setHomeDirectory(Module, config.homedir); + + const moduleLoaded = new Promise((r) => (Module.postRun = r)); // locateFile tells Emscripten where to find the data files that initialize // the file system. @@ -294,15 +282,15 @@ export async function loadPyodide( }; const pyodide_py_tar = await pyodide_py_tar_promise; - unpackPyodidePy(pyodide_py_tar); + unpackPyodidePy(Module, pyodide_py_tar); Module._pyodide_init(); - let pyodide = finalizeBootstrap(config); - // Module.runPython works starting here. + const pyodide = finalizeBootstrap(API, config); + // API.runPython works starting here. - await packageIndexReady; + await API.packageIndexReady; if (config.fullStdLib) { - await loadPackage(["distutils"]); + await pyodide.loadPackage(["distutils"]); } pyodide.runPython("print('Python initialization complete')"); return pyodide; diff --git a/src/js/rollup.config.js b/src/js/rollup.config.js index 5e08ef42a..a62771280 100644 --- a/src/js/rollup.config.js +++ b/src/js/rollup.config.js @@ -3,15 +3,12 @@ import { nodeResolve } from "@rollup/plugin-node-resolve"; import { terser } from "rollup-plugin-terser"; import ts from "rollup-plugin-ts"; -function config({ input, format, minify, ext }) { - const dir = `dist/`; - // const minifierSuffix = minify ? ".min" : ""; - const minifierSuffix = ""; +function config({ input, output, name, format, minify }) { return { input: `./src/js/${input}.ts`, output: { - name: "loadPyodide", - file: `${dir}/pyodide${minifierSuffix}.${ext}`, + file: output, + name, format, sourcemap: true, }, @@ -36,8 +33,23 @@ function config({ input, format, minify, ext }) { } export default [ - // { input: "pyodide", format: "esm", minify: false, ext: "mjs" }, - { input: "pyodide", format: "esm", minify: true, ext: "mjs" }, - // { input: "pyodide", format: "umd", minify: false }, - { input: "pyodide.umd", format: "umd", minify: true, ext: "js" }, + { + input: "pyodide", + output: "dist/pyodide.mjs", + format: "esm", + minify: true, + }, + { + input: "pyodide.umd", + output: "dist/pyodide.js", + format: "umd", + name: "loadPyodide", + minify: true, + }, + { + input: "api", + output: "src/js/_pyodide.out.js", + format: "iife", + minify: true, + }, ].map(config); diff --git a/src/tests/test_pyodide.py b/src/tests/test_pyodide.py index 75a3c5a2f..a0f31447f 100644 --- a/src/tests/test_pyodide.py +++ b/src/tests/test_pyodide.py @@ -892,14 +892,14 @@ def test_js_stackframes(selenium): ["test.html", "d3"], ["test.html", "d2"], ["test.html", "d1"], - ["pyodide.js", "runPython"], + ["pyodide.asm.js", "runPython"], ["_pyodide/_base.py", "eval_code"], ["_pyodide/_base.py", "run"], ["", ""], ["", "c2"], ["", "c1"], ["test.html", "b"], - ["pyodide.js", "pyimport"], + ["pyodide.asm.js", "pyimport"], ["importlib/__init__.py", "import_module"], ] assert normalize_tb(res[: len(frames)]) == frames