File: packaging.rst

package info (click to toggle)
nanobind 2.9.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,060 kB
  • sloc: cpp: 11,838; python: 5,862; ansic: 4,820; makefile: 22; sh: 15
file content (332 lines) | stat: -rw-r--r-- 12,130 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
.. _packaging:

Packaging
=========

A Python *wheel* is a self-contained binary file that bundles Python code and
extension libraries along with metadata such as versioned package dependencies.
Wheels are easy to download and install, and they are the recommended mechanism
for distributing extensions created using nanobind.

This section walks through the recommended sequence of steps to build wheels
and optionally automate this process to simultaneously target many platforms
(Linux, Windows, macOS) and processors (i386, x86_64, arm64) using the `GitHub
Actions <https://github.com/features/actions>`__ CI service.

Note that all of the recommended practices have already been implemented in the
`nanobind_example repository <https://github.com/wjakob/nanobind_example>`_,
which is a minimal C++ project with nanobind-based bindings. You may therefore
prefer to clone this repository and modify its contents.

Step 1: Overview
----------------

The example project has a simple directory structure:

.. code-block:: text

   ├── README.md
   ├── CMakeLists.txt
   ├── pyproject.toml
   └── src/
       ├── my_ext.cpp
       └── my_ext/
           └── __init__.py

The file ``CMakeLists.txt`` contains the C++-specifics part of the build
system, while ``pyproject.toml`` explains how to turn the example into a wheel.
The file ``README.md`` should ideally explain how to use the project in more
detail. Its contents are arbitrary, but the file must exist for the following
build system to work.

All source code is located in a ``src`` directory containing the Python package
as a subdirectory.

Compilation will turn ``my_ext.cpp`` into a shared library in the package
directory, which has an underscored platform-dependent name (e.g.,
``_my_ext_impl.cpython-311-darwin.so``) to indicate that it is an
implementation detail. The ``src/my_ext/__init__.py`` imports the extension and
exposes relevant functionality. In this small example project, it only contains
a single line:

.. code-block:: python

   from ._my_ext_impl import hello

The file ``src/my_ext.cpp`` contains minimal bindings for an example function:

.. code-block:: cpp

   #include <nanobind/nanobind.h>

   NB_MODULE(_my_ext_impl, m) {
       m.def("hello", []() { return "Hello world!"; });
   }

The next two steps will set up the infrastructure needed for wheel generation.

Step 2: Specify build dependencies and metadata
-----------------------------------------------

In the root directory of the project, create a file named ``pyproject.toml``
listing *build-time dependencies*. Note that runtime dependencies *do not* need
to be added here. The following core dependencies are required by nanobind:

.. code-block:: toml

   [build-system]
   requires = ["scikit-build-core >=0.4.3", "nanobind >=1.3.2"]
   build-backend = "scikit_build_core.build"

You may need to increase the minimum nanobind version in the above snippet if
you are using features from versions newer than 1.3.2.

Just below the list of build-time requirements, specify project metadata including:

- The project's name (which must be a valid package name)
- The version number
- A brief (1-line) description of the project
- The name of a more detailed README file
- The list of authors with email addresses.
- The software license
- The project web page
- Runtime dependencies, if applicable

An example is shown below:

.. code-block:: toml

   [project]
   name = "my_ext"
   version = "0.0.1"
   description = "A brief description of what this project does"
   readme = "README.md"
   requires-python = ">=3.8"
   authors = [
       { name = "Your Name", email = "your.email@address.com" },
   ]
   classifiers = [
       "License :: BSD",
   ]
   # Optional: runtime dependency specification
   # dependencies = [ "cryptography >=41.0" ]

   [project.urls]
   Homepage = "https://github.com/your/project"

We will use `scikit-build-core
<https://github.com/scikit-build/scikit-build-core>`__ to build wheels, and
this tool also has its own configuration block in ``pyproject.toml``. The
following defaults are recommended:

.. code-block:: toml

   [tool.scikit-build]
   # Protect the configuration against future changes in scikit-build-core
   minimum-version = "0.4"

   # Setuptools-style build caching in a local directory
   build-dir = "build/{wheel_tag}"

   # Build stable ABI wheels for CPython 3.12+
   wheel.py-api = "cp312"

Step 3: Set up a CMake build system
-----------------------------------

Next, we will set up a suitable ``CMakeLists.txt`` file in the root directory.
Since this build system is designed to be invoked through
``scikit-build-core``, it does not make sense to perform a standalone CMake
build. The message at the top warns users attempting to do this.

.. code-block:: cmake

   # Set the minimum CMake version and policies for highest tested version
   cmake_minimum_required(VERSION 3.15...3.27)

   # Set up the project and ensure there is a working C++ compiler
   project(my_ext LANGUAGES CXX)

   # Warn if the user invokes CMake directly
   if (NOT SKBUILD)
     message(WARNING "\
     This CMake file is meant to be executed using 'scikit-build-core'.
     Running it directly will almost certainly not produce the desired
     result. If you are a user trying to install this package, use the
     command below, which will install all necessary build dependencies,
     compile the package in an isolated environment, and then install it.
     =====================================================================
      $ pip install .
     =====================================================================
     If you are a software developer, and this is your own package, then
     it is usually much more efficient to install the build dependencies
     in your environment once and use the following command that avoids
     a costly creation of a new virtual environment at every compilation:
     =====================================================================
      $ pip install nanobind scikit-build-core[pyproject]
      $ pip install --no-build-isolation -ve .
     =====================================================================
     You may optionally add -Ceditable.rebuild=true to auto-rebuild when
     the package is imported. Otherwise, you need to rerun the above
     after editing C++ files.")
   endif()

Next, import Python and nanobind including the ``Development.SABIModule``
component that can be used to create `stable ABI
<https://docs.python.org/3/c-api/stable.html>`__ builds.

.. code-block:: cmake

   # Try to import all Python components potentially needed by nanobind
   find_package(Python 3.8
     REQUIRED COMPONENTS Interpreter Development.Module
     OPTIONAL_COMPONENTS Development.SABIModule)

   # Import nanobind through CMake's find_package mechanism
   find_package(nanobind CONFIG REQUIRED)

The last two steps build and install the actual extension

.. code-block:: cmake

    # We are now ready to compile the actual extension module
    nanobind_add_module(
      # Name of the extension
      _my_ext_impl

      # Target the stable ABI for Python 3.12+, which reduces
      # the number of binary wheels that must be built. This
      # does nothing on older Python versions
      STABLE_ABI

      # Source code goes here
      src/my_ext.cpp
    )

    # Install directive for scikit-build-core
    install(TARGETS _my_ext_impl LIBRARY DESTINATION my_ext)


Step 4: Install the package locally
-----------------------------------

To install the package, run

.. code-block:: bash

   $ cd <project-directory>
   $ pip install .

``pip`` will parse the ``pyproject.toml`` file and create a fresh environment
containing all needed dependencies. Following this, you should be able to
install and access the extension.

.. code-block:: python

   >>> import my_ext
   >>> my_ext.hello()
   'Hello world!'

Alternatively, you can use the following command to generate a ``.whl`` file
instead of installing the package.

.. code-block:: bash

   $ pip wheel .

Step 5: Incremental rebuilds
----------------------------

The ``pip install`` and ``pip wheel`` commands are extremely conservative to
ensure reproducible builds. They create a pristine virtual environment and
install build-time dependencies before compiling the extension *from scratch*.

It can be frustrating to wait for this lengthy sequence of steps after every
small change to a source file during the active development phase of a project.
To avoid this, first install the project's build dependencies, e.g.:

.. code-block:: bash

   $ pip install nanobind scikit-build-core[pyproject]

Next, install the project without build isolation to enable incremental builds:

.. code-block:: bash

   $ pip install --no-build-isolation -ve .

This command will need to be run after every change to reinstall the updated package.
For an even more interactive experience, use

.. code-block:: bash

   $ pip install --no-build-isolation -Ceditable.rebuild=true -ve .

This will automatically rebuild any code (if needed) whenever the ``my_ext``
package is imported into a Python session.

Step 6: Build wheels in the cloud
---------------------------------

On my machine, the ``pip wheel`` command produces a file named
``my_ext-0.0.1-cp311-cp311-macosx_13_0_arm64.whl`` that is specific to Python
3.11 running on an arm64 macOS machine. Other Python versions and operating
systems each require their own wheels, which leads to a dauntingly large build
matrix (though nanobind's stable ABI support will help to significantly reduce
the size of this matrix once Python 3.12 is more widespread). 

Rather than building these wheels manually on different machines, it is far
more efficient to use GitHub actions along with the powerful `cibuildwheel
<https://cibuildwheel.readthedocs.io/en/stable/>`__ package to fully automate
the process.

To do so, create a file named ``.github/workflows/wheels.yml`` containing
the contents of the `following file
<https://github.com/wjakob/nanobind_example/blob/master/.github/workflows/wheels.yml>`__.
You may want to remove the ``on: push:`` lines, otherwise, the action will run
after every commit, which is perhaps a bit excessive. In this case, you can
still trigger the action manually on the *Actions* tab of the GitHub project
page.

Furthermore, append the following ``cibuildwheel``-specific configuration to
``pyproject.toml``:

.. code-block:: toml

    [tool.cibuildwheel]
    # Necessary to see build output from the actual compilation
    build-verbosity = 1

    # Optional: run pytest to ensure that the package was correctly built
    # test-command = "pytest {project}/tests"
    # test-requires = "pytest"

    # Needed for full C++17 support on macOS
    [tool.cibuildwheel.macos.environment]
    MACOSX_DEPLOYMENT_TARGET = "10.14"

Following each run, the action provides a downloadable *build artifact*, which
is a ZIP file containing all the individual wheel files for each platform.

By default, ``cibuildwheel`` will launch a very large build matrix, and it is
possible that your extension is not compatible with every single configuration.
For example, suppose that the project depends on Python 3.9+ and a 64 bit
processor. In this case, add further entries to the ``[tool.cibuildwheel]``
block to remove incompatible configurations from the matrix:

.. code-block:: toml

    skip = ["cp38-*", "pp38-*"] # Skip CPython and PyPy 3.8
    archs = ["auto64"]          # Only target 64 bit architectures

The `cibuildwheel documentation
<https://cibuildwheel.readthedocs.io/en/stable/options/>`__ explains the
possible options.

If you set up a GitHub actions `secret
<https://docs.github.com/en/actions/security-guides/encrypted-secrets>`__ named
``pypi_password`` containing a PyPI authentication token, the action will
automatically upload the generated wheels to the `Python Package Index (PyPI)
<https://pypi.org>`__ when the action is triggered by a `software release event
<https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository>`__.