From a40b1a79e4c9ec853f532cc19b375e0a15039396 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Sun, 20 Sep 2026 12:50:31 -0500 Subject: [PATCH 1/5] ENH: Install the module build files an external module includes An external module starts with include(ITKModuleExternal), but that file and the ones it reaches are not installed, so configuring a module against an installed ITK fails immediately. Adds the transitive closure of that entry point over ITK-owned CMake files, the templates they configure, and BuildHeaderTest.py, which the header test runs by a path that only resolves in a source tree. About 115 KB. --- CMake/ITKModuleHeaderTest.cmake | 13 +++++++++++-- CMakeLists.txt | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CMake/ITKModuleHeaderTest.cmake b/CMake/ITKModuleHeaderTest.cmake index 22745f5c78b..cb7a1d04873 100644 --- a/CMake/ITKModuleHeaderTest.cmake +++ b/CMake/ITKModuleHeaderTest.cmake @@ -4,6 +4,16 @@ # primary purpose of this test is to make sure there are not missing module # dependencies. +# /CMake in a build tree, the installed package directory otherwise. +if(EXISTS "${ITK_CMAKE_DIR}/../Utilities/Maintenance/BuildHeaderTest.py") + set( + ITK_BUILD_HEADER_TEST_SCRIPT + "${ITK_CMAKE_DIR}/../Utilities/Maintenance/BuildHeaderTest.py" + ) +else() + set(ITK_BUILD_HEADER_TEST_SCRIPT "${ITK_CMAKE_DIR}/BuildHeaderTest.py") +endif() + # Improve performance of MSVC GUI, by reducing number of header tests. set(MAXIMUM_NUMBER_OF_HEADERS_default 35) if(MSVC) @@ -114,8 +124,7 @@ macro(itk_module_headertest _name) OUTPUT ${_header_test_src} COMMAND - ${Python3_EXECUTABLE} - ${ITK_CMAKE_DIR}/../Utilities/Maintenance/BuildHeaderTest.py ${_name} + ${Python3_EXECUTABLE} ${ITK_BUILD_HEADER_TEST_SCRIPT} ${_name} ${${_name}_SOURCE_DIR} ${${_name}_BINARY_DIR} ${MAXIMUM_NUMBER_OF_HEADERS} ${_test_num} ) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a17ab6f239..06777c24f4d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -878,6 +878,26 @@ install( CMake/itkTransformIOFactoryRegisterManager.h.in CMake/itkMeshIOFactoryRegisterManager.h.in CMake/ITKInitializeCXXStandard.cmake + # The transitive closure of include(ITKModuleExternal). + CMake/ITKModuleExternal.cmake + CMake/ITKModuleMacros.cmake + CMake/ITKModuleTest.cmake + CMake/ITKModuleInfo.cmake.in + CMake/ITKSetPython3Vars.cmake + CMake/ITKInitializeBuildType.cmake + CMake/ITKWindowsUtf8.cmake + CMake/ITKExternalData.cmake + CMake/ITKDownloadSetup.cmake + CMake/ExternalData.cmake + CMake/ExternalData_config.cmake.in + CMake/WrappingConfigCommon.cmake + CMake/itkRemoveTestFiles.cmake + CMake/ITKModuleDoxygen.cmake + CMake/ITKModuleHeaderTest.cmake + CMake/ITKModuleKWStyleTest.cmake + CMake/ITKModuleCPPCheckTest.cmake + CMake/CppcheckTargets.cmake + Utilities/Maintenance/BuildHeaderTest.py DESTINATION ${ITK_INSTALL_PACKAGE_DIR} COMPONENT Development ) From 06c20f5cb9e9ed460d1b78afec3f289ce53cb0bf Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Sun, 20 Sep 2026 12:50:48 -0500 Subject: [PATCH 2/5] ENH: Allow wrapping an external module against an installed ITK The wrapping infrastructure and the generated SWIG type indices exist only in a build tree, so an installed ITK cannot wrap an external module. ITK_INSTALL_WRAPPING_DEVELOPMENT_FILES=ON installs both under the package directory, where ITK_CMAKE_DIR and ITK_DIR already point for an installed consumer. It defaults to OFF because the indices are tens of megabytes. Also repairs the install-tree branch in ITKModuleExternal.cmake: it read EXISTS"${ITK_CMAKE_DIR}/..." with no space, which CMake evaluates as false even when the file is present, so the branch was unreachable. --- CMake/ITKModuleExternal.cmake | 27 +++++----- .../docs/contributing/module_workflows.md | 22 ++++++++ Wrapping/CMakeLists.txt | 54 +++++++++++++++++++ Wrapping/WrappingOptions.cmake | 10 ++++ 4 files changed, 99 insertions(+), 14 deletions(-) diff --git a/CMake/ITKModuleExternal.cmake b/CMake/ITKModuleExternal.cmake index 2aa1f99c93c..9e695477110 100644 --- a/CMake/ITKModuleExternal.cmake +++ b/CMake/ITKModuleExternal.cmake @@ -255,23 +255,22 @@ if(ITK_WRAPPING) "${CMAKE_CURRENT_SOURCE_DIR}/wrapping/CMakeLists.txt" ) set(EXTERNAL_WRAP_ITK_PROJECT ON) - set(WRAP_ITK_CMAKE_DIR "${ITK_CMAKE_DIR}/../Wrapping") - include("${WRAP_ITK_CMAKE_DIR}/TypedefMacros.cmake") - # Build tree + # /CMake in a build tree, the installed package directory otherwise. if(EXISTS "${ITK_CMAKE_DIR}/../Wrapping/CMakeLists.txt") - add_subdirectory( - "${ITK_CMAKE_DIR}/../Wrapping" - ${CMAKE_CURRENT_BINARY_DIR}/Wrapping - ) - # Install tree - elseif(EXISTS"${ITK_CMAKE_DIR}/Wrapping/CMakeLists.txt") - add_subdirectory( - "${ITK_CMAKE_DIR}/Wrapping" - ${CMAKE_CURRENT_BINARY_DIR}/Wrapping - ) + set(WRAP_ITK_CMAKE_DIR "${ITK_CMAKE_DIR}/../Wrapping") + elseif(EXISTS "${ITK_CMAKE_DIR}/Wrapping/CMakeLists.txt") + set(WRAP_ITK_CMAKE_DIR "${ITK_CMAKE_DIR}/Wrapping") else() - message(FATAL_ERROR "Could not find wrapping infrastructure.") + message( + FATAL_ERROR + "Could not find wrapping infrastructure at ${ITK_CMAKE_DIR}. To wrap a module against an installed ITK, that ITK must be reconfigured with ITK_INSTALL_WRAPPING_DEVELOPMENT_FILES=ON, then rebuilt and reinstalled." + ) endif() + include("${WRAP_ITK_CMAKE_DIR}/TypedefMacros.cmake") + add_subdirectory( + "${WRAP_ITK_CMAKE_DIR}" + ${CMAKE_CURRENT_BINARY_DIR}/Wrapping + ) endif() endif() # Create target to download data from the ITKData group. This must come after diff --git a/Documentation/docs/contributing/module_workflows.md b/Documentation/docs/contributing/module_workflows.md index e4bdf98c7cc..9e7ee5d20c8 100644 --- a/Documentation/docs/contributing/module_workflows.md +++ b/Documentation/docs/contributing/module_workflows.md @@ -36,6 +36,28 @@ jobs: pypi_password: ${{ secrets.pypi_password }} ``` +## Building an external module against an installed ITK + +An external module can be configured, built, and Python-wrapped against an +installed ITK prefix rather than an ITK build tree. Point `ITK_DIR` at the +installed package directory, for example +`/lib/cmake/ITK-6.0`, and configure the module as usual. + +Wrapping additionally requires the SWIG type indices and the wrapping CMake +infrastructure, which ITK installs only on request because the payload is +tens of megabytes. To make an installed prefix usable as a wrapping SDK, the +ITK being installed must be configured with: + +```bash +cmake -DITK_WRAP_PYTHON:BOOL=ON \ + -DITK_INSTALL_WRAPPING_DEVELOPMENT_FILES:BOOL=ON +``` + +then rebuilt and reinstalled. Configuring a module with `ITK_WRAP_PYTHON=ON` +against a prefix that lacks these files fails with "Could not find wrapping +infrastructure"; the remedy is to reconfigure, rebuild, and reinstall the ITK +the module points at, not to change anything in the module. + ## Further Reading For more information visit README documentation at the [ITKRemoteModuleBuildTestPackageAction](https://github.com/InsightSoftwareConsortium/ITKRemoteModuleBuildTestPackageAction/blob/main/README.md#itkremotemodulebuildtestpackageaction) project. diff --git a/Wrapping/CMakeLists.txt b/Wrapping/CMakeLists.txt index 1f4d8c95cd3..c09bcd38aae 100644 --- a/Wrapping/CMakeLists.txt +++ b/Wrapping/CMakeLists.txt @@ -267,3 +267,57 @@ if(ITK_WRAP_PYTHON) unset(ITK_STUB_DIR CACHE) unset(ITK_PKL_DIR CACHE) endif() + +# Lands under the package directory, where an installed consumer's ITK_DIR already points. +if(ITK_INSTALL_WRAPPING_DEVELOPMENT_FILES) + set(_itk_wrapping_install_dir "${ITK_INSTALL_PACKAGE_DIR}/Wrapping") + + macro(_itk_install_wrapping_development _source_dir _destination) + install( + DIRECTORY + "${_source_dir}/" + DESTINATION "${_destination}" + COMPONENT ${WRAP_ITK_INSTALL_COMPONENT_IDENTIFIER}WrappingDevelopment + FILES_MATCHING + ${ARGN} + ) + endmacro() + + _itk_install_wrapping_development( + "${WrapITK_SOURCE_DIR}" + "${_itk_wrapping_install_dir}" + PATTERN + "*.cmake" + PATTERN + "CMakeLists.txt" + PATTERN + "*.i" + PATTERN + "*.in" + PATTERN + "*.py" + PATTERN + "*.h" + PATTERN + "images" + EXCLUDE + PATTERN + "Tests" + EXCLUDE + ) + + _itk_install_wrapping_development( + "${WRAP_ITK_TYPEDEFS_DIRECTORY}" + "${_itk_wrapping_install_dir}/Typedefs" + PATTERN + "*.i" + PATTERN + "*.idx" + PATTERN + "*.mdx" + PATTERN + "*.h" + ) + + unset(_itk_wrapping_install_dir) +endif() diff --git a/Wrapping/WrappingOptions.cmake b/Wrapping/WrappingOptions.cmake index 8b1fd7d71a4..cd4ff6817fe 100644 --- a/Wrapping/WrappingOptions.cmake +++ b/Wrapping/WrappingOptions.cmake @@ -13,6 +13,16 @@ else() set(ITK_WRAPPING OFF CACHE INTERNAL "Build external languages support" FORCE) endif() +# Opt-in: the SWIG type indices are tens of MB. +cmake_dependent_option( + ITK_INSTALL_WRAPPING_DEVELOPMENT_FILES + "Install the wrapping infrastructure and SWIG type indices so that external modules can be wrapped against an installed ITK" + OFF + "ITK_WRAPPING" + OFF +) +mark_as_advanced(ITK_INSTALL_WRAPPING_DEVELOPMENT_FILES) + cmake_dependent_option( ITK_PYTHON_RELEASE_GIL "Release Python Global Interpreter Lock (GIL) during ITK operations" From d88b1ade5a428d8e20e61531ec3054877a23f85a Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Mon, 21 Sep 2026 07:17:50 -0500 Subject: [PATCH 3/5] COMP: Give castxml ITK's include directories Wrapping an external module against an installed ITK failed in castxml with "'itkCommand.h' file not found" although the header was installed: the generated response file carried only the module's own include directories. In a build tree the dependency targets supply ITK's; from an install tree they contribute nothing. ITKConfig.cmake sets ITK_INCLUDE_DIRS in both trees, so it covers the installed case. It is not empty in a build tree: it accumulates the include directories of every module find_package requested, so the response file widens there too. --- Wrapping/macro_files/itk_auto_load_submodules.cmake | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Wrapping/macro_files/itk_auto_load_submodules.cmake b/Wrapping/macro_files/itk_auto_load_submodules.cmake index 4ca246ebc1e..32cd4af007d 100644 --- a/Wrapping/macro_files/itk_auto_load_submodules.cmake +++ b/Wrapping/macro_files/itk_auto_load_submodules.cmake @@ -105,6 +105,10 @@ function(generate_castxml_commandline_flags) # create the files used to pass the file to include to castxml set(include_dir_list ${WRAPPER_LIBRARY_INCLUDE_DIRECTORIES}) + # From an install tree the dependency targets carry no ITK include dirs. + if(ITK_INCLUDE_DIRS) + list(APPEND include_dir_list ${ITK_INCLUDE_DIRS}) + endif() list(REMOVE_DUPLICATES include_dir_list) # CONFIG_CASTXML_INC_CONTENTS - variable used for building contents to write with file(GENERATE) From b8c8753c4d6186c609c4eff9d64b34c5d01ead65 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 09:58:13 -0500 Subject: [PATCH 4/5] COMP: Wrap external modules without the IO factory register manager A consumer tree that includes UseITK gets ITK__FACTORY_REGISTER_MANAGER on every target, including an external module's Python wrapping library. Wrapped classes that include itkImageFileReader.h then reference the register function of every IO factory the installed ITK enables, and the wrapping library, which links only its module's dependencies, fails to load with unresolved symbols. Python registers the factories at import, so ITKModuleExternal asks the wrapping directory to drop them and itk_wrap_module removes the ones ITK_FACTORY_LIST names. The request uses a variable only itk_wrap_module reads, so a scope added after the wrapping directory, such as a module's examples, cannot lose its factory registration. ITK's own build never includes UseITK, so in-tree wrapping is unchanged, and a module's library, tests, and examples keep their managers. --- CMake/ITKModuleExternal.cmake | 2 ++ Wrapping/TypedefMacros.cmake | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/CMake/ITKModuleExternal.cmake b/CMake/ITKModuleExternal.cmake index 9e695477110..6347832be2b 100644 --- a/CMake/ITKModuleExternal.cmake +++ b/CMake/ITKModuleExternal.cmake @@ -267,6 +267,8 @@ if(ITK_WRAPPING) ) endif() include("${WRAP_ITK_CMAKE_DIR}/TypedefMacros.cmake") + # Python registers the factories at import; the wrapping library must not. + set(ITK_WRAPPING_NO_FACTORY_REGISTER_MANAGER ON) add_subdirectory( "${WRAP_ITK_CMAKE_DIR}" ${CMAKE_CURRENT_BINARY_DIR}/Wrapping diff --git a/Wrapping/TypedefMacros.cmake b/Wrapping/TypedefMacros.cmake index 4d034eab42d..71a74ba01e0 100644 --- a/Wrapping/TypedefMacros.cmake +++ b/Wrapping/TypedefMacros.cmake @@ -133,6 +133,15 @@ macro(itk_wrap_module library_name) set(WRAPPER_LIBRARY_NAME "${library_name}") message(STATUS "${WRAPPER_LIBRARY_NAME}: Creating module.") + # Drop the register managers the consumer tree's UseITK put on this directory. + if(ITK_WRAPPING_NO_FACTORY_REGISTER_MANAGER) + foreach(_factory_name ${ITK_FACTORY_LIST}) + string(TOUPPER ${_factory_name} _factory_uc) + remove_definitions(-DITK_${_factory_uc}_FACTORY_REGISTER_MANAGER) + endforeach() + unset(_factory_uc) + endif() + # Mark the current source dir for inclusion because it may contain header files. include_directories(BEFORE "${CMAKE_CURRENT_SOURCE_DIR}") include_directories(BEFORE ${WRAPPER_LIBRARY_INCLUDE_DIRECTORIES}) From 79cb0cc5d77a96e40686761d5342625f19f7c67a Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 13:17:14 -0500 Subject: [PATCH 5/5] COMP: Build external module tests against an installed ITK With BUILD_TESTING=ON, ITK_USE_KWSTYLE defaults on, and when no KWStyle is found the module build includes Utilities/KWStyle/BuildKWStyle.cmake to build one, which exists only in a source tree; an external module then fails to configure against an installed ITK. Build KWStyle only when the source tree provides it, so an install-tree consumer falls through to the existing branch that turns ITK_USE_KWSTYLE off. The InDoxygenGroup test ran Utilities/Doxygen/mcdoc.py by a source-relative path. Install the script into the package directory and resolve it the way BuildHeaderTest.py is resolved. The header test and the test driver pick up UseITK's IO factory register manager through itkImageFileReader.h, which references every IO factory the ITK enables. The header test linked only the module, and the test driver linked the module's test dependencies and ITKTestKernel, which covers the default IO modules but not the ones an ITK built with extra IO modules enables; both failed with unresolved symbols. Link ITK_LIBRARIES for external modules, as any consumer executable must. --- CMake/ITKModuleDoxygen.cmake | 9 ++++++++- CMake/ITKModuleHeaderTest.cmake | 4 ++++ CMake/ITKModuleKWStyleTest.cmake | 4 ++++ CMake/ITKModuleTest.cmake | 4 ++++ CMakeLists.txt | 1 + 5 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CMake/ITKModuleDoxygen.cmake b/CMake/ITKModuleDoxygen.cmake index 7a1ca952840..3af92802f76 100644 --- a/CMake/ITKModuleDoxygen.cmake +++ b/CMake/ITKModuleDoxygen.cmake @@ -3,6 +3,13 @@ # ${_name}.dot file which defines the local dependency as graph # which will be then processed by dot +# /CMake in a build tree, the installed package directory otherwise. +if(EXISTS "${ITK_CMAKE_DIR}/../Utilities/Doxygen/mcdoc.py") + set(ITK_MCDOC_SCRIPT "${ITK_CMAKE_DIR}/../Utilities/Doxygen/mcdoc.py") +else() + set(ITK_MCDOC_SCRIPT "${ITK_CMAKE_DIR}/mcdoc.py") +endif() + macro(itk_module_doxygen _name) # _content defines the content of the ${_name}.dox file set(_content "/**\n") @@ -49,7 +56,7 @@ macro(itk_module_doxygen _name) NAME ${_name}InDoxygenGroup COMMAND ${Python3_EXECUTABLE} - "${ITK_CMAKE_DIR}/../Utilities/Doxygen/mcdoc.py" + "${ITK_MCDOC_SCRIPT}" check ${_name} ${${_name}_SOURCE_DIR}/include diff --git a/CMake/ITKModuleHeaderTest.cmake b/CMake/ITKModuleHeaderTest.cmake index cb7a1d04873..d9570f7cd90 100644 --- a/CMake/ITKModuleHeaderTest.cmake +++ b/CMake/ITKModuleHeaderTest.cmake @@ -135,6 +135,10 @@ macro(itk_module_headertest _name) ${ITK_MODULE_${_name}_TARGETS_NAMESPACE}${_name}Module ${ITK_MODULE_${_name}_TARGETS_NAMESPACE}ITKKWSysModule ) + # UseITK's register manager references every IO factory the ITK enables. + if(NOT ITK_SOURCE_DIR) + target_link_libraries(${_test_name} PRIVATE ${ITK_LIBRARIES}) + endif() target_link_options( ${_test_name} diff --git a/CMake/ITKModuleKWStyleTest.cmake b/CMake/ITKModuleKWStyleTest.cmake index 69a5060303a..bc5abbca804 100644 --- a/CMake/ITKModuleKWStyleTest.cmake +++ b/CMake/ITKModuleKWStyleTest.cmake @@ -32,7 +32,11 @@ if( AND NOT CMAKE_CROSSCOMPILING + AND + EXISTS + "${ITK_CMAKE_DIR}/../Utilities/KWStyle/BuildKWStyle.cmake" ) + # Only an ITK source tree carries BuildKWStyle.cmake; an install tree does not. include(${ITK_CMAKE_DIR}/../Utilities/KWStyle/BuildKWStyle.cmake) elseif(NOT KWSTYLE_FOUND) set(ITK_USE_KWSTYLE OFF) diff --git a/CMake/ITKModuleTest.cmake b/CMake/ITKModuleTest.cmake index 86aefd6c49b..720f4a4409a 100644 --- a/CMake/ITKModuleTest.cmake +++ b/CMake/ITKModuleTest.cmake @@ -87,6 +87,10 @@ EM_ASM( ${KIT_LIBS} ${ITKTestKernel_LIBRARIES} ) + # UseITK's register manager references every IO factory the ITK enables. + if(NOT ITK_SOURCE_DIR) + target_link_libraries(${KIT}TestDriver PRIVATE ${ITK_LIBRARIES}) + endif() target_link_options( ${KIT}TestDriver PRIVATE diff --git a/CMakeLists.txt b/CMakeLists.txt index 06777c24f4d..508dce5fc8e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -898,6 +898,7 @@ install( CMake/ITKModuleCPPCheckTest.cmake CMake/CppcheckTargets.cmake Utilities/Maintenance/BuildHeaderTest.py + Utilities/Doxygen/mcdoc.py DESTINATION ${ITK_INSTALL_PACKAGE_DIR} COMPONENT Development )