Skip to content

Commit 0e3c5b3

Browse files
sebcrozetclaude
andauthored
feat: generate python bindings (#938)
* feat: generate python bindings * fix(python): make vehicle CI tests architecture-robust + fix typos The `test` job failed on ubuntu/windows (x86_64) while passing on macOS (arm64): rapier is not bit-reproducible across architectures, and two vehicle tests encoded arm64-specific results. - test_vehicle_brakes_decelerate: measure `current_vehicle_speed` (chassis forward axis) instead of `linvel.x`. The vehicle's heading drifts and the drift differs per arch, so the world-x component is not forward speed. - test_example_runs[vehicle/drive.py]: assert output shape + direction via regex instead of an exact speed snapshot. - typos: reword `mis-decode`/`rapierNd`/"PNGs"; allowlist the `ba` and `typ` identifiers in .typos.toml. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: don’t run python CI on all pushes * feat(python): support more 3D shapes and avoid repeated allocations --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0831245 commit 0e3c5b3

264 files changed

Lines changed: 50328 additions & 1465 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cargo/config.toml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
[target.wasm32-unknown-unknown]
22
runner = "wasm-server-runner"
33
# Needed for getrandom/uuid: https://github.com/uuid-rs/uuid/issues/792
4-
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']
4+
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']
5+
6+
# macOS: PyO3 cdylibs built with the `extension-module` feature don't link
7+
# against libpython (Python provides those symbols at runtime). `cargo build`
8+
# doesn't know that, so we tell the linker not to fail on the unresolved
9+
# Python symbols. (Maturin handles this for us; this only affects bare
10+
# `cargo build` invocations.)
11+
[target.'cfg(target_os = "macos")']
12+
rustflags = [
13+
"-C", "link-arg=-undefined",
14+
"-C", "link-arg=dynamic_lookup",
15+
]
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: Python bindings
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
branches: [master]
8+
9+
env:
10+
CARGO_TERM_COLOR: always
11+
12+
# The four engine packages live under python/rapier-py-{2,3}d{,-f64}; the
13+
# pure-Python testbed under python/rapier-testbed; the shared parity test
14+
# suite under python/tests (imports all four engine packages at once).
15+
jobs:
16+
lint-rust:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@v4
20+
- uses: actions/cache@v4
21+
with:
22+
path: |
23+
~/.cargo/registry
24+
~/.cargo/git
25+
target
26+
key: ${{ runner.os }}-cargo-lint-${{ hashFiles('**/Cargo.lock') }}
27+
restore-keys: |
28+
${{ runner.os }}-cargo-lint-
29+
${{ runner.os }}-cargo-
30+
- name: Cargo fmt
31+
run: cargo fmt --check
32+
- name: Cargo clippy (Python bindings)
33+
run: |
34+
cargo clippy --no-deps \
35+
-p rapier-py-core \
36+
-p rapier-py-2d -p rapier-py-2d-f64 \
37+
-p rapier-py-3d -p rapier-py-3d-f64 \
38+
-- -D warnings
39+
40+
test:
41+
needs: lint-rust
42+
strategy:
43+
fail-fast: false
44+
matrix:
45+
os: [ubuntu-latest, macos-14, windows-latest]
46+
runs-on: ${{ matrix.os }}
47+
steps:
48+
- uses: actions/checkout@v4
49+
- name: Set up Python 3.11
50+
uses: actions/setup-python@v5
51+
with:
52+
python-version: "3.11"
53+
- uses: actions/cache@v4
54+
with:
55+
path: |
56+
~/.cargo/registry
57+
~/.cargo/git
58+
target
59+
key: ${{ runner.os }}-cargo-py-${{ hashFiles('**/Cargo.lock') }}
60+
restore-keys: |
61+
${{ runner.os }}-cargo-py-
62+
${{ runner.os }}-cargo-
63+
- name: Install tooling
64+
run: |
65+
python -m pip install --upgrade pip
66+
python -m pip install maturin pytest pytest-timeout hypothesis numpy matplotlib
67+
# Build + install all four engine packages into the runner's environment
68+
# so the cross-flavor parity suite can import every variant.
69+
- name: Build & install engine packages
70+
shell: bash
71+
run: |
72+
for crate in rapier-py-2d rapier-py-2d-f64 rapier-py-3d rapier-py-3d-f64; do
73+
python -m pip install --no-build-isolation "./python/$crate"
74+
done
75+
- name: Install testbed (no deps; engine packages already present)
76+
shell: bash
77+
run: python -m pip install --no-deps ./python/rapier-testbed
78+
- name: Smoke (import surface)
79+
run: python -m pytest python/tests/test_smoke.py python/tests/test_math.py python/tests/test_math_2d.py -v
80+
- name: Full parity test suite
81+
run: python -m pytest python/tests/ -q --timeout=120
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: Python wheels
2+
3+
# Build abi3 wheels for the four engine packages across every supported
4+
# platform and (on a `python-v*` tag) publish them to PyPI via trusted
5+
# publishing.
6+
#
7+
# abi3-py39 means ONE wheel per (package, platform, arch) covers Python 3.9+,
8+
# so there is deliberately no Python-version axis.
9+
#
10+
# NOTE: no sdist is published. The rapier2d/3d engine crates share their
11+
# source via the repo-root `src/` tree (`[lib] path = "../../src/lib.rs"`),
12+
# which lives outside any vendored crate directory, and maturin's sdist
13+
# packer cannot reach parent paths (`..` is rejected in `include`). A source
14+
# build would therefore be incomplete. The wheel matrix below covers all
15+
# mainstream platforms; source builds use a full git checkout of the repo.
16+
# Restoring an sdist is tracked as a follow-up (needs an upstream layout
17+
# change or registry-based engine deps).
18+
on:
19+
push:
20+
branches: [master]
21+
tags: ["python-v*"]
22+
pull_request:
23+
branches: [master]
24+
paths:
25+
- "python/**"
26+
- ".github/workflows/python-wheels.yml"
27+
workflow_dispatch:
28+
29+
permissions:
30+
contents: read
31+
32+
jobs:
33+
build:
34+
name: wheel ${{ matrix.package.name }} ${{ matrix.platform.target }}
35+
runs-on: ${{ matrix.platform.runner }}
36+
strategy:
37+
fail-fast: false
38+
matrix:
39+
package:
40+
- { dir: rapier-py-3d, name: rapier3d }
41+
- { dir: rapier-py-3d-f64, name: rapier3d-f64 }
42+
- { dir: rapier-py-2d, name: rapier2d }
43+
- { dir: rapier-py-2d-f64, name: rapier2d-f64 }
44+
platform:
45+
- { runner: ubuntu-latest, target: x86_64-unknown-linux-gnu, manylinux: auto }
46+
- { runner: ubuntu-latest, target: aarch64-unknown-linux-gnu, manylinux: auto }
47+
- { runner: ubuntu-latest, target: x86_64-unknown-linux-musl, manylinux: musllinux_1_2 }
48+
- { runner: ubuntu-latest, target: aarch64-unknown-linux-musl, manylinux: musllinux_1_2 }
49+
- { runner: macos-14, target: aarch64-apple-darwin }
50+
- { runner: windows-latest, target: x86_64-pc-windows-msvc }
51+
steps:
52+
- uses: actions/checkout@v4
53+
- name: Build wheel
54+
uses: PyO3/maturin-action@v1
55+
with:
56+
command: build
57+
target: ${{ matrix.platform.target }}
58+
manylinux: ${{ matrix.platform.manylinux }}
59+
args: --release --out dist -m python/${{ matrix.package.dir }}/Cargo.toml
60+
sccache: "true"
61+
- uses: actions/upload-artifact@v4
62+
with:
63+
name: wheels-${{ matrix.package.name }}-${{ matrix.platform.target }}
64+
path: dist
65+
66+
publish:
67+
name: Publish to PyPI
68+
needs: [build]
69+
runs-on: ubuntu-latest
70+
if: startsWith(github.ref, 'refs/tags/python-v')
71+
environment: pypi
72+
permissions:
73+
id-token: write # OIDC for trusted publishing
74+
steps:
75+
- uses: actions/download-artifact@v4
76+
with:
77+
path: dist
78+
pattern: "wheels-*"
79+
merge-multiple: true
80+
- name: Publish all four packages
81+
uses: pypa/gh-action-pypi-publish@release/v1
82+
with:
83+
packages-dir: dist

.gitignore

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,22 @@ package-lock.json
99
.history
1010
.vscode/
1111
*.autosave.json
12-
.claude
12+
.claude
13+
14+
# Python bindings: compiled extension modules that `maturin develop` stages
15+
# next to each package's Python sources (e.g. `_rapier3d.abi3.so`) and the
16+
# Sphinx build output.
17+
python/rapier-py-*/python/**/_rapier*.so
18+
python/rapier-py-*/python/**/_rapier*.pyd
19+
python/rapier-py-*/python/**/_rapier*.dylib
20+
python/docs/_build/
21+
**/__pycache__/
22+
*.pyc
23+
*.egg-info/
24+
dist/
25+
build/
26+
.pytest_cache/
27+
.mypy_cache/
28+
.ruff_cache/
29+
.venv/
30+
venv/

.typos.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ iit_softfoot = "iit_softfoot"
2222
FoV = "FoV"
2323
# Shepperd's method: named after S. W. Shepperd (quaternion extraction).
2424
Shepperd = "Shepperd"
25+
# Short local identifiers in the Python bindings/tests: `ba` (body a, paired
26+
# with `bb`) and `typ` (a "type" value — `type` is reserved in Rust/Python).
27+
ba = "ba"
28+
typ = "typ"
29+
# Template mesh data in the testbed instancer: `tpos`/`tnrm`/`tidx`
30+
# (template positions/normals/indices). Only `tpos` collides with a word.
31+
tpos = "tpos"
2532

2633
# Case insensitive, matches inside word.
2734
[default.extend-words]

Cargo.toml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,33 @@ members = [
1515
"crates/rapier3d-meshloader",
1616
"crates/mjcf-rs",
1717
"crates/rapier3d-mjcf",
18+
"python/rapier-py-core",
19+
"python/rapier-py-2d",
20+
"python/rapier-py-2d-f64",
21+
"python/rapier-py-3d",
22+
"python/rapier-py-3d-f64",
23+
]
24+
# Bare `cargo build` / `cargo test` / `cargo clippy` only touches the
25+
# pure-Rust crates. The Python-binding crates need a Python build env
26+
# (pyo3 `extension-module`) and are exercised by the dedicated
27+
# `.github/workflows/python-bindings.yml` workflow via explicit
28+
# `-p rapier-py-*` invocations.
29+
default-members = [
30+
"crates/rapier2d",
31+
"crates/rapier2d-f64",
32+
"crates/rapier_testbed2d",
33+
"crates/rapier_testbed2d-f64",
34+
"examples2d",
35+
"crates/rapier3d",
36+
"crates/rapier3d-f64",
37+
"crates/rapier_testbed3d",
38+
"crates/rapier_testbed3d-f64",
39+
"examples3d",
40+
"examples3d-f64",
41+
"crates/rapier3d-urdf",
42+
"crates/rapier3d-meshloader",
43+
"crates/mjcf-rs",
44+
"crates/rapier3d-mjcf",
1845
]
1946
resolver = "2"
2047

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,18 @@ The easiest way to get started with Rapier is to:
4949
Their source code are available on the `examples2d/` and `examples3d/` directory.
5050
3. Don't hesitate to ask for help on [Discord](https://discord.gg/vt9DJSW), or by opening an issue on GitHub.
5151

52+
## Python bindings
53+
54+
Python bindings are under development. They ship as four PyPI packages —
55+
`rapier2d`, `rapier3d`, `rapier2d-f64`, `rapier3d-f64` — one per
56+
(dimension, scalar) flavor. See [`python/README.md`](python/README.md) for how
57+
to build the bindings, the docs, and the testbed from a checkout, and
58+
[`python/docs/`](python/docs/) for the API documentation.
59+
5260
## AI coding disclaimer and policy
5361

54-
AI coding is extensively used for the implementation and maintenance of the following crates: `mjcf-rs`, `rapier3d-mjcf`.
62+
AI coding is extensively used for the implementation and maintenance of the following crates: `mjcf-rs`,
63+
`rapier3d-mjcf`, as well as the Python bindings (`python/rapier-py*`), including their tests, examples, and docs.
5564

5665
We actively use AI assistance (with human reviews) for the following tasks:
5766
- Documentation generation.

crates/mjcf-rs/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
//!
3333
//! - Simulate anything. Pair this with `rapier3d-mjcf` for that.
3434
//! - Cover MJCF features that are out of scope for `rapier3d-mjcf`. See the
35-
//! `rapier3d-mjcf` planning docs for the complete list.
35+
//! `rapier3d-mjcf` README for the complete list.
3636
3737
#![warn(missing_docs)]
3838

crates/rapier3d-mjcf/README.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ robot.insert_using_impulse_joints(&mut bodies, &mut colliders, &mut impulse_join
4242
## Feature matrix
4343

4444
This crate is rolled out in phases. The table tracks what each phase
45-
delivers and the current implementation status. See [`docs/`](docs/) for
46-
the planning rationale.
45+
delivers and the current implementation status.
4746

4847
Legend: ✅ supported · ⚠️ partial / approximated · 📦 preserved as
4948
metadata · ❌ out of scope.
@@ -172,8 +171,6 @@ convention. (Tracked for a future polish pass.)
172171
- `<compiler coordinate="global">` (deprecated MJCF feature).
173172
- MJCF write-back (the parser is read-only).
174173

175-
See [`docs/04-out-of-scope.md`](docs/04-out-of-scope.md) for rationale.
176-
177174
## Limitations
178175

179176
The mappings noted with ⚠️ above are deliberate trade-offs. In particular:

0 commit comments

Comments
 (0)