Skip to content
Merged
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
115 changes: 114 additions & 1 deletion .github/workflows/python-wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@ name: Build wheels
# scaffolded but commented out below: it needs a 3.15 interpreter, and 3.15 is
# still in beta. Uncomment the `abi3t` steps (and the `3.15t` interpreter
# installs) once 3.15 is released.
#
# On pull requests, only the `emscripten` job runs (every other job is gated
# with `if: github.event_name != 'pull_request'`). This catches regressions in
# the emscripten/Pyodide build — the most fragile target — without rebuilding
# every native wheel on every PR.

on:
push:
tags:
- "v*"
pull_request:
workflow_dispatch:

permissions:
Expand All @@ -27,6 +33,7 @@ concurrency:

jobs:
linux:
if: github.event_name != 'pull_request'
runs-on: ${{ matrix.platform.runner }}
strategy:
matrix:
Expand Down Expand Up @@ -78,6 +85,7 @@ jobs:
path: dist

musllinux:
if: github.event_name != 'pull_request'
runs-on: ${{ matrix.platform.runner }}
strategy:
matrix:
Expand Down Expand Up @@ -129,6 +137,7 @@ jobs:
path: dist

windows:
if: github.event_name != 'pull_request'
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
Expand Down Expand Up @@ -169,6 +178,7 @@ jobs:
path: dist

macos:
if: github.event_name != 'pull_request'
runs-on: ${{ matrix.platform.runner }}
strategy:
matrix:
Expand Down Expand Up @@ -216,7 +226,110 @@ jobs:
name: wheels-macos-${{ matrix.platform.target }}
path: dist

# PEP 783 (pyemscripten) wheel for Pyodide, built for Python 3.14 only.
#
# The Emscripten version, rustflags, and platform tag are all defined by
# pyodide-build for the target Python version. Two things differ from the
# native jobs:
#
# 1. The wheel is built `--no-default-features` (sync-only). The default
# `async` feature pulls in object_store / tokio / icechunk, which don't
# build for emscripten.
# 2. We override the Rust toolchain via `matrix.rust-toolchain-override`
# (falling back to the one Pyodide pins). zarrista's MSRV (1.94) is newer
# than the toolchain Pyodide pins for 3.14 (1.93), so we override to the
# MSRV. 3.14's ABI needs no nightly-only rustflags, so stable works. Drop
# the override once Pyodide's pinned toolchain reaches the MSRV.
#
# Unlike the native jobs, this also runs on every pull request to catch
# emscripten build regressions (e.g. a dependency update that no longer
# compiles or links for this target) before release time.
emscripten:
runs-on: ubuntu-latest
# Debug build on pull requests (faster, only checks it compiles and links);
# release build on tag pushes and manual dispatch.
env:
RELEASE_FLAG: ${{ github.event_name != 'pull_request' && '--release' || '' }}
strategy:
fail-fast: false
matrix:
python-version: ["3.14"]
include:
- python-version: "3.14"
rust-toolchain-override: "1.94"
steps:
- uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
python-version: ${{ matrix.python-version }}

- name: Install pyodide-build
run: uv sync --only-group pyodide

- name: Get pyodide config
id: pyodide-config
run: |
{
echo "rust-toolchain=$(uv run --no-sync pyodide config get rust_toolchain)"
echo "pyodide-abi-version=$(uv run --no-sync pyodide config get pyodide_abi_version)"
echo "rustflags=$(uv run --no-sync pyodide config get rustflags)"
echo "cflags=$(uv run --no-sync pyodide config get cflags)"
} >> "$GITHUB_OUTPUT"

# Install Emscripten via the Pyodide cross-build environment rather than a
# stock emsdk: this pins the Emscripten version matching the target
# Pyodide ABI, and applies Pyodide's patches to Emscripten, several of
# which affect dynamic linking of Rust side modules.
# https://github.com/pyodide/pyodide/tree/main/emsdk/patches
- name: Install emsdk (Pyodide xbuildenv)
env:
PYODIDE_XBUILDENV_PATH: ${{ github.workspace }}/.pyodide-xbuildenv
run: |
uv run --no-sync pyodide xbuildenv install
uv run --no-sync pyodide xbuildenv install-emscripten
# shellcheck disable=SC1090
source "$PYODIDE_XBUILDENV_PATH/$(uv run --no-sync pyodide xbuildenv version)/emsdk/emsdk_env.sh"
{
echo "EMSDK=$EMSDK"
echo "EMSDK_NODE=$EMSDK_NODE"
} >> "$GITHUB_ENV"
{
echo "$EMSDK"
echo "$EMSDK/upstream/emscripten"
dirname "$EMSDK_NODE"
} >> "$GITHUB_PATH"

- name: Build emscripten wheel
uses: PyO3/maturin-action@v1
env:
CARGO_TARGET_WASM32_UNKNOWN_EMSCRIPTEN_RUSTFLAGS: ${{ steps.pyodide-config.outputs.rustflags }}
CFLAGS_wasm32_unknown_emscripten: ${{ steps.pyodide-config.outputs.cflags }}
MATURIN_PYEMSCRIPTEN_PLATFORM_VERSION: ${{ steps.pyodide-config.outputs.pyodide-abi-version }}
with:
target: wasm32-unknown-emscripten
rust-toolchain: ${{ matrix.rust-toolchain-override || steps.pyodide-config.outputs.rust-toolchain }}
args: --out dist -i ${{ matrix.python-version }} --no-default-features ${{ env.RELEASE_FLAG }}
sccache: "true"

- name: Verify pyemscripten platform tag
# PyPI rejects the legacy `emscripten_x_y_z` tag; fail before upload if
# maturin didn't emit the PEP 783 `pyemscripten_*` tag (this needs
# `MATURIN_PYEMSCRIPTEN_PLATFORM_VERSION` and a recent maturin).
run: |
ls -lh dist/
ls dist/*-pyemscripten_*_wasm32.whl >/dev/null

- name: Upload wheels
uses: actions/upload-artifact@v7
with:
name: wheels-emscripten-py${{ matrix.python-version }}
path: dist

sdist:
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
Expand All @@ -243,7 +356,7 @@ jobs:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
if: startsWith(github.ref, 'refs/tags/')
needs: [linux, musllinux, windows, macos, sdist]
needs: [linux, musllinux, windows, macos, emscripten, sdist]
steps:
- uses: actions/download-artifact@v8
with:
Expand Down
34 changes: 21 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,41 +16,49 @@ name = "_zarrista"
crate-type = ["cdylib"]

[features]
default = ["async"]
# Build a single abi3 wheel compatible with GIL-enabled Python 3.11+.
abi3-py311 = ["pyo3/abi3-py311"]
# Build a single abi3t wheel (free-threaded stable ABI, PEP 803) loadable by
# both GIL-enabled and free-threaded Python 3.15+.
abi3t-py315 = ["pyo3/abi3t-py315"]
async = [
"dep:async-trait",
"dep:icechunk",
"dep:object_store",
"dep:pyo3-async-runtimes",
"dep:pyo3-object_store",
"zarrs/async",
"dep:zarrs_icechunk",
"dep:zarrs_object_store",
]
# https://www.maturin.rs/distribution.html#cross-compile-to-windows
generate-import-lib = ["pyo3/generate-import-lib"]

[dependencies]
arrow-array = "59"
arrow-buffer = "59"
arrow-schema = "59"
async-trait = "0.1"
async-trait = { version = "0.1", optional = true }
bytes = "1.12.0"
dlpark = { git = "https://github.com/kylebarron/dlpark", rev = "31c6f49c064e634326c97172d39a00acecd854b6", features = [
"pyo3",
] }
icechunk = { version = "2.0.0", default-features = false }
object_store = "0.13"
icechunk = { version = "2.0.0", default-features = false, optional = true }
object_store = { version = "0.13", optional = true }
pyo3 = { version = "0.29", features = ["macros", "multiple-pymethods"] }
pyo3-arrow = "0.19"
pyo3-async-runtimes = { version = "0.29", features = ["tokio-runtime"] }
pyo3-async-runtimes = { version = "0.29", features = [
"tokio-runtime",
], optional = true }
pyo3-bytes = "0.7.1"
pyo3-object_store = { version = "0.11.0", default-features = false }
pyo3-object_store = { version = "0.11.0", default-features = false, optional = true }
pythonize = "0.29"
serde_json = "1"
thiserror = "2.0.18"
zarrs = { version = "0.23", features = [
"async",
"bitround",
"ndarray",
"sharding",
] }
zarrs_icechunk = "0.5"
zarrs_object_store = { version = "0.6.2" }
zarrs = { version = "0.23", features = ["bitround", "ndarray", "sharding"] }
zarrs_icechunk = { version = "0.5", optional = true }
zarrs_object_store = { version = "0.6.2", optional = true }

[dev-dependencies]
# `auto-initialize` lets `cargo test` start a Python interpreter for `Python::attach`.
Expand Down
103 changes: 103 additions & 0 deletions DEVELOP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Development

Requires a Rust toolchain and Python 3.11+. We use
[uv](https://docs.astral.sh/uv/) and [maturin](https://www.maturin.rs/).

```bash
# Create a dev environment and install the dev dependencies
uv sync --no-install-package zarrista

# Build the Rust extension and install it into the environment (debug build)
uv run --no-project maturin develop --uv

# Or, in release mode:
uv run --no-project maturin develop --uv --release

# Run the tests
uv run --no-project pytest
```

The `--no-project` is annoying but unavoidable in our current setup. Otherwise `uv` will try to build the rust library _in release mode, as a dependency of the project_ before reaching `uv sync` or `uv run`.

## Docs Website

```bash
uv run --group docs mkdocs serve
```

## Emscripten Python wheels

Emscripten wheels (PEP 783) are built once per Python version. The entire
toolchain config (Rust toolchain, Emscripten version, ABI tag, rustflags) is
defined by `pyodide-build` *running under that same Python version* — e.g.
Python 3.14 maps to ABI `2026_0`/Emscripten 5.0.3. Use `uvx -p` to query the
config for a given Python version without touching the project venv:

```bash
PYTHON_VERSION=3.14
# The `pyodide` executable lives in pyodide-cli; most subcommands (config,
# xbuildenv) are plugins provided by pyodide-build, so both packages are
# needed.
pyodide_cmd() {
uvx -p "$PYTHON_VERSION" --from pyodide-cli --with pyodide-build pyodide "$@"
}
PYODIDE_RUST_TOOLCHAIN=$(pyodide_cmd config get rust_toolchain)
PYODIDE_ABI_VERSION=$(pyodide_cmd config get pyodide_abi_version)
PYODIDE_RUSTFLAGS=$(pyodide_cmd config get rustflags)
PYODIDE_CFLAGS=$(pyodide_cmd config get cflags)

echo "PYODIDE_RUST_TOOLCHAIN: $PYODIDE_RUST_TOOLCHAIN"
echo "PYODIDE_ABI_VERSION: $PYODIDE_ABI_VERSION"
echo "PYODIDE_RUSTFLAGS: $PYODIDE_RUSTFLAGS"
echo "PYODIDE_CFLAGS: $PYODIDE_CFLAGS"
```

Install a Rust toolchain and the wasm target.

Note we do **not** use the toolchain Pyodide pins for 3.14 (rustc 1.93): it is
older than zarrista's MSRV (1.94). Force a newer version instead

```bash
RUST_TOOLCHAIN=1.94
rustup toolchain install $RUST_TOOLCHAIN
rustup target add --toolchain $RUST_TOOLCHAIN wasm32-unknown-emscripten
```

Install Emscripten via the Pyodide cross-build environment rather than a
stock emsdk. This pins the Emscripten version matching the target Pyodide ABI
automatically, and applies [Pyodide's patches to
Emscripten](https://github.com/pyodide/pyodide/tree/main/emsdk/patches) —
several of which affect dynamic linking of Rust side modules:

```bash
export PYODIDE_XBUILDENV_PATH="$HOME/.cache/pyodide-xbuildenv"
pyodide_cmd xbuildenv install
pyodide_cmd xbuildenv install-emscripten
source "$PYODIDE_XBUILDENV_PATH/$(pyodide_cmd xbuildenv version)/emsdk/emsdk_env.sh"
```

Build the wheel. Notes on the environment variables:

- `MATURIN_PYEMSCRIPTEN_PLATFORM_VERSION` is required for the wheel to get the
PyPI-accepted `pyemscripten_*` platform tag instead of the legacy
`emscripten_x_y_z` tag PyPI rejects (this also needs a recent maturin, hence
`uvx maturin` rather than the project venv's maturin).
- `CFLAGS_wasm32_unknown_emscripten` is needed for crates that compile C code
(e.g. zstd-sys, pulled in by zarrs): Pyodide's cflags include `-fPIC`, without which
the C objects can't be linked into a `SIDE_MODULE` (errors like "relocation
R_WASM_MEMORY_ADDR_SLEB cannot be used ...; recompile with -fPIC").
- Always build with `--release`: debug builds are ~10x larger (full DWARF) and
slow.

```bash
RUSTUP_TOOLCHAIN=$RUST_TOOLCHAIN \
CARGO_TARGET_WASM32_UNKNOWN_EMSCRIPTEN_RUSTFLAGS="$PYODIDE_RUSTFLAGS" \
CFLAGS_wasm32_unknown_emscripten="$PYODIDE_CFLAGS" \
MATURIN_PYEMSCRIPTEN_PLATFORM_VERSION=$PYODIDE_ABI_VERSION \
uvx maturin build \
--release \
--no-default-features \
-o dist \
--target wasm32-unknown-emscripten \
-i python$PYTHON_VERSION
```
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ docs = [
# installed to format signatures in the docs
"black>=26",
]
# Toolchain for building PEP 783 (pyemscripten) wheels for Pyodide; see the
# `emscripten` job in `.github/workflows/python-wheels.yml` and `DEVELOP.md`.
# `pyodide-cli` provides the `pyodide` entrypoint; `pyodide-build` provides the
# `config` and `xbuildenv` subcommands. Both require Python 3.12+.
pyodide = [
"pyodide-build>=0.35.0 ; python_version >= '3.12'",
"pyodide-cli>=0.2.2 ; python_version >= '3.12'",
]

[tool.ruff]
# A REPL/notebook-style scratch file using top-level `await`; not a real test.
Expand Down
Loading
Loading