Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog

```{include} ../CHANGELOG.md
:start-line: 2
```
11 changes: 11 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
"sphinx.ext.napoleon",
"myst_parser",
"sphinx_autodoc_typehints",
"sphinx_design",
]

myst_enable_extensions = [
"colon_fence",
]

intersphinx_mapping = {
Expand All @@ -38,6 +43,12 @@
html_logo = "_static/img/pyodide-logo.png"
html_static_path = ["_static"]

html_theme_options = {
"show_toc_level": 2,
"show_navbar_depth": 2,
"home_page_in_toc": True,
}


sys.path.append(Path(__file__).parent.parent.as_posix())

Expand Down
94 changes: 94 additions & 0 deletions docs/explanation/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# How pyodide-build Works

This page explains the internals of pyodide-build — how it turns a regular Python package into a WebAssembly wheel.

## The build pipeline

When you run `pyodide build .`, the following happens:

```
┌────────────────┐
│ pyodide build │ CLI entry point
└───────┬────────┘
┌────────────────┐
│ Environment │ Install xbuildenv + Emscripten SDK (if needed)
│ setup │ Set up sysconfig, headers, env vars
└───────┬────────┘
┌────────────────┐
│ pypa/build │ Standard PEP 517 build frontend
│ │ Invokes your build backend (setuptools, meson-python, etc.)
└───────┬────────┘
│ Build backend calls gcc, g++, cmake, meson, cargo...
┌────────────────┐
│ pywasmcross │ Compiler wrapper — intercepts all tool calls
│ │ Redirects to Emscripten (emcc, em++, emar, etc.)
│ │ Filters incompatible flags, adds Wasm flags
└───────┬────────┘
┌────────────────┐
│ Emscripten │ Compiles C/C++ → WebAssembly (.o, .a, .so)
│ (emcc/em++) │ Links as SIDE_MODULE
└───────┬────────┘
┌────────────────┐
│ Wheel output │ .so files (Wasm) + Python files → .whl
│ │ Tagged: pyemscripten_YYYY_P_wasm32
└────────────────┘
```

## The compiler wrapper (pywasmcross)

The core of pyodide-build is `pywasmcross` — a compiler wrapper that transparently redirects native compiler calls to Emscripten.

### How it works

When pyodide-build sets up the build environment, it creates symlinks named after common compiler tools:

| Symlink | Redirects to |
|---|---|
| `cc`, `gcc` | `emcc` |
| `c++`, `g++` | `em++` |
| `ar` | `emar` |
| `ranlib` | `emranlib` |
| `strip` | `emstrip` |
| `cmake` | `emcmake cmake` (with toolchain flags) |
| `meson` | `meson` (with cross file injected) |
| `cargo` | `cargo` (with Emscripten target) |

These symlinks all point to `pywasmcross.py`. When invoked, pywasmcross:

1. Detects which tool it's impersonating (from the symlink name)
2. Rewrites the command line for Emscripten
3. Filters out incompatible flags
4. Adds WebAssembly-specific flags
5. Executes the real Emscripten tool

When `pyodide build` runs:

1. `Makefile.envs` is parsed to set compiler flags, paths, and environment variables
2. The `sysconfig` module is patched to return target-platform values instead of host values
3. `PATH` is modified so pywasmcross symlinks take priority over native compilers
4. Environment variables (`SIDE_MODULE_CFLAGS`, `CMAKE_TOOLCHAIN_FILE`, `MESON_CROSS_FILE`, etc.) are set

## Build isolation

By default, `pyodide build` uses [pypa/build](https://build.pypa.io/) in isolated mode — just like `python -m build`. This means:

1. A temporary virtual environment is created
2. Build dependencies from `[build-system].requires` are installed
3. The build backend is invoked
4. The virtual environment is discarded

The `--no-isolation` flag skips this and uses the current environment, which is useful for complex builds where you need to manage dependencies yourself.

## Further reading

- [Emscripten documentation](https://emscripten.org/docs/) — the underlying compiler toolchain
- [PEP 517](https://peps.python.org/pep-0517/) — Python build system interface
47 changes: 47 additions & 0 deletions docs/faq.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# FAQ

## Which packages work with Pyodide?

- **Pure Python** — always works. No special tooling needed.
- **C/C++ extensions** — usually works with pyodide-build. Some packages need minor adjustments (e.g., disabling optional native dependencies, guarding platform-specific code).
- **Rust extensions (PyO3)** — works with pyodide-build and the correct Rust nightly toolchain.
- **Threading / multiprocessing** — not supported. Packages that require threads will not work.
- **Networking (sockets)** — not supported. Packages that open raw sockets will not work. High-level HTTP libraries like `requests` and `httpx` have Pyodide-specific fallbacks.
- **Subprocesses** — not supported. Packages that call `subprocess.run()` will not work.

## Do I need the full Pyodide repository?

No. pyodide-build is a standalone package. Install it with `pip install pyodide-build` and you're ready to build. You don't need to clone the Pyodide repository.

## Do I need pyodide-build for pure-Python packages?

No. A pure-Python wheel built with `python -m build`, `hatch`, `flit`, or any standard build frontend is already compatible with Pyodide. pyodide-build is only needed for packages with compiled extensions (C, C++, Rust).

## Should I use `pyodide build` directly or cibuildwheel?

- **cibuildwheel** — if you already build native wheels for Linux/macOS and want to add Pyodide as another platform in the same CI config.
- **`pyodide build` directly** — if you only target Pyodide, want more control, or don't use cibuildwheel yet.

See [CI with cibuildwheel](how-to/cibuildwheel.md) and [CI without cibuildwheel](how-to/ci-direct.md).

## Can I use meson-python / scikit-build-core / maturin?

Yes. pyodide-build supports all major Python build backends:

- **setuptools** — works out of the box
- **meson-python** — cross file injected automatically. See [Tutorial: Meson](tutorials/meson.md).
- **scikit-build-core** — CMake toolchain handled automatically. See [Tutorial: CMake](tutorials/cmake.md).
- **maturin** — Rust target and flags set automatically. See [Tutorial: Rust](tutorials/rust.md).
- **hatchling / flit** — pure-Python only (no compiled extensions), so no pyodide-build needed.

## What Node.js version do I need?

Node.js >= 24 is recommended for `pyodide venv`. Node.js is only needed for testing — not for building.

## What Python versions are supported?

pyodide-build requires Python 3.12 or later. The Python version must match the target Pyodide cross-build environment version.

## What's the difference between `pyodide build` and `python -m build`?

`pyodide build` wraps `python -m build` with a cross-compilation layer. Your build configuration stays the same — pyodide-build intercepts compiler calls and redirects them to Emscripten. See [Concepts](getting-started/concepts.md) for a detailed comparison.
109 changes: 109 additions & 0 deletions docs/getting-started/concepts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Concepts

This page explains the key ideas behind pyodide-build. Understanding these concepts will help you debug build issues and make sense of the configuration options.

## Why cross-compilation?

When you run `python -m build` on your laptop, your C extensions are compiled for your machine's architecture — x86_64 on most desktops, arm64 on Apple Silicon. The resulting wheel only works on that platform.

Pyodide runs Python inside WebAssembly, which is a completely different compilation target. You can't run `gcc` or `clang` and get a `.so` that works in WebAssembly — you need [Emscripten](https://emscripten.org/), a compiler toolchain that produces WebAssembly output from C/C++ source code.

pyodide-build automates this cross-compilation. When you run `pyodide build`, it:

1. Invokes your package's normal build system (setuptools, meson-python, scikit-build-core, etc.)
2. Intercepts all compiler and linker calls
3. Redirects them through Emscripten with the right flags for WebAssembly
4. Produces a standard wheel tagged for the Emscripten platform

Your build scripts don't need to change — pyodide-build handles the translation transparently.

## The cross-build environment

Cross-compilation needs more than just a compiler. Your package's build system needs to find Python headers, link against the right libraries, and query Python's `sysconfig` for the target platform — not the host. The **cross-build environment** (xbuildenv) provides all of this:

- **CPython headers and sysconfig data** compiled for Emscripten/WebAssembly
- **Pre-built package stubs** for packages like NumPy and SciPy that other packages link against at build time
- **Emscripten SDK** — the compiler toolchain itself (installed automatically)

When you run `pyodide build`, pyodide-build automatically downloads and sets up the cross-build environment if one isn't already installed. It's cached in your platform's user cache directory so subsequent builds are fast.

You can also manage the cross-build environment explicitly:

```bash
pyodide xbuildenv install # install (or update) the cross-build environment
pyodide xbuildenv install 0.27.0 # install a specific Pyodide version
pyodide xbuildenv versions # list installed versions
```

See [Managing Cross-Build Environments](../how-to/xbuildenv.md) for more details.

## Emscripten

[Emscripten](https://emscripten.org/) is the compiler toolchain that turns C and C++ code into WebAssembly. It provides drop-in replacements for standard compilers:

| Standard tool | Emscripten equivalent |
|---|---|
| `gcc` / `cc` | `emcc` |
| `g++` / `c++` | `em++` |
| `ar` | `emar` |
| `ranlib` | `emranlib` |

pyodide-build manages Emscripten automatically — it installs the correct version as part of the cross-build environment and handles all compiler redirection. You don't need to install or configure Emscripten yourself.

```{important}
Each Pyodide version requires a **specific** Emscripten version. pyodide-build enforces this to ensure ABI compatibility. You can check the required version with `pyodide config get emscripten_version`.
```

## Platform tags

Python wheels include a [platform tag](https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/) that identifies which systems they can run on. For example:

- `manylinux_2_17_x86_64` — Linux on x86_64
- `macosx_14_0_arm64` — macOS on Apple Silicon
- `pyemscripten_2025_0_wasm32` — Emscripten/WebAssembly

The Emscripten platform tag, standardized by [PEP 783](https://peps.python.org/pep-0783/), has the format:

```
pyemscripten_{year}_{patch}_wasm32
```

Where `{year}_{patch}` is the platform ABI version (e.g., `2025_0`). This version determines which Emscripten SDK version and CPython build are used. Wheels built for one ABI version are **not** compatible with another.

A complete wheel filename looks like:

```
numpy-2.2.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl
│ │ │ │
│ │ │ └── platform tag
│ │ └── Python ABI tag
│ └── Python version tag
└── package version
```

```{note}
Older Pyodide versions (before PEP 783) used the tag `pyodide_{year}_{patch}_wasm32`. The `pyemscripten_*` tag is the standardized form going forward.
```

## `pyodide build` vs `python -m build`

`pyodide build` is designed to be a drop-in replacement for `python -m build` when targeting WebAssembly. Here's what's the same and what's different:

**The same:**
- Uses your existing `pyproject.toml` build configuration
- Supports the same build backends (setuptools, meson-python, scikit-build-core, hatchling, etc.)
- Produces a standard `.whl` file
- Supports `-C` / `--config-setting` to pass options to the build backend
- Supports `--no-isolation` for custom build environments

**Different:**
- Compiler calls are intercepted and redirected to Emscripten
- Some compiler flags are filtered out (e.g., `-pthread`, x86 SIMD flags) because they don't apply to WebAssembly
- The output wheel has an Emscripten platform tag instead of a native one
- A cross-build environment must be available (installed automatically on first use)

## What's next?

- [Quick Start](quickstart.md) — build your first WebAssembly wheel
- [Managing Cross-Build Environments](../how-to/xbuildenv.md) — advanced xbuildenv management
- [Platform Tags & Compatibility](../reference/platform.md) — full compatibility matrix
42 changes: 42 additions & 0 deletions docs/getting-started/installation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Installation

## Requirements

- **Linux or macOS** — pyodide-build works best on Linux and macOS. Windows is not supported.
- **Python 3.12 or later** — **must** match the Python version targeted by the Pyodide cross-build environment you install.
- **Node.js** — required for testing with [`pyodide venv`](testing.md). Not needed for building. Node.js >= 24 is recommended.

## Install pyodide-build

::::{tab-set}

:::{tab-item} pip
```bash
pip install pyodide-build
```
:::

:::{tab-item} pipx
```bash
pipx install pyodide-build
```
:::

:::{tab-item} uv
```bash
uv tool install pyodide-cli --with pyodide-build
```
:::

::::

Verify the installation:

```bash
pyodide --version
```

## What's next?

- [Concepts](concepts.md) — understand cross-compilation, the cross-build environment, and platform tags
- [Quick Start](quickstart.md) — build your first WebAssembly wheel
72 changes: 72 additions & 0 deletions docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Quick Start

This guide walks you through building a Python package with C extensions for WebAssembly using pyodide-build. Make sure you've [installed pyodide-build](installation.md) first.

```{note}
**Pure-Python packages do not need pyodide-build.** If your package has no C, C++, or Rust extensions, a standard wheel built with `python -m build` already works with Pyodide.
```

## Build from your source tree

If you have a package with compiled extensions locally, build it the same way you would with `python -m build`:

```bash
pyodide build .
```

The output wheel is placed in `./dist/` by default:

```
dist/your_package-1.0.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl
```

On the first run, pyodide-build automatically downloads and sets up the cross-build environment and Emscripten SDK. This may take a minute — subsequent builds are fast.

You can specify a different output directory with `--outdir` / `-o`:

```bash
pyodide build . -o wheelhouse/
```

## Passing options to the build backend

Use `-C` / `--config-setting` to pass options to your build backend, just like with `python -m build`:

```bash
# Meson project: pass the cross-compilation file
pyodide build . -C setup-args=-Dblas=none -C setup-args=-Dlapack=none

# setuptools project: pass extra compile args
pyodide build . -C "--build-option=--some-flag"
```

## Verify the wheel

You can inspect the built wheel to confirm it has the correct platform tag:

```bash
unzip -l dist/your_package-*.whl | head -20
```

The wheel should contain `.so` files (compiled extensions) alongside your Python source, and the filename should include the `pyemscripten_*_wasm32` platform tag.

## Test the wheel

Create a [Pyodide virtual environment](testing.md) to test the wheel:

```bash
pyodide venv .venv-pyodide
source .venv-pyodide/bin/activate
pip install dist/your_package-*.whl
python -c "import your_package; print('it works!')"
```

See [Testing with `pyodide venv`](testing.md) for a full walkthrough.

## What's next?

- [Testing with `pyodide venv`](testing.md) — verify your wheel in a Pyodide environment
- [CI with cibuildwheel](../how-to/cibuildwheel.md) — automate Pyodide builds in CI
- [Tutorial: Meson Package](../tutorials/meson.md) — building packages with Meson
- [Tutorial: CMake Package](../tutorials/cmake.md) — building packages with CMake
- [Tutorial: Rust Package](../tutorials/rust.md) — building packages with Rust/PyO3 extensions
Loading