Skip to content
7 changes: 6 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,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
11 changes: 11 additions & 0 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ 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)
* If you enable the optional nanobind (WIP) backend for source/CMake
builds (`OIIO_PYTHON_BINDINGS_BACKEND` is `nanobind` or `both`):
* 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 +161,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 +257,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 ()
92 changes: 72 additions & 20 deletions src/cmake/testing.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,23 @@ set(OIIO_TESTSUITE_IMAGEDIR "${PROJECT_BINARY_DIR}/testsuite" CACHE PATH
"Location of oiio-images, openexr-images, libtiffpic, etc.." )


# Build a single ENVIRONMENT list entry "PYTHONPATH=..." for CTest.
# On Windows, keep this deterministic and do not append inherited PYTHONPATH:
# semicolon-separated values can be split by CMake list processing when used
# as test ENVIRONMENT entries.
function (oiio_tests_pythonpath_env_entry out_var prefix_dir)
if (WIN32)
set (_pythonpath "${prefix_dir}")
else ()
if (DEFINED ENV{PYTHONPATH} AND NOT "$ENV{PYTHONPATH}" STREQUAL "")
string (CONCAT _pythonpath "${prefix_dir}" ":" "$ENV{PYTHONPATH}")
else ()
set (_pythonpath "${prefix_dir}")
endif ()
endif ()
set (${out_var} "PYTHONPATH=${_pythonpath}" PARENT_SCOPE)
endfunction ()


# oiio_add_tests() - add a set of test cases.
#
Expand Down Expand Up @@ -228,25 +245,60 @@ 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
)
if (WIN32)
# On Windows CI we run the install target before tests. Use the
# installed package path to avoid multi-config output layout quirks.
set (_installed_python_site_packages
"${CMAKE_INSTALL_PREFIX}/${PYTHON_SITE_ROOT_DIR}")
oiio_tests_pythonpath_env_entry (_pybind_tests_pythonpath
"${_installed_python_site_packages}")
oiio_tests_pythonpath_env_entry (_nanobind_tests_pythonpath
"${CMAKE_BINARY_DIR}/lib/python/nanobind/$<CONFIG>")
else ()
oiio_tests_pythonpath_env_entry (_pybind_tests_pythonpath
"${CMAKE_BINARY_DIR}/lib/python/site-packages")
oiio_tests_pythonpath_env_entry (_nanobind_tests_pythonpath
"${CMAKE_BINARY_DIR}/lib/python/nanobind")
endif ()
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 "${_pybind_tests_pythonpath}"
)
# These Python tests also need access to oiio-images
oiio_add_tests (
python-imageinput python-imagebufalgo
IMAGEDIR oiio-images
ENVIRONMENT "${_pybind_tests_pythonpath}"
)
else ()
set (nanobind_python_test_suffix "")
endif ()

if (OIIO_BUILD_PYTHON_NANOBIND)
oiio_add_tests (
${nanobind_python_tests}
SUFFIX ${nanobind_python_test_suffix}
ENVIRONMENT "${_nanobind_tests_pythonpath}"
)
endif ()
endif ()

oiio_add_tests (oiiotool-color
Expand All @@ -266,7 +318,7 @@ macro (oiio_add_all_tests)

oiio_add_tests ( python-imagebufalgo
FOUNDVAR hwy_FOUND
ENABLEVAR OIIO_USE_HWY USE_PYTHON
ENABLEVAR OIIO_USE_HWY USE_PYTHON OIIO_BUILD_PYTHON_PYBIND11
DISABLEVAR BUILD_OIIOUTIL_ONLY SANITIZE
SUFFIX ".hwy"
ENVIRONMENT "OPENIMAGEIO_ENABLE_HWY=1"
Expand Down
32 changes: 32 additions & 0 deletions src/python-nanobind/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 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")
if (SKBUILD)
install (FILES __init__.py DESTINATION . COMPONENT user)
else ()
install (FILES __init__.py
DESTINATION ${PYTHON_SITE_DIR} COMPONENT user)
endif ()
endif ()
Loading
Loading