├── .gitmodules ├── js ├── preJs.js └── postJs.js.in ├── README.md ├── hacks.patch ├── Makefile └── mapfiles.py /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "python"] 2 | path = python 3 | url = https://github.com/python/cpython.git 4 | -------------------------------------------------------------------------------- /js/preJs.js: -------------------------------------------------------------------------------- 1 | this.init_empython = function (initialized_callback, print_hook) { 2 | 3 | var root = { 4 | Module: (function () { 5 | var Module = { 6 | noInitialRun: true, 7 | noExitRuntime: true, 8 | preRun: [], 9 | postRun: [], 10 | print: print_hook, 11 | printErr: print_hook, 12 | }; 13 | -------------------------------------------------------------------------------- /js/postJs.js.in: -------------------------------------------------------------------------------- 1 | Module.FS = FS; 2 | Module.ENV = ENV; 3 | Module.onRuntimeInitialized = onRuntimeInitialized; 4 | return Module; 5 | })() 6 | }; 7 | 8 | // Undo pollution of window 9 | delete window.Module; 10 | 11 | // Init emscripten stuff 12 | root.Module.run(); 13 | 14 | function onRuntimeInitialized() { 15 | root.Initialize = root.Module.cwrap('Py_Initialize', 'number', []); 16 | root.Run = root.Module.cwrap('PyRun_SimpleString', 'number', [ 17 | 'string' // string to eval 18 | ]); 19 | root.Initialize(); 20 | // empython comes out from this callback 21 | initialized_callback(root); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | = empython 2 | 3 | == Get it 4 | 5 | First, clone and initialise submodules: 6 | 7 | ``` 8 | $ git clone git@github.com:aidanhs/empython.git 9 | $ cd empython 10 | $ git submodule update --init 11 | ``` 12 | 13 | == Build it 14 | 15 | If you have emscripten 2.0.7 installed (other versions may or may not work): 16 | 17 | ``` 18 | $ cd python 19 | $ make -f ../Makefile prep 20 | $ make -f ../Makefile em 21 | $ cd .. 22 | $ make empython.js 23 | ``` 24 | 25 | If you have docker or podman (below I'll use podman, I think you'll need to add `-u $(id -u):$(id -g)` for Docker): 26 | 27 | ``` 28 | $ podman pull emscripten/emsdk:2.0.7 29 | $ podman run -it --rm -v $(pwd):/src emscripten/emsdk:2.0.7 bash -c "cd python && make -f ../Makefile prep && make -f ../Makefile em && cd .. && make empython.js" 30 | $ ls -l empython.{js,wasm} 31 | .rw-r--r-- 2.9M aidanhs 25 Oct 14:52 empython.js 32 | .rwxr-xr-x 1.9M aidanhs 25 Oct 14:52 empython.wasm 33 | ``` 34 | -------------------------------------------------------------------------------- /hacks.patch: -------------------------------------------------------------------------------- 1 | --- pyconfig.h.orig 2020-10-24 22:13:50.745086908 +0100 2 | +++ pyconfig.h 2020-10-24 23:04:46.647877114 +0100 3 | @@ -979,7 +979,7 @@ 4 | #define HAVE_SYS_FILE_H 1 5 | 6 | /* Define to 1 if you have the header file. */ 7 | -#define HAVE_SYS_IOCTL_H 1 8 | +//#define HAVE_SYS_IOCTL_H 1 9 | 10 | /* Define to 1 if you have the header file. */ 11 | /* #undef HAVE_SYS_KERN_CONTROL_H */ 12 | --- Modules/Setup.orig 2020-10-24 23:04:17.224003619 +0100 13 | +++ Modules/Setup 2020-10-24 23:04:24.079974258 +0100 14 | @@ -358,7 +358,7 @@ 15 | # Andrew Kuchling's zlib module. 16 | # This require zlib 1.1.3 (or later). 17 | # See http://www.gzip.org/zlib/ 18 | -#zlib zlibmodule.c -I$(prefix)/include -L$(exec_prefix)/lib -lz 19 | +zlib zlibmodule.c -IModules/zlib -LModules/zlib -lz 20 | 21 | # Interface to the Expat XML parser 22 | # 23 | --- setup.py 24 | +++ setup.py 25 | @@ -16,7 +16,7 @@ from distutils.command.install_lib import install_lib 26 | from distutils.command.build_scripts import build_scripts 27 | from distutils.spawn import find_executable 28 | 29 | -cross_compiling = "_PYTHON_HOST_PLATFORM" in os.environ 30 | +cross_compiling = True 31 | 32 | # Add special CFLAGS reserved for building the interpreter and the stdlib 33 | # modules (Issue #21121). 34 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Enable optimisations by default 2 | EMPYOPT?=1 3 | COPT=$$([ $(EMPYOPT) = 1 ] && echo -Oz || echo "-O0 -g") 4 | 5 | EMDEBUG=\ 6 | -O0\ 7 | -g3\ 8 | --js-opts 0\ 9 | --llvm-opts 0\ 10 | --llvm-lto 0\ 11 | -s ASSERTIONS=2\ 12 | 13 | EMOPT=\ 14 | -O3\ 15 | -g0\ 16 | --js-opts 1\ 17 | --llvm-opts 3\ 18 | --llvm-lto 3\ 19 | -s ASSERTIONS=0\ 20 | #--closure 0 21 | 22 | EMFLAGS=\ 23 | --pre-js js/preJs.js --post-js js/postJs.js\ 24 | --memory-init-file 0\ 25 | -s INCLUDE_FULL_LIBRARY=0\ 26 | -s EMULATE_FUNCTION_POINTER_CASTS=1\ 27 | $$([ $(EMPYOPT) = 1 ] && echo $(EMOPT) || echo $(EMDEBUG)) 28 | 29 | EMEXPORTS=\ 30 | -s EXPORTED_FUNCTIONS="['_Py_Initialize', '_PyRun_SimpleString']" -s "EXTRA_EXPORTED_RUNTIME_METHODS=['cwrap']" 31 | 32 | empython.js: python/libpython3.5.a 33 | ./mapfiles.py python/Lib datafilezip > js/postJs.js 34 | cat js/postJs.js.in >> js/postJs.js 35 | emcc $(EMFLAGS) $(EMEXPORTS) -o $@ $< python/Modules/zlib/libz.a 36 | 37 | CONFFLAGS=OPT="$(COPT)" --without-threads --without-pymalloc --disable-shared --disable-ipv6 38 | prep: 39 | ./configure 40 | make python 41 | cp python ../python.native 42 | make clean 43 | git clean -f -x -d 44 | em: 45 | cd Modules/zlib && emconfigure ./configure --static && emmake make libz.a 46 | (export BASECFLAGS=-m32 LDFLAGS=-m32 && emconfigure ./configure $(CONFFLAGS)) 47 | git apply ../hacks.patch 48 | emmake make || true # errors on running python 49 | mv python python.bc # only useful if replacing the emscripten test .bc file 50 | cp ../python.native python && chmod +x python 51 | emmake make 52 | -------------------------------------------------------------------------------- /mapfiles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import print_function 3 | 4 | import os 5 | import sys 6 | import py_compile 7 | from zipfile import ZipFile, ZIP_DEFLATED 8 | from io import BytesIO 9 | from subprocess import check_call 10 | import base64 11 | 12 | def mk_contents(data): 13 | #return '[' + ','.join(str(ord(i)) for i in data) + ']' 14 | return ( 15 | '(function(){' 16 | 'var t=atob("%s"),a=new Uint8Array(%s),i;' 17 | 'for(i=0;i<%s;i++)a[i]=t.charCodeAt(i);' 18 | 'return a' 19 | '})()' 20 | ) % (base64.b64encode(data).decode('ascii'), len(data), len(data)) 21 | 22 | def files_to_datafilecalls(fpaths): 23 | basedir = '/usr/local/lib/python3.5' 24 | commands = [] 25 | dpaths = set([basedir]) 26 | for fpath, targetdir in fpaths: 27 | 28 | dpath = os.path.abspath(os.path.join(basedir, targetdir)) 29 | 30 | commands.append('FS.createDataFile("%s", "%s", %s, true, true)' % ( 31 | dpath, os.path.basename(fpath), mk_contents(open(fpath, 'rb').read()) 32 | )) 33 | 34 | # Make sure we're adding all required directories 35 | while dpath not in dpaths: 36 | dpaths.add(dpath) 37 | dpath = os.path.dirname(dpath) 38 | 39 | dpaths.remove(basedir) 40 | for dpath in sorted(dpaths, key=len, reverse=True): 41 | commands.insert(0, 'FS.mkdirTree("%s")' % (dpath,)) 42 | commands.insert(0, 'FS.createPath("/", "' + basedir[1:] + '", true, true)') 43 | 44 | return commands 45 | 46 | def files_to_datafilezipcall(fpaths): 47 | zf = BytesIO() 48 | zipfile = ZipFile(zf, 'w', ZIP_DEFLATED) 49 | for fpath, targetdir in fpaths: 50 | zipfile.write(fpath, os.path.join(targetdir, os.path.basename(fpath))) 51 | zipfile.close() 52 | 53 | target = '/usr/local/lib/python35.zip' 54 | commands = [] 55 | commands.insert(0, 'FS.createPath("/", "' + os.path.dirname(target)[1:] + '", true, true)') 56 | commands.append('FS.createDataFile("%s", "%s", %s, true, true)' % ( 57 | os.path.dirname(target), os.path.basename(target), mk_contents(zf.getvalue()) 58 | )) 59 | return commands 60 | 61 | def main(root): 62 | os.chdir(root) 63 | fpaths = [] 64 | for (dirpath, dirnames, filenames) in os.walk('.'): 65 | for dirname in dirnames[:]: 66 | should_remove = any([ 67 | dirname in ['tests', 'test'], # python 3 tests will error! 68 | dirname == 'unittest', # gets crippled by the above 69 | dirname.startswith('plat-') and dirname != 'plat-linux2', # emscripten is ~linux 70 | dirname == 'lib2to3', # we don't package the necessary grammar files 71 | dirname in ['idlelib', 'lib-tk'], # Tk doesn't even compile yet 72 | dirname == 'ctypes', # we obviously can't call C functions 73 | dirname == 'distutils', # not going to be running pip any time soon 74 | dirname == 'bsddb', # needs compiling, deprecated anyway 75 | dirname == 'multiprocessing', # doesn't really make sense in JS 76 | dirname == 'curses', # we don't have the terminal interface (yet) 77 | dirname == 'sqlite3', # doesn't get compiled yet 78 | dirname == 'msilib', # doesn't get compiled and who cares anyway 79 | dirname == 'hotshot', # doesn't get compiled, unmaintained 80 | dirname == 'wsgiref', # not going to be building any web servers 81 | dirname == 'pydoc_data', # don't bundle documentation 82 | ]) 83 | if should_remove: 84 | dirnames.remove(dirname) 85 | 86 | for filename in filenames: 87 | fpaths.append((os.path.join(dirpath, filename), dirpath)) 88 | 89 | # _sysconfigdata is created by the build process 90 | fpaths.append(('../build/lib.linux-x86_64-3.5/_sysconfigdata.py', '.')) 91 | 92 | # Some checks and assertions 93 | assert all([targetdir[0] == '.' for fpath, targetdir in fpaths]) 94 | fpaths = [ 95 | (fpath, targetdir) for fpath, targetdir in fpaths 96 | if os.path.splitext(fpath)[1] == '.py' 97 | ] 98 | ## Compile to save space and time in the parser 99 | #check_call(['python3', '-OO', '-m', 'py_compile'] + [fpath for fpath, _ in fpaths]) 100 | #fpaths = [(fpath + 'o', targetdir) for fpath, targetdir in fpaths] 101 | 102 | if sys.argv[2] == 'datafiles': 103 | commands = files_to_datafilecalls(fpaths) 104 | elif sys.argv[2] == 'datafilezip': 105 | commands = files_to_datafilezipcall(fpaths) 106 | else: 107 | assert False 108 | 109 | # Start out in a writeable folder. 110 | commands.append('FS.mkdirTree("/sandbox")') 111 | commands.append('FS.currentPath = "/sandbox"') 112 | 113 | # http://bugs.python.org/issue22689 114 | #commands = ['ENV["PYTHONHOME"] = "%s"' % (pyhomedir,)] 115 | 116 | print(';'.join([c for c in commands if c != '']) + ';', end='') 117 | 118 | if __name__ == '__main__': 119 | if len(sys.argv) != 3 or sys.argv[2] not in ['datafiles', 'datafilezip']: 120 | print('Usage: %s root datafiles|datafilezip' % sys.argv[0], file=sys.stderr) 121 | sys.exit(1) 122 | else: 123 | main(sys.argv[1]) 124 | --------------------------------------------------------------------------------