hatch-maturin-build
A hatchling build hook that compiles Rust extension modules with maturin, so a project can use hatch for environments, versioning, and packaging while maturin handles only the Rust.
Why
maturin is an excellent PEP 517 backend, but it is the backend — you cannot compose it with the rest of hatchling's build pipeline. Anything that wants a Rust extension and generated stubs, a rendered README, a VCS-derived version, or any other build hook currently has to choose.
This plugin inverts the relationship. hatchling stays the backend and owns metadata, file selection, editable installs, and the wheel itself; maturin is invoked for the one thing it is uniquely good at.
Installation
[build-system]
requires = ["hatchling", "hatch-maturin-build"]
build-backend = "hatchling.build"
cargo must be on PATH. maturin is pulled in as a dependency of this plugin.
Usage
[build-system]
requires = ["hatchling", "hatch-maturin-build"]
build-backend = "hatchling.build"
[project]
name = "mypkg"
dynamic = ["version"]
# Read by maturin, exactly as it always was. This plugin does not reparse it.
[tool.maturin]
python-source = "python"
module-name = "mypkg.mypkg"
[tool.hatch.version]
path = "python/mypkg/__init__.py"
[tool.hatch.build.targets.wheel]
packages = ["python/mypkg"]
[tool.hatch.build.targets.wheel.hooks.maturin]
features = ["pyo3/abi3-py39"]
Then build with anything: hatch build, python -m build, pip install ., uv build.
Configuration lives in three layers, in increasing precedence:
[tool.maturin]— what the crate is. Owned by maturin, untouched by this plugin, and still valid if you switch back tobuild-backend = "maturin".[tool.hatch.build.targets.<target>.hooks.maturin]— how this build target compiles it.- Environment variables — the per-invocation escape hatch (see below).
Options
| Option | Type | Default | maturin flag |
|---|---|---|---|
manifest-path |
str | Cargo.toml |
--manifest-path |
profile |
str | release |
--profile |
target |
str | host | --target |
target-dir |
str | cargo default | --target-dir |
bindings |
str | auto | --bindings |
auditwheel |
str | maturin default | --auditwheel |
jobs |
int | cargo default | --jobs |
features |
list[str] | [] |
--features (repeated) |
compatibility |
list[str] | auto | --compatibility (repeated) |
config |
list[str] | [] |
--config (repeated) |
all-features |
bool | false |
--all-features |
no-default-features |
bool | false |
--no-default-features |
generate-stubs |
bool | false |
--generate-stubs |
include-debuginfo |
bool | false |
--include-debuginfo |
strip |
bool | false |
--strip |
zig |
bool | false |
--zig |
locked / frozen / offline |
bool | false |
same |
args |
list[str] | [] |
appended verbatim |
executable |
str | auto | path to the maturin binary |
interpreter |
str | sys.executable |
--interpreter |
sdist-include |
list[str] | [] |
extra files for the sdist |
Unknown options are an error rather than a silent no-op. Reach for args when you need a flag this
table does not cover.
Note that profile defaults to release even though bare maturin build defaults to debug. A
PEP 517 build should be optimized, which is what maturin's own backend does.
Environment variables
hatchling discards PEP 517 config_settings entirely — every hook in hatchling/build.py marks the
parameter unused — so pip install --config-settings=... cannot reach a build hook. Environment
variables are the only channel that works through pip, build, and uv alike:
| Variable | Effect |
|---|---|
HATCH_MATURIN_PROFILE |
overrides profile, e.g. dev for a debug build |
HATCH_MATURIN_TARGET |
overrides target |
HATCH_MATURIN_BINDINGS |
overrides bindings |
HATCH_MATURIN_FEATURES |
overrides features (comma- or space-separated) |
HATCH_MATURIN_ARGS |
extra flags, shell-split |
HATCH_MATURIN_EXECUTABLE |
path to the maturin binary |
HATCH_MATURIN_PROFILE=dev pip install --no-build-isolation -e .
Inside hatch, put them in [tool.hatch.envs.<env>.env-vars] instead.
How it works
maturin has no "just build the extension" output mode: every file-producing command emits a wheel,
emits an sdist, or installs into a virtualenv. So the hook runs maturin build into a scratch
directory, unpacks the resulting wheel, discards the .dist-info, and hands the remainder to
hatchling through build_data.
That round trip is deliberate rather than lazy. On Linux maturin defaults to AuditWheelMode::Repair,
which derives the real manylinux/musllinux platform tag from what the extension actually links
against, bundles external shared libraries beside it, and rewrites RPATH. Shelling out to a bare
cargo build and copying the .so would skip all of that and produce a wheel that is tagged wrong
and missing its vendored libraries.
Three consequences worth knowing:
- The tag comes from maturin. The hook sets
build_data["tag"]explicitly rather than using hatchling'sinfer_tag, which only knows the host interpreter and is therefore wrong for both abi3 and cross builds. - hatchling owns the Python files. Any member of maturin's wheel that hatchling already ships is
dropped, so
force_includenever shadows a file another build hook may have rewritten. Files maturin generates (cffi glue, bin shims, stubs) have no source counterpart and are kept. *.data/is re-homed. Scripts and data go throughbuild_data["shared_scripts"]and["shared_data"]so hatchling names the.datadirectory. maturin falls back to theCargo.tomlversion whenever hatch owns the version dynamically, so the directory name it picked cannot be trusted.
Editable installs
hatchling's editable wheel points an import hook at your source directories and forces a
py3-none-any tag. An extension force-included into such a wheel lands in site-packages, where
nothing will ever look for it. So for editable builds the hook copies the compiled artifacts into
the source tree, the way maturin develop does, and records what it wrote in
<cargo target dir>/.hatch-maturin-editable.json so hatch clean can remove them again.
The hook refuses to overwrite any file in the source tree that it did not write itself.
pip install --no-build-isolation -e .
--no-build-isolation is what recovers most of the speed difference against maturin develop; the
rest is the wheel zip round trip, which is small next to a cargo build.
sdists
hatchling's sdist is VCS-based, so a git-tracked crate is already covered. The hook force-includes
Cargo.toml and Cargo.lock as a safety net, plus anything in sdist-include.
Known gap: path dependencies outside the project root are not enumerated. There is no way to do
that without cargo package --list, which demands a far stricter workspace state than a build hook
should impose.
Limitations
These are limits of building through maturin's wheel output rather than of the hook itself, and are tracked upstream in PyO3/maturin#1419:
- One cargo build per wheel. hatchling invokes the hook once per wheel, so a non-abi3 build for N interpreters is N compiles. Prefer abi3.
- Stubs cost a second compile.
maturin generate-stubsruns its own cargo build. - Output discovery is a convention, not a contract. "Everything that is not
.dist-infois mine" is not a promise maturin makes, so a maturin release could change what the hook sees. - Metadata is computed twice. maturin needs a
[project]table valid enough not to error, even though its metadata output is discarded.
All four dissolve if maturin grows a build-extension subcommand that emits artifacts plus a JSON
manifest. The manifest this hook assembles internally is deliberately shaped like that future
output, so adopting it would be a change to a single method.
Development
Run the unit tests on the current interpreter:
hatch test
Run them across every supported interpreter:
hatch test --all
Run the integration test, which compiles a real pyo3 crate and therefore needs cargo and a network. It is deselected by default:
hatch test -- -m integration
Lint, format, and type check:
hatch check
Serve the documentation locally:
hatch run docs:serve
Releasing
Publishing runs on PyPI Trusted Publishing — there are
no API tokens in the repository. Bump the version in src/hatch_maturin_build/__about__.py, then
push a matching tag:
git tag v0.1.0 && git push origin v0.1.0
The workflow refuses to publish if the tag and the package version disagree.
License
hatch-maturin-build is distributed under the terms of the MIT
license.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hatch_maturin_build-0.1.0.tar.gz.
File metadata
- Download URL: hatch_maturin_build-0.1.0.tar.gz
- Upload date:
- Size: 29.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e7071ca57ad15cef8f26ad091665cb52752112e085adf220e213880a4e6561e4
|
|
| MD5 |
0d0f54cd1203eab51da8c4de075df5e8
|
|
| BLAKE2b-256 |
78679dcfa7fd5ef6e8e627892d4d46cdd514b51a51a62b2b42737109f45a321f
|
Provenance
The following attestation bundles were made for hatch_maturin_build-0.1.0.tar.gz:
Publisher:
publish.yml on cjames23/hatch-maturin-build
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hatch_maturin_build-0.1.0.tar.gz -
Subject digest:
e7071ca57ad15cef8f26ad091665cb52752112e085adf220e213880a4e6561e4 - Sigstore transparency entry: 2405091376
- Sigstore integration time:
-
Permalink:
cjames23/hatch-maturin-build@91db0b418659b0a2e49074ed3d0e2ea4c6b54d7d -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/cjames23
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@91db0b418659b0a2e49074ed3d0e2ea4c6b54d7d -
Trigger Event:
push
-
Statement type:
File details
Details for the file hatch_maturin_build-0.1.0-py3-none-any.whl.
File metadata
- Download URL: hatch_maturin_build-0.1.0-py3-none-any.whl
- Upload date:
- Size: 17.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ed3ad900a240d0c0f38e8189c78a4fbc1ea141ed9f4f8dfebc09987a8ded3dd3
|
|
| MD5 |
16abe203b55d193a04c9f9de370d48cc
|
|
| BLAKE2b-256 |
2852ae928a9d6327474f45c16105903df83f736ad12da5478c7c9905009371f5
|
Provenance
The following attestation bundles were made for hatch_maturin_build-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on cjames23/hatch-maturin-build
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hatch_maturin_build-0.1.0-py3-none-any.whl -
Subject digest:
ed3ad900a240d0c0f38e8189c78a4fbc1ea141ed9f4f8dfebc09987a8ded3dd3 - Sigstore transparency entry: 2405092117
- Sigstore integration time:
-
Permalink:
cjames23/hatch-maturin-build@91db0b418659b0a2e49074ed3d0e2ea4c6b54d7d -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/cjames23
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@91db0b418659b0a2e49074ed3d0e2ea4c6b54d7d -
Trigger Event:
push
-
Statement type: