Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f5087bf
Initial nanobind experimental
soswow Mar 12, 2026
bf089e4
A bit of flavour
soswow Mar 12, 2026
c9b35b1
Revert FMT changes
soswow Mar 12, 2026
b1c459a
formatter
soswow Mar 12, 2026
96cc95a
Remove python shim
soswow Mar 12, 2026
043b829
changing from extension testing to package
soswow Mar 12, 2026
05787ad
add more of ROI support
soswow Mar 13, 2026
603dc45
one python test file to rule them all
soswow Mar 13, 2026
3c1cdd4
push things around for less of a changeset
soswow Mar 13, 2026
39d3315
No more experiments
soswow Mar 13, 2026
36386bb
even less experiment
soswow Mar 13, 2026
630ff31
comment out unused cli related binding
soswow Mar 13, 2026
db26a0e
revert some white spacing
soswow Mar 14, 2026
740c8c9
Less changes in the test
soswow Mar 14, 2026
29e028c
Less diff
soswow Mar 14, 2026
590dd74
better naming and extracting common bit
soswow Mar 14, 2026
af233ef
Add comments to make it clearer
soswow Mar 14, 2026
bd381ee
Fix small issues
soswow Mar 14, 2026
2833cdb
Add typedesc to nanobind
soswow Mar 15, 2026
9cf494d
Add a bit more coverage to both
soswow Mar 15, 2026
c475d30
Remove extra test folder need entirely. nanobind version is tested au…
soswow Mar 15, 2026
7c963d7
minimize need for changes even more
soswow Mar 15, 2026
563d34d
less changes
soswow Mar 15, 2026
20b8ba5
even less less changes
soswow Mar 15, 2026
811ea91
Python version fix
soswow Mar 15, 2026
614b9e0
fix tests
soswow Mar 15, 2026
a90ecdc
Add nanobind ImageSpec and ParamValue bindings
soswow Apr 18, 2026
583aea9
Move nanobind test duplication into CMake
soswow Apr 18, 2026
a9c5f8d
revert some whitespace changes
soswow Apr 18, 2026
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: 6 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,12 @@ else ()
set (_py_dev_found Python3_Development.Module_FOUND)
endif ()
if (USE_PYTHON AND ${_py_dev_found} AND NOT BUILD_OIIOUTIL_ONLY)
add_subdirectory (src/python)
if (OIIO_BUILD_PYTHON_PYBIND11)
add_subdirectory (src/python)
endif ()
if (OIIO_BUILD_PYTHON_NANOBIND)
add_subdirectory (src/python-nanobind)
endif ()
else ()
message (STATUS "Not building Python bindings: USE_PYTHON=${USE_PYTHON}, Python3_Development.Module_FOUND=${Python3_Development.Module_FOUND}")
endif ()
Expand Down
12 changes: 12 additions & 0 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ NEW or CHANGED MINIMUM dependencies since the last major release are **bold**.
* Python >= 3.9 (tested through 3.13).
* pybind11 >= 2.7 (tested through 3.0)
* NumPy (tested through 2.4.4)
* For the experimental nanobind migration backend:
* NumPy (tested through 2.2.4)
* For the nanobind (WIP) migration backend used in source/CMake builds:
* nanobind discoverable by CMake, or installed in the active Python
environment so `python -m nanobind --cmake_dir` works
* If you want support for PNG files:
* libPNG >= 1.6.0 (tested though 1.6.56)
* If you want support for camera "RAW" formats:
Expand Down Expand Up @@ -157,6 +162,12 @@ Make wrapper (`make PkgName_ROOT=...`).

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

`OIIO_PYTHON_BINDINGS_BACKEND=pybind11|nanobind|both` : Select which Python
binding backend(s) to configure for source/CMake builds. `both` keeps the
existing pybind11 module and also builds the nanobind (WIP) module. The
Python packaging path driven by `pyproject.toml` still targets the production
pybind11 bindings today.

`OIIO_BUILD_TESTS=0` : Omits building tests (you probably don't need them
unless you are a developer of OIIO or want to verify that your build
passes all tests).
Expand Down Expand Up @@ -247,6 +258,7 @@ Additionally, a few helpful modifiers alter some build-time options:
| make USE_QT=0 ... | Skip anything that needs Qt |
| make MYCC=xx MYCXX=yy ... | Use custom compilers |
| make USE_PYTHON=0 ... | Don't build the Python binding |
| make OIIO_PYTHON_BINDINGS_BACKEND=both ... | For source/CMake builds, build the existing pybind11 bindings and the nanobind (WIP) module |
| make BUILD_SHARED_LIBS=0 | Build static library instead of shared |
| make IGNORE_HOMEBREWED_DEPS=1 | Ignore homebrew-managed dependencies |
| make LINKSTATIC=1 ... | Link with static external libraries when possible |
Expand Down
6 changes: 5 additions & 1 deletion src/cmake/externalpackages.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,13 @@ endif()
if (USE_PYTHON)
find_python()
endif ()
if (USE_PYTHON)
if (USE_PYTHON AND OIIO_BUILD_PYTHON_PYBIND11)
checked_find_package (pybind11 REQUIRED VERSION_MIN 2.7)
endif ()
if (USE_PYTHON AND OIIO_BUILD_PYTHON_NANOBIND)
discover_nanobind_cmake_dir()
checked_find_package (nanobind CONFIG REQUIRED)
endif ()


###########################################################################
Expand Down
124 changes: 123 additions & 1 deletion src/cmake/pythonutils.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,31 @@ set (PYTHON_VERSION "" CACHE STRING "Target version of python to find")
option (PYLIB_INCLUDE_SONAME "If ON, soname/soversion will be set for Python module library" OFF)
option (PYLIB_LIB_PREFIX "If ON, prefix the Python module with 'lib'" OFF)
set (PYMODULE_SUFFIX "" CACHE STRING "Suffix to add to Python module init namespace")
set (OIIO_PYTHON_BINDINGS_BACKEND "pybind11" CACHE STRING
"Which Python binding backend(s) to build: pybind11, nanobind, or both")
set_property (CACHE OIIO_PYTHON_BINDINGS_BACKEND PROPERTY STRINGS
pybind11 nanobind both)

# Normalize and validate the user-facing backend selector early so the rest
# of the file can make simple boolean decisions.
string (TOLOWER "${OIIO_PYTHON_BINDINGS_BACKEND}" OIIO_PYTHON_BINDINGS_BACKEND)
if (NOT OIIO_PYTHON_BINDINGS_BACKEND MATCHES "^(pybind11|nanobind|both)$")
message (FATAL_ERROR
"OIIO_PYTHON_BINDINGS_BACKEND must be one of: pybind11, nanobind, both")
endif ()

# Derive internal switches used by the top-level CMakeLists and the Python
# helper macros below.
set (OIIO_BUILD_PYTHON_PYBIND11 OFF)
set (OIIO_BUILD_PYTHON_NANOBIND OFF)
if (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "pybind11"
OR OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "both")
set (OIIO_BUILD_PYTHON_PYBIND11 ON)
endif ()
if (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "nanobind"
OR OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "both")
set (OIIO_BUILD_PYTHON_NANOBIND ON)
endif ()
if (WIN32)
set (PYLIB_LIB_TYPE SHARED CACHE STRING "Type of library to build for python module (MODULE or SHARED)")
else ()
Expand Down Expand Up @@ -54,6 +79,15 @@ macro (find_python)
Python3_Development.Module_FOUND
Python3_Interpreter_FOUND )

if (OIIO_BUILD_PYTHON_NANOBIND)
# nanobind's CMake package expects the generic FindPython targets and
# variables (Python::Module, Python_EXECUTABLE, etc.), not the
# versioned Python3::* targets that the rest of OIIO uses today.
find_package (Python ${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}
EXACT REQUIRED
COMPONENTS ${_py_components})
endif ()

# The version that was found may not be the default or user
# defined one.
set (PYTHON_VERSION_FOUND ${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR})
Expand All @@ -63,15 +97,44 @@ macro (find_python)
set (PythonInterp3_FIND_VERSION PYTHON_VERSION_FOUND)
set (PythonInterp3_FIND_VERSION_MAJOR ${Python3_VERSION_MAJOR})

if (NOT DEFINED PYTHON_SITE_ROOT_DIR)
set (PYTHON_SITE_ROOT_DIR
"${CMAKE_INSTALL_LIBDIR}/python${PYTHON_VERSION_FOUND}/site-packages")
endif ()
if (NOT DEFINED PYTHON_SITE_DIR)
set (PYTHON_SITE_DIR "${CMAKE_INSTALL_LIBDIR}/python${PYTHON_VERSION_FOUND}/site-packages/OpenImageIO")
set (PYTHON_SITE_DIR "${PYTHON_SITE_ROOT_DIR}/OpenImageIO")
endif ()
message (VERBOSE " Python site packages dir ${PYTHON_SITE_DIR}")
message (VERBOSE " Python site packages root ${PYTHON_SITE_ROOT_DIR}")
message (VERBOSE " Python to include 'lib' prefix: ${PYLIB_LIB_PREFIX}")
message (VERBOSE " Python to include SO version: ${PYLIB_INCLUDE_SONAME}")
endmacro()


# Help CMake locate nanobind when it was installed as a Python package.
macro (discover_nanobind_cmake_dir)
if (nanobind_DIR OR nanobind_ROOT OR "$ENV{nanobind_DIR}" OR "$ENV{nanobind_ROOT}")
return()
endif ()

if (NOT Python3_Interpreter_FOUND)
return()
endif ()

execute_process (
COMMAND ${Python3_EXECUTABLE} -m nanobind --cmake_dir
RESULT_VARIABLE _oiio_nanobind_result
OUTPUT_VARIABLE _oiio_nanobind_cmake_dir
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET)
if (_oiio_nanobind_result EQUAL 0
AND EXISTS "${_oiio_nanobind_cmake_dir}/nanobind-config.cmake")
set (nanobind_DIR "${_oiio_nanobind_cmake_dir}" CACHE PATH
"Path to the nanobind CMake package" FORCE)
endif ()
endmacro()


###########################################################################
# pybind11

Expand Down Expand Up @@ -163,3 +226,62 @@ macro (setup_python_module)

endmacro ()


###########################################################################
# nanobind

macro (setup_python_module_nanobind)
cmake_parse_arguments (lib "" "TARGET;MODULE"
"SOURCES;LIBS;INCLUDES;SYSTEM_INCLUDE_DIRS;PACKAGE_FILES"
${ARGN})

set (target_name ${lib_TARGET})

if (NOT COMMAND nanobind_add_module)
discover_nanobind_cmake_dir()
find_package (nanobind CONFIG REQUIRED)
endif ()

nanobind_add_module(${target_name} ${lib_SOURCES})
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND TARGET nanobind-static)
target_compile_options (nanobind-static PRIVATE -Wno-error=format-nonliteral)
endif ()

target_include_directories (${target_name}
PRIVATE ${lib_INCLUDES})
target_include_directories (${target_name}
SYSTEM PRIVATE ${lib_SYSTEM_INCLUDE_DIRS})
target_link_libraries (${target_name}
PRIVATE ${lib_LIBS})

set (_module_LINK_FLAGS "${VISIBILITY_MAP_COMMAND} ${EXTRA_DSO_LINK_ARGS}")
if (UNIX AND NOT APPLE)
set (_module_LINK_FLAGS "${_module_LINK_FLAGS} -Wl,--exclude-libs,ALL")
endif ()
set_target_properties (${target_name} PROPERTIES
LINK_FLAGS ${_module_LINK_FLAGS}
OUTPUT_NAME ${lib_MODULE}
DEBUG_POSTFIX "")

if (SKBUILD)
set (_nanobind_install_dir .)
else ()
set (_nanobind_install_dir ${PYTHON_SITE_DIR})
endif ()

# Keep nanobind modules isolated in the build tree so they don't alter
# how the existing top-level OpenImageIO module is imported during tests.
set_target_properties (${target_name} PROPERTIES
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/nanobind/OpenImageIO
ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/python/nanobind/OpenImageIO
)

install (TARGETS ${target_name}
RUNTIME DESTINATION ${_nanobind_install_dir} COMPONENT user
LIBRARY DESTINATION ${_nanobind_install_dir} COMPONENT user)

if (lib_PACKAGE_FILES)
install (FILES ${lib_PACKAGE_FILES}
DESTINATION ${_nanobind_install_dir} COMPONENT user)
endif ()
endmacro ()
73 changes: 53 additions & 20 deletions src/cmake/testing.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ set(OIIO_TESTSUITE_IMAGEDIR "${PROJECT_BINARY_DIR}/testsuite" CACHE PATH
# [ DISABLEVAR variable_name ... ]
# [ SUFFIX suffix ]
# [ ENVIRONMENT "VAR=value" ... ]
# [ ENVIRONMENT_MODIFICATION "VAR=op:value" ... ]
# )
#
# The optional argument IMAGEDIR is used to check whether external test images
Expand All @@ -55,8 +56,12 @@ set(OIIO_TESTSUITE_IMAGEDIR "${PROJECT_BINARY_DIR}/testsuite" CACHE PATH
# The optional ENVIRONMENT is a list of environment variables to set for the
# test.
#
# The optional ENVIRONMENT_MODIFICATION is a list of environment variable
# modifications in the format accepted by the CTest ENVIRONMENT_MODIFICATION
# property.
#
macro (oiio_add_tests)
cmake_parse_arguments (_ats "" "SUFFIX;TESTNAME" "URL;IMAGEDIR;LABEL;FOUNDVAR;ENABLEVAR;DISABLEVAR;ENVIRONMENT" ${ARGN})
cmake_parse_arguments (_ats "" "SUFFIX;TESTNAME" "URL;IMAGEDIR;LABEL;FOUNDVAR;ENABLEVAR;DISABLEVAR;ENVIRONMENT;ENVIRONMENT_MODIFICATION" ${ARGN})
# Arguments: <prefix> <options> <one_value_keywords> <multi_value_keywords> args...
set (_ats_testdir "${OIIO_TESTSUITE_IMAGEDIR}/${_ats_IMAGEDIR}")
# If there was a FOUNDVAR param specified and that variable name is
Expand Down Expand Up @@ -131,6 +136,11 @@ macro (oiio_add_tests)
"OIIO_TESTSUITE_CUR=${_testdir}"
"Python_EXECUTABLE=${Python3_EXECUTABLE}"
${_ats_ENVIRONMENT})
if (_ats_ENVIRONMENT_MODIFICATION)
set_property(TEST ${_testname} APPEND PROPERTY
ENVIRONMENT_MODIFICATION
${_ats_ENVIRONMENT_MODIFICATION})
endif ()
if (NOT ${_ats_testdir} STREQUAL "")
set_property(TEST ${_testname} APPEND PROPERTY ENVIRONMENT
"OIIO_TESTSUITE_IMAGEDIR=${_ats_testdir}")
Expand Down Expand Up @@ -228,25 +238,48 @@ macro (oiio_add_all_tests)
# Python interpreter itself won't be linked with the right asan
# libraries to run correctly.
if (USE_PYTHON AND NOT BUILD_OIIOUTIL_ONLY AND NOT SANITIZE)
oiio_add_tests (
docs-examples-python
python-colorconfig
python-deep
python-imagebuf
python-imagecache
python-imageoutput
python-imagespec
python-paramlist
python-roi
python-texturesys
python-typedesc
filters
)
# These Python tests also need access to oiio-images
oiio_add_tests (
python-imageinput python-imagebufalgo
IMAGEDIR oiio-images
)
set (pybind11_python_path_mod
"PYTHONPATH=path_list_prepend:${CMAKE_BINARY_DIR}/lib/python/site-packages")
set (nanobind_python_tests
python-imagespec
python-paramlist
python-roi
python-typedesc)
set (nanobind_python_test_suffix ".nanobind")
if (OIIO_BUILD_PYTHON_PYBIND11)
oiio_add_tests (
docs-examples-python
python-colorconfig
python-deep
python-imagebuf
python-imagecache
python-imageoutput
python-imagespec
python-paramlist
python-roi
python-texturesys
python-typedesc
filters
ENVIRONMENT_MODIFICATION ${pybind11_python_path_mod}
)
# These Python tests also need access to oiio-images
oiio_add_tests (
python-imageinput python-imagebufalgo
IMAGEDIR oiio-images
ENVIRONMENT_MODIFICATION ${pybind11_python_path_mod}
)
else ()
set (nanobind_python_test_suffix "")
endif ()

if (OIIO_BUILD_PYTHON_NANOBIND)
oiio_add_tests (
${nanobind_python_tests}
SUFFIX ${nanobind_python_test_suffix}
ENVIRONMENT_MODIFICATION
"PYTHONPATH=path_list_prepend:${CMAKE_BINARY_DIR}/lib/python/nanobind"
)
endif ()
endif ()

oiio_add_tests (oiiotool-color
Expand Down
28 changes: 28 additions & 0 deletions src/python-nanobind/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Copyright Contributors to the OpenImageIO project.
# SPDX-License-Identifier: Apache-2.0
# https://github.com/AcademySoftwareFoundation/OpenImageIO

set (nanobind_srcs
py_oiio.cpp
py_paramvalue.cpp
py_roi.cpp
py_imagespec.cpp
py_typedesc.cpp)

set (nanobind_build_package_dir ${CMAKE_BINARY_DIR}/lib/python/nanobind/OpenImageIO)
file (MAKE_DIRECTORY ${nanobind_build_package_dir})
configure_file (__init__.py
${nanobind_build_package_dir}/__init__.py
COPYONLY)

setup_python_module_nanobind (
TARGET PyOpenImageIONanobind
MODULE _OpenImageIO
SOURCES ${nanobind_srcs}
LIBS OpenImageIO
)

if (OIIO_PYTHON_BINDINGS_BACKEND STREQUAL "nanobind")
install (FILES __init__.py
DESTINATION ${PYTHON_SITE_DIR} COMPONENT user)
endif ()
37 changes: 37 additions & 0 deletions src/python-nanobind/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright Contributors to the OpenImageIO project.
# SPDX-License-Identifier: Apache-2.0
# https://github.com/AcademySoftwareFoundation/OpenImageIO

import os
import sys
import platform

_here = os.path.abspath(os.path.dirname(__file__))

# Set $OpenImageIO_ROOT if not already set before importing helper modules.
if not os.getenv("OpenImageIO_ROOT"):
if all([os.path.exists(os.path.join(_here, i)) for i in ["share", "bin", "lib"]]):
os.environ["OpenImageIO_ROOT"] = _here

if platform.system() == "Windows":
_bin_dir = os.path.join(_here, "bin")
if os.path.exists(_bin_dir):
os.add_dll_directory(_bin_dir)
elif sys.version_info >= (3, 8):
if os.getenv("OPENIMAGEIO_PYTHON_LOAD_DLLS_FROM_PATH", "0") == "1":
for path in os.getenv("PATH", "").split(os.pathsep):
if os.path.exists(path) and path != ".":
os.add_dll_directory(path)

from . import _OpenImageIO as _ext # noqa: E402
from ._OpenImageIO import * # type: ignore # noqa: E402, F401, F403

__doc__ = """
OpenImageIO Python package exposing the nanobind migration bindings.
The production pybind11 bindings are not installed in this configuration.
"""[1:-1]

__version__ = getattr(_ext, "__version__", "")

# TODO: Restore the Python CLI entry-point trampolines when the nanobind
# package ships the full wheel-style bin/lib/share layout.
Loading
Loading