tinkerbell/web/pyodide.js

465 lines
15 KiB
JavaScript

/**
* 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://cdn.jsdelivr.net/pyodide/v0.15.0/full/';
baseURL = baseURL.substr(0, baseURL.lastIndexOf('/')) + '/';
////////////////////////////////////////////////////////////
// Package loading
let loadedPackages = {};
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, errorCallback) => {
if (messageCallback == undefined) {
messageCallback = () => {};
}
if (errorCallback == undefined) {
errorCallback = () => {};
}
let _messageCallback = (msg) => {
console.log(msg);
messageCallback(msg);
};
let _errorCallback = (errMsg) => {
console.error(errMsg);
errorCallback(errMsg);
};
// 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 = {};
while (queue.length) {
let package_uri = queue.pop();
const pkg = _uri_to_package_name(package_uri);
if (pkg == null) {
_errorCallback(`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]) {
_errorCallback(`URI mismatch, attempting to load package ` +
`${pkg} from ${package_uri} while it is already ` +
`loaded from ${loadedPackages[pkg]}!`);
return;
} else {
_messageCallback(`${pkg} already loaded from ${loadedPackages[pkg]}`)
}
} else if (pkg in toLoad) {
if (package_uri != toLoad[pkg]) {
_errorCallback(`URI mismatch, attempting to load package ` +
`${pkg} from ${package_uri} while it is already ` +
`being loaded from ${toLoad[pkg]}!`);
return;
}
} else {
console.log(
`${pkg} to be loaded from ${package_uri}`); // debug level info.
toLoad[pkg] = package_uri;
if (packages.hasOwnProperty(pkg)) {
packages[pkg].forEach((subpackage) => {
if (!(subpackage in loadedPackages) && !(subpackage in toLoad)) {
queue.push(subpackage);
}
});
} else {
_errorCallback(`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;
}
let packageList = Array.from(Object.keys(toLoad));
_messageCallback(`Loading ${packageList.join(', ')}`)
// 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);
let resolveMsg = `Loaded `;
if (packageList.length > 0) {
resolveMsg += packageList.join(', ');
} else {
resolveMsg += 'no packages'
}
if (!isFirefox) {
preloadWasm().then(() => {
console.log(resolveMsg);
resolve(resolveMsg);
});
} else {
console.log(resolveMsg);
resolve(resolveMsg);
}
}
};
// 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}`;
}
_messageCallback(`Loading ${pkg} from ${scriptSrc}`)
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, and remove
// the package from packageList so we don't say that it was loaded.
_errorCallback(`Couldn't load package from URL ${scriptSrc}`);
delete toLoad[pkg];
let packageListIndex = packageList.indexOf(pkg);
if (packageListIndex !== -1) {
packageList.splice(packageListIndex, 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, errorCallback) => {
/* 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, errorCallback));
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',
'autocomplete',
];
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, wasm_fetch = fetch(wasmURL);
const compileBuffer = () =>
wasm_fetch.then(response => response.arrayBuffer())
.then(bytes => WebAssembly.compile(bytes));
if (WebAssembly.compileStreaming === undefined) {
wasm_promise = compileBuffer();
} else {
wasm_promise = WebAssembly.compileStreaming(wasm_fetch);
wasm_promise = wasm_promise.catch(e => {
if (e instanceof TypeError) {
console.error("pyodide streaming compilation failed:", e,
"- falling back to buffered compilation");
return compileBuffer()
}
throw e;
});
}
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.autocomplete =
function(path) {
var pyodide_module = Module.pyimport("pyodide");
return pyodide_module.get_completions(path);
}
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;
if (self.iodide !== undefined) {
// Perform some completions immediately so there isn't a delay on
// the first call to autocomplete
self.pyodide.runPython('import pyodide');
self.pyodide.runPython('pyodide.get_completions("")');
}
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 = {};
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