diff --git a/.gitignore b/.gitignore index 7db7aa43..09640479 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,22 @@ build/ .vscode/ pyvenv.cfg +python/pythonmonkey/version.py +python/pythonmonkey/node_modules bin/ lib/* .pytest_cache .DS_Store firefox-*.tar.xz firefox-*/ -tests/__pycache__/* -tests/python/__pycache__/* +__pycache__ Testing/Temporary _spidermonkey_install -__pycache__ +__pycache__/* dist *.so +_spidermonkey_install/* +*~ *.dylib *.dll *.pyd diff --git a/CMakeLists.txt b/CMakeLists.txt index 8215fe7c..e56562da 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,7 +55,7 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) set(PYTHON_INCLUDE_DIR ${Python_INCLUDE_DIRS}) set(PYTHON_LIBRARIES ${Python_LIBRARIES}) message("Apple - Using Python:${Python_VERSION_MAJOR} - Libraries:${PYTHON_LIBRARIES} - IncludeDirs: ${PYTHON_INCLUDE_DIR}") - elseif(LINUX) + elseif(UNIX) find_package(Python 3.8 COMPONENTS Interpreter Development REQUIRED) set(Python_FIND_VIRTUALENV FIRST) # (require cmake >= v3.15 and this is the default) use the Python version configured by pyenv if available set(PYTHON_LIBRARIES ${Python_LIBRARIES}) diff --git a/README.md b/README.md index 5d0c9ca4..6f89be25 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ PythonMonkey is a Mozilla [SpiderMonkey](https://firefox-source-docs.mozilla.org/js/index.html) JavaScript engine embedded into the Python VM, using the Python engine to provide the JS host environment. -This product is in an early stage, approximately 65% to MVP as of March 2023. It is under active development by Distributive Corp., +This product is in an early stage, approximately 80% to MVP as of May 2023. It is under active development by Distributive Corp., https://distributive.network/. External contributions and feedback are welcome and encouraged. The goal is to make writing code in either JS or Python a developer preference, with libraries commonly used in either language @@ -30,15 +30,15 @@ this package to execute our complex `dcp-client` library, which is written in JS - [done] JS functions coerce to Python function wrappers - [done] JS exceptions propagate to Python - [done] Implement `eval()` function in Python which accepts JS code and returns JS->Python coerced values -- [underway] NodeJS+NPM-compatible CommonJS module system +- [done] NodeJS+NPM-compatible CommonJS module system - [done] Python strings coerce to JS strings - [done] Python intrinsics coerce to JS intrinsics -- Python dicts coerce to JS objects -- Python `require` function, returns a coerced dict of module exports +- [done] Python dicts coerce to JS objects +- [done] Python `require` function, returns a coerced dict of module exports - [done] Python functions coerce to JS function wrappers -- CommonJS module system .py loader, loads Python modules for use by JS +- [done] CommonJS module system .py loader, loads Python modules for use by JS - JS object->Python dict coercion supports inherited-property lookup (via __getattribute__?) -- Python host environment supplies event loop, including EventEmitter, setTimeout, etc. +- [done] Python host environment supplies event loop, including EventEmitter, setTimeout, etc. - Python host environment supplies XMLHttpRequest (other project?) - Python host environment supplies basic subsets of NodeJS's fs, path, process, etc, modules; as-needed by dcp-client (other project?) - Python TypedArrays coerce to JS TypeArrays @@ -53,6 +53,7 @@ this package to execute our complex `dcp-client` library, which is written in JS - rust - python3.8 or later with header files (python3-dev) - spidermonkey 102.2.0 or later + - npm (nodejs) - [Poetry](https://python-poetry.org/docs/#installation) - [poetry-dynamic-versioning](https://github.com/mtkennerly/poetry-dynamic-versioning) @@ -95,3 +96,23 @@ Type "help", "copyright", "credits" or "license" for more information. ``` Alternatively, you can build a `wheel` package by running `poetry build --format=wheel`, and install it by `pip install dist/*.whl`. + +## Examples + +* [examples/](examples/) +* https://github.com/Distributive-Network/PythonMonkey-examples +* https://github.com/Distributive-Network/PythonMonkey-Crypto-JS-Fullstack-Example + +# Troubleshooting Tips + +## REPL - pmjs +A basic JavaScript shell, `pmjs`, ships with PythonMonkey. + +## CommonJS (require) +If you are having trouble with the CommonJS require function, set environment variable DEBUG='ctx-module*' and you can see the filenames it tries to laod. + +### Extra Symbols +Loading the CommonJS subsystem declares some extra symbols which may be helpful in debugging - +- `python.print` - the Python print function +- `python.getenv` - the Python getenv function + diff --git a/build_script.sh b/build_script.sh index 61eca40f..c34e92c2 100755 --- a/build_script.sh +++ b/build_script.sh @@ -1,3 +1,12 @@ +#! /bin/bash +# +# @file build.sh +# @author Giovanni Tedesco +# @date Aug 2022 + +cd `dirname "$0"` +topDir=`pwd` + # Get number of CPU cores CPUS=$(getconf _NPROCESSORS_ONLN 2>/dev/null || getconf NPROCESSORS_ONLN 2>/dev/null || echo 1) @@ -14,3 +23,8 @@ else cmake .. fi cmake --build . -j$CPUS --config Release + +cd ../python/pythonmonkey +# npm is used to load JS components, see package.json +cd "${topDir}/python/pythonmonkey/" +npm i diff --git a/examples/use-python-module.py b/examples/use-python-module.py new file mode 100644 index 00000000..912a517a --- /dev/null +++ b/examples/use-python-module.py @@ -0,0 +1,9 @@ +# @file use-require.py +# Sample code which demonstrates how to use require +# @author Wes Garland, wes@distributive.network +# @date Jun 2023 + +import pythonmonkey as pm + +require = pm.createRequire(__file__) +require('./use-python-module'); diff --git a/examples/use-python-module/index.js b/examples/use-python-module/index.js new file mode 100644 index 00000000..288dc10b --- /dev/null +++ b/examples/use-python-module/index.js @@ -0,0 +1,3 @@ +const { helloWorld } = require('./my-python-module'); +helloWorld() + diff --git a/examples/use-python-module/my-python-module.py b/examples/use-python-module/my-python-module.py new file mode 100644 index 00000000..236b873d --- /dev/null +++ b/examples/use-python-module/my-python-module.py @@ -0,0 +1,5 @@ +def helloWorld(): + print('hello, world!') + +exports['helloWorld'] = helloWorld + diff --git a/examples/use-require.py b/examples/use-require.py new file mode 100644 index 00000000..9f0b1b1a --- /dev/null +++ b/examples/use-require.py @@ -0,0 +1,11 @@ +# @file use-require.py +# Sample code which demonstrates how to use require +# @author Wes Garland, wes@distributive.network +# @date Jun 2023 + +import pythonmonkey as pm + +require = pm.createRequire(__file__) +require('./use-require/test1'); +print("Done") + diff --git a/examples/use-require/test1.js b/examples/use-require/test1.js new file mode 100644 index 00000000..c58753e8 --- /dev/null +++ b/examples/use-require/test1.js @@ -0,0 +1,5 @@ +'use strict' + +const makeOutput = require('./test2').makeOutput; + +makeOutput('hello world'); diff --git a/examples/use-require/test2.js b/examples/use-require/test2.js new file mode 100644 index 00000000..92076222 --- /dev/null +++ b/examples/use-require/test2.js @@ -0,0 +1,9 @@ +'use strict' + +exports.makeOutput = function makeOutput() +{ + const argv = Array.from(arguments); + argv.unshift('TEST OUTPUT: '); + python.print.apply(null, argv); +} + diff --git a/js-test-runner b/js-test-runner new file mode 100755 index 00000000..a5490589 --- /dev/null +++ b/js-test-runner @@ -0,0 +1,16 @@ +#! /usr/bin/env python3 +# @file js-test-runner +# A simple test driver for peter-jr that runs JS code via PythonMonkey.require +# @author Wes Garland, wes@distributive.network +# @date Jun 2023 + +import sys, os +import pythonmonkey as pm + +# Main +require = pm.createRequire(__file__) +require('console'); + +testName = os.path.realpath(sys.argv[1]); +testDir = os.path.dirname(testName); +pm.createRequire(testName)(testName); diff --git a/peter-jr b/peter-jr new file mode 100755 index 00000000..5adb1635 --- /dev/null +++ b/peter-jr @@ -0,0 +1,130 @@ +#! /bin/bash +# +# @file peter-js +# A simple test framework in the spirit of Peter (npmjs.com/packages/peter) for testing +# basic PythonMonkey functionality. +# +# Exit Codes: +# 0 - all tests passed +# 1 - one or more tests failed +# 2 - no tests ran +# 3 - internal error +# +# @author Wes Garland, wes@distributive.network +# @date Jun 2023 +# +runDir=`pwd` +cd `dirname "$0"` +topDir=`pwd` +cd "$runDir" + +set -u +set -o pipefail +peter_exit_code=2 + +if [ "${1:-}" = "-v" ]; then + VERBOSE=1 + shift +else + VERBOSE="${VERBOSE:-}" +fi + +[ "${1:-}" ] || set "${topDir}/tests/js" +testLocations=("$@") + +function panic() +{ + echo "PANIC: $*" >&2 + exit 3 +} + +TMP=`mktemp -d` +([ "${TMP}" ] && [ -d "${TMP}" ]) || exit 3 + +trap "rm -r \"${TMP}\"" EXIT + +if [ "$VERBOSE" ]; then + stdout="/dev/stdout" + stderr="/dev/stderr" +else + stdout="$TMP/stdout" + stderr="$TMP/stderr" +fi + +red() +{ + printf "\e[0;31m%s\e[0m" "$*" +} + +green() +{ + printf "\e[0;32m%s\e[0m" "$*" +} + +yellow() +{ + printf "\e[0;33m%s\e[0m" "$*" +} + +grey() +{ + printf "\e[0;90m%s\e[0m" "$*" +} + +( + for loc in "${testLocations[@]}" + do + find $(realpath "$loc") -type f -name \*.simple + find $(realpath "$loc") -type f -name \*.bash + done +) \ +| while read file + do + sfile=$(realpath --relative-to="${runDir}" "${file}") + printf 'Testing %-40s ... ' "${sfile}" + ext="${file##*.}" + ( + case "$ext" in + "simple") + "${topDir}/js-test-runner" "$file" + exitCode="$?" + ;; + "bash") + bash "$file" + exitCode="$?" + ;; + *) + echo + panic "${file}: invalid extension '$ext'" + ;; + esac + + exit "$exitCode" + )> $stdout 2>$stderr + exitCode="$?" + + if [ "$exitCode" = "0" ]; then + echo "$(green PASS)" + [ "${peter_exit_code}" = "2" ] && peter_exit_code=0 + printf "\e[0;31m" + [ "$VERBOSE" ] || cat "$stderr" + printf "\e[0m" + else + echo "$(red FAIL)" + peter_exit_code=1 + if [ ! "$VERBOSE" ]; then + echo + echo "$(grey --) $(yellow ${file}) $(grey vvvvvvvvvvvvvv)" + cat "$stdout" | sed 's/^/ /' + printf "\e[0;31m" + cat "$stderr" | sed -e 's/^/ /' + printf "\e[0m" + echo "$(grey --) $(yellow ${file}) $(grey ^^^^^^^^^^^^^^)" + echo + fi + fi + (exit ${peter_exit_code}) + done +peter_exit_code="$?" + +exit ${peter_exit_code} diff --git a/pmjs b/pmjs new file mode 100755 index 00000000..b23b3742 --- /dev/null +++ b/pmjs @@ -0,0 +1,181 @@ +#! /usr/bin/env python3 +# @file pmjs - PythonMonkey REPL +# @author Wes Garland, wes@distributive.network +# @date June 2023 + +import sys, os, readline, signal +import pythonmonkey as pm + +globalThis = pm.eval("globalThis;") +globalThis.python.write = sys.stdout.write +pm.createRequire(__file__)('./pmjs-require') + +pm.eval("""'use strict'; +const cmds = {}; + +cmds.help = function help() { + return '' + +`.exit Exit the REPL +.help Print this help message +.load Load JS from a file into the REPL session +.save Save all evaluated commands in this REPL session to a file + +Press Ctrl+C to abort current expression, Ctrl+D to exit the REPL` +}; + +cmds.exit = python.exit; + +/** + * Handle a .XXX command. Invokes function cmds[XXX], passing arguments that the user typed + * as the function arguments. The function arguments space-delimited arguments; arguments + * surrounded by quotes can include spaces, similar to how bash parses arguments. Argument + * parsing cribbed from stackoverflow user Tsuneo Yoshioka, question 4031900. + * + * @param {string} cmdLine the command the user typed, without the leading . + * @returns {string} to display + */ +globalThis.replCmd = function replCmd(cmdLine) +{ + const cmdName = (cmdLine.match(/^[^ ]+/) || ['help'])[0]; + const args = cmdLine.slice(cmdName.length).trim(); + const argv = args.match(/\\\\?.|^$/g).reduce((p, c) => { + if (c === '"') + p.quote ^= 1; + else if (!p.quote && c === ' ') + p.a.push(''); + else + p.a[p.a.length-1] += c.replace(/\\\\(.)/,"$1"); + return p; + }, {a: ['']}).a; + + if (!cmds.hasOwnProperty(cmdName)) + return `Invalid REPL keyword`; + return cmds[cmdName].apply(null, argv); +} + +/** Return String(val) surrounded by appropriate ANSI escape codes to change the console text colour. */ +function colour(colourCode, val) +{ + const esc=String.fromCharCode(27); + return `${esc}[${colourCode}m${val}${esc}[0m` +} + +/** + * Format result more intelligently than toString. Inspired by Node.js util.inspect, but far less + * capable. + */ +globalThis.formatResult = function formatResult(result) +{ + switch (typeof result) + { + case 'undefined': + return colour(90, result); + case 'function': + return colour(36, result); + case 'string': + return colour(32, `'${result}'`); + case 'boolean': + case 'number': + return colour(33, result); + case 'object': + if (result instanceof Date) + return colour(35, result.toISOString()); + if (result instanceof Error) + { + const error = result; + const LF = String.fromCharCode(10); + const stackEls = error.stack + .split(LF) + .filter(a => a.length > 0) + .map(a => ` ${a}`); + return (`${error.name}: ${error.message}` + LF + + stackEls[0] + LF + + colour(90, stackEls.slice(1).join(LF)) + ); + } + return JSON.stringify(result); + default: + return colour(31, `${result}`); + } +} + +/** + * Evaluate a complete statement, built by the Python readline loop. + */ +globalThis.replEval = function replEval(statement) +{ + const indirectEval = eval; + try + { + const result = indirectEval(`${statement}`); + return formatResult(result); + } + catch(error) + { + return formatResult(error); + } +} +"""); + +readline.parse_and_bind('set editing-mode emacs') +histfile = os.path.expanduser('~/.pmjs_history') +if (os.path.exists(histfile)): + readline.read_history_file(histfile) + +print('Welcome to PythonMonkey v' + pm.__version__ +'.') +print('Type ".help" for more information.') + +def quit(): + readline.write_history_file(histfile) + sys.exit(0) +globalThis.python.exit = quit + +got_sigint = 0 +building_statement = False + +# Try to handle ^C by aborting the entry of the current line and quitting when double-struck. Note that +# does not currently work properly because there doesn't seem to be an easy way to abort data entry via +# input() via gnu readline in Python. +def sigint_handler(signum, frame): + global got_sigint + global building_statement + got_sigint = got_sigint + 1 + if (got_sigint == 1 and building_statement == False): + print('(To exit, press Ctrl+C again or Ctrl+D or type .exit)') + if (got_sigint > 1 and building_statement == False): + quit() + readline.redisplay() +signal.signal(signal.SIGINT, sigint_handler) + +# Main Program Loop ##### +# +# Read lines entered by the user and collect them in a statement. Once the statement is a candiate for +# JavaScript evaluation (determined by pm.isCompilableUnit(), send it to replEval(). Statement beginning +# with a . are interpreted as REPL commands and sent to replCmd(). +while got_sigint < 2: + try: + building_statement = False + statement = input('> ') + + if (len(statement) == 0): + continue + if (statement[0] == '.'): + print(globalThis.replCmd(statement[1:])) + continue + if (pm.isCompilableUnit(statement)): + print(globalThis.replEval(statement)) + got_sigint = 0 + else: + building_statement = True + got_sigint = 0 + while (got_sigint == 0): + more = input('... ') + statement = statement + '\n' + more + if (pm.isCompilableUnit(statement)): + print(globalThis.replEval(statement)) + break + got_sigint = 0 + building_statement = False + except EOFError: + print() + quit() diff --git a/pmjs-require.js b/pmjs-require.js new file mode 100644 index 00000000..005dbd95 --- /dev/null +++ b/pmjs-require.js @@ -0,0 +1 @@ +globalThis.require = require diff --git a/pyproject.toml b/pyproject.toml index c9630bb6..f80e88ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "pythonmonkey" version = "0" # automatically set by poetry-dynamic-versioning description = "" -authors = ["Caleb Aikens ", "Tom Tang "] +authors = ["Caleb Aikens ", "Tom Tang ", "Wes Garland ", "Hamada Gasmallah "] readme = "README.md" packages = [ { include = "pythonmonkey", from = "python" }, @@ -11,11 +11,14 @@ include = [ # Linux and macOS "python/pythonmonkey/pythonmonkey.so", "python/pythonmonkey/libmozjs*", + "python/pythonmonkey/builtin_modules/**/*", # Windows "python/pythonmonkey/pythonmonkey.pyd", "python/pythonmonkey/mozjs-*.dll", + "python/pythonmonkey/node_modules/**/*", + # include all files for source distribution { path = "src", format = "sdist" }, { path = "include", format = "sdist" }, diff --git a/python/pythonmonkey/__init__.py b/python/pythonmonkey/__init__.py index dc0eaa62..1cf4ee82 100644 --- a/python/pythonmonkey/__init__.py +++ b/python/pythonmonkey/__init__.py @@ -1,4 +1,5 @@ from .pythonmonkey import * +from .require import * # Expose the package version import importlib.metadata diff --git a/python/pythonmonkey/builtin_modules/console.js b/python/pythonmonkey/builtin_modules/console.js new file mode 100644 index 00000000..655b0f77 --- /dev/null +++ b/python/pythonmonkey/builtin_modules/console.js @@ -0,0 +1,19 @@ +/** + * @file console.js + * Temporary implementation of console.log etc. + * @author Wes Garland, wes@distributive.network + * @date June 2023 + */ +function Console(print) +{ + this.log = print; + this.debug = print; + this.error = print; + this.warn = print; + this.info = print; +} + +if (!globalThis.console) + globalThis.console = new Console(python.print); + +exports.Console = Console; diff --git a/python/pythonmonkey/package-lock.json b/python/pythonmonkey/package-lock.json new file mode 100644 index 00000000..b66a51d6 --- /dev/null +++ b/python/pythonmonkey/package-lock.json @@ -0,0 +1,434 @@ +{ + "name": "pythonmonkey", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "requires": { + "bn.js": "^5.0.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", + "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "requires": { + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.3", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + } + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==" + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "ctx-module": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/ctx-module/-/ctx-module-1.0.12.tgz", + "integrity": "sha512-iaple0ZSdEOq5Wdx7/eh6VvVZ5Rm4IICzvEulDiXXER+eGMgUlaPBBPZsV3aPhyy5LJNVwA4pKSqc0hIBvP0sA==", + "requires": { + "buffer": "^6.0.3", + "crypto-browserify": "^3.12.0" + } + }, + "des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, + "parse-asn1": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "requires": { + "asn1.js": "^5.2.0", + "browserify-aes": "^1.0.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "pbkdf2": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + } + } +} diff --git a/python/pythonmonkey/package.json b/python/pythonmonkey/package.json new file mode 100644 index 00000000..71b0f8b6 --- /dev/null +++ b/python/pythonmonkey/package.json @@ -0,0 +1,26 @@ +{ + "name": "pythonmonkey", + "version": "0.0.1", + "description": "PythonMonkey - JS Components", + "main": "index.js", + "directories": { + "doc": "docs", + "test": "tests" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Distributive-Network/PythonMonkey.git" + }, + "author": "Distributive Corp.", + "license": "MIT", + "bugs": { + "url": "https://github.com/Distributive-Network/PythonMonkey/issues" + }, + "homepage": "https://github.com/Distributive-Network/PythonMonkey#readme", + "dependencies": { + "ctx-module": "^1.0.12" + } +} diff --git a/python/pythonmonkey/require.py b/python/pythonmonkey/require.py new file mode 100644 index 00000000..b2cd315e --- /dev/null +++ b/python/pythonmonkey/require.py @@ -0,0 +1,248 @@ +# @file require.py +# Implementation of CommonJS "require" for PythonMonkey. This implementation uses the +# ctx-module npm package to do the heavy lifting. That package makes a complete module +# system, obstensibly in a separate context, but our implementation here reuses the +# PythonMonkey global context for both. +# +# The context that ctx-module runs in needs a require function supporting +# - require('debug') => returns a debug function which prints to the console when +# debugging; see the node debug built-in +# - require('fs') => returns an object which has an implementation of readFileSync +# and statSync. The implementation of statSync only needs to +# return the mode member. The fs module also needs +# constants.S_IFDIR available. +# - require('vm') => returns an object which has an implementation of evalInContext +# +# In order to implement this basic require function for bootstrapping ctxModule, we +# have simply made global variables of the form xxxModule where xxx is the module +# identifier, and injected a require function which understands this. A better +# implementation in Python that doesn't leak global symbols should be possible once +# some PythonMonkey bugs are fixed. +# +# @author Wes Garland, wes@distributive.network +# @date May 2023 +# + +import sys, os, types +from typing import Union, Dict, Callable +import importlib +from importlib import machinery +from importlib import util + +from . import pythonmonkey as pm + +# Add some python functions to the global python object for code in this file to use. +globalThis = pm.eval("globalThis;"); +pm.eval("globalThis.python = { pythonMonkey: {} }"); +globalThis.pmEval = pm.eval; +globalThis.python.print = print; +globalThis.python.getenv = os.getenv; +globalThis.python.pythonMonkey.dir = os.path.dirname(__file__); +globalThis.python.paths = ':'.join(sys.path); +pm.eval("python.paths = python.paths.split(':'); true"); # fix when pm supports arrays + +# bootstrap is effectively a scoping object which keeps us from polluting the global JS scope. +# The idea is that we hold a reference to the bootstrap object in Python-load, for use by the +# innermost code in ctx-module, without forcing ourselves to expose this minimalist code to +# userland-require. +bootstrap = pm.eval(""" +'use strict'; (function IIFE(python) { + +const bootstrap = { + modules: { + vm: { runInContext: eval }, + 'ctx-module': {}, + }, +} + +/* ctx-module needs require() when it loads that can find vm and fs */ +bootstrap.require = function bootstrapRequire(mid) +{ + if (bootstrap.modules.hasOwnProperty(mid)) + return bootstrap.modules[mid]; + + throw new Error('module not found: ' + mid); +} + +bootstrap.modules.vm.runInContext_broken = function runInContext(code, _unused_contextifiedObject, options) +{ + var evalOptions = {}; + + if (arguments.length === 2) + options = arguments[2]; + + if (options.filename) evalOptions.filename = options.filename; + if (options.lineOffset) evalOptions.lineno = options.lineOffset; + if (options.columnOffset) evalOptions.column = options.columnOffset; + + return pmEval(code, evalOptions); +} + +/** + * The debug module has as its exports a function which may, depending on the DEBUG env var, emit + * debugging statemnts to stdout. This is quick implementation of the node built-in. + */ +bootstrap.modules.debug = function debug(selector) +{ + var idx, colour; + const esc = String.fromCharCode(27); + const noColour = `${esc}[0m`; + + debug.selectors = debug.selectors || []; + idx = debug.selectors.indexOf(selector); + if (idx === -1) + { + idx = debug.selectors.length; + debug.selectors.push(selector); + } + + colour = `${esc}[${91 + ((idx + 1) % 6)}m`; + const debugEnv = python.getenv('DEBUG'); + + if (debugEnv) + { + for (let sym of debugEnv.split(' ')) + { + const re = new RegExp('^' + sym.replace('*', '.*') + '$'); + if (re.test(selector)) + { + return (function debugInner() { + python.print(`${colour}${selector}${noColour} ` + Array.from(arguments).join(' ')) + }); + } + } + } + + /* no match => silent */ + return (function debugDummy() {}); +} + +/** + * The fs module is like the Node.js fs module, except it only implements exactly what the ctx-module + * module requires to load CommonJS modules. It is augmented heavily below by Python methods. + */ +bootstrap.modules.fs = { + constants: { S_IFDIR: 16384 }, + statSync: function statSync(filename) { + const ret = bootstrap.modules.fs.statSync_inner(filename); + if (ret) + return ret; + + const err = new Error('file not found: ' + filename); + err.code='ENOENT'; + throw err; + }, +}; + +/* Modules which will be available to all requires */ +bootstrap.builtinModules = { debug: bootstrap.modules.debug }; + +/* temp workaround for PythonMonkey bug */ +globalThis.bootstrap = bootstrap; + +return bootstrap; +})(globalThis.python)""") + +def statSync_inner(filename: str) -> Union[Dict[str, int], bool]: + """ + Inner function for statSync. + + Returns: + Union[Dict[str, int], False]: The mode of the file or False if the file doesn't exist. + """ + from os import stat + if (os.path.exists(filename)): + sb = stat(filename) + return { 'mode': sb.st_mode } + else: + return False + +def readFileSync(filename, charset) -> str: + """ + Utility function for reading files. + Returns: + str: The contents of the file + """ + with open(filename, "r", encoding=charset) as fileHnd: + return fileHnd.read() + +bootstrap.modules.fs.statSync_inner = statSync_inner +bootstrap.modules.fs.readFileSync = readFileSync +bootstrap.modules.fs.existsSync = os.path.exists + +# Read ctx-module module from disk and invoke so that this file is the "main module" and ctx-module has +# require and exports symbols injected from the bootstrap object above. Current PythonMonkey bugs +# prevent us from injecting names properly so they are stolen from trail left behind in the global +# scope until that can be fixed. +with open(os.path.dirname(__file__) + "/node_modules/ctx-module/ctx-module.js", "r") as ctxModuleSource: + initCtxModule = pm.eval("""'use strict'; +(function moduleWrapper_forCtxModule(broken_require, broken_exports) +{ + const require = bootstrap.require; + const exports = bootstrap.modules['ctx-module']; +""" + ctxModuleSource.read() + """ +}) +"""); +#broken initCtxModule(bootstrap.require, bootstrap.modules['ctx-module'].exports); +initCtxModule(); + +def load(filename: str) -> Dict: + """ + Loads a python module using the importlib machinery sourcefileloader, prefills it with an exports object and returns the module. + If the module is already loaded, returns it. + + Args: + filename (str): The filename of the python module to load. + + Returns: + : The loaded python module + """ + + name = os.path.basename(filename) + if name not in sys.modules: + sourceFileLoader = machinery.SourceFileLoader(name, filename) + spec = importlib.util.spec_from_loader(sourceFileLoader.name, sourceFileLoader) + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + module.exports = {} + spec.loader.exec_module(module) + else: + module = sys.modules[name] + module_exports = {} + for key in dir(module): + module_exports[key] = getattr(module, key) + return module_exports +globalThis.python.load = load + +""" +API - createRequire +returns a require function that resolves modules relative to the filename argument. +Conceptually the same as node:module.createRequire(). + +example: + from pythonmonkey import createRequire + require = createRequire(__file__) + require('./my-javascript-module') +""" +def createRequire(filename): + createRequireInner = pm.eval("""'use strict';( +function createRequire(filename, bootstrap_broken) +{ + const bootstrap = globalThis.bootstrap; /** @bug PM-65 */ + const CtxModule = bootstrap.modules['ctx-module'].CtxModule; + + function loadPythonModule(module, filename) + { + module.exports = python.load(filename); + } + + const module = new CtxModule(globalThis, filename, bootstrap.builtinModules); + for (let path of python.paths) + module.paths.push(path + '/node_modules'); + if (module.require.path.length === 0) + module.require.path.push(python.pythonMonkey.dir + '/builtin_modules'); + module.require.extensions['.py'] = loadPythonModule; + + return module.require; +})""") + return createRequireInner(filename) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c00f9af1..69332a3a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,10 +1,10 @@ include_directories(${CMAKE_CURRENT_LIST_DIR}) set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) -if(LINUX) - set(CMAKE_INSTALL_RPATH "$ORIGIN") -else(APPLE) +if(APPLE) set(CMAKE_INSTALL_RPATH "@loader_path") +elseif(UNIX) + set(CMAKE_INSTALL_RPATH "$ORIGIN") endif() list(APPEND PYTHONMONKEY_SOURCE_FILES ${SOURCE_FILES} "${CMAKE_SOURCE_DIR}/src/modules/pythonmonkey/pythonmonkey.cc") @@ -46,4 +46,4 @@ endif() target_link_libraries(pythonmonkey ${SPIDERMONKEY_LIBRARIES}) target_include_directories(pythonmonkey PRIVATE ${PYTHON_INCLUDE_DIR}) -target_include_directories(pythonmonkey PRIVATE ${SPIDERMONKEY_INCLUDE_DIR}) \ No newline at end of file +target_include_directories(pythonmonkey PRIVATE ${SPIDERMONKEY_INCLUDE_DIR}) diff --git a/src/modules/pythonmonkey/pythonmonkey.cc b/src/modules/pythonmonkey/pythonmonkey.cc index 3854b5ab..71227120 100644 --- a/src/modules/pythonmonkey/pythonmonkey.cc +++ b/src/modules/pythonmonkey/pythonmonkey.cc @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -150,17 +151,72 @@ static PyObject *asUCS4(PyObject *self, PyObject *args) { return str->asUCS4(); } -static PyObject *eval(PyObject *self, PyObject *args) { +static bool getEvalOption(PyObject *evalOptions, const char *optionName, const char **s_p) { + PyObject *value; + + value = PyDict_GetItemString(evalOptions, optionName); + if (value) + *s_p = PyUnicode_AsUTF8(value); + return value != NULL; +} + +static bool getEvalOption(PyObject *evalOptions, const char *optionName, unsigned long *l_p) { + PyObject *value; + + value = PyDict_GetItemString(evalOptions, optionName); + if (value) + *l_p = PyLong_AsUnsignedLong(value); + return value != NULL; +} + +static bool getEvalOption(PyObject *evalOptions, const char *optionName, bool *b_p) { + PyObject *value; + value = PyDict_GetItemString(evalOptions, optionName); + if (value) { + if (PyLong_Check(value)) + *b_p = PyBool_FromLong(PyLong_AsLong(value)); + else + *b_p = value != Py_False; + } + return value != NULL; +} + +static PyObject *eval(PyObject *self, PyObject *args) { StrType *code = new StrType(PyTuple_GetItem(args, 0)); + PyObject *evalOptions = PyTuple_GET_SIZE(args) == 2 ? PyTuple_GetItem(args, 1) : NULL; + if (!PyUnicode_Check(code->getPyObject())) { PyErr_SetString(PyExc_TypeError, "pythonmonkey.eval expects a string as its first argument"); return NULL; } + if (evalOptions && !PyDict_Check(evalOptions)) { + PyErr_SetString(PyExc_TypeError, "pythonmonkey.eval expects a dict as its (optional) second argument"); + return NULL; + } + JSAutoRealm ar(GLOBAL_CX, *global); JS::CompileOptions options (GLOBAL_CX); - options.setFileAndLine("noname", 1); + options.setFileAndLine("@evaluate", 1) + .setIsRunOnce(true) + .setNoScriptRval(false) + .setIntroductionType("pythonmonkey eval"); + + if (evalOptions) { + const char *s; + unsigned long l; + bool b; + + if (getEvalOption(evalOptions, "filename", &s)) options.setFile(s); + if (getEvalOption(evalOptions, "lineno", &l)) options.setLine(l); + if (getEvalOption(evalOptions, "column", &l)) options.setColumn(l); + if (getEvalOption(evalOptions, "mutedErrors", &b)) options.setMutedErrors(b); + if (getEvalOption(evalOptions, "noScriptRval", &b)) options.setNoScriptRval(b); + if (getEvalOption(evalOptions, "selfHosting", &b)) options.setSelfHostingMode(b); + if (getEvalOption(evalOptions, "strict", &b)) if (b) options.setForceStrictMode(); + if (getEvalOption(evalOptions, "module", &b)) if (b) options.setModule(); + } // initialize JS context JS::SourceText source; @@ -202,8 +258,28 @@ static PyObject *eval(PyObject *self, PyObject *args) { } } +static PyObject *isCompilableUnit(PyObject *self, PyObject *args) { + StrType *buffer = new StrType(PyTuple_GetItem(args, 0)); + const char *bufferUtf8; + bool compilable; + + if (!PyUnicode_Check(buffer->getPyObject())) { + PyErr_SetString(PyExc_TypeError, "pythonmonkey.eval expects a string as its first argument"); + return NULL; + } + + bufferUtf8 = buffer->getValue(); + compilable = JS_Utf8BufferIsCompilableUnit(GLOBAL_CX, *global, bufferUtf8, strlen(bufferUtf8)); + + if (compilable) + Py_RETURN_TRUE; + else + Py_RETURN_FALSE; +} + PyMethodDef PythonMonkeyMethods[] = { {"eval", eval, METH_VARARGS, "Javascript evaluator in Python"}, + {"isCompilableUnit", isCompilableUnit, METH_VARARGS, "Hint if a string might be compilable Javascript"}, {"collect", collect, METH_VARARGS, "Calls the spidermonkey garbage collector"}, {"asUCS4", asUCS4, METH_VARARGS, "Expects a python string in UTF16 encoding, and returns a new equivalent string in UCS4. Undefined behaviour if the string is not in UTF16."}, {NULL, NULL, 0, NULL} @@ -244,9 +320,9 @@ static bool setTimeout(JSContext *cx, unsigned argc, JS::Value *vp) { // Wrap the job function into a bound function with the given additional arguments // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind JS::RootedVector bindArgs(cx); - bindArgs.append(JS::ObjectValue(**thisv)); + (void)bindArgs.append(JS::ObjectValue(**thisv)); /** @todo XXXwg handle return value */ for (size_t j = 2; j < args.length(); j++) { - bindArgs.append(args[j]); + (void)bindArgs.append(args[j]); /** @todo XXXwg handle return value */ } JS::RootedObject jobArgObj = JS::RootedObject(cx, &jobArgVal.toObject()); JS_CallFunctionName(cx, jobArgObj, "bind", JS::HandleValueArray(bindArgs), jobArg); // jobArg = jobArg.bind(thisv, ...bindArgs) @@ -321,6 +397,12 @@ PyMODINIT_FUNC PyInit_pythonmonkey(void) return NULL; } + JS::ContextOptionsRef(GLOBAL_CX) + .setWasm(true) + .setAsmJS(true) + .setAsyncStack(true) + .setSourcePragmas(true); + JOB_QUEUE = new JobQueue(); if (!JOB_QUEUE->init(GLOBAL_CX)) { PyErr_SetString(SpiderMonkeyError, "Spidermonkey could not create the event-loop."); @@ -394,4 +476,4 @@ PyMODINIT_FUNC PyInit_pythonmonkey(void) return NULL; } return pyModule; -} \ No newline at end of file +} diff --git a/tests/js/console-smoke.simple b/tests/js/console-smoke.simple new file mode 100644 index 00000000..07c203a5 --- /dev/null +++ b/tests/js/console-smoke.simple @@ -0,0 +1,12 @@ +/** + * @file console-smoke.simple + * Simple smoke test which ensures that the global console is initialized + * @author Wes Garland, wes@distributive.network + * @date June 2023 + */ +console.log('one'); +console.info('two'); +console.debug('three'); +console.error('four'); +console.warn('five'); + diff --git a/tests/js/is-compilable-unit.simple b/tests/js/is-compilable-unit.simple new file mode 100644 index 00000000..46de5c37 --- /dev/null +++ b/tests/js/is-compilable-unit.simple @@ -0,0 +1,16 @@ +/** + * @file is-compilabe-unit.simple + * Simple test which ensures that pm.isCompilableUnit works from JS as expected + * written in Python instead of JavaScript + * @author Wes Garland, wes@distributive.network + * @date June 2023 + */ + +const { isCompilableUnit } = require('./modules/vm-tools'); + +if (isCompilableUnit('()=>')) + throw new Error('isCompilableUnit lied about ()=>'); + +if (!isCompilableUnit('123')) + throw new Error('isCompilableUnit lied about 123'); + diff --git a/tests/js/load-cjs-module.simple b/tests/js/load-cjs-module.simple new file mode 100644 index 00000000..ee01061e --- /dev/null +++ b/tests/js/load-cjs-module.simple @@ -0,0 +1,10 @@ +/** + * @file load-cjs-module.js + * Simple smoke test which ensures that we can load a CommonJS Module + * @author Wes Garland, wes@distributive.network + * @date June 2023 + */ + +const { helloWorld } = require('./modules/cjs-module'); + +helloWorld(); diff --git a/tests/js/load-cjs-python-module.simple b/tests/js/load-cjs-python-module.simple new file mode 100644 index 00000000..9e02dca0 --- /dev/null +++ b/tests/js/load-cjs-python-module.simple @@ -0,0 +1,11 @@ +/** + * @file load-cjs-python-module.js + * Simple smoke test which ensures that we can load a CommonJS Module which happens to be + * written in Python instead of JavaScript + * @author Wes Garland, wes@distributive.network + * @date June 2023 + */ + +const { helloWorld } = require('./modules/python-cjs-module'); + +helloWorld(); diff --git a/tests/js/modules/cjs-module.js b/tests/js/modules/cjs-module.js new file mode 100644 index 00000000..e239dbab --- /dev/null +++ b/tests/js/modules/cjs-module.js @@ -0,0 +1,13 @@ +/** + * @file cjs-module.js + * A CommonJS Module written in JavaScript + * @author Wes Garland, wes@distributive.network + * @date Jun 2023 +*/ + +function helloWorld() +{ + console.log('hello, world!') +} + +exports.helloWorld = helloWorld; diff --git a/tests/js/modules/python-cjs-module.py b/tests/js/modules/python-cjs-module.py new file mode 100644 index 00000000..71c892de --- /dev/null +++ b/tests/js/modules/python-cjs-module.py @@ -0,0 +1,9 @@ +# @file python-cjs-module.py +# A CommonJS Module written Python. +# @author Wes Garland, wes@distributive.network +# @date Jun 2023 + +def helloWorld(): + print('hello, world!') + +exports['helloWorld'] = helloWorld diff --git a/tests/js/modules/vm-tools.py b/tests/js/modules/vm-tools.py new file mode 100644 index 00000000..1ae78843 --- /dev/null +++ b/tests/js/modules/vm-tools.py @@ -0,0 +1,10 @@ +# @file vm-tools.py +# A CommonJS Module written Python which feeds some Python- and JS-VM-related methods +# back up to JS so we can use them during tests. +# @author Wes Garland, wes@distributive.network +# @date Jun 2023 + +import pythonmonkey as pm + +exports['isCompilableUnit'] = pm.isCompilableUnit +print('MODULE LOADED') diff --git a/tests/js/not-strict-mode.simple b/tests/js/not-strict-mode.simple new file mode 100644 index 00000000..9b202115 --- /dev/null +++ b/tests/js/not-strict-mode.simple @@ -0,0 +1,16 @@ +/** + * @file not-strict-mode.simple + * Simple test which ensures that tests are evaluated correctly, such that the interpreter + * does not run tests in strict mode unless explicitly directed to do so. + * @author Wes Garland, wes@distributive.network + * @date June 2023 + */ +function fun(abc) +{ + arguments[0] = 123; + + if (abc !== 123) + throw new Error('interpreter is in strict mode'); +} + +fun(456); diff --git a/tests/js/use-strict.simple b/tests/js/use-strict.simple new file mode 100644 index 00000000..8e927de4 --- /dev/null +++ b/tests/js/use-strict.simple @@ -0,0 +1,18 @@ +/** + * @file use-strict.simple + * Simple test which ensures that tests are evaluated correctly, such that the "use strict" + * directive has meaning. + * @author Wes Garland, wes@distributive.network + * @date June 2023 + */ +"use strict"; + +function fun(abc) +{ + arguments[0] = 123; + + if (abc === 123) + throw new Error('"use strict" did not put interpreter in strict mode'); +} + +fun(456); diff --git a/tests/python/test_event_loop.py b/tests/python/test_event_loop.py index aa8dc674..b280ea13 100644 --- a/tests/python/test_event_loop.py +++ b/tests/python/test_event_loop.py @@ -110,7 +110,7 @@ async def coro_fn(x): # JS Promise to Python awaitable coercion assert 100 == await pm.eval("new Promise((r)=>{ r(100) })") assert 10010 == await pm.eval("Promise.resolve(10010)") - with pytest.raises(pm.SpiderMonkeyError, match="TypeError: .+ is not a constructor"): + with pytest.raises(pm.SpiderMonkeyError, match="^TypeError: (.|\\n)+ is not a constructor$"): await pm.eval("Promise.resolve")(10086) assert 10086 == await pm.eval("Promise.resolve.bind(Promise)")(10086) diff --git a/tests/python/test_reentrance_smoke.py b/tests/python/test_reentrance_smoke.py new file mode 100644 index 00000000..623c6bd4 --- /dev/null +++ b/tests/python/test_reentrance_smoke.py @@ -0,0 +1,18 @@ +# @file reentrance-smoke.py +# Basic smoke test which shows that JS->Python->JS->Python calls work. Failures are +# indicated by segfaults. +# @author Wes Garland, wes@distributive.network +# @date June 2023 + +import sys, os +import pythonmonkey as pm + + +def test_reentrance(): + globalThis = pm.eval("globalThis;"); + globalThis.pmEval = pm.eval; + globalThis.pyEval = eval; + + abc=(pm.eval("() => { return {def: pyEval('123')} };"))() + assert(abc['def'] == 123) + print(pm.eval("pmEval(`pyEval(\"'test passed'\")`)"))