├── .devcontainer └── devcontainer.json ├── .github ├── actions │ └── prepare │ │ └── action.yml └── workflows │ └── ci.yml ├── .gitignore ├── README.md ├── browser-ui ├── index.html ├── index.js ├── server.py ├── styles.css ├── wasm-terminal.js ├── worker-manager.js └── worker │ ├── .gitignore │ └── worker.js ├── build-python-build.sh ├── build-python-emscripten-browser.sh ├── build-python-emscripten-node.sh ├── build-python-wasi.sh ├── clean-host.sh ├── fetch-python.sh ├── run-python-browser.sh ├── run-python-node.sh ├── run-python-wasi.sh ├── test-emscripten-node.sh └── test-wasi.sh /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "quay.io/tiran/cpythonbuild:emsdk3" 3 | } 4 | -------------------------------------------------------------------------------- /.github/actions/prepare/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Common setup' 2 | description: 'Common setup routines' 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Create container cache directory 7 | shell: bash 8 | run: mkdir -p /tmp/container-cache 9 | # container is fetched once a day 10 | - name: Get date cache key 11 | id: get-date 12 | shell: bash 13 | run: | 14 | echo "TODAY=$(/bin/date -u "+%Y%m%d")" >> $GITHUB_ENV 15 | - name: Get container cache 16 | id: container-cache 17 | uses: actions/cache@v1 18 | with: 19 | path: /tmp/container-cache 20 | key: image-cache-${{ env.TODAY }} 21 | - name: "Pull build image" 22 | if: ${{ steps.container-cache.outputs.cache-hit != 'true' }} 23 | shell: bash 24 | run: | 25 | docker pull quay.io/tiran/cpythonbuild:emsdk3 26 | docker save -o /tmp/container-cache/cpemsdk3.tar quay.io/tiran/cpythonbuild:emsdk3 27 | - name: "Load container from cache" 28 | if: ${{ steps.container-cache.outputs.cache-hit == 'true' }} 29 | shell: bash 30 | run: | 31 | docker load -i /tmp/container-cache/cpemsdk3.tar 32 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | types: [opened, synchronize, reopened, closed] 12 | schedule: 13 | - cron: '30 2 * * *' 14 | workflow_dispatch: 15 | inputs: 16 | git-ref: 17 | required: false 18 | 19 | jobs: 20 | pull-buildcontainer: 21 | name: "Pull & cache build container" 22 | runs-on: "ubuntu-latest" 23 | steps: 24 | - name: "checkout python-wasm" 25 | uses: "actions/checkout@v2" 26 | - name: "Common prepare step" 27 | uses: ./.github/actions/prepare 28 | build-python: 29 | name: "Build build Python ${{ matrix.pythonbranch }}" 30 | runs-on: "ubuntu-latest" 31 | needs: pull-buildcontainer 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | pythonbranch: [main, 3.11] 36 | steps: 37 | - name: "checkout python-wasm" 38 | uses: "actions/checkout@v2" 39 | - name: "checkout CPython" 40 | uses: "actions/checkout@v2" 41 | with: 42 | repository: python/cpython 43 | path: cpython 44 | ref: ${{ matrix.pythonbranch }} 45 | - name: "Verify checkout" 46 | shell: bash 47 | run: | 48 | test -x build-python-build.sh || exit 1 49 | test -x cpython/configure || exit 2 50 | - name: "Common prepare step" 51 | uses: ./.github/actions/prepare 52 | - name: "Build build Python" 53 | run: docker run --rm -v $(pwd):/build -w /build quay.io/tiran/cpythonbuild:emsdk3 ./build-python-build.sh 54 | - name: "Store CPython commit hash" 55 | run: git --git-dir=cpython/.git rev-parse HEAD > cpython/commit.txt 56 | - name: "Cache build Python" 57 | uses: actions/cache@v2 58 | with: 59 | path: cpython 60 | key: cpython-${{ matrix.pythonbranch }}-${{ runner.os }}-${{ env.TODAY }}-${{ github.sha }} 61 | emscripte-node: 62 | name: "Build Emscripten node ${{ matrix.pythonbranch }}" 63 | runs-on: "ubuntu-latest" 64 | needs: build-python 65 | strategy: 66 | fail-fast: false 67 | matrix: 68 | pythonbranch: [main, 3.11] 69 | steps: 70 | - name: "checkout python-wasm" 71 | uses: "actions/checkout@v2" 72 | - name: "Common prepare step" 73 | uses: ./.github/actions/prepare 74 | - name: "Fetch cached build Python" 75 | uses: actions/cache@v2 76 | with: 77 | path: cpython 78 | key: cpython-${{ matrix.pythonbranch }}-${{ runner.os }}-${{ env.TODAY }}-${{ github.sha }} 79 | - name: "Check build Python" 80 | run: | 81 | test -e cpython/builddir/build/python || exit 1 82 | test -e cpython/configure || exit 2 83 | - name: "Build emscripten Python for node" 84 | run: docker run --rm -v $(pwd):/build -w /build quay.io/tiran/cpythonbuild:emsdk3 ./build-python-emscripten-node.sh 85 | - name: "Check artifacts" 86 | run: | 87 | ls -la --si cpython/builddir/emscripten-node/python* 88 | test -e cpython/builddir/emscripten-node/python.wasm || exit 1 89 | - name: "Print test.pythoninfo" 90 | run: docker run --rm -v $(pwd):/build -w /build quay.io/tiran/cpythonbuild:emsdk3 ./run-python-node.sh -m test.pythoninfo 91 | - name: "Run tests" 92 | run: docker run --rm -v $(pwd):/build -w /build quay.io/tiran/cpythonbuild:emsdk3 ./test-emscripten-node.sh -u all -W --slowest --fail-env-changed 93 | - name: "Copy stdlib" 94 | run: | 95 | sudo chown $(id -u):$(id -g) -R cpython 96 | cp cpython/commit.txt cpython/builddir/emscripten-node/ 97 | cp cpython/LICENSE cpython/builddir/emscripten-node/ 98 | cp -R cpython/Lib cpython/builddir/emscripten-node/ 99 | pushd cpython/builddir/emscripten-node/ 100 | rm -rf Lib/curses Lib/ensurepip/ Lib/distutils/ Lib/idlelib/ Lib/test/ Lib/tkinter/ Lib/turtledemo/ Lib/venv/ 101 | find -name __pycache__ | xargs rm -rf 102 | popd 103 | - name: "Upload node build artifacts" 104 | uses: actions/upload-artifact@v2 105 | with: 106 | name: emscripten-node-${{ matrix.pythonbranch }} 107 | path: | 108 | cpython/builddir/emscripten-node/commit.txt 109 | cpython/builddir/emscripten-node/LICENSE 110 | cpython/builddir/emscripten-node/python.wasm 111 | cpython/builddir/emscripten-node/python.worker.js 112 | cpython/builddir/emscripten-node/python.js 113 | cpython/builddir/emscripten-node/pybuilddir.txt 114 | cpython/builddir/emscripten-node/build/lib.emscripten-wasm32-3.*/_sysconfigdata__emscripten_wasm32-emscripten.py 115 | cpython/builddir/emscripten-node/Lib/ 116 | if-no-files-found: error 117 | - name: "Upload build artifacts" 118 | uses: actions/upload-artifact@v2 119 | with: 120 | name: build-node-${{ matrix.pythonbranch }} 121 | path: | 122 | cpython/builddir/emscripten-node/config.log 123 | cpython/builddir/emscripten-node/config.cache 124 | cpython/builddir/emscripten-node/Makefile 125 | cpython/builddir/emscripten-node/pyconfig.h 126 | cpython/builddir/emscripten-node/libpython*.a 127 | cpython/builddir/emscripten-node/Modules/Setup.local 128 | cpython/builddir/emscripten-node/Modules/Setup.stdlib 129 | cpython/builddir/emscripten-node/Modules/config.c 130 | cpython/builddir/emscripten-node/Modules/_decimal/libmpdec/libmpdec.a 131 | cpython/builddir/emscripten-node/Modules/expat/libexpat.a 132 | cpython/builddir/emscripten-node/Programs/python.o 133 | if-no-files-found: error 134 | emscripte-browser: 135 | name: "Build Emscripten browser ${{ matrix.pythonbranch }}" 136 | runs-on: "ubuntu-latest" 137 | needs: build-python 138 | strategy: 139 | fail-fast: false 140 | matrix: 141 | pythonbranch: [main, 3.11] 142 | steps: 143 | - name: "checkout python-wasm" 144 | uses: "actions/checkout@v2" 145 | - name: "Common prepare step" 146 | uses: ./.github/actions/prepare 147 | - name: "Fetch cached build Python" 148 | uses: actions/cache@v2 149 | with: 150 | path: cpython 151 | key: cpython-${{ matrix.pythonbranch }}-${{ runner.os }}-${{ env.TODAY }}-${{ github.sha }} 152 | - name: "Check build Python" 153 | run: | 154 | test -e cpython/builddir/build/python || exit 1 155 | test -e cpython/configure || exit 2 156 | - name: "Build emscripten Python for browser" 157 | run: docker run --rm -v $(pwd):/build -w /build quay.io/tiran/cpythonbuild:emsdk3 ./build-python-emscripten-browser.sh 158 | - name: "Check artifacts" 159 | run: | 160 | ls -la --si cpython/builddir/emscripten-browser/python* 161 | ls -la cpython/builddir/emscripten-browser/Modules/ 162 | test -e cpython/builddir/emscripten-browser/python.data || exit 1 163 | - name: "Copy commit.txt and LICENSE" 164 | run: | 165 | sudo chown $(id -u):$(id -g) -R cpython 166 | cp cpython/commit.txt cpython/builddir/emscripten-browser/ 167 | cp cpython/LICENSE cpython/builddir/emscripten-browser/ 168 | - name: "Upload browser build artifacts" 169 | uses: actions/upload-artifact@v2 170 | with: 171 | name: emscripten-browser-${{ matrix.pythonbranch }} 172 | path: | 173 | cpython/builddir/emscripten-browser/commit.txt 174 | cpython/builddir/emscripten-browser/LICENSE 175 | cpython/builddir/emscripten-browser/python.wasm 176 | cpython/builddir/emscripten-browser/python.html 177 | cpython/builddir/emscripten-browser/python.js 178 | cpython/builddir/emscripten-browser/python.worker.js 179 | cpython/builddir/emscripten-browser/python.data 180 | if-no-files-found: error 181 | - name: "Upload build artifacts" 182 | uses: actions/upload-artifact@v2 183 | with: 184 | name: build-browser-${{ matrix.pythonbranch }} 185 | path: | 186 | cpython/builddir/emscripten-browser/config.log 187 | cpython/builddir/emscripten-browser/config.cache 188 | cpython/builddir/emscripten-browser/Makefile 189 | cpython/builddir/emscripten-browser/pyconfig.h 190 | cpython/builddir/emscripten-browser/pybuilddir.txt 191 | cpython/builddir/emscripten-browser/libpython*.a 192 | cpython/builddir/emscripten-browser/Modules/Setup.local 193 | cpython/builddir/emscripten-browser/Modules/Setup.stdlib 194 | cpython/builddir/emscripten-browser/Modules/config.c 195 | cpython/builddir/emscripten-browser/Modules/_decimal/libmpdec/libmpdec.a 196 | cpython/builddir/emscripten-browser/Modules/expat/libexpat.a 197 | cpython/builddir/emscripten-browser/Programs/python.o 198 | if-no-files-found: error 199 | wasi: 200 | name: "Build WASI ${{ matrix.pythonbranch }}" 201 | runs-on: "ubuntu-latest" 202 | needs: build-python 203 | strategy: 204 | fail-fast: false 205 | matrix: 206 | pythonbranch: [main, 3.11] 207 | steps: 208 | - name: "checkout python-wasm" 209 | uses: "actions/checkout@v2" 210 | - name: "Common prepare step" 211 | uses: ./.github/actions/prepare 212 | - name: "Fetch cached build Python" 213 | uses: actions/cache@v2 214 | with: 215 | path: cpython 216 | key: cpython-${{ matrix.pythonbranch }}-${{ runner.os }}-${{ env.TODAY }}-${{ github.sha }} 217 | - name: "Check build Python" 218 | run: | 219 | test -e cpython/builddir/build/python || exit 1 220 | test -e cpython/configure || exit 2 221 | - name: "Build WASI Python" 222 | run: docker run --rm -v $(pwd):/build -w /build quay.io/tiran/cpythonbuild:emsdk3 ./build-python-wasi.sh 223 | - name: "Check artifacts" 224 | run: | 225 | ls -la --si cpython/builddir/wasi/python* 226 | test -e cpython/builddir/wasi/python.wasm || exit 1 227 | - name: "Print test.pythoninfo" 228 | run: docker run --rm -v $(pwd):/build -w /build quay.io/tiran/cpythonbuild:emsdk3 ./run-python-wasi.sh -m test.pythoninfo 229 | - name: "Run tests" 230 | run: docker run --rm -v $(pwd):/build -w /build quay.io/tiran/cpythonbuild:emsdk3 ./test-wasi.sh -u all -W --slowest --fail-env-changed 231 | # some WASI tests are failing 232 | continue-on-error: true 233 | - name: "Copy stdlib" 234 | run: | 235 | sudo chown $(id -u):$(id -g) -R cpython 236 | cp cpython/commit.txt cpython/builddir/wasi/ 237 | cp cpython/LICENSE cpython/builddir/wasi/ 238 | cp -R cpython/Lib cpython/builddir/wasi/ 239 | pushd cpython/builddir/wasi/ 240 | rm -rf Lib/curses Lib/ensurepip/ Lib/distutils/ Lib/idlelib/ Lib/test/ Lib/tkinter/ Lib/turtledemo/ Lib/venv/ 241 | find -name __pycache__ | xargs rm -rf 242 | popd 243 | - name: "Upload WASI artifacts" 244 | uses: actions/upload-artifact@v2 245 | with: 246 | name: wasi-${{ matrix.pythonbranch }} 247 | path: | 248 | cpython/builddir/wasi/LICENSE 249 | cpython/builddir/wasi/commit.txt 250 | cpython/builddir/wasi/python.wasm 251 | cpython/builddir/wasi/pybuilddir.txt 252 | cpython/builddir/wasi/build/lib.wasi-wasm32-3.*/_sysconfigdata__wasi_wasm32-wasi.py 253 | cpython/builddir/wasi/Lib/ 254 | if-no-files-found: error 255 | - name: "Upload build artifacts" 256 | uses: actions/upload-artifact@v2 257 | with: 258 | name: build-wasi-${{ matrix.pythonbranch }} 259 | path: | 260 | cpython/builddir/wasi/config.log 261 | cpython/builddir/wasi/config.cache 262 | cpython/builddir/wasi/Makefile 263 | cpython/builddir/wasi/pyconfig.h 264 | cpython/builddir/wasi/libpython*.a 265 | cpython/builddir/wasi/Modules/Setup.local 266 | cpython/builddir/wasi/Modules/Setup.stdlib 267 | cpython/builddir/wasi/Modules/config.c 268 | cpython/builddir/wasi/Modules/_decimal/libmpdec/libmpdec.a 269 | cpython/builddir/wasi/Modules/expat/libexpat.a 270 | cpython/builddir/wasi/Programs/python.o 271 | if-no-files-found: error 272 | ghpages: 273 | name: "Upload to GitHub pages" 274 | runs-on: "ubuntu-latest" 275 | needs: emscripte-browser 276 | # Relies on `on` restricting which branches trigger this job. 277 | if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }} 278 | steps: 279 | - uses: actions/checkout@v2 280 | - uses: actions/download-artifact@v2 281 | with: 282 | name: emscripten-browser-main 283 | path: wasm 284 | - name: "Prepare artifacts for Github Pages" 285 | run: | 286 | cp -r browser-ui/* wasm/ 287 | - name: Deploy CPython on WASM 🚀 288 | uses: JamesIves/github-pages-deploy-action@4.1.7 289 | with: 290 | branch: gh-pages 291 | folder: wasm 292 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | cpython/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CPython on WASM 2 | 3 | Build scripts and configuration for building CPython for Emscripten. 4 | 5 | Check out Christian Heimes' talk about the effort at PyConDE: https://www.youtube.com/watch?v=oa2LllRZUlU 6 | 7 | Pretty straight forward. First, [install emscripten](https://emscripten.org/docs/getting_started/downloads.html). 8 | Then, run the following commands: 9 | 10 | ```shell 11 | # get the Python sources 12 | ./fetch-python.sh 13 | # build Python for the machine we are building on, needed before cross compiling for emscripten 14 | ./build-python-build.sh 15 | # build Python cross-compiling to emscripten 16 | ./build-python-emscripten-browser.sh 17 | ``` 18 | 19 | There will probably be errors, but that's just part of the fun of experimental platforms. 20 | 21 | Assuming things compiled correctly, you can have emscripten serve the Python executable and then open http://localhost:8000/python.html in your browser: 22 | 23 | ``` 24 | ./run-python-browser.sh 25 | ``` 26 | 27 | The CLI input is done via an input modal which is rather annoying. Also to get output you need to click `Cancel` on the modal... 28 | 29 | ## Developing 30 | Once you've built the Emscripten'd Python, you can rebuild it via 31 | 32 | ``` 33 | ./clean-host.sh 34 | ./build-python-emscripten-browser.sh 35 | ``` 36 | which will rebuild Python targeting emscripten and re-generate the `python.{html, wasm, js}` 37 | 38 | ## Test build artifacts 39 | 40 | You can also download builds from our [CI workflow](https://github.com/ethanhs/python-wasm/actions?query=branch%3Amain) 41 | and test WASM builds locally. 42 | 43 | ### Emscripten browser build 44 | 45 | * download and unzip the ``emscripten-browser-main.zip`` build artifact 46 | * run a local webserver in the same directory as ``python.html``, 47 | e.g. ``python3 -m http.server`` 48 | * open http://localhost:8000/python.html 49 | * enter commands into the browser modal window and check the web developer 50 | console (*F12*) for output. You may need to hit "Cancel" on the modal after sending input for output to appear. 51 | 52 | ### Emscripten NodeJS build 53 | 54 | * download and unzip the ``emscripten-node-main.zip`` build artifact 55 | * run ``node python.js`` (older versions may need ``--experimental-wasm-bigint``) 56 | 57 | ### WASI 58 | 59 | * download and unzip the ``wasi-main.zip`` build artifact 60 | * install [wasmtime](https://wasmtime.dev/) 61 | * run ``wasmtime run --dir . -- python.wasm`` 62 | -------------------------------------------------------------------------------- /browser-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | wasm-python terminal 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 |
18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /browser-ui/index.js: -------------------------------------------------------------------------------- 1 | import { WorkerManager } from "./worker-manager.js"; 2 | import { WasmTerminal } from "./wasm-terminal.js"; 3 | 4 | const replButton = document.getElementById('repl') 5 | const clearButton = document.getElementById('clear') 6 | 7 | window.onload = () => { 8 | const terminal = new WasmTerminal() 9 | terminal.open(document.getElementById('terminal')) 10 | 11 | const stdio = { 12 | stdout: (s) => { terminal.print(s) }, 13 | stderr: (s) => { terminal.print(s) }, 14 | stdin: async () => { 15 | return await terminal.prompt() 16 | } 17 | } 18 | 19 | replButton.addEventListener('click', (e) => { 20 | // Need to use "-i -" to force interactive mode. 21 | // Looks like isatty always returns false in emscripten 22 | pythonWorkerManager.run({args: ['-i', '-'], files: {}}) 23 | }) 24 | 25 | clearButton.addEventListener('click', (e) => { 26 | terminal.clear() 27 | }) 28 | 29 | const readyCallback = () => { 30 | replButton.removeAttribute('disabled') 31 | clearButton.removeAttribute('disabled') 32 | } 33 | 34 | const pythonWorkerManager = new WorkerManager('/worker/worker.js', stdio, readyCallback) 35 | } 36 | -------------------------------------------------------------------------------- /browser-ui/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import mimetypes 4 | 5 | mimetypes.init() 6 | if ".wasm" not in mimetypes.types_map: 7 | mimetypes.add_type("application/wasm", ".wasm") 8 | 9 | import argparse 10 | from http import server 11 | 12 | parser = argparse.ArgumentParser(description='Start a local webserver with a Python terminal.') 13 | parser.add_argument('--port', type=int, default=8000, help='port for the http server to listen on') 14 | args = parser.parse_args() 15 | 16 | class MyHTTPRequestHandler(server.SimpleHTTPRequestHandler): 17 | def end_headers(self): 18 | self.send_my_headers() 19 | 20 | super().end_headers() 21 | 22 | def send_my_headers(self): 23 | self.send_header("Cross-Origin-Opener-Policy", "same-origin") 24 | self.send_header("Cross-Origin-Embedder-Policy", "require-corp") 25 | 26 | 27 | server.test(HandlerClass=MyHTTPRequestHandler, protocol="HTTP/1.1", port=args.port) 28 | -------------------------------------------------------------------------------- /browser-ui/styles.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | max-width: 800px; 4 | margin: 0 auto 5 | } 6 | 7 | #code { 8 | width: 100%; 9 | height: 180px; 10 | } 11 | 12 | .button-container { 13 | display: flex; 14 | justify-content: end; 15 | height: 50px; 16 | align-items: center; 17 | gap: 10px; 18 | } 19 | 20 | button { 21 | padding: 6px 18px; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /browser-ui/wasm-terminal.js: -------------------------------------------------------------------------------- 1 | 2 | export class WasmTerminal { 3 | 4 | constructor() { 5 | this.input = '' 6 | this.resolveInput = null 7 | this.activeInput = false 8 | this.inputStartCursor = null 9 | 10 | this.xterm = new Terminal( 11 | { scrollback: 10000, fontSize: 14, theme: { background: '#1a1c1f' }, cols: 100} 12 | ); 13 | 14 | this.xterm.onKey((keyEvent) => { 15 | // Fix for iOS Keyboard Jumping on space 16 | if (keyEvent.key === " ") { 17 | keyEvent.domEvent.preventDefault(); 18 | } 19 | }); 20 | 21 | this.xterm.onData(this.handleTermData) 22 | } 23 | 24 | open(container) { 25 | this.xterm.open(container); 26 | } 27 | 28 | handleReadComplete(lastChar) { 29 | this.resolveInput(this.input + lastChar) 30 | this.activeInput = false 31 | } 32 | 33 | handleTermData = (data) => { 34 | if (!this.activeInput) { 35 | return 36 | } 37 | const ord = data.charCodeAt(0); 38 | let ofs; 39 | 40 | // TODO: Handle ANSI escape sequences 41 | if (ord === 0x1b) { 42 | // Handle special characters 43 | } else if (ord < 32 || ord === 0x7f) { 44 | switch (data) { 45 | case "\r": // ENTER 46 | case "\x0a": // CTRL+J 47 | case "\x0d": // CTRL+M 48 | this.xterm.write('\r\n'); 49 | this.handleReadComplete('\n'); 50 | break; 51 | case "\x7F": // BACKSPACE 52 | case "\x08": // CTRL+H 53 | case "\x04": // CTRL+D 54 | this.handleCursorErase(true); 55 | break; 56 | 57 | } 58 | } else { 59 | this.handleCursorInsert(data); 60 | } 61 | } 62 | 63 | handleCursorInsert(data) { 64 | this.input += data; 65 | this.xterm.write(data) 66 | } 67 | 68 | handleCursorErase() { 69 | // Don't delete past the start of input 70 | if (this.xterm.buffer.active.cursorX <= this.inputStartCursor) { 71 | return 72 | } 73 | this.input = this.input.slice(0, -1) 74 | this.xterm.write('\x1B[D') 75 | this.xterm.write('\x1B[P') 76 | } 77 | 78 | prompt = async () => { 79 | this.activeInput = true 80 | // Hack to allow stdout/stderr to finish before we figure out where input starts 81 | setTimeout(() => {this.inputStartCursor = this.xterm.buffer.active.cursorX}, 1) 82 | return new Promise((resolve, reject) => { 83 | this.resolveInput = (value) => { 84 | this.input = '' 85 | resolve(value) 86 | } 87 | }) 88 | } 89 | 90 | clear() { 91 | this.xterm.clear(); 92 | } 93 | 94 | print(message) { 95 | const normInput = message.replace(/[\r\n]+/g, "\n").replace(/\n/g, "\r\n"); 96 | this.xterm.write(normInput); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /browser-ui/worker-manager.js: -------------------------------------------------------------------------------- 1 | 2 | export class WorkerManager { 3 | constructor(workerURL, standardIO, readyCallBack) { 4 | this.workerURL = workerURL 5 | this.worker = null 6 | this.standardIO = standardIO 7 | this.readyCallBack = readyCallBack 8 | 9 | this.initialiseWorker() 10 | } 11 | 12 | async initialiseWorker() { 13 | if (!this.worker) { 14 | this.worker = new Worker(this.workerURL) 15 | this.worker.addEventListener('message', this.handleMessageFromWorker) 16 | } 17 | } 18 | 19 | async run(options) { 20 | this.worker.postMessage({ 21 | type: 'run', 22 | args: options.args || [], 23 | files: options.files || {} 24 | }) 25 | } 26 | 27 | handleStdinData(inputValue) { 28 | if (this.stdinbuffer && this.stdinbufferInt) { 29 | let startingIndex = 1 30 | if (this.stdinbufferInt[0] > 0) { 31 | startingIndex = this.stdinbufferInt[0] 32 | } 33 | const data = new TextEncoder().encode(inputValue) 34 | data.forEach((value, index) => { 35 | this.stdinbufferInt[startingIndex + index] = value 36 | }) 37 | 38 | this.stdinbufferInt[0] = startingIndex + data.length - 1 39 | Atomics.notify(this.stdinbufferInt, 0, 1) 40 | } 41 | } 42 | 43 | handleMessageFromWorker = (event) => { 44 | const type = event.data.type 45 | if (type === 'ready') { 46 | this.readyCallBack() 47 | } else if (type === 'stdout') { 48 | this.standardIO.stdout(event.data.stdout) 49 | } else if (type === 'stderr') { 50 | this.standardIO.stderr(event.data.stderr) 51 | } else if (type === 'stdin') { 52 | // Leave it to the terminal to decide whether to chunk it into lines 53 | // or send characters depending on the use case. 54 | this.stdinbuffer = event.data.buffer 55 | this.stdinbufferInt = new Int32Array(this.stdinbuffer) 56 | this.standardIO.stdin().then((inputValue) => { 57 | this.handleStdinData(inputValue) 58 | }) 59 | } else if (type === 'finished') { 60 | this.standardIO.stderr(`Exited with status: ${event.data.returnCode}\r\n`) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /browser-ui/worker/.gitignore: -------------------------------------------------------------------------------- 1 | python.* 2 | -------------------------------------------------------------------------------- /browser-ui/worker/worker.js: -------------------------------------------------------------------------------- 1 | class StdinBuffer { 2 | constructor() { 3 | this.sab = new SharedArrayBuffer(128 * Int32Array.BYTES_PER_ELEMENT) 4 | this.buffer = new Int32Array(this.sab) 5 | this.readIndex = 1; 6 | this.numberOfCharacters = 0; 7 | this.sentNull = true 8 | } 9 | 10 | prompt() { 11 | this.readIndex = 1 12 | Atomics.store(this.buffer, 0, -1) 13 | postMessage({ 14 | type: 'stdin', 15 | buffer: this.sab 16 | }) 17 | Atomics.wait(this.buffer, 0, -1) 18 | this.numberOfCharacters = this.buffer[0] 19 | } 20 | 21 | stdin = () => { 22 | if (this.numberOfCharacters + 1 === this.readIndex) { 23 | if (!this.sentNull) { 24 | // Must return null once to indicate we're done for now. 25 | this.sentNull = true 26 | return null 27 | } 28 | this.sentNull = false 29 | this.prompt() 30 | } 31 | const char = this.buffer[this.readIndex] 32 | this.readIndex += 1 33 | // How do I send an EOF?? 34 | return char 35 | } 36 | } 37 | 38 | const stdoutBufSize = 128; 39 | const stdoutBuf = new Int32Array() 40 | let index = 0; 41 | 42 | const stdout = (charCode) => { 43 | if (charCode) { 44 | postMessage({ 45 | type: 'stdout', 46 | stdout: String.fromCharCode(charCode), 47 | }) 48 | } else { 49 | console.log(typeof charCode, charCode) 50 | } 51 | } 52 | 53 | const stderr = (charCode) => { 54 | if (charCode) { 55 | postMessage({ 56 | type: 'stderr', 57 | stderr: String.fromCharCode(charCode), 58 | }) 59 | } else { 60 | console.log(typeof charCode, charCode) 61 | } 62 | } 63 | 64 | const stdinBuffer = new StdinBuffer() 65 | 66 | var Module = { 67 | noInitialRun: true, 68 | stdin: stdinBuffer.stdin, 69 | stdout: stdout, 70 | stderr: stderr, 71 | onRuntimeInitialized: () => { 72 | postMessage({type: 'ready', stdinBuffer: stdinBuffer.sab}) 73 | } 74 | } 75 | 76 | onmessage = (event) => { 77 | if (event.data.type === 'run') { 78 | // TODO: Set up files from event.data.files 79 | const ret = callMain(event.data.args) 80 | postMessage({ 81 | type: 'finished', 82 | returnCode: ret 83 | }) 84 | } 85 | } 86 | 87 | importScripts('python.js') 88 | -------------------------------------------------------------------------------- /build-python-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | mkdir -p cpython/builddir/build 5 | pushd cpython/builddir/build 6 | ../../configure -C 7 | make -j$(nproc) 8 | popd 9 | -------------------------------------------------------------------------------- /build-python-emscripten-browser.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | if command -v ccache 2>&1 >/dev/null; then 5 | export EM_COMPILER_WRAPPER=ccache 6 | fi 7 | 8 | mkdir -p cpython/builddir/emscripten-browser 9 | 10 | # install emcc ports so configure is able to detect the dependencies 11 | embuilder build zlib bzip2 12 | 13 | pushd cpython/builddir/emscripten-browser 14 | CONFIG_SITE=../../Tools/wasm/config.site-wasm32-emscripten \ 15 | emconfigure ../../configure -C \ 16 | --host=wasm32-unknown-emscripten \ 17 | --build=$(../../config.guess) \ 18 | --with-emscripten-target=browser \ 19 | --enable-wasm-dynamic-linking=no \ 20 | --with-build-python=$(pwd)/../build/python \ 21 | "$@" 22 | 23 | emmake make -j$(nproc) 24 | 25 | popd 26 | -------------------------------------------------------------------------------- /build-python-emscripten-node.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | if command -v ccache 2>&1 >/dev/null; then 5 | export EM_COMPILER_WRAPPER=ccache 6 | fi 7 | 8 | mkdir -p cpython/builddir/emscripten-node 9 | 10 | # install emcc ports so configure is able to detect the dependencies 11 | embuilder build zlib bzip2 12 | 13 | pushd cpython/builddir/emscripten-node 14 | CONFIG_SITE=../../Tools/wasm/config.site-wasm32-emscripten \ 15 | emconfigure ../../configure -C \ 16 | --host=wasm32-unknown-emscripten \ 17 | --build=$(../../config.guess) \ 18 | --with-emscripten-target=node \ 19 | --enable-wasm-dynamic-linking=no \ 20 | --with-build-python=$(pwd)/../build/python \ 21 | "$@" 22 | 23 | emmake make -j$(nproc) 24 | 25 | popd 26 | -------------------------------------------------------------------------------- /build-python-wasi.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # https://github.com/WebAssembly/wasi-sdk 5 | WASI_SDK=/opt/wasi-sdk 6 | # https://github.com/singlestore-labs/wasix 7 | WASIX_DIR=/opt/wasix 8 | 9 | export PATH=${WASI_SDK}/bin:$PATH 10 | export CC="ccache ${WASI_SDK}/bin/clang" 11 | export LDSHARED="${WASI_SDK}/bin/wasm-ld" 12 | export AR="${WASI_SDK}/bin/llvm-ar" 13 | 14 | export CFLAGS="-isystem ${WASIX_DIR}/include" 15 | export LDFLAGS="-L${WASIX_DIR}/lib -lwasix" 16 | 17 | mkdir -p cpython/builddir/wasi 18 | 19 | pushd cpython/builddir/wasi 20 | CONFIG_SITE=../../Tools/wasm/config.site-wasm32-wasi \ 21 | ../../configure -C \ 22 | --host=wasm32-unknown-wasi \ 23 | --build=$(../../config.guess) \ 24 | --with-build-python=$(pwd)/../build/python \ 25 | --disable-ipv6 26 | 27 | make -j$(nproc) 28 | popd 29 | 30 | # sentinel for getpath.py 31 | touch cpython/Modules/Setup.local 32 | -------------------------------------------------------------------------------- /clean-host.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | rm -rf cpython/builddir/emscripten-browser 5 | mkdir -p cpython/builddir/emscripten-browser 6 | rm -rf cpython/builddir/emscripten-node 7 | mkdir -p cpython/builddir/emscripten-node 8 | -------------------------------------------------------------------------------- /fetch-python.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | git clone --depth 1 https://github.com/python/cpython.git 5 | # make build directories for build (the current system architecture) 6 | # and host, the emscripten/wasi architecture 7 | mkdir -p cpython/builddir/build 8 | mkdir -p cpython/builddir/emscripten-browser 9 | mkdir -p cpython/builddir/emscripten-node 10 | -------------------------------------------------------------------------------- /run-python-browser.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cp cpython/builddir/emscripten-browser/python.* browser-ui/worker/ 4 | 5 | pushd . 6 | cd browser-ui 7 | python3 server.py $@ 8 | popd 9 | -------------------------------------------------------------------------------- /run-python-node.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | pushd cpython/builddir/emscripten-node 5 | exec node \ 6 | --experimental-wasm-threads \ 7 | --experimental-wasm-bulk-memory \ 8 | --experimental-wasm-bigint \ 9 | python.js "$@" 10 | -------------------------------------------------------------------------------- /run-python-wasi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | export PATH="$PATH:/root/.wasmtime/bin" 5 | 6 | cd cpython/builddir/wasi 7 | 8 | # PYTHONPATH is relative to mapped cpython/ directory. 9 | exec wasmtime run \ 10 | --env PYTHONPATH=/builddir/wasi/$(cat pybuilddir.txt) \ 11 | --mapdir /::../../ -- \ 12 | python.wasm "$@" 13 | -------------------------------------------------------------------------------- /test-emscripten-node.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | exec ./run-python-node.sh -m test "$@" 5 | -------------------------------------------------------------------------------- /test-wasi.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | exec ./run-python-wasi.sh -m test "$@" 5 | --------------------------------------------------------------------------------