Skip to content

Commit 2f5a354

Browse files
committed
enhance(python): unify Python binding configuration for pybind11 and nanobind
Refactor CMake configuration to support both pybind11 and nanobind backends through a single option, OIIO_PYTHON_BINDINGS_BACKEND, allowing for flexible builds. Update CMakeLists.txt to conditionally include source directories based on the selected backend. Modify INSTALL.md to clarify usage instructions for enabling either backend or both. Adjust CI workflows to build with the 'both' option, ensuring compatibility with both binding systems. Update various source files to streamline the integration of nanobind and pybind11, including changes to installation paths and test configurations. Assisted-by: Cursor / Auto Signed-off-by: Aleksandr Motsjonov <soswow@gmail.com>
1 parent d7b6699 commit 2f5a354

17 files changed

Lines changed: 273 additions & 132 deletions

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ jobs:
300300

301301
- desc: latest releases gcc15 C++23 py3.12 avx2 exr3.4 ocio2.4
302302
nametag: linux-latest-releases
303-
oiio_python_bindings_backend: nanobind
303+
oiio_python_bindings_backend: both
304304
runner: ubuntu-24.04
305305
# cc_compiler: gcc-13
306306
# cxx_compiler: g++-13
@@ -601,7 +601,7 @@ jobs:
601601
- desc: MacOS-14-ARM aclang15/C++20/py3.13
602602
runner: macos-14
603603
nametag: macos14-arm-py313
604-
oiio_python_bindings_backend: nanobind
604+
oiio_python_bindings_backend: both
605605
cc_compiler: /usr/bin/clang
606606
cxx_compiler: /usr/bin/clang++
607607
cxx_std: 20
@@ -685,4 +685,4 @@ jobs:
685685
ctest_test_timeout: "240"
686686
setenvs: export OPENIMAGEIO_PYTHON_LOAD_DLLS_FROM_PATH=1
687687
benchmark: 1
688-
oiio_python_bindings_backend: nanobind
688+
oiio_python_bindings_backend: both

CMakeLists.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,14 @@ else ()
314314
set (_py_dev_found Python3_Development.Module_FOUND)
315315
endif ()
316316
if (USE_PYTHON AND ${_py_dev_found} AND NOT BUILD_OIIOUTIL_ONLY)
317-
add_subdirectory (src/python)
317+
if (OIIO_BUILD_PYTHON_PYBIND11
318+
OR OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "nanobind")
319+
add_subdirectory (src/python)
320+
endif ()
321+
if (OIIO_BUILD_PYTHON_NANOBIND
322+
AND OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "both")
323+
add_subdirectory (src/python-nanobind)
324+
endif ()
318325
else ()
319326
message (STATUS "Not building Python bindings: USE_PYTHON=${USE_PYTHON}, Python3_Development.Module_FOUND=${Python3_Development.Module_FOUND}")
320327
endif ()

INSTALL.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ NEW or CHANGED MINIMUM dependencies since the last major release are **bold**.
4444
* Python >= 3.9 (tested through 3.13).
4545
* pybind11 >= 2.7 (tested through 3.0)
4646
* NumPy (tested through 2.4.4)
47-
* If you enable the nanobind (WIP) backend for source/CMake builds
48-
(`OIIO_PYTHON_BINDINGS_BACKEND=nanobind`):
47+
* If you enable the optional nanobind (WIP) backend for source/CMake
48+
builds (`OIIO_PYTHON_BINDINGS_BACKEND` is `nanobind` or `both`):
4949
* nanobind discoverable by CMake, or installed in the active Python
5050
environment so `python -m nanobind --cmake_dir` works
5151
* If you want support for PNG files:
@@ -163,10 +163,11 @@ Make wrapper (`make PkgName_ROOT=...`).
163163

164164
`USE_PYTHON=0` : Omits building the Python bindings.
165165

166-
`OIIO_PYTHON_BINDINGS_BACKEND=pybind11|nanobind` : Select the Python binding
167-
backend for source/CMake builds (`pybind11` is the default production
168-
backend; `nanobind` is the in-progress migration backend). The Python
169-
packaging path driven by `pyproject.toml` still targets pybind11 today.
166+
`OIIO_PYTHON_BINDINGS_BACKEND=pybind11|nanobind|both` : Select which Python
167+
binding backend(s) to configure for source/CMake builds. `both` keeps the
168+
existing pybind11 module and also builds the nanobind (WIP) module. The
169+
Python packaging path driven by `pyproject.toml` still targets the production
170+
pybind11 bindings today.
170171

171172
`OIIO_BUILD_TESTS=0` : Omits building tests (you probably don't need them
172173
unless you are a developer of OIIO or want to verify that your build
@@ -258,7 +259,7 @@ Additionally, a few helpful modifiers alter some build-time options:
258259
| make USE_QT=0 ... | Skip anything that needs Qt |
259260
| make MYCC=xx MYCXX=yy ... | Use custom compilers |
260261
| make USE_PYTHON=0 ... | Don't build the Python binding |
261-
| make OIIO_PYTHON_BINDINGS_BACKEND=nanobind ... | For source/CMake builds, use the nanobind (WIP) Python bindings instead of pybind11 |
262+
| make OIIO_PYTHON_BINDINGS_BACKEND=both ... | For source/CMake builds, build the existing pybind11 bindings and the nanobind (WIP) module |
262263
| make BUILD_SHARED_LIBS=0 | Build static library instead of shared |
263264
| make IGNORE_HOMEBREWED_DEPS=1 | Ignore homebrew-managed dependencies |
264265
| make LINKSTATIC=1 ... | Link with static external libraries when possible |

src/cmake/externalpackages.cmake

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,10 @@ endif()
118118
if (USE_PYTHON)
119119
find_python()
120120
endif ()
121-
if (USE_PYTHON AND OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "pybind11")
121+
if (USE_PYTHON AND OIIO_BUILD_PYTHON_PYBIND11)
122122
checked_find_package (pybind11 REQUIRED VERSION_MIN 2.7)
123123
endif ()
124-
if (USE_PYTHON AND OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "nanobind")
124+
if (USE_PYTHON AND OIIO_BUILD_PYTHON_NANOBIND)
125125
discover_nanobind_cmake_dir()
126126
checked_find_package (nanobind CONFIG REQUIRED)
127127
endif ()

src/cmake/pythonutils.cmake

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,29 @@ option (PYLIB_INCLUDE_SONAME "If ON, soname/soversion will be set for Python mod
99
option (PYLIB_LIB_PREFIX "If ON, prefix the Python module with 'lib'" OFF)
1010
set (PYMODULE_SUFFIX "" CACHE STRING "Suffix to add to Python module init namespace")
1111
set (OIIO_PYTHON_BINDINGS_BACKEND "pybind11" CACHE STRING
12-
"Which Python binding backend to build: pybind11 or nanobind")
12+
"Which Python binding backend(s) to build: pybind11, nanobind, or both")
1313
set_property (CACHE OIIO_PYTHON_BINDINGS_BACKEND PROPERTY STRINGS
14-
pybind11 nanobind)
14+
pybind11 nanobind both)
1515

16+
# Normalize and validate the user-facing backend selector early so the rest
17+
# of the file can make simple boolean decisions.
1618
string (TOLOWER "${OIIO_PYTHON_BINDINGS_BACKEND}" OIIO_PYTHON_BINDINGS_BACKEND)
17-
if (NOT OIIO_PYTHON_BINDINGS_BACKEND MATCHES "^(pybind11|nanobind)$")
19+
if (NOT OIIO_PYTHON_BINDINGS_BACKEND MATCHES "^(pybind11|nanobind|both)$")
1820
message (FATAL_ERROR
19-
"OIIO_PYTHON_BINDINGS_BACKEND must be pybind11 or nanobind")
21+
"OIIO_PYTHON_BINDINGS_BACKEND must be one of: pybind11, nanobind, both")
22+
endif ()
23+
24+
# Derive internal switches used by the top-level CMakeLists and the Python
25+
# helper macros below.
26+
set (OIIO_BUILD_PYTHON_PYBIND11 OFF)
27+
set (OIIO_BUILD_PYTHON_NANOBIND OFF)
28+
if (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "pybind11"
29+
OR OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "both")
30+
set (OIIO_BUILD_PYTHON_PYBIND11 ON)
31+
endif ()
32+
if (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "nanobind"
33+
OR OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "both")
34+
set (OIIO_BUILD_PYTHON_NANOBIND ON)
2035
endif ()
2136

2237
if (WIN32)
@@ -65,7 +80,7 @@ macro (find_python)
6580
Python3_Development.Module_FOUND
6681
Python3_Interpreter_FOUND )
6782

68-
if (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "nanobind")
83+
if (OIIO_BUILD_PYTHON_NANOBIND)
6984
# nanobind's CMake package expects the generic FindPython targets and
7085
# variables (Python::Module, Python_EXECUTABLE, etc.), not the
7186
# versioned Python3::* targets that the rest of OIIO uses today.
@@ -218,7 +233,7 @@ endmacro ()
218233

219234
macro (setup_python_module_nanobind)
220235
cmake_parse_arguments (lib "" "TARGET;MODULE"
221-
"SOURCES;LIBS;INCLUDES;SYSTEM_INCLUDE_DIRS"
236+
"SOURCES;LIBS;INCLUDES;SYSTEM_INCLUDE_DIRS;PACKAGE_FILES"
222237
${ARGN})
223238

224239
set (target_name ${lib_TARGET})
@@ -249,20 +264,44 @@ macro (setup_python_module_nanobind)
249264
OUTPUT_NAME ${lib_MODULE}
250265
DEBUG_POSTFIX "")
251266

252-
if (SKBUILD)
253-
set (PYTHON_SITE_DIR .)
254-
endif ()
267+
if (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "both")
268+
if (SKBUILD)
269+
set (_nanobind_install_dir .)
270+
else ()
271+
set (_nanobind_install_dir ${PYTHON_SITE_DIR})
272+
endif ()
255273

256-
set_target_properties (${target_name} PROPERTIES
257-
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/site-packages
258-
ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/site-packages
259-
)
274+
# Keep nanobind modules isolated in the build tree so they don't alter
275+
# how the existing top-level OpenImageIO module is imported during tests.
276+
set_target_properties (${target_name} PROPERTIES
277+
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/nanobind/OpenImageIO
278+
ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/nanobind/OpenImageIO
279+
)
260280

261-
install (TARGETS ${target_name}
262-
RUNTIME DESTINATION ${PYTHON_SITE_DIR} COMPONENT user
263-
LIBRARY DESTINATION ${PYTHON_SITE_DIR} COMPONENT user)
281+
install (TARGETS ${target_name}
282+
RUNTIME DESTINATION ${_nanobind_install_dir} COMPONENT user
283+
LIBRARY DESTINATION ${_nanobind_install_dir} COMPONENT user)
264284

265-
install (FILES __init__.py stubs/OpenImageIO/__init__.pyi stubs/OpenImageIO/py.typed
266-
DESTINATION ${PYTHON_SITE_DIR} COMPONENT user)
285+
if (lib_PACKAGE_FILES)
286+
install (FILES ${lib_PACKAGE_FILES}
287+
DESTINATION ${_nanobind_install_dir} COMPONENT user)
288+
endif ()
289+
else ()
290+
if (SKBUILD)
291+
set (PYTHON_SITE_DIR .)
292+
endif ()
293+
294+
set_target_properties (${target_name} PROPERTIES
295+
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/site-packages
296+
ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/site-packages
297+
)
298+
299+
install (TARGETS ${target_name}
300+
RUNTIME DESTINATION ${PYTHON_SITE_DIR} COMPONENT user
301+
LIBRARY DESTINATION ${PYTHON_SITE_DIR} COMPONENT user)
302+
303+
install (FILES __init__.py stubs/OpenImageIO/__init__.pyi stubs/OpenImageIO/py.typed
304+
DESTINATION ${PYTHON_SITE_DIR} COMPONENT user)
305+
endif ()
267306

268307
endmacro ()

src/cmake/testing.cmake

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -247,15 +247,25 @@ macro (oiio_add_all_tests)
247247
# libraries to run correctly.
248248
if (USE_PYTHON AND NOT BUILD_OIIOUTIL_ONLY AND NOT SANITIZE)
249249
if (WIN32)
250+
# On Windows CI we run the install target before tests. Use the
251+
# installed package path to avoid multi-config output layout quirks.
250252
set (_installed_python_site_packages
251253
"${CMAKE_INSTALL_PREFIX}/${PYTHON_SITE_ROOT_DIR}")
252-
oiio_tests_pythonpath_env_entry (_python_tests_pythonpath
254+
oiio_tests_pythonpath_env_entry (_pybind_tests_pythonpath
253255
"${_installed_python_site_packages}")
254256
else ()
255-
oiio_tests_pythonpath_env_entry (_python_tests_pythonpath
257+
oiio_tests_pythonpath_env_entry (_pybind_tests_pythonpath
256258
"${CMAKE_BINARY_DIR}/lib/python/site-packages")
257259
endif ()
258-
if (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "pybind11")
260+
oiio_tests_pythonpath_env_entry (_nanobind_tests_pythonpath
261+
"${CMAKE_BINARY_DIR}/lib/python/nanobind")
262+
set (nanobind_python_tests
263+
python-imagespec
264+
python-paramlist
265+
python-roi
266+
python-typedesc)
267+
set (nanobind_python_test_suffix ".nanobind")
268+
if (OIIO_BUILD_PYTHON_PYBIND11)
259269
oiio_add_tests (
260270
docs-examples-python
261271
python-colorconfig
@@ -269,20 +279,23 @@ macro (oiio_add_all_tests)
269279
python-texturesys
270280
python-typedesc
271281
filters
272-
ENVIRONMENT "${_python_tests_pythonpath}"
282+
ENVIRONMENT "${_pybind_tests_pythonpath}"
273283
)
284+
# These Python tests also need access to oiio-images
274285
oiio_add_tests (
275286
python-imageinput python-imagebufalgo
276287
IMAGEDIR oiio-images
277-
ENVIRONMENT "${_python_tests_pythonpath}"
288+
ENVIRONMENT "${_pybind_tests_pythonpath}"
278289
)
279-
elseif (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "nanobind")
290+
else ()
291+
set (nanobind_python_test_suffix "")
292+
endif ()
293+
294+
if (OIIO_BUILD_PYTHON_NANOBIND)
280295
oiio_add_tests (
281-
python-imagespec
282-
python-paramlist
283-
python-roi
284-
python-typedesc
285-
ENVIRONMENT "${_python_tests_pythonpath}"
296+
${nanobind_python_tests}
297+
SUFFIX ${nanobind_python_test_suffix}
298+
ENVIRONMENT "${_nanobind_tests_pythonpath}"
286299
)
287300
endif ()
288301
endif ()
@@ -304,7 +317,7 @@ macro (oiio_add_all_tests)
304317

305318
oiio_add_tests ( python-imagebufalgo
306319
FOUNDVAR hwy_FOUND
307-
ENABLEVAR OIIO_USE_HWY USE_PYTHON
320+
ENABLEVAR OIIO_USE_HWY USE_PYTHON OIIO_BUILD_PYTHON_PYBIND11
308321
DISABLEVAR BUILD_OIIOUTIL_ONLY SANITIZE
309322
SUFFIX ".hwy"
310323
ENVIRONMENT "OPENIMAGEIO_ENABLE_HWY=1"

src/python-nanobind/CMakeLists.txt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Copyright Contributors to the OpenImageIO project.
2+
# SPDX-License-Identifier: Apache-2.0
3+
# https://github.com/AcademySoftwareFoundation/OpenImageIO
4+
5+
set (nanobind_srcs
6+
py_oiio.cpp
7+
py_paramvalue.cpp
8+
py_roi.cpp
9+
py_imagespec.cpp
10+
py_typedesc.cpp)
11+
12+
set (nanobind_src_paths "")
13+
foreach (src ${nanobind_srcs})
14+
list (APPEND nanobind_src_paths
15+
${CMAKE_CURRENT_SOURCE_DIR}/../python/${src})
16+
endforeach ()
17+
18+
set (nanobind_build_package_dir ${CMAKE_BINARY_DIR}/lib/python/nanobind/OpenImageIO)
19+
file (MAKE_DIRECTORY ${nanobind_build_package_dir})
20+
configure_file (__init__.py
21+
${nanobind_build_package_dir}/__init__.py
22+
COPYONLY)
23+
24+
setup_python_module_nanobind (
25+
TARGET PyOpenImageIONanobind
26+
MODULE _OpenImageIO
27+
SOURCES ${nanobind_src_paths}
28+
LIBS OpenImageIO
29+
PACKAGE_FILES __init__.py
30+
)
31+
32+
target_compile_definitions (PyOpenImageIONanobind
33+
PRIVATE OIIO_PY_BACKEND_NANOBIND
34+
OIIO_PY_NANOBIND_ISOLATED_PACKAGE)
35+
target_include_directories (PyOpenImageIONanobind
36+
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../python)

src/python-nanobind/__init__.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright Contributors to the OpenImageIO project.
2+
# SPDX-License-Identifier: Apache-2.0
3+
# https://github.com/AcademySoftwareFoundation/OpenImageIO
4+
5+
import os
6+
import sys
7+
import platform
8+
9+
_here = os.path.abspath(os.path.dirname(__file__))
10+
11+
# Set $OpenImageIO_ROOT if not already set before importing helper modules.
12+
if not os.getenv("OpenImageIO_ROOT"):
13+
if all([os.path.exists(os.path.join(_here, i)) for i in ["share", "bin", "lib"]]):
14+
os.environ["OpenImageIO_ROOT"] = _here
15+
16+
if platform.system() == "Windows":
17+
# MSVC multi-config builds often put _OpenImageIO*.pyd under OpenImageIO/<Config>/
18+
# while this __init__.py lives in OpenImageIO/. Extend the package search path
19+
# so `from . import _OpenImageIO` resolves (no CMake per-config output juggling).
20+
for _cfg in ("Release", "Debug", "RelWithDebInfo", "MinSizeRel"):
21+
_subdir = os.path.join(_here, _cfg)
22+
if os.path.isdir(_subdir) and _subdir not in __path__:
23+
__path__.append(_subdir)
24+
25+
_bin_dir = os.path.join(_here, "bin")
26+
if os.path.exists(_bin_dir):
27+
os.add_dll_directory(_bin_dir)
28+
elif sys.version_info >= (3, 8):
29+
if os.getenv("OPENIMAGEIO_PYTHON_LOAD_DLLS_FROM_PATH", "0") == "1":
30+
for path in os.getenv("PATH", "").split(os.pathsep):
31+
if os.path.exists(path) and path != ".":
32+
os.add_dll_directory(path)
33+
34+
from . import _OpenImageIO as _ext # noqa: E402
35+
from ._OpenImageIO import * # type: ignore # noqa: E402, F401, F403
36+
37+
__doc__ = """
38+
OpenImageIO Python package exposing the nanobind migration bindings.
39+
The production pybind11 bindings are not installed in this configuration.
40+
"""[1:-1]
41+
42+
__version__ = getattr(_ext, "__version__", "")
43+
44+
# TODO: Restore the Python CLI entry-point trampolines when the nanobind
45+
# package ships the full wheel-style bin/lib/share layout.

src/python/CMakeLists.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ set (python_dual_backend_srcs
1010
py_imagespec.cpp
1111
py_paramvalue.cpp)
1212

13-
if (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "pybind11")
13+
if (OIIO_BUILD_PYTHON_PYBIND11)
1414
file (GLOB python_srcs *.cpp)
1515
setup_python_module (TARGET PyOpenImageIO
1616
MODULE OpenImageIO
@@ -23,7 +23,9 @@ if (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "pybind11")
2323
UNITY_BUILD_BATCH_SIZE ${UNITY_SMALL_BATCH_SIZE})
2424
set_source_files_properties (${python_srcs} PROPERTIES
2525
UNITY_GROUP PyOpenImageIO)
26-
elseif (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "nanobind")
26+
endif ()
27+
28+
if (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "nanobind")
2729
setup_python_module_nanobind (
2830
TARGET PyOpenImageIO
2931
MODULE OpenImageIO

src/python/MIGRATION_STATUS.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
Nanobind shares binding sources with pybind11 under `src/python/` (see
44
`py_backend.h` and `python_dual_backend_srcs` in `CMakeLists.txt`). Configure
5-
with `-DOIIO_PYTHON_BINDINGS_BACKEND=nanobind` to build the nanobind target
6-
`PyOpenImageIO` / module `OpenImageIO`; nanobind-only code paths live in
7-
`py_oiio.h` / `py_oiio.cpp` behind `OIIO_PY_BACKEND_NANOBIND`.
5+
with `-DOIIO_PYTHON_BINDINGS_BACKEND=nanobind` for nanobind-only (`PyOpenImageIO`
6+
/ module `OpenImageIO` in site-packages), or `both` to also build
7+
`PyOpenImageIONanobind` (`_OpenImageIO` under `lib/python/nanobind/OpenImageIO`).
8+
Nanobind-only code paths live in `py_oiio.h` / `py_oiio.cpp` behind
9+
`OIIO_PY_BACKEND_NANOBIND`.
810

911
## Migrated — full parity with pybind (no known gaps for this surface)
1012

0 commit comments

Comments
 (0)