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
3 changes: 2 additions & 1 deletion docs/_includes/pyproject.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ Changelog = "https://github.com/org/package/releases"
<!-- prettier-ignore-end -->
<!-- [[[end]]] -->

You can read more about each field, and all allowed fields, in
In this example, `"package"` is the name of the thing you are working on. You
can read more about each field, and all allowed fields, in
[packaging.python.org][metadata],
[Flit](https://flit.readthedocs.io/en/latest/pyproject_toml.html#new-style-metadata)
or [Whey](https://whey.readthedocs.io/en/latest/configuration.html). Only the
Expand Down
155 changes: 116 additions & 39 deletions docs/pages/guides/packaging_compiled.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ parent: Topical Guides

{% include toc.html %}

<!-- [[[cog
from cog_helpers import code_fence, render_cookie, TOMLMatcher
with render_cookie(backend="skbuild", vcs=False) as skbuild:
skbuild_cmakelists_txt = skbuild.joinpath("CMakeLists.txt").read_text(encoding="utf-8").strip()
skbuild_src_main_cpp = skbuild.joinpath("src/main.cpp").read_text(encoding="utf-8").strip()
skbuild_pyproject = TOMLMatcher.from_file(skbuild / "pyproject.toml")
with render_cookie(backend="mesonpy", vcs=False) as mesonpy:
mesonpy_meson_build = mesonpy.joinpath("meson.build").read_text(encoding="utf-8").strip()
mesonpy_src_main_cpp = mesonpy.joinpath("src/main.cpp").read_text(encoding="utf-8").strip()
mesonpy_pyproject = TOMLMatcher.from_file(mesonpy / "pyproject.toml")
assert "tool.meson-python" not in mesonpy_pyproject
with render_cookie(backend="maturin", vcs=False) as maturin:
maturin_cargo_toml = maturin.joinpath("Cargo.toml").read_text(encoding="utf-8").strip()
maturin_src_lib_rs = maturin.joinpath("src/lib.rs").read_text(encoding="utf-8").strip()
maturin_pyproject = TOMLMatcher.from_file(maturin / "pyproject.toml")
]]] -->
<!-- [[[end]]] -->

# Packaging Compiled Projects

There are a variety of ways to package compiled projects. In the past, the only
Expand Down Expand Up @@ -52,27 +70,48 @@ selects the backend:

{% tabs %} {% tab skbc Scikit-build-core %}

<!-- [[[cog
with code_fence("toml"):
print(skbuild_pyproject.get_source("build-system"))
]]] -->
<!-- prettier-ignore-start -->
```toml
[build-system]
requires = ["scikit-build-core"]
requires = ["pybind11", "scikit-build-core>=0.11"]
build-backend = "scikit_build_core.build"
```
<!-- prettier-ignore-end -->
<!-- [[[end]]] -->

{% endtab %} {% tab meson Meson-python %}

<!-- [[[cog
with code_fence("toml"):
print(mesonpy_pyproject.get_source("build-system"))
]]] -->
<!-- prettier-ignore-start -->
```toml
[build-system]
requires = ["meson-python"]
requires = ["meson-python>=0.18", "pybind11"]
build-backend = "mesonpy"
```
<!-- prettier-ignore-end -->
<!-- [[[end]]] -->

{% endtab %} {% tab maturin Maturin %}

<!-- [[[cog
with code_fence("toml"):
print(maturin_pyproject.get_source("build-system"))
]]] -->
<!-- prettier-ignore-start -->
```toml
[build-system]
requires = ["maturin"]
requires = ["maturin>=1.9,<2"]
build-backend = "maturin"
```
<!-- prettier-ignore-end -->
<!-- [[[end]]] -->

{% endtab %} {% endtabs %}

Expand All @@ -83,20 +122,48 @@ build-backend = "maturin"
These tools all read the project table. They also have extra configuration
options in `tool.*` settings.

{% tabs %} {% tab skbc Scikit-build-core %}

<!-- [[[cog
from cog_helpers import code_fence, render_cookie
with render_cookie(backend="skbuild") as skbuild:
skbuild_cmakelists_txt = skbuild.joinpath("CMakeLists.txt").read_text(encoding="utf-8").strip()
skbuild_src_main_cpp = skbuild.joinpath("src/main.cpp").read_text(encoding="utf-8").strip()
with render_cookie(backend="mesonpy") as mesonpy:
mesonpy_meson_build = mesonpy.joinpath("meson.build").read_text(encoding="utf-8").strip()
mesonpy_src_main_cpp = mesonpy.joinpath("src/main.cpp").read_text(encoding="utf-8").strip()
with render_cookie(backend="maturin") as maturin:
maturin_cargo_toml = maturin.joinpath("Cargo.toml").read_text(encoding="utf-8").strip()
maturin_src_lib_rs = maturin.joinpath("src/lib.rs").read_text(encoding="utf-8").strip()
with code_fence("toml"):
print(skbuild_pyproject.get_source("tool.scikit-build"))
]]] -->
<!-- prettier-ignore-start -->
```toml
[tool.scikit-build]
minimum-version = "build-system.requires"
build-dir = "build/{wheel_tag}"
```
<!-- prettier-ignore-end -->
<!-- [[[end]]] -->

These options are not required, but can improve your experience.

{% endtab %} {% tab meson Meson-python %}

No `tool.meson-python` configuration required for this example.

{% endtab %} {% tab maturin Maturin %}

<!-- [[[cog
with code_fence("toml"):
print(maturin_pyproject.get_source("tool.maturin"))
]]] -->
<!-- prettier-ignore-start -->
```toml
[tool.maturin]
module-name = "package._core"
python-source = "src"
sdist-generator = "git" # default is cargo
```
<!-- prettier-ignore-end -->
<!-- [[[end]]] -->

Maturin assumes you follow Rust's package structure, so we need a little bit of
configuration here to follow the convention of the other tools here.

{% endtab %} {% endtabs %}

## Backend specific files

{% tabs %} {% tab skbc Scikit-build-core %}
Expand Down Expand Up @@ -180,18 +247,18 @@ with code_fence("toml"):
[package]
name = "package"
version = "0.1.0"
edition = "2018"
edition = "2021"

[lib]
name = "_core"
# "cdylib" is necessary to produce a shared library for Python to import from.
crate-type = ["cdylib"]

[dependencies]
rand = "0.8.3"
rand = "0.9.2"

[dependencies.pyo3]
version = "0.19.1"
version = "0.27.2"
# "extension-module" tells pyo3 we want to build an extension module (skips linking against libpython.so)
# "abi3-py310" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.10
features = ["extension-module", "abi3-py310"]
Expand All @@ -203,6 +270,11 @@ features = ["extension-module", "abi3-py310"]

## Example compiled file

This example will make a `_core` extension inside your package; this pattern
allows you to easily provide both Python files and compiled extensions, and
keeping the details of your compiled extension private. You can select whatever
name you wish, though, or even make your compiled extension a top level module.

{% tabs %} {% tab skbc Scikit-build-core %}

Example `src/main.cpp` file:
Expand Down Expand Up @@ -297,26 +369,29 @@ with code_fence("rs"):
```rs
use pyo3::prelude::*;

#[pyfunction]
fn add(x: i64, y: i64) -> i64 {
x + y
}

#[pyfunction]
fn subtract(x: i64, y: i64) -> i64 {
x - y
}

/// A Python module implemented in Rust. The name of this function must match
/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to
/// import the module.
#[pymodule]
fn _core(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(add, m)?)?;
m.add_function(wrap_pyfunction!(subtract, m)?)?;
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
mod _core {
use super::*;

#[pyfunction]
fn add(x: i64, y: i64) -> i64 {
x + y
}

#[pyfunction]
fn subtract(x: i64, y: i64) -> i64 {
x - y
}


Ok(())
#[pymodule_init]
fn pymodule_init(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
Ok(())
}
}
```
<!-- prettier-ignore-end -->
Expand All @@ -327,7 +402,8 @@ fn _core(_py: Python, m: &PyModule) -> PyResult<()> {
## Package structure

The recommendation (followed above) is to have source code in `/src`, and the
Python package files in `/src/<package>`.
Python package files in `/src/<package>`. The compiled files also can go in
`/src`.

## Versioning

Expand All @@ -343,25 +419,26 @@ though the defaults are reasonable.

Unlike pure Python, you'll need to build redistributable wheels for each
platform and supported Python version if you want to avoid compilation on the
user's system. See [the CI page on wheels][gha_wheels] for a suggested workflow.
user's system using cibuildwheel. See [the CI page on wheels][gha_wheels] for a
suggested workflow.

## Special considerations

### NumPy

Modern versions of NumPy (1.25+) allow you to target older versions when
building, which is _highly_ recommended, and this will become required in NumPy
2.0. Now you add:
building, which is _highly_ recommended, and this became required in NumPy 2.0.
Now you add:

```cpp
#define NPY_TARGET_VERSION NPY_1_22_API_VERSION
```

(Where that number is whatever version you support as a minimum) then make sure
you build with NumPy 1.25+ (or 2.0+ when it comes out). Before 1.25, it was
necessary to actually pin the oldest NumPy you supported (the
`oldest-supported-numpy` package is the easiest method). If you support Python <
3.9, you'll have to use the old method for those versions.
you build with NumPy 1.25+ (or 2.0+). Before 1.25, it was necessary to actually
pin the oldest NumPy you supported (the `oldest-supported-numpy` package is the
easiest method). If you support Python < 3.9, you'll have to use the old method
for those versions.

If using pybind11, you don't need NumPy at build-time in the first place.

Expand Down
8 changes: 8 additions & 0 deletions helpers/cog_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ def get_source(self, dotted_name: str, /) -> str:
)
return tomlkit.dumps(toml).strip()

def __contains__(self, dotted_name: str, /) -> bool:
names = dotted_name.split(".")
try:
functools.reduce(lambda d, k: d[k], names, self.toml)
except KeyError:
return False
return True


@contextlib.contextmanager
def code_fence(lang: str, /, *, width: int = 3) -> Generator[None, None, None]:
Expand Down
1 change: 0 additions & 1 deletion {{cookiecutter.project_name}}/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ sdist.include = ["src/{{ cookiecutter.__project_slug }}/_version.py"]

[tool.maturin]
module-name = "{{ cookiecutter.__project_slug }}._core"
python-packages = ["{{ cookiecutter.__project_slug }}"]
python-source = "src"
sdist-generator = "git" # default is cargo

Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
use pyo3::prelude::*;

#[pyfunction]
fn add(x: i64, y: i64) -> i64 {
x + y
}

#[pyfunction]
fn subtract(x: i64, y: i64) -> i64 {
x - y
}

/// A Python module implemented in Rust. The name of this function must match
/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to
/// import the module.
#[pymodule]
fn _core(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(add, m)?)?;
m.add_function(wrap_pyfunction!(subtract, m)?)?;
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
mod _core {
use super::*;

#[pyfunction]
fn add(x: i64, y: i64) -> i64 {
x + y
}

#[pyfunction]
fn subtract(x: i64, y: i64) -> i64 {
x - y
}


Ok(())
#[pymodule_init]
fn pymodule_init(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
Ok(())
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
[package]
name = "{{ cookiecutter.__project_slug }}"
version = "0.1.0"
edition = "2018"
edition = "2021"

[lib]
name = "_core"
# "cdylib" is necessary to produce a shared library for Python to import from.
crate-type = ["cdylib"]

[dependencies]
rand = "0.8.3"
rand = "0.9.2"

[dependencies.pyo3]
version = "0.19.1"
version = "0.27.2"
# "extension-module" tells pyo3 we want to build an extension module (skips linking against libpython.so)
# "abi3-py310" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.10
features = ["extension-module", "abi3-py310"]
Loading