Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Updated the Emscripten web example to use ES6 modules and be built into a
distinct ``web_example`` subfolder.
106 changes: 73 additions & 33 deletions Tools/wasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ sourced. Otherwise the source script removes the environment variable.
export EM_COMPILER_WRAPPER=ccache
```

### Compile and build Python interpreter
#### Compile and build Python interpreter

You can use `python Tools/wasm/emscripten` to compile and build targetting
Emscripten. You can do everything at once with:
Expand All @@ -70,6 +70,78 @@ instance, to do a debug build, you can use:
python Tools/wasm/emscripten build --with-py-debug
```

### Running from node

If you want to run the normal Python cli, you can use `python.sh`. It takes the
same options as the normal Python cli entrypoint, though the REPL does not
function and will crash.

`python.sh` invokes `node_entry.mjs` which imports the Emscripten module for the
Python process and starts it up with the appropriate settings. If you wish to
make a node application that "embeds" the interpreter instead of acting like the
CLI you will need to write your own alternative to `node_entry.mjs`.


### The Web Example

When building for Emscripten, the web example will be built automatically. It is
in the ``web_example`` directory. The web example uses ``SharedArrayBuffer``.
For security reasons browsers only provide ``SharedArrayBuffer`` in secure
environments with cross-origin isolation. The webserver must send cross-origin
headers and correct MIME types for the JavaScript and WebAssembly files.
Otherwise the terminal will fail to load with an error message like
``ReferenceError: SharedArrayBuffer is not defined``. See more information here:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements

If you serve the web example with ``python server.py`` and then visit
``localhost:8000`` in a browser it should work.

Note that ``SharedArrayBuffer`` is _not required_ to use Python itself, only the
web example. If cross-origin isolation is not appropriate for your use case you
may make your own application embedding `python.mjs` which does not use
``SharedArrayBuffer`` and serve it without the cross-origin isolation headers.

### Embedding Python in a custom JavaScript application

You can look at `python.worker.mjs` and `node_entry.mjs` for inspiration. At a
minimum you must import ``createEmscriptenModule`` and you need to call
``createEmscriptenModule`` with an appropriate settings object. This settings
object will need a prerun hook that installs the Python standard library into
the Emscripten file system.

#### NodeJs

In Node, you can use the NodeFS to mount the standard library in your native
file system into the Emscripten file system:
```js
import createEmscriptenModule from "./python.mjs";

await createEmscriptenModule({
preRun(Module) {
Module.FS.mount(Module.FS.filesystems.NODEFS, { root: "/path/to/python/stdlib" }, "/lib/");
}
});
```

#### Browser

In the browser, the simplest approach is to put the standard library in a zip
file it and install it. With Python 3.14 this could look like:
```js
import createEmscriptenModule from "./python.mjs";

await createEmscriptenModule({
preRun(Module) {
Module.FS.mkdirTree("/lib/python3.14/lib-dynload/");
Module.addRunDependency("install-stdlib");
const resp = await fetch("python3.14.zip");
const stdlibBuffer = await resp.arrayBuffer();
Module.FS.writeFile(`/lib/python314.zip`, new Uint8Array(stdlibBuffer), { canOwn: true });
Module.removeRunDependency("install-stdlib");
}
});
```

### Limitations and issues

#### Network stack
Expand Down Expand Up @@ -151,38 +223,6 @@ python Tools/wasm/emscripten build --with-py-debug
- Test modules are disabled by default. Use ``--enable-test-modules`` build
test modules like ``_testcapi``.

### wasm32-emscripten in node

Node builds use ``NODERAWFS``.

- Node RawFS allows direct access to the host file system without need to
perform ``FS.mount()`` call.

### Hosting Python WASM builds

The simple REPL terminal uses SharedArrayBuffer. For security reasons
browsers only provide the feature in secure environments with cross-origin
isolation. The webserver must send cross-origin headers and correct MIME types
for the JavaScript and WASM files. Otherwise the terminal will fail to load
with an error message like ``Browsers disable shared array buffer``.

#### Apache HTTP .htaccess

Place a ``.htaccess`` file in the same directory as ``python.wasm``.

```
# .htaccess
Header set Cross-Origin-Opener-Policy same-origin
Header set Cross-Origin-Embedder-Policy require-corp

AddType application/javascript js
AddType application/wasm wasm

<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html application/javascript application/wasm
</IfModule>
```

## WASI (wasm32-wasi)

See [the devguide on how to build and run for WASI](https://devguide.python.org/getting-started/setup-building/#wasi).
Expand Down
21 changes: 19 additions & 2 deletions Tools/wasm/emscripten/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,29 @@ def configure_emscripten_python(context, working_dir):
exec_script = working_dir / "python.sh"
exec_script.write_text(
dedent(
f"""\
"""\
#!/bin/sh

# Macs come with a defective fork of coreutils so feature detect and
# work around it.
if which grealpath > /dev/null; then
# It has brew installed gnu core utils, use that
REALPATH="grealpath -s"
elif which readlink > /dev/null && realpath --version | grep GNU > /dev/null; then
# realpath points to GNU realpath so use it.
REALPATH="realpath -s"
else
# Shim for macs without GNU coreutils
abs_path () {
echo "$(cd $(dirname "$1");pwd)/$(basename "$2")"
}
REALPATH=abs_path
fi
"""
f"""\
# We compute our own path, not following symlinks and pass it in so that
# node_entry.mjs can set sys.executable correctly.
exec {host_runner} {node_entry} "$(realpath -s $0)" "$@"
exec {host_runner} {node_entry} "$($REALPATH $0)" "$@"
"""
)
)
Expand Down
Loading