Skip to content

Commit 6864718

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

File tree

1 file changed

+299
-0
lines changed

1 file changed

+299
-0
lines changed

docs/building-extension-modules.md

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
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 `cpython`, `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+
63+
```toml
64+
[tool.poetry.build]
65+
script = "build-extension.py"
66+
```
67+
68+
{{% note %}}
69+
The name of the build script is arbitrary. Common practice has been to name it `build.py`, however this could have
70+
undesired consequences. It is also recommended that the script be, if feasible placed inside a subdirectory like
71+
`contrib` or `src`.
72+
{{% /note %}}
73+
74+
{{< /step >}}
75+
76+
{{< step >}}
77+
**Specify Distribution Files**
78+
79+
{{% warning %}}
80+
The following is an example, and should not be considered as complete.
81+
{{% /warning %}}
82+
83+
```toml
84+
packages = [
85+
{ include = "package", from = "src" }
86+
]
87+
include = [
88+
{ path = "src/package/**/*.so", format = "wheel" },
89+
{ path = "build.py", format = "sdist" }
90+
]
91+
exclude = [
92+
{ path = "build.py", format = "wheel" },
93+
{ path = "src/package/**/*.c", format = "wheel" }
94+
]
95+
```
96+
97+
The key takeaway here should be the following. You can refer to the [`pyproject.toml`]({{< relref "pyproject#exclude-and-include" >}})
98+
documentation for information on each of the relevant sections.
99+
100+
1. Include your build outputs in your wheel.
101+
2. Exclude your build inputs from your wheel.
102+
3. Include your build inputs to your source distribution.
103+
104+
{{< /step >}}
105+
106+
{{< /steps >}}
107+
108+
# Example Snippets
109+
110+
## Cython
111+
112+
{{< tabs tabTotal="2" tabID1="cython-pyproject" tabName1="pyproject.toml" tabID2="cython-build-script" tabName2="build-extension.py">}}
113+
114+
{{< tab tabID="cython-pyproject" >}}
115+
116+
```toml
117+
[tool.poetry.build]
118+
script = "build-extension.py"
119+
```
120+
121+
{{< /tab >}}
122+
123+
{{< tab tabID="cython-build-script" >}}
124+
125+
```py
126+
from __future__ import annotations
127+
128+
import os
129+
import shutil
130+
131+
from Cython.Build import cythonize
132+
from setuptools import Distribution
133+
from setuptools import Extension
134+
from setuptools.command.build_ext import build_ext
135+
136+
COMPILE_ARGS = ["-march=native", "-O3", "-msse", "-msse2", "-mfma", "-mfpmath=sse"]
137+
LINK_ARGS = []
138+
INCLUDE_DIRS = []
139+
LIBRARIES = ["m"]
140+
141+
142+
def build():
143+
extensions = [
144+
Extension(
145+
"*",
146+
["src/package/*.pyx"],
147+
extra_compile_args=COMPILE_ARGS,
148+
extra_link_args=LINK_ARGS,
149+
include_dirs=INCLUDE_DIRS,
150+
libraries=LIBRARIES,
151+
)
152+
]
153+
ext_modules = cythonize(
154+
extensions,
155+
include_path=INCLUDE_DIRS,
156+
compiler_directives={"binding": True, "language_level": 3},
157+
)
158+
159+
distribution = Distribution({
160+
"name": "extended",
161+
"ext_modules": ext_modules
162+
})
163+
164+
cmd = build_ext(distribution)
165+
cmd.ensure_finalized()
166+
cmd.run()
167+
168+
# Copy built extensions back to the project
169+
for output in cmd.get_outputs():
170+
relative_extension = os.path.relpath(output, cmd.build_lib)
171+
shutil.copyfile(output, relative_extension)
172+
mode = os.stat(relative_extension).st_mode
173+
mode |= (mode & 0o444) >> 2
174+
os.chmod(relative_extension, mode)
175+
176+
177+
if __name__ == "__main__":
178+
build()
179+
```
180+
181+
{{< /tab >}}
182+
183+
{{< /tabs >}}
184+
185+
## Meson
186+
187+
{{< tabs tabTotal="2" tabID1="meson-pyproject" tabName1="pyproject.toml" tabID2="meson-build-script" tabName2="build-extension.py">}}
188+
189+
{{< tab tabID="meson-pyproject" >}}
190+
191+
```toml
192+
[tool.poetry.build]
193+
script = "build-extension.py"
194+
195+
[build-system]
196+
requires = ["poetry-core", "meson"]
197+
build-backend = "poetry.core.masonry.api"
198+
```
199+
200+
{{< /tab >}}
201+
202+
{{< tab tabID="meson-build-script" >}}
203+
204+
```py
205+
from __future__ import annotations
206+
207+
import subprocess
208+
209+
from pathlib import Path
210+
211+
212+
def meson(*args):
213+
subprocess.call(["meson", *args])
214+
215+
216+
def build():
217+
build_dir = Path(__file__).parent.joinpath("build")
218+
build_dir.mkdir(parents=True, exist_ok=True)
219+
220+
meson("setup", build_dir.as_posix())
221+
meson("compile", "-C", build_dir.as_posix())
222+
meson("install", "-C", build_dir.as_posix())
223+
224+
225+
if __name__ == "__main__":
226+
build()
227+
```
228+
229+
{{< /tab >}}
230+
231+
{{< /tabs >}}
232+
233+
## Maturin
234+
235+
{{< tabs tabTotal="2" tabID1="maturin-pyproject" tabName1="pyproject.toml" tabID2="maturin-build-script" tabName2="build-extension.py">}}
236+
237+
{{< tab tabID="maturin-pyproject" >}}
238+
239+
```toml
240+
[tool.poetry.build]
241+
script = "build-extension.py"
242+
243+
[build-system]
244+
requires = ["poetry-core", "maturin"]
245+
build-backend = "poetry.core.masonry.api"
246+
```
247+
248+
{{< /tab >}}
249+
250+
{{< tab tabID="maturin-build-script" >}}
251+
252+
```py
253+
import os
254+
import shlex
255+
import shutil
256+
import subprocess
257+
import zipfile
258+
259+
from pathlib import Path
260+
261+
262+
def maturin(*args):
263+
subprocess.call(["maturin", *list(args)])
264+
265+
266+
def build():
267+
build_dir = Path(__file__).parent.joinpath("build")
268+
build_dir.mkdir(parents=True, exist_ok=True)
269+
270+
wheels_dir = Path(__file__).parent.joinpath("target/wheels")
271+
if wheels_dir.exists():
272+
shutil.rmtree(wheels_dir)
273+
274+
cargo_args = []
275+
if os.getenv("MATURIN_BUILD_ARGS"):
276+
cargo_args = shlex.split(os.getenv("MATURIN_BUILD_ARGS", ""))
277+
278+
maturin("build", "-r", *cargo_args)
279+
280+
# We won't use the wheel built by maturin directly since
281+
# we want Poetry to build it but, we need to retrieve the
282+
# compiled extensions from the maturin wheel.
283+
wheel = next(iter(wheels_dir.glob("*.whl")))
284+
with zipfile.ZipFile(wheel.as_posix()) as whl:
285+
whl.extractall(wheels_dir.as_posix())
286+
287+
for extension in wheels_dir.rglob("**/*.so"):
288+
shutil.copyfile(extension, Path(__file__).parent.joinpath(extension.name))
289+
290+
shutil.rmtree(wheels_dir)
291+
292+
293+
if __name__ == "__main__":
294+
build()
295+
```
296+
297+
{{< /tab >}}
298+
299+
{{< /tabs >}}

0 commit comments

Comments
 (0)