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
7 changes: 4 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,20 @@ jobs:
- uses: astral-sh/setup-uv@v8.0.0
- run: uvx nox -s ${{ matrix.session }}

free-threading:
cibuildwheel:
runs-on: ${{ matrix.runs-on }}
strategy:
fail-fast: false
matrix:
runs-on: [ubuntu-latest, windows-latest, macos-14]
name: cibw on ${{ matrix.runs-on }}
package: [hello-free-threading, core-nanobind-shared]
name: cibw ${{ matrix.package }} on ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v8.0.0
- uses: pypa/cibuildwheel@v2.21
with:
package-dir: projects/hello-free-threading
package-dir: projects/${{ matrix.package }}
env:
CIBW_PRERELEASE_PYTHONS: True

Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ A collection of small, self-contained example packages under `projects/`, each d
Projects fall into distinct families; know which one you're editing before changing build config:

- **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`.
- **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]`.
- **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]`.
- **Hatchling + scikit-build-core plugin** (`build-backend = "hatchling.build"`): `hatchling-pybind11-hello`, using `[tool.hatch.build.targets.wheel.hooks.scikit-build]`.

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.
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.

## Commands

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

- `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**).
- `long_hello_list` (built by `dist`): `hello_list` plus `pen2-cython`, `core-c-hello`, `core-pybind11-hello`, `hatchling-pybind11-hello`.
- `hello-free-threading` is wheel-built separately via cibuildwheel in CI (`free-threading` job), not through nox.
- `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.

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

Expand Down
1 change: 1 addition & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"hello-pybind11",
"hello-cython",
"core-cython-hello",
"core-nanobind-shared",
"hello-cmake-package",
"pi-fortran",
]
Expand Down
41 changes: 41 additions & 0 deletions projects/core-nanobind-shared/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
cmake_minimum_required(VERSION 3.15...3.30)

project(${SKBUILD_PROJECT_NAME} VERSION ${SKBUILD_PROJECT_VERSION} LANGUAGES CXX)

find_package(
Python
COMPONENTS Interpreter Development.Module
REQUIRED)
find_package(nanobind CONFIG REQUIRED)

# An ordinary C++ shared library that knows nothing about Python. In a real
# project this could be a pre-existing library or an imported target.
add_library(say_hello SHARED cpp/hello.cpp)
target_include_directories(say_hello PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/cpp")
# MSVC exports nothing by default; real projects usually use
# generate_export_header() and explicit __declspec macros instead.
set_target_properties(say_hello PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)

# The Python extension module, linked to the shared library like any other
# CMake consumer.
nanobind_add_module(_hello src/bindings.cpp)
target_link_libraries(_hello PRIVATE say_hello)

# Both binaries are installed side by side into the "hello" package, so the
# extension must look next to itself for the library at import time. Windows
# has no RPATH; there the DLL is found because it sits next to the .pyd.
if(APPLE)
set_target_properties(_hello PROPERTIES INSTALL_RPATH "@loader_path")
elseif(UNIX)
set_target_properties(_hello PROPERTIES INSTALL_RPATH "$ORIGIN")
endif()

# Destinations are relative to the root of the wheel (platlib). LIBRARY covers
# the extension and the .so/.dylib, RUNTIME covers the .dll on Windows, and
# the MSVC import library (ARCHIVE) is a build-time artifact that doesn't
# belong in the wheel.
install(
TARGETS _hello say_hello
LIBRARY DESTINATION hello
RUNTIME DESTINATION hello
ARCHIVE DESTINATION hello EXCLUDE_FROM_ALL)
40 changes: 40 additions & 0 deletions projects/core-nanobind-shared/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# core-nanobind-shared

A minimal [scikit-build-core](https://scikit-build-core.readthedocs.io) +
[nanobind](https://nanobind.readthedocs.io) project where the extension module
links against a plain C++ **shared** library that ships inside the wheel.
Requested in
[scikit-build-core#1034](https://github.com/scikit-build/scikit-build-core/issues/1034).

## Layout

- `cpp/`: an ordinary C++ shared library (`say_hello`) with no Python
knowledge. Stand-in for any existing `add_library(... SHARED ...)` target or
third-party library you build yourself.
- `src/bindings.cpp`: the nanobind module `_hello`, which just links to the
library with `target_link_libraries`.
- `src/hello/`: the pure Python package; scikit-build-core picks it up
automatically because it matches the project name.

## The parts that trip people up

- **Install with relative destinations.** `install(TARGETS ... DESTINATION
hello)` puts both binaries inside the `hello` package in the wheel. Never
use `${CMAKE_INSTALL_PREFIX}` in destinations; scikit-build-core installs
into a staging directory that becomes the wheel's platlib root.
- **RPATH.** At import time the extension must find the shared library next to
itself, so its `INSTALL_RPATH` is set to `$ORIGIN` (Linux) or `@loader_path`
(macOS). On macOS this works together with the default `@rpath/...` install
name CMake gives shared libraries.
- **Windows.** There is no RPATH; the DLL is found because CPython loads
extension modules with their own directory on the DLL search path, so
installing the DLL next to the `.pyd` is enough (`RUNTIME DESTINATION`).
The MSVC import library (`.lib`, the `ARCHIVE` artifact) is only needed at
build time and is excluded from the wheel.
- **No `SOVERSION`/`VERSION` on the library.** Versioned shared libraries are
installed as symlinks, but wheels cannot contain symlinks — you would ship
multiple full copies of the library.

If you link against libraries you do *not* ship in the wheel, this RPATH setup
does not apply; use `auditwheel` (Linux) / `delocate` (macOS) /
`delvewheel` (Windows) to vendor them into repaired wheels instead.
5 changes: 5 additions & 0 deletions projects/core-nanobind-shared/cpp/hello.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#include "hello.hpp"

int add(int a, int b) { return a + b; }

std::string greet(const std::string &name) { return "Hello, " + name + "!"; }
6 changes: 6 additions & 0 deletions projects/core-nanobind-shared/cpp/hello.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#pragma once

#include <string>

int add(int a, int b);
std::string greet(const std::string &name);
24 changes: 24 additions & 0 deletions projects/core-nanobind-shared/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[build-system]
requires = ["scikit-build-core", "nanobind"]
build-backend = "scikit_build_core.build"

[project]
name = "hello"
version = "1.2.3"
authors = [
{ name = "The scikit-build team" },
]
description = "a minimal example package (nanobind + shared library version)"
requires-python = ">=3.9"
classifiers = [
"License :: OSI Approved :: MIT License",
]

# One version is enough here; the point is exercising the wheel-repair step
# (auditwheel/delocate) over the shipped shared library.
[tool.cibuildwheel]
build-frontend = "build[uv]"
build = "cp312-*"
archs = ["auto64"]
test-command = "pytest {package}"
test-requires = ["pytest"]
12 changes: 12 additions & 0 deletions projects/core-nanobind-shared/src/bindings.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#include <nanobind/nanobind.h>
#include <nanobind/stl/string.h>

#include "hello.hpp"

namespace nb = nanobind;

NB_MODULE(_hello, m) {
m.doc() = "nanobind bindings for the say_hello shared library";
m.def("add", &add, nb::arg("a"), nb::arg("b"), "Add two integers");
m.def("greet", &greet, nb::arg("name"), "Greet somebody by name");
}
3 changes: 3 additions & 0 deletions projects/core-nanobind-shared/src/hello/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ._hello import add, greet

__all__ = ["add", "greet"]
9 changes: 9 additions & 0 deletions projects/core-nanobind-shared/tests/test_hello.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import hello


def test_add():
assert hello.add(2, 3) == 5


def test_greet():
assert hello.greet("World") == "Hello, World!"
Loading