├── .gitignore
├── browser-ui
├── worker
│ ├── .gitignore
│ └── worker.js
├── styles.css
├── index.html
├── server.py
├── index.js
├── worker-manager.js
└── wasm-terminal.js
├── .devcontainer
└── devcontainer.json
├── test-wasi.sh
├── test-emscripten-node.sh
├── run-python-browser.sh
├── clean-host.sh
├── run-python-node.sh
├── run-python-wasi.sh
├── fetch-python.sh
├── .github
├── actions
│ └── prepare
│ │ └── action.yml
└── workflows
│ └── ci.yml
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | cpython/
3 |
--------------------------------------------------------------------------------
/browser-ui/worker/.gitignore:
--------------------------------------------------------------------------------
1 | python.*
2 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "image": "quay.io/tiran/cpythonbuild:emsdk3"
3 | }
4 |
--------------------------------------------------------------------------------
/test-wasi.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -e
3 |
4 | exec ./run-python-wasi.sh -m test "$@"
5 |
--------------------------------------------------------------------------------
/test-emscripten-node.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -e
3 |
4 | exec ./run-python-node.sh -m test "$@"
5 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------