Skip to content

Commit 6e1ddcf

Browse files
authored
feat(core-nanobind-shared): add nanobind + shared library sample (#76)
* feat(core-nanobind-shared): add nanobind + shared library sample Adds the example requested in scikit-build/scikit-build-core#1034: a nanobind extension module linked against a plain C++ shared library shipped inside the wheel, covering relative install destinations, $ORIGIN/@loader_path RPATH, and Windows DLL/import-library handling. Assisted-by: ClaudeCode:claude-fable-5 * ci: build and test core-nanobind-shared with cibuildwheel Generalizes the free-threading job into a package matrix so the shared-library wheel goes through auditwheel/delocate repair in CI. Assisted-by: ClaudeCode:claude-fable-5
1 parent ec74f25 commit 6e1ddcf

11 files changed

Lines changed: 148 additions & 6 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,20 @@ jobs:
4545
- uses: astral-sh/setup-uv@v8.0.0
4646
- run: uvx nox -s ${{ matrix.session }}
4747

48-
free-threading:
48+
cibuildwheel:
4949
runs-on: ${{ matrix.runs-on }}
5050
strategy:
5151
fail-fast: false
5252
matrix:
5353
runs-on: [ubuntu-latest, windows-latest, macos-14]
54-
name: cibw on ${{ matrix.runs-on }}
54+
package: [hello-free-threading, core-nanobind-shared]
55+
name: cibw ${{ matrix.package }} on ${{ matrix.runs-on }}
5556
steps:
5657
- uses: actions/checkout@v4
5758
- uses: astral-sh/setup-uv@v8.0.0
5859
- uses: pypa/cibuildwheel@v2.21
5960
with:
60-
package-dir: projects/hello-free-threading
61+
package-dir: projects/${{ matrix.package }}
6162
env:
6263
CIBW_PRERELEASE_PYTHONS: True
6364

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ A collection of small, self-contained example packages under `projects/`, each d
99
Projects fall into distinct families; know which one you're editing before changing build config:
1010

1111
- **Legacy `scikit-build`** (`build-backend = "setuptools.build_meta"`, has `setup.py`): `hello-pure`, `hello-cpp`, `hello-cython`, `hello-pybind11`, `pen2-cython`, `tower-of-babel`. Build requires include `scikit-build`, `cmake`, `ninja` in `[build-system].requires`. Metadata lives in `setup.py`.
12-
- **Modern `scikit-build-core`** (`build-backend = "scikit_build_core.build"`, pyproject-only, no `setup.py`): `core-c-hello`, `core-pybind11-hello`, `hello-free-threading`, `pi-fortran`, `hello-cmake-package`. Metadata lives in `[project]`; configure via `[tool.scikit-build]`.
12+
- **Modern `scikit-build-core`** (`build-backend = "scikit_build_core.build"`, pyproject-only, no `setup.py`): `core-c-hello`, `core-cython-hello`, `core-pybind11-hello`, `core-nanobind-shared`, `hello-free-threading`, `pi-fortran`, `hello-cmake-package`. Metadata lives in `[project]`; configure via `[tool.scikit-build]`.
1313
- **Hatchling + scikit-build-core plugin** (`build-backend = "hatchling.build"`): `hatchling-pybind11-hello`, using `[tool.hatch.build.targets.wheel.hooks.scikit-build]`.
1414

15-
Language bindings vary per project: pure Python, raw C/C++, pybind11, Cython, Fortran (`pi-fortran`). `tower-of-babel` is the broadest, exercising Boost + Cython static/shared linkage.
15+
Language bindings vary per project: pure Python, raw C/C++, pybind11, nanobind (`core-nanobind-shared`, which also demonstrates linking to a shared library shipped in the wheel), Cython, Fortran (`pi-fortran`). `tower-of-babel` is the broadest, exercising Boost + Cython static/shared linkage.
1616

1717
## Commands
1818

@@ -32,7 +32,7 @@ Development is driven by nox (`noxfile.py`). Use `uvx nox` (uv is the assumed ru
3232

3333
- `hello_list` (tested by `test`): the four `hello-*` setuptools projects, plus `hello-cmake-package` and `pi-fortran` (**non-Windows only**), plus `hello-free-threading` (**Python 3.13+ only**).
3434
- `long_hello_list` (built by `dist`): `hello_list` plus `pen2-cython`, `core-c-hello`, `core-pybind11-hello`, `hatchling-pybind11-hello`.
35-
- `hello-free-threading` is wheel-built separately via cibuildwheel in CI (`free-threading` job), not through nox.
35+
- `hello-free-threading` and `core-nanobind-shared` are also wheel-built and tested via cibuildwheel in CI (`cibuildwheel` job); `hello-free-threading` only through that job, not nox.
3636

3737
A new project is invisible to CI until added to the appropriate list here.
3838

noxfile.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"hello-pybind11",
1212
"hello-cython",
1313
"core-cython-hello",
14+
"core-nanobind-shared",
1415
"hello-cmake-package",
1516
"pi-fortran",
1617
]
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
cmake_minimum_required(VERSION 3.15...3.30)
2+
3+
project(${SKBUILD_PROJECT_NAME} VERSION ${SKBUILD_PROJECT_VERSION} LANGUAGES CXX)
4+
5+
find_package(
6+
Python
7+
COMPONENTS Interpreter Development.Module
8+
REQUIRED)
9+
find_package(nanobind CONFIG REQUIRED)
10+
11+
# An ordinary C++ shared library that knows nothing about Python. In a real
12+
# project this could be a pre-existing library or an imported target.
13+
add_library(say_hello SHARED cpp/hello.cpp)
14+
target_include_directories(say_hello PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/cpp")
15+
# MSVC exports nothing by default; real projects usually use
16+
# generate_export_header() and explicit __declspec macros instead.
17+
set_target_properties(say_hello PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
18+
19+
# The Python extension module, linked to the shared library like any other
20+
# CMake consumer.
21+
nanobind_add_module(_hello src/bindings.cpp)
22+
target_link_libraries(_hello PRIVATE say_hello)
23+
24+
# Both binaries are installed side by side into the "hello" package, so the
25+
# extension must look next to itself for the library at import time. Windows
26+
# has no RPATH; there the DLL is found because it sits next to the .pyd.
27+
if(APPLE)
28+
set_target_properties(_hello PROPERTIES INSTALL_RPATH "@loader_path")
29+
elseif(UNIX)
30+
set_target_properties(_hello PROPERTIES INSTALL_RPATH "$ORIGIN")
31+
endif()
32+
33+
# Destinations are relative to the root of the wheel (platlib). LIBRARY covers
34+
# the extension and the .so/.dylib, RUNTIME covers the .dll on Windows, and
35+
# the MSVC import library (ARCHIVE) is a build-time artifact that doesn't
36+
# belong in the wheel.
37+
install(
38+
TARGETS _hello say_hello
39+
LIBRARY DESTINATION hello
40+
RUNTIME DESTINATION hello
41+
ARCHIVE DESTINATION hello EXCLUDE_FROM_ALL)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# core-nanobind-shared
2+
3+
A minimal [scikit-build-core](https://scikit-build-core.readthedocs.io) +
4+
[nanobind](https://nanobind.readthedocs.io) project where the extension module
5+
links against a plain C++ **shared** library that ships inside the wheel.
6+
Requested in
7+
[scikit-build-core#1034](https://github.com/scikit-build/scikit-build-core/issues/1034).
8+
9+
## Layout
10+
11+
- `cpp/`: an ordinary C++ shared library (`say_hello`) with no Python
12+
knowledge. Stand-in for any existing `add_library(... SHARED ...)` target or
13+
third-party library you build yourself.
14+
- `src/bindings.cpp`: the nanobind module `_hello`, which just links to the
15+
library with `target_link_libraries`.
16+
- `src/hello/`: the pure Python package; scikit-build-core picks it up
17+
automatically because it matches the project name.
18+
19+
## The parts that trip people up
20+
21+
- **Install with relative destinations.** `install(TARGETS ... DESTINATION
22+
hello)` puts both binaries inside the `hello` package in the wheel. Never
23+
use `${CMAKE_INSTALL_PREFIX}` in destinations; scikit-build-core installs
24+
into a staging directory that becomes the wheel's platlib root.
25+
- **RPATH.** At import time the extension must find the shared library next to
26+
itself, so its `INSTALL_RPATH` is set to `$ORIGIN` (Linux) or `@loader_path`
27+
(macOS). On macOS this works together with the default `@rpath/...` install
28+
name CMake gives shared libraries.
29+
- **Windows.** There is no RPATH; the DLL is found because CPython loads
30+
extension modules with their own directory on the DLL search path, so
31+
installing the DLL next to the `.pyd` is enough (`RUNTIME DESTINATION`).
32+
The MSVC import library (`.lib`, the `ARCHIVE` artifact) is only needed at
33+
build time and is excluded from the wheel.
34+
- **No `SOVERSION`/`VERSION` on the library.** Versioned shared libraries are
35+
installed as symlinks, but wheels cannot contain symlinks — you would ship
36+
multiple full copies of the library.
37+
38+
If you link against libraries you do *not* ship in the wheel, this RPATH setup
39+
does not apply; use `auditwheel` (Linux) / `delocate` (macOS) /
40+
`delvewheel` (Windows) to vendor them into repaired wheels instead.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#include "hello.hpp"
2+
3+
int add(int a, int b) { return a + b; }
4+
5+
std::string greet(const std::string &name) { return "Hello, " + name + "!"; }
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#pragma once
2+
3+
#include <string>
4+
5+
int add(int a, int b);
6+
std::string greet(const std::string &name);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[build-system]
2+
requires = ["scikit-build-core", "nanobind"]
3+
build-backend = "scikit_build_core.build"
4+
5+
[project]
6+
name = "hello"
7+
version = "1.2.3"
8+
authors = [
9+
{ name = "The scikit-build team" },
10+
]
11+
description = "a minimal example package (nanobind + shared library version)"
12+
requires-python = ">=3.9"
13+
classifiers = [
14+
"License :: OSI Approved :: MIT License",
15+
]
16+
17+
# One version is enough here; the point is exercising the wheel-repair step
18+
# (auditwheel/delocate) over the shipped shared library.
19+
[tool.cibuildwheel]
20+
build-frontend = "build[uv]"
21+
build = "cp312-*"
22+
archs = ["auto64"]
23+
test-command = "pytest {package}"
24+
test-requires = ["pytest"]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#include <nanobind/nanobind.h>
2+
#include <nanobind/stl/string.h>
3+
4+
#include "hello.hpp"
5+
6+
namespace nb = nanobind;
7+
8+
NB_MODULE(_hello, m) {
9+
m.doc() = "nanobind bindings for the say_hello shared library";
10+
m.def("add", &add, nb::arg("a"), nb::arg("b"), "Add two integers");
11+
m.def("greet", &greet, nb::arg("name"), "Greet somebody by name");
12+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from ._hello import add, greet
2+
3+
__all__ = ["add", "greet"]

0 commit comments

Comments
 (0)