Skip to content

Commit 7f9b79a

Browse files
committed
doc: add documentation for using extension modules
1 parent c70cbf4 commit 7f9b79a

File tree

1 file changed

+317
-0
lines changed

1 file changed

+317
-0
lines changed

docs/building-extension-modules.md

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
---
2+
title: "Building extension modules"
3+
draft: false
4+
type: docs
5+
layout: single
6+
7+
menu:
8+
docs:
9+
weight: 125
10+
---
11+
12+
# Building Extension Modules
13+
14+
{{% warning %}}
15+
While this feature has been around since almost the beginning of the Poetry project and has needed minimal changes,
16+
it is still considered unstable. You can participate in the discussions about stabilizing this feature
17+
[here](https://github.com/python-poetry/poetry/issues/2740).
18+
19+
And as always, your contributions towards the goal of improving this feature are also welcome.
20+
{{% /warning %}}
21+
22+
Poetry allows a project developer to introduce support for, build and distribute native extensions within their project.
23+
In order to achieve this, at the highest level, the following steps are required.
24+
25+
{{< steps >}}
26+
{{< step >}}
27+
**Add Build Dependencies**
28+
29+
The build dependencies, in this context, refer to those Python packages that required in order to successfully execute
30+
your build script. Common examples include `cython`, `meson`, `maturin`, `setuptools` etc., depending on how your
31+
extension is built.
32+
33+
{{% note %}}
34+
You must assume that only python built-ins are available by default in a build environment. This means, if you use need
35+
even packages like `setuptools`, it must be explicitly declared.
36+
{{% /note %}}
37+
38+
The necessary build dependencies must be added to the `build-system.requires` section of your `pyproject.toml` file.
39+
40+
```toml
41+
[build-system]
42+
requires = ["poetry-core", "setuptools", "cython"]
43+
build-backend = "poetry.core.masonry.api"
44+
```
45+
46+
{{% note %}}
47+
If you wish to develop the build script within your project's virtual environment, then you must also add the
48+
dependencies to your project explicitly to a dependency group - the name of which is not important.
49+
50+
```sh
51+
poetry add --group=build setuptools cython
52+
```
53+
{{% /note %}}
54+
55+
{{< /step >}}
56+
57+
{{< step >}}
58+
**Add Build Script**
59+
60+
The build script can be free-form Python script that uses any dependency specified in the previous step. This can be
61+
named as needed, but **must** be located in the project root and also **must** be included in your source distribution.
62+
You can see the [example snippets section]({{< relref "#example-snippets" >}}) for inspiration.
63+
64+
```toml
65+
[tool.poetry.build]
66+
script = "build-extension.py"
67+
```
68+
69+
{{% note %}}
70+
The name of the build script is arbitrary. Common practice has been to name it `build.py`, however this could have
71+
undesired consequences. It is also recommended that the script be, if feasible placed inside a subdirectory like
72+
`contrib` or `src`.
73+
{{% /note %}}
74+
75+
{{< /step >}}
76+
77+
{{< step >}}
78+
**Specify Distribution Files**
79+
80+
{{% warning %}}
81+
The following is an example, and should not be considered as complete.
82+
{{% /warning %}}
83+
84+
```toml
85+
packages = [
86+
{ include = "package", from = "src" }
87+
]
88+
include = [
89+
{ path = "src/package/**/*.so", format = "wheel" },
90+
{ path = "build.py", format = "sdist" }
91+
]
92+
exclude = [
93+
{ path = "build.py", format = "wheel" },
94+
{ path = "src/package/**/*.c", format = "wheel" }
95+
]
96+
```
97+
98+
The key takeaway here should be the following. You can refer to the [`pyproject.toml`]({{< relref "pyproject#exclude-and-include" >}})
99+
documentation for information on each of the relevant sections.
100+
101+
1. Include your build outputs in your wheel.
102+
2. Exclude your build inputs from your wheel.
103+
3. Include your build inputs to your source distribution.
104+
105+
{{< /step >}}
106+
107+
{{< /steps >}}
108+
109+
## Example Snippets
110+
111+
### Cython
112+
113+
{{< tabs tabTotal="2" tabID1="cython-pyproject" tabName1="pyproject.toml" tabID2="cython-build-script" tabName2="build-extension.py">}}
114+
115+
{{< tab tabID="cython-pyproject" >}}
116+
117+
```toml
118+
[tool.poetry.build]
119+
script = "build-extension.py"
120+
121+
[build-system]
122+
requires = ["poetry-core", "cython"]
123+
build-backend = "poetry.core.masonry.api"
124+
```
125+
126+
{{< /tab >}}
127+
128+
{{< tab tabID="cython-build-script" >}}
129+
130+
```py
131+
from __future__ import annotations
132+
133+
import os
134+
import shutil
135+
136+
from Cython.Build import cythonize
137+
from setuptools import Distribution
138+
from setuptools import Extension
139+
from setuptools.command.build_ext import build_ext
140+
141+
COMPILE_ARGS = ["-march=native", "-O3", "-msse", "-msse2", "-mfma", "-mfpmath=sse"]
142+
LINK_ARGS = []
143+
INCLUDE_DIRS = []
144+
LIBRARIES = ["m"]
145+
146+
147+
def build():
148+
extensions = [
149+
Extension(
150+
"*",
151+
["src/package/*.pyx"],
152+
extra_compile_args=COMPILE_ARGS,
153+
extra_link_args=LINK_ARGS,
154+
include_dirs=INCLUDE_DIRS,
155+
libraries=LIBRARIES,
156+
)
157+
]
158+
ext_modules = cythonize(
159+
extensions,
160+
include_path=INCLUDE_DIRS,
161+
compiler_directives={"binding": True, "language_level": 3},
162+
)
163+
164+
distribution = Distribution({
165+
"name": "extended",
166+
"ext_modules": ext_modules
167+
})
168+
169+
cmd = build_ext(distribution)
170+
cmd.ensure_finalized()
171+
cmd.run()
172+
173+
# Copy built extensions back to the project
174+
for output in cmd.get_outputs():
175+
relative_extension = os.path.relpath(output, cmd.build_lib)
176+
shutil.copyfile(output, relative_extension)
177+
mode = os.stat(relative_extension).st_mode
178+
mode |= (mode & 0o444) >> 2
179+
os.chmod(relative_extension, mode)
180+
181+
182+
if __name__ == "__main__":
183+
build()
184+
```
185+
186+
{{< /tab >}}
187+
188+
{{< /tabs >}}
189+
190+
### Meson
191+
192+
{{< tabs tabTotal="2" tabID1="meson-pyproject" tabName1="pyproject.toml" tabID2="meson-build-script" tabName2="build-extension.py">}}
193+
194+
{{< tab tabID="meson-pyproject" >}}
195+
196+
```toml
197+
[tool.poetry.build]
198+
script = "build-extension.py"
199+
200+
[build-system]
201+
requires = ["poetry-core", "meson"]
202+
build-backend = "poetry.core.masonry.api"
203+
```
204+
205+
{{< /tab >}}
206+
207+
{{< tab tabID="meson-build-script" >}}
208+
209+
```py
210+
from __future__ import annotations
211+
212+
import subprocess
213+
214+
from pathlib import Path
215+
216+
217+
def meson(*args):
218+
subprocess.call(["meson", *args])
219+
220+
221+
def build():
222+
build_dir = Path(__file__).parent.joinpath("build")
223+
build_dir.mkdir(parents=True, exist_ok=True)
224+
225+
meson("setup", build_dir.as_posix())
226+
meson("compile", "-C", build_dir.as_posix())
227+
meson("install", "-C", build_dir.as_posix())
228+
229+
230+
if __name__ == "__main__":
231+
build()
232+
```
233+
234+
{{< /tab >}}
235+
236+
{{< /tabs >}}
237+
238+
### Maturin
239+
240+
{{< tabs tabTotal="2" tabID1="maturin-pyproject" tabName1="pyproject.toml" tabID2="maturin-build-script" tabName2="build-extension.py">}}
241+
242+
{{< tab tabID="maturin-pyproject" >}}
243+
244+
```toml
245+
[tool.poetry.build]
246+
script = "build-extension.py"
247+
248+
[build-system]
249+
requires = ["poetry-core", "maturin"]
250+
build-backend = "poetry.core.masonry.api"
251+
```
252+
253+
{{< /tab >}}
254+
255+
{{< tab tabID="maturin-build-script" >}}
256+
257+
```py
258+
import os
259+
import shlex
260+
import shutil
261+
import subprocess
262+
import zipfile
263+
264+
from pathlib import Path
265+
266+
267+
def maturin(*args):
268+
subprocess.call(["maturin", *list(args)])
269+
270+
271+
def build():
272+
build_dir = Path(__file__).parent.joinpath("build")
273+
build_dir.mkdir(parents=True, exist_ok=True)
274+
275+
wheels_dir = Path(__file__).parent.joinpath("target/wheels")
276+
if wheels_dir.exists():
277+
shutil.rmtree(wheels_dir)
278+
279+
cargo_args = []
280+
if os.getenv("MATURIN_BUILD_ARGS"):
281+
cargo_args = shlex.split(os.getenv("MATURIN_BUILD_ARGS", ""))
282+
283+
maturin("build", "-r", *cargo_args)
284+
285+
# We won't use the wheel built by maturin directly since
286+
# we want Poetry to build it but, we need to retrieve the
287+
# compiled extensions from the maturin wheel.
288+
wheel = next(iter(wheels_dir.glob("*.whl")))
289+
with zipfile.ZipFile(wheel.as_posix()) as whl:
290+
whl.extractall(wheels_dir.as_posix())
291+
292+
for extension in wheels_dir.rglob("**/*.so"):
293+
shutil.copyfile(extension, Path(__file__).parent.joinpath(extension.name))
294+
295+
shutil.rmtree(wheels_dir)
296+
297+
298+
if __name__ == "__main__":
299+
build()
300+
```
301+
302+
{{< /tab >}}
303+
304+
{{< /tabs >}}
305+
306+
## FAQ
307+
### When is my build script executed?
308+
If your project uses a build script, it is run implicitly in the following scenarios.
309+
310+
1. When `poetry install` is run, it is executed prior to installing the project's root package.
311+
2. When `poetry build` is run, it is executed prior to building distributions.
312+
3. When a PEP 517 build is triggered from source or sdist by another build frontend.
313+
314+
### How does Poetry ensure my build script's dependencies are met?
315+
Prior to executing the build script, Poetry creates a temporary virtual environment with your project's active Python
316+
version and then installs all dependencies specified under `build-system.requires` into this environment. It should be
317+
noted that no packages will be present in this environment at the time of creation.

0 commit comments

Comments
 (0)