diff options
Diffstat (limited to 'external/pyodide/pyodide.js')
-rw-r--r-- | external/pyodide/pyodide.js | 412 |
1 files changed, 412 insertions, 0 deletions
diff --git a/external/pyodide/pyodide.js b/external/pyodide/pyodide.js new file mode 100644 index 00000000..d0b6dfa0 --- /dev/null +++ b/external/pyodide/pyodide.js @@ -0,0 +1,412 @@ +/** + * The main bootstrap script for loading pyodide. + */ + +var languagePluginLoader = new Promise((resolve, reject) => { + // This is filled in by the Makefile to be either a local file or the + // deployed location. TODO: This should be done in a less hacky + // way. + var baseURL = self.languagePluginUrl || 'https://pyodide.cdn.iodide.io/'; + baseURL = baseURL.substr(0, baseURL.lastIndexOf('/')) + '/'; + + //////////////////////////////////////////////////////////// + // Package loading + let loadedPackages = new Array(); + var loadPackagePromise = new Promise((resolve) => resolve()); + // Regexp for validating package name and URI + var package_name_regexp = '[a-z0-9_][a-z0-9_\-]*' + var package_uri_regexp = + new RegExp('^https?://.*?(' + package_name_regexp + ').js$', 'i'); + var package_name_regexp = new RegExp('^' + package_name_regexp + '$', 'i'); + + let _uri_to_package_name = (package_uri) => { + // Generate a unique package name from URI + + if (package_name_regexp.test(package_uri)) { + return package_uri; + } else if (package_uri_regexp.test(package_uri)) { + let match = package_uri_regexp.exec(package_uri); + // Get the regexp group corresponding to the package name + return match[1]; + } else { + return null; + } + }; + + // clang-format off + let preloadWasm = () => { + // On Chrome, we have to instantiate wasm asynchronously. Since that + // can't be done synchronously within the call to dlopen, we instantiate + // every .so that comes our way up front, caching it in the + // `preloadedWasm` dictionary. + + let promise = new Promise((resolve) => resolve()); + let FS = pyodide._module.FS; + + function recurseDir(rootpath) { + let dirs; + try { + dirs = FS.readdir(rootpath); + } catch { + return; + } + for (let entry of dirs) { + if (entry.startsWith('.')) { + continue; + } + const path = rootpath + entry; + if (entry.endsWith('.so')) { + if (Module['preloadedWasm'][path] === undefined) { + promise = promise + .then(() => Module['loadWebAssemblyModule']( + FS.readFile(path), {loadAsync: true})) + .then((module) => { + Module['preloadedWasm'][path] = module; + }); + } + } else if (FS.isDir(FS.lookupPath(path).node.mode)) { + recurseDir(path + '/'); + } + } + } + + recurseDir('/'); + + return promise; + } + // clang-format on + + function loadScript(url, onload, onerror) { + if (self.document) { // browser + const script = self.document.createElement('script'); + script.src = url; + script.onload = (e) => { onload(); }; + script.onerror = (e) => { onerror(); }; + self.document.head.appendChild(script); + } else if (self.importScripts) { // webworker + try { + self.importScripts(url); + onload(); + } catch { + onerror(); + } + } + } + + let _loadPackage = (names, messageCallback) => { + // DFS to find all dependencies of the requested packages + let packages = self.pyodide._module.packages.dependencies; + let loadedPackages = self.pyodide.loadedPackages; + let queue = [].concat(names || []); + let toLoad = new Array(); + while (queue.length) { + let package_uri = queue.pop(); + + const pkg = _uri_to_package_name(package_uri); + + if (pkg == null) { + console.error(`Invalid package name or URI '${package_uri}'`); + return; + } else if (pkg == package_uri) { + package_uri = 'default channel'; + } + + if (pkg in loadedPackages) { + if (package_uri != loadedPackages[pkg]) { + console.error(`URI mismatch, attempting to load package ` + + `${pkg} from ${package_uri} while it is already ` + + `loaded from ${loadedPackages[pkg]}!`); + return; + } + } else if (pkg in toLoad) { + if (package_uri != toLoad[pkg]) { + console.error(`URI mismatch, attempting to load package ` + + `${pkg} from ${package_uri} while it is already ` + + `being loaded from ${toLoad[pkg]}!`); + return; + } + } else { + console.log(`Loading ${pkg} from ${package_uri}`); + + toLoad[pkg] = package_uri; + if (packages.hasOwnProperty(pkg)) { + packages[pkg].forEach((subpackage) => { + if (!(subpackage in loadedPackages) && !(subpackage in toLoad)) { + queue.push(subpackage); + } + }); + } else { + console.error(`Unknown package '${pkg}'`); + } + } + } + + self.pyodide._module.locateFile = (path) => { + // handle packages loaded from custom URLs + let pkg = path.replace(/\.data$/, ""); + if (pkg in toLoad) { + let package_uri = toLoad[pkg]; + if (package_uri != 'default channel') { + return package_uri.replace(/\.js$/, ".data"); + }; + }; + return baseURL + path; + }; + + let promise = new Promise((resolve, reject) => { + if (Object.keys(toLoad).length === 0) { + resolve('No new packages to load'); + return; + } + + const packageList = Array.from(Object.keys(toLoad)).join(', '); + if (messageCallback !== undefined) { + messageCallback(`Loading ${packageList}`); + } + + // monitorRunDependencies is called at the beginning and the end of each + // package being loaded. We know we are done when it has been called + // exactly "toLoad * 2" times. + var packageCounter = Object.keys(toLoad).length * 2; + + self.pyodide._module.monitorRunDependencies = () => { + packageCounter--; + if (packageCounter === 0) { + for (let pkg in toLoad) { + self.pyodide.loadedPackages[pkg] = toLoad[pkg]; + } + delete self.pyodide._module.monitorRunDependencies; + self.removeEventListener('error', windowErrorHandler); + if (!isFirefox) { + preloadWasm().then(() => {resolve(`Loaded ${packageList}`)}); + } else { + resolve(`Loaded ${packageList}`); + } + } + }; + + // Add a handler for any exceptions that are thrown in the process of + // loading a package + var windowErrorHandler = (err) => { + delete self.pyodide._module.monitorRunDependencies; + self.removeEventListener('error', windowErrorHandler); + // Set up a new Promise chain, since this one failed + loadPackagePromise = new Promise((resolve) => resolve()); + reject(err.message); + }; + self.addEventListener('error', windowErrorHandler); + + for (let pkg in toLoad) { + let scriptSrc; + let package_uri = toLoad[pkg]; + if (package_uri == 'default channel') { + scriptSrc = `${baseURL}${pkg}.js`; + } else { + scriptSrc = `${package_uri}`; + } + loadScript(scriptSrc, () => {}, () => { + // If the package_uri fails to load, call monitorRunDependencies twice + // (so packageCounter will still hit 0 and finish loading), and remove + // the package from toLoad so we don't mark it as loaded. + console.error(`Couldn't load package from URL ${scriptSrc}`) + let index = toLoad.indexOf(pkg); + if (index !== -1) { + toLoad.splice(index, 1); + } + for (let i = 0; i < 2; i++) { + self.pyodide._module.monitorRunDependencies(); + } + }); + } + + // We have to invalidate Python's import caches, or it won't + // see the new files. This is done here so it happens in parallel + // with the fetching over the network. + self.pyodide.runPython('import importlib as _importlib\n' + + '_importlib.invalidate_caches()\n'); + }); + + return promise; + }; + + let loadPackage = (names, messageCallback) => { + /* We want to make sure that only one loadPackage invocation runs at any + * given time, so this creates a "chain" of promises. */ + loadPackagePromise = + loadPackagePromise.then(() => _loadPackage(names, messageCallback)); + return loadPackagePromise; + }; + + //////////////////////////////////////////////////////////// + // Fix Python recursion limit + function fixRecursionLimit(pyodide) { + // The Javascript/Wasm call stack may be too small to handle the default + // Python call stack limit of 1000 frames. This is generally the case on + // Chrom(ium), but not on Firefox. Here, we determine the Javascript call + // stack depth available, and then divide by 50 (determined heuristically) + // to set the maximum Python call stack depth. + + let depth = 0; + function recurse() { + depth += 1; + recurse(); + } + try { + recurse(); + } catch (err) { + ; + } + + let recursionLimit = depth / 50; + if (recursionLimit > 1000) { + recursionLimit = 1000; + } + pyodide.runPython( + `import sys; sys.setrecursionlimit(int(${recursionLimit}))`); + }; + + //////////////////////////////////////////////////////////// + // Rearrange namespace for public API + let PUBLIC_API = [ + 'globals', + 'loadPackage', + 'loadedPackages', + 'pyimport', + 'repr', + 'runPython', + 'runPythonAsync', + 'checkABI', + 'version', + ]; + + function makePublicAPI(module, public_api) { + var namespace = {_module : module}; + for (let name of public_api) { + namespace[name] = module[name]; + } + return namespace; + } + + //////////////////////////////////////////////////////////// + // Loading Pyodide + let wasmURL = `${baseURL}pyodide.asm.wasm`; + let Module = {}; + self.Module = Module; + + Module.noImageDecoding = true; + Module.noAudioDecoding = true; + Module.noWasmDecoding = true; + Module.preloadedWasm = {}; + let isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; + + let wasm_promise; + if (WebAssembly.compileStreaming === undefined) { + wasm_promise = fetch(wasmURL) + .then(response => response.arrayBuffer()) + .then(bytes => WebAssembly.compile(bytes)); + } else { + wasm_promise = WebAssembly.compileStreaming(fetch(wasmURL)); + } + + Module.instantiateWasm = (info, receiveInstance) => { + wasm_promise.then(module => WebAssembly.instantiate(module, info)) + .then(instance => receiveInstance(instance)); + return {}; + }; + + Module.checkABI = function(ABI_number) { + if (ABI_number !== parseInt('1')) { + var ABI_mismatch_exception = + `ABI numbers differ. Expected 1, got ${ABI_number}`; + console.error(ABI_mismatch_exception); + throw ABI_mismatch_exception; + } + return true; + }; + + Module.locateFile = (path) => baseURL + path; + var postRunPromise = new Promise((resolve, reject) => { + Module.postRun = () => { + delete self.Module; + fetch(`${baseURL}packages.json`) + .then((response) => response.json()) + .then((json) => { + fixRecursionLimit(self.pyodide); + self.pyodide.globals = + self.pyodide.runPython('import sys\nsys.modules["__main__"]'); + self.pyodide = makePublicAPI(self.pyodide, PUBLIC_API); + self.pyodide._module.packages = json; + resolve(); + }); + }; + }); + + var dataLoadPromise = new Promise((resolve, reject) => { + Module.monitorRunDependencies = + (n) => { + if (n === 0) { + delete Module.monitorRunDependencies; + resolve(); + } + } + }); + + Promise.all([ postRunPromise, dataLoadPromise ]).then(() => resolve()); + + const data_script_src = `${baseURL}pyodide.asm.data.js`; + loadScript(data_script_src, () => { + const scriptSrc = `${baseURL}pyodide.asm.js`; + loadScript(scriptSrc, () => { + // The emscripten module needs to be at this location for the core + // filesystem to install itself. Once that's complete, it will be replaced + // by the call to `makePublicAPI` with a more limited public API. + self.pyodide = pyodide(Module); + self.pyodide.loadedPackages = new Array(); + self.pyodide.loadPackage = loadPackage; + }, () => {}); + }, () => {}); + + //////////////////////////////////////////////////////////// + // Iodide-specific functionality, that doesn't make sense + // if not using with Iodide. + if (self.iodide !== undefined) { + // Load the custom CSS for Pyodide + let link = document.createElement('link'); + link.rel = 'stylesheet'; + link.type = 'text/css'; + link.href = `${baseURL}renderedhtml.css`; + document.getElementsByTagName('head')[0].appendChild(link); + + // Add a custom output handler for Python objects + self.iodide.addOutputRenderer({ + shouldRender : (val) => { + return (typeof val === 'function' && + pyodide._module.PyProxy.isPyProxy(val)); + }, + + render : (val) => { + let div = document.createElement('div'); + div.className = 'rendered_html'; + var element; + if (val._repr_html_ !== undefined) { + let result = val._repr_html_(); + if (typeof result === 'string') { + div.appendChild(new DOMParser() + .parseFromString(result, 'text/html') + .body.firstChild); + element = div; + } else { + element = result; + } + } else { + let pre = document.createElement('pre'); + pre.textContent = val.toString(); + div.appendChild(pre); + element = div; + } + return element.outerHTML; + } + }); + } +}); +languagePluginLoader |