From ddd04313d4bde22a0a595e523a43dd87c4aa40aa Mon Sep 17 00:00:00 2001 From: Cavan Riley Date: Sun, 21 Jun 2026 13:59:42 -0500 Subject: [PATCH 01/14] ENH: Add in-tree Python wheel definitions that slice a pre-built ITK Add Wrapping/Packaging/, the wheel definitions that build ITK's Python wheels from this repository by slicing a single pre-built, Python-wrapped ITK tree into per-group wheels, with zero ITK recompilation per wheel. ITK already emits a per-module install component (PythonWheelRuntimeLibraries) when configured with WRAP_ITK_INSTALL_COMPONENT_PER_MODULE=ON; that is the slicing seam. This change adds the wheel definitions on top of it: * one per-group pyproject.toml (core, numerics, io, filtering, registration, segmentation) plus a platform/ABI-tagged "itk" umbrella, each a standard PEP 517 project consumed by scikit-build-core; * the module placement and per-component install CMake (relocated from ITKPythonPackage, Apache-2.0), driven by ITKPythonPackage_WHEEL_NAME; * a shared CMakeLists.txt that reuses a pre-built ITK tree (ITKPythonPackage_ITK_BINARY_REUSE) instead of recompiling. Each group wheel tracks ITK's version; the "itk" meta wheel carries platform and ABI tags (matching the PyPI itk wheel) rather than py3-none-any so that "pip install itk" only resolves where the group wheels exist. --- Wrapping/Packaging/.gitignore | 5 + Wrapping/Packaging/CMakeLists.txt | 36 +++ Wrapping/Packaging/WHEEL_NAMES.txt | 7 + .../cmake/ITKPythonPackage_BuildWheels.cmake | 258 ++++++++++++++++++ .../cmake/ITKPythonPackage_Utils.cmake | 256 +++++++++++++++++ Wrapping/Packaging/core/pyproject.toml | 24 ++ Wrapping/Packaging/filtering/pyproject.toml | 22 ++ Wrapping/Packaging/io/pyproject.toml | 22 ++ Wrapping/Packaging/meta/CMakeLists.txt | 6 + Wrapping/Packaging/meta/pyproject.toml | 30 ++ Wrapping/Packaging/numerics/pyproject.toml | 22 ++ .../Packaging/registration/pyproject.toml | 22 ++ .../Packaging/segmentation/pyproject.toml | 22 ++ 13 files changed, 732 insertions(+) create mode 100644 Wrapping/Packaging/.gitignore create mode 100644 Wrapping/Packaging/CMakeLists.txt create mode 100644 Wrapping/Packaging/WHEEL_NAMES.txt create mode 100644 Wrapping/Packaging/cmake/ITKPythonPackage_BuildWheels.cmake create mode 100644 Wrapping/Packaging/cmake/ITKPythonPackage_Utils.cmake create mode 100644 Wrapping/Packaging/core/pyproject.toml create mode 100644 Wrapping/Packaging/filtering/pyproject.toml create mode 100644 Wrapping/Packaging/io/pyproject.toml create mode 100644 Wrapping/Packaging/meta/CMakeLists.txt create mode 100644 Wrapping/Packaging/meta/pyproject.toml create mode 100644 Wrapping/Packaging/numerics/pyproject.toml create mode 100644 Wrapping/Packaging/registration/pyproject.toml create mode 100644 Wrapping/Packaging/segmentation/pyproject.toml diff --git a/Wrapping/Packaging/.gitignore b/Wrapping/Packaging/.gitignore new file mode 100644 index 00000000000..fa8babf73bb --- /dev/null +++ b/Wrapping/Packaging/.gitignore @@ -0,0 +1,5 @@ +# Wheel build outputs (produced by Utilities/Maintenance/build_wheels.py) +dist/ +*/build/ +*.whl +__pycache__/ diff --git a/Wrapping/Packaging/CMakeLists.txt b/Wrapping/Packaging/CMakeLists.txt new file mode 100644 index 00000000000..beb1498e641 --- /dev/null +++ b/Wrapping/Packaging/CMakeLists.txt @@ -0,0 +1,36 @@ +# Shared scikit-build-core source for every ITK wheel group. +cmake_minimum_required(VERSION 3.26.6 FATAL_ERROR) # first cmake with SABIModule + +project(ITKWheels CXX) + +# Default to the repository root (two levels up). +if(NOT DEFINED ITK_SOURCE_DIR) + get_filename_component( + ITK_SOURCE_DIR + "${CMAKE_CURRENT_SOURCE_DIR}/../.." + ABSOLUTE + ) +endif() + +# Pre-built, Python-wrapped ITK tree to reuse; supplied by the driver. +if( + NOT + DEFINED + ITK_BINARY_DIR + OR + NOT + EXISTS + "${ITK_BINARY_DIR}/cmake_install.cmake" +) + message( + FATAL_ERROR + "ITK_BINARY_DIR must point at a pre-built ITK build tree containing " + "cmake_install.cmake. Got: '${ITK_BINARY_DIR}'" + ) +endif() + +message(STATUS "ITK_SOURCE_DIR = ${ITK_SOURCE_DIR}") +message(STATUS "ITK_BINARY_DIR = ${ITK_BINARY_DIR} (reused, not rebuilt)") + +include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/ITKPythonPackage_Utils.cmake) +include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/ITKPythonPackage_BuildWheels.cmake) diff --git a/Wrapping/Packaging/WHEEL_NAMES.txt b/Wrapping/Packaging/WHEEL_NAMES.txt new file mode 100644 index 00000000000..434c06c1a6a --- /dev/null +++ b/Wrapping/Packaging/WHEEL_NAMES.txt @@ -0,0 +1,7 @@ +itk-core +itk-numerics +itk-io +itk-filtering +itk-registration +itk-segmentation +itk-meta diff --git a/Wrapping/Packaging/cmake/ITKPythonPackage_BuildWheels.cmake b/Wrapping/Packaging/cmake/ITKPythonPackage_BuildWheels.cmake new file mode 100644 index 00000000000..a2b6a3b797d --- /dev/null +++ b/Wrapping/Packaging/cmake/ITKPythonPackage_BuildWheels.cmake @@ -0,0 +1,258 @@ +#----------------------------------------------------------------------------- +#------------------------------------------------------ +#---------------------------------- +# ITKPythonPackage_SUPERBUILD: OFF +#---------------------------------- +#------------------------------------------------------ +#----------------------------------------------------------------------------- +if(NOT DEFINED ITKPythonPackage_WHEEL_NAME) + message(FATAL_ERROR "ITKPythonPackage_WHEEL_NAME must be defined") +endif() + +message( + STATUS + "SuperBuild - ITKPythonPackage_WHEEL_NAME:${ITKPythonPackage_WHEEL_NAME}" +) + +set(components "PythonWheelRuntimeLibraries") + +message(STATUS "ITKPythonPackage_WHEEL_NAME: ${ITKPythonPackage_WHEEL_NAME}") + +# Extract ITK group name from wheel name +message(STATUS "") +set(msg "Extracting ITK_WHEEL_GROUP") +message(STATUS ${msg}) +ipp_wheel_to_group(${ITKPythonPackage_WHEEL_NAME} ITK_WHEEL_GROUP) +message(STATUS "${msg} - done [${ITK_WHEEL_GROUP}]") + +# +# Considering that +# +# * Every ITK module is associated with exactly one ITK group. +# * ITK module dependencies are specified independently of ITK groups +# +# we semi-arbitrarily defined a collection of wheels (see ``ITK_WHEEL_GROUPS``) +# that will roughly bundle the modules associated with each group. +# +# Based on the module dependency graph, the code below will determine which module +# should be packaged in which wheel. +# + +# List of ITK wheel groups +set(ITK_WHEEL_GROUPS "") +file(STRINGS "${CMAKE_SOURCE_DIR}/WHEEL_NAMES.txt" ITK_WHEELS REGEX "^itk-.+") +foreach(wheel_name IN LISTS ITK_WHEELS) + ipp_wheel_to_group(${wheel_name} group) + list(APPEND ITK_WHEEL_GROUPS ${group}) +endforeach() + +# Define below a reasonable dependency graph for ITK groups +set(ITK_GROUP_Core_DEPENDS) +set(ITK_GROUP_IO_DEPENDS Core) +set(ITK_GROUP_Numerics_DEPENDS Core) +set(ITK_GROUP_Filtering_DEPENDS Numerics) +set(ITK_GROUP_Segmentation_DEPENDS Filtering) +set(ITK_GROUP_Registration_DEPENDS Filtering) +set(ITK_GROUP_Video_DEPENDS Core) + +# ITK is needed to retrieve ITK module information +set(ITK_DIR ${ITK_BINARY_DIR}) +find_package(ITK REQUIRED) +set( + CMAKE_MODULE_PATH + ${ITK_CMAKE_DIR} + ${CMAKE_MODULE_PATH} +) + +# Sort wheel groups +include(TopologicalSort) +topological_sort(ITK_WHEEL_GROUPS ITK_GROUP_ _DEPENDS) + +# Set ``ITK_MODULE__DEPENDS`` variables +# +# Notes: +# +# * ``_DEPENDS`` variables are set after calling ``find_package(ITK REQUIRED)`` +# +# * This naming convention corresponds to what is used internally in ITK and allow +# to differentiate with variable like ``ITK_GROUP__DEPENDS`` set above. +# +foreach(module IN LISTS ITK_MODULES_ENABLED) + set(ITK_MODULE_${module}_DEPENDS "${${module}_DEPENDS}") +endforeach() + +# Set ``ITK_MODULE__DEPENDEES`` variables +foreach(module IN LISTS ITK_MODULES_ENABLED) + ipp_get_module_dependees(${module} ITK_MODULE_${module}_DEPENDEES) +endforeach() + +# Set ``ITK_GROUPS`` variable +file(GLOB group_dirs "${ITK_SOURCE_DIR}/Modules/*") +set(ITK_GROUPS) +foreach(dir IN LISTS group_dirs) + file(RELATIVE_PATH group "${ITK_SOURCE_DIR}/Modules" "${dir}") + if(NOT IS_DIRECTORY "${dir}" OR "${group}" MATCHES "^External$") + continue() + endif() + list(APPEND ITK_GROUPS ${group}) +endforeach() +message(STATUS "") +message(STATUS "ITK_GROUPS:${ITK_GROUPS}") + +# Set ``ITK_MODULE__GROUP`` variables +foreach(group IN LISTS ITK_GROUPS) + file( + GLOB_RECURSE _${group}_module_files + ${ITK_SOURCE_DIR}/Modules/${group}/itk-module.cmake + ) + foreach(_module_file ${_${group}_module_files}) + file(READ ${_module_file} _module_file_content) + string( + REGEX + MATCH + "itk_module[ \n]*(\\([ \n]*)([A-Za-z0-9]*)" + _module_name + ${_module_file_content} + ) + set(_module_name ${CMAKE_MATCH_2}) + list(APPEND _${group}_module_list ${_module_name}) + set(ITK_MODULE_${_module_name}_GROUP ${group}) + endforeach() +endforeach() + +# Initialize ``ITK_WHEEL__MODULES`` variables that will contain list of modules +# to package in each wheel. +foreach(group IN LISTS ITK_WHEEL_GROUPS) + set(ITK_WHEEL_${group}_MODULES "") +endforeach() + +# Configure table display +set( + row_widths + 40 + 20 + 20 + 10 + 90 + 12 +) +set( + row_headers + MODULE_NAME + MODULE_GROUP + WHEEL_GROUP + IS_LEAF + MODULE_DEPENDEES_GROUPS + IS_WRAPPED +) +message(STATUS "") +ipp_display_table_row("${row_headers}" "${row_widths}") + +# Update ``ITK_WHEEL__MODULES`` variables +foreach(module IN LISTS ITK_MODULES_ENABLED) + ipp_is_module_leaf(${module} leaf) + set(dependees_groups) + if(NOT leaf) + set(dependees "") + ipp_recursive_module_dependees(${module} dependees) + foreach(dep IN LISTS dependees) + list(APPEND dependees_groups ${ITK_MODULE_${dep}_GROUP}) + endforeach() + if(dependees_groups) + list(REMOVE_DUPLICATES dependees_groups) + endif() + endif() + + # Filter out group not associated with a wheel + set(dependees_wheel_groups) + foreach(group IN LISTS dependees_groups) + list(FIND ITK_WHEEL_GROUPS ${group} _index) + if(_index EQUAL -1) + continue() + endif() + list(APPEND dependees_wheel_groups ${group}) + endforeach() + + set(wheel_group) + list(LENGTH dependees_wheel_groups _length) + + # Sanity check + if(leaf AND _length GREATER 0) + message(FATAL_ERROR "leaf module should not module depending on them !") + endif() + + if(_length EQUAL 0) + set(wheel_group "${ITK_MODULE_${module}_GROUP}") + elseif(_length EQUAL 1) + # Since packages depending on this module belong to one group, also package this module + set(wheel_group "${dependees_wheel_groups}") + elseif(_length GREATER 1) + # If more than one group is associated with the dependees, package the module in the + # "common ancestor" group. + set(common_ancestor_index 999999) + foreach(g IN LISTS dependees_wheel_groups) + list(FIND ITK_WHEEL_GROUPS ${g} _index) + if(NOT _index EQUAL -1 AND _index LESS common_ancestor_index) + set(common_ancestor_index ${_index}) + endif() + endforeach() + list(GET ITK_WHEEL_GROUPS ${common_ancestor_index} wheel_group) + endif() + + set(wheel_group_display ${wheel_group}) + + # XXX Hard-coded dispatch + if(module STREQUAL "ITKBridgeNumPy") + set(new_wheel_group "Core") + set(wheel_group_display "${new_wheel_group} (was ${wheel_group})") + set(wheel_group ${new_wheel_group}) + endif() + if(module STREQUAL "ITKVTK") + set(new_wheel_group "Core") + set(wheel_group_display "${new_wheel_group} (was ${wheel_group})") + set(wheel_group ${new_wheel_group}) + endif() + + # Associate module with a wheel + list(APPEND ITK_WHEEL_${wheel_group}_MODULES ${module}) + + # Display module info + ipp_is_module_python_wrapped(${module} is_wrapped) + ipp_list_to_string("^^" "${dependees_groups}" dependees_groups_str) + set( + row_values + "${module};${ITK_MODULE_${module}_GROUP};${wheel_group_display};${leaf};${dependees_groups_str};${is_wrapped}" + ) + ipp_display_table_row("${row_values}" "${row_widths}") +endforeach() + +# Set list of components to install +set(components "") +foreach(module IN LISTS ITK_WHEEL_${ITK_WHEEL_GROUP}_MODULES) + list(APPEND components ${module}PythonWheelRuntimeLibraries) +endforeach() + +if(MSVC AND ITKPythonPackage_WHEEL_NAME STREQUAL "itk-core") + message(STATUS "Adding install rules for compiler runtime libraries") + # Put the runtime libraries next to the "itk/_*.pyd" C-extensions so they + # are found. + set(CMAKE_INSTALL_SYSTEM_RUNTIME_DESTINATION "itk") + include(InstallRequiredSystemLibraries) +endif() + +#----------------------------------------------------------------------------- +# Install ITK components +message(STATUS "Adding install rules for components:") +foreach(component IN LISTS components) + message(STATUS " ${component}") + install( + CODE + " +unset(CMAKE_INSTALL_COMPONENT) +set(COMPONENT \"${component}\") +set(CMAKE_INSTALL_DO_STRIP 1) +include(\"${ITK_BINARY_DIR}/cmake_install.cmake\") +unset(CMAKE_INSTALL_COMPONENT) +" + ) +endforeach() diff --git a/Wrapping/Packaging/cmake/ITKPythonPackage_Utils.cmake b/Wrapping/Packaging/cmake/ITKPythonPackage_Utils.cmake new file mode 100644 index 00000000000..fd41c140bd1 --- /dev/null +++ b/Wrapping/Packaging/cmake/ITKPythonPackage_Utils.cmake @@ -0,0 +1,256 @@ +# ipp_ExternalProject_Add_Empty( ) +# +# Add an empty external project +# +function(ipp_ExternalProject_Add_Empty proj depends) + set(depends_args) + if(NOT depends STREQUAL "") + set( + depends_args + DEPENDS + ${depends} + ) + endif() + ExternalProject_Add( + ${proj} + SOURCE_DIR ${CMAKE_BINARY_DIR}/${proj} + DOWNLOAD_COMMAND + "" + UPDATE_COMMAND + "" + CONFIGURE_COMMAND + "" + BUILD_COMMAND + "" + BUILD_IN_SOURCE 1 + BUILD_ALWAYS 1 + INSTALL_COMMAND + "" ${depends_args} + ) +endfunction() + +# ipp_get_module_dependees( ) +# +# Collect all modules depending on ````. +# +function(ipp_get_module_dependees itk-module output_var) + set(dependees "") + foreach(m_enabled IN LISTS ITK_MODULES_ENABLED) + list(FIND ITK_MODULE_${m_enabled}_DEPENDS ${itk-module} _index) + if(NOT _index EQUAL -1) + list(APPEND dependees ${m_enabled}) + endif() + endforeach() + list(REMOVE_DUPLICATES dependees) + set(${output_var} ${dependees} PARENT_SCOPE) +endfunction() + +function(_recursive_deps item-type item-category itk-item output_var) + set(_${itk-item}_deps) + foreach(dep IN LISTS ITK_${item-type}_${itk-item}_${item-category}) + list(APPEND _${itk-item}_deps ${dep}) + _recursive_deps(${item-type} ${item-category} ${dep} _${itk-item}_deps) + endforeach() + list(APPEND ${output_var} ${_${itk-item}_deps}) + list(REMOVE_DUPLICATES ${output_var}) + set(${output_var} ${${output_var}} PARENT_SCOPE) +endfunction() + +# ipp_recursive_module_dependees( ) +# +# Recursively collect all modules depending on ````. +# +function(ipp_recursive_module_dependees itk-module output_var) + set(_${itk-module}_deps) + _recursive_deps("MODULE" "DEPENDEES" ${itk-module} ${output_var}) + set(${output_var} ${${output_var}} PARENT_SCOPE) +endfunction() + +# ipp_is_module_leaf( ) +# +# If ```` has no dependencies, set `` to 1 +# otherwise set `` to 0. +# +function(ipp_is_module_leaf itk-module output_var) + set(leaf 1) + foreach(m_enabled IN LISTS ITK_MODULES_ENABLED) + list(FIND ITK_MODULE_${m_enabled}_DEPENDS ${itk-module} _index) + if(NOT _index EQUAL -1) + set(leaf 0) + break() + endif() + endforeach() + set(${output_var} ${leaf} PARENT_SCOPE) +endfunction() + +# ipp_is_module_python_wrapped( ) +# +# If ```` is wrapped in python, set `` to 1 +# otherwise set `` to 0. +# +function(ipp_is_module_python_wrapped itk-module output_var) + set(wrapped 0) + if(NOT DEFINED ITK_MODULE_${itk-module}_GROUP) + message( + AUTHOR_WARNING + "Variable ITK_MODULE_${itk-module}_GROUP is not defined" + ) + else() + set(group ${ITK_MODULE_${itk-module}_GROUP}) + set(module_folder ${itk-module}) + # if any, strip ITK prefix + if(module_folder MATCHES "^ITK.+$") + string(REGEX REPLACE "^ITK(.+)$" "\\1" module_folder ${module_folder}) + endif() + if( + EXISTS + ${ITK_SOURCE_DIR}/Modules/${group}/${itk-module}/wrapping/CMakeLists.txt + OR + EXISTS + ${ITK_SOURCE_DIR}/Modules/${group}/${module_folder}/wrapping/CMakeLists.txt + ) + set(wrapped 1) + endif() + endif() + set(${output_var} ${wrapped} PARENT_SCOPE) +endfunction() + +# ipp_wheel_to_group( ) +# +# Extract ITK group name from wheel name (e.g 'itk-core' -> 'Core'). +# +# If the group name has less than 3 characters, take the uppercase +# value (e.g 'itk-io' -> 'IO'). +# +function(ipp_wheel_to_group wheel_name group_name_var) + string(REPLACE "itk-" "" _group ${wheel_name}) + string(SUBSTRING ${_group} 0 1 _first) + string(TOUPPER ${_first} _first_uc) + string(SUBSTRING ${_group} 1 -1 _remaining) + set(group_name "${_first_uc}${_remaining}") + # Convert to upper case if length <= 2 + string(LENGTH ${group_name} _length) + if(_length LESS 3) + string(TOUPPER ${group_name} group_name) + endif() + set(${group_name_var} ${group_name} PARENT_SCOPE) +endfunction() + +# ipp_pad_text( ) +# +# Example: +# +# set(row "Apple") +# ipp_pad_text(${row} 20 row) +# +# set(row "${row}Banana") +# ipp_pad_text(${row} 40 row) +# +# set(row "${row}Kiwi") +# ipp_pad_text(${row} 60 row) +# +# message(${row}) +# +# Output: +# +# Apple Banana Kiwi +# +function(ipp_pad_text text text_right_jusitfy_length output_var) + set(fill_char " ") + string(LENGTH "${text}" text_length) + math(EXPR pad_length "${text_right_jusitfy_length} - ${text_length} - 1") + if(pad_length GREATER 0) + string(RANDOM LENGTH ${pad_length} ALPHABET ${fill_char} text_dots) + set(${output_var} "${text} ${text_dots}" PARENT_SCOPE) + else() + set(${output_var} "${text}" PARENT_SCOPE) + endif() +endfunction() + +# ipp_display_table_row( ) +# +# Example: +# +# ipp_display_table_row("Apple^^Banana^^Kiwi" "20;20;20") +# ipp_display_table_row("Eiger^^Rainer^^Sajama" "20;20;20") +# +# Output: +# +# Apple Banana Kiwi +# Eiger Rainer Sajama +# +function(ipp_display_table_row values widths) + list(LENGTH values length) + set(text "") + math(EXPR range "${length} - 1") + foreach(index RANGE ${range}) + list(GET widths ${index} width) + list(GET values ${index} value) + string(REPLACE "^^" ";" value "${value}") + ipp_pad_text("${value}" ${width} value) + set(text "${text}${value}") + endforeach() + message(STATUS "${text}") +endfunction() + +# ipp_list_to_string( ) +# +# Example: +# +# set(values Foo Bar Oof) +# message("${values}") +# ipp_list_to_string("^^" "${values}" values) +# message("${values}") +# +# Output: +# +# Foo;Bar;Oof +# Foo^^Bar^^Oof +# +# Copied from Slicer/CMake/ListToString.cmake +# +function(ipp_list_to_string separator input_list output_string_var) + set(_string "") + # Get list length + list(LENGTH input_list list_length) + # If the list has 0 or 1 element, there is no need to loop over. + if(list_length LESS 2) + set(_string "${input_list}") + else() + math(EXPR last_element_index "${list_length} - 1") + foreach(index RANGE ${last_element_index}) + # Get current item_value + list(GET input_list ${index} item_value) + if(NOT item_value STREQUAL "") + # .. and append non-empty value to output string + set(_string "${_string}${item_value}") + # Append separator if current element is NOT the last one. + if(NOT index EQUAL last_element_index) + set(_string "${_string}${separator}") + endif() + endif() + endforeach() + endif() + set(${output_string_var} ${_string} PARENT_SCOPE) +endfunction() + +# No-op function allowing to shut-up "Manually-specified variables were not used by the project" +# warnings. +function(ipp_unused_vars) +endfunction() + +# +# Unused +# + +function(recursive_module_deps itk-module output_var) + set(_${itk-module}_deps) + _recursive_deps("MODULE" "DEPENDS" ${itk-module} ${output_var}) + set(${output_var} ${${output_var}} PARENT_SCOPE) +endfunction() + +function(recursive_group_deps itk-group output_var) + set(_${itk-group}_deps) + _recursive_deps("GROUP" "DEPENDS" ${itk-group} ${output_var}) + set(${output_var} ${${output_var}} PARENT_SCOPE) +endfunction() diff --git a/Wrapping/Packaging/core/pyproject.toml b/Wrapping/Packaging/core/pyproject.toml new file mode 100644 index 00000000000..d5df7118e8a --- /dev/null +++ b/Wrapping/Packaging/core/pyproject.toml @@ -0,0 +1,24 @@ +# itk-core — foundation wheel. Build defines are passed at build time; see +# ../README.md. +[build-system] +requires = ["scikit-build-core>=0.10"] +build-backend = "scikit_build_core.build" + +[project] +name = "itk-core" +version = "6.0b2" # tracks the ITK version this wheel slices +description = "ITK core modules — image classes, the NumPy bridge, and runtime." +readme = "../README.md" +requires-python = ">=3.9" +dependencies = [ + "numpy", # core depends on nothing else in the ITK set +] + +[tool.scikit-build] +# Shared CMake source; the wheel is selected by ITKPythonPackage_WHEEL_NAME. +cmake.source-dir = ".." +cmake.build-type = "Release" +cmake.version = ">=3.26.6" +build-dir = "build/{wheel_tag}" +wheel.platlib = true # carries compiled .so/.pyd +install.strip = true diff --git a/Wrapping/Packaging/filtering/pyproject.toml b/Wrapping/Packaging/filtering/pyproject.toml new file mode 100644 index 00000000000..0acb3ba8ac2 --- /dev/null +++ b/Wrapping/Packaging/filtering/pyproject.toml @@ -0,0 +1,22 @@ +# itk-filtering — built by slicing the pre-built ITK tree (see ../README.md). +[build-system] +requires = ["scikit-build-core>=0.10"] +build-backend = "scikit_build_core.build" + +[project] +name = "itk-filtering" +version = "6.0b2" +description = "ITK filtering modules (image filters, smoothing, thresholding)." +readme = "../README.md" +requires-python = ">=3.9" +dependencies = [ + "itk-numerics==6.0b2", +] + +[tool.scikit-build] +cmake.source-dir = ".." +cmake.build-type = "Release" +cmake.version = ">=3.26.6" +build-dir = "build/{wheel_tag}" +wheel.platlib = true +install.strip = true diff --git a/Wrapping/Packaging/io/pyproject.toml b/Wrapping/Packaging/io/pyproject.toml new file mode 100644 index 00000000000..d4379b4e256 --- /dev/null +++ b/Wrapping/Packaging/io/pyproject.toml @@ -0,0 +1,22 @@ +# itk-io — built by slicing the pre-built ITK tree (see ../README.md). +[build-system] +requires = ["scikit-build-core>=0.10"] +build-backend = "scikit_build_core.build" + +[project] +name = "itk-io" +version = "6.0b2" +description = "ITK I/O modules (image and mesh file formats)." +readme = "../README.md" +requires-python = ">=3.9" +dependencies = [ + "itk-core==6.0b2", +] + +[tool.scikit-build] +cmake.source-dir = ".." +cmake.build-type = "Release" +cmake.version = ">=3.26.6" +build-dir = "build/{wheel_tag}" +wheel.platlib = true +install.strip = true diff --git a/Wrapping/Packaging/meta/CMakeLists.txt b/Wrapping/Packaging/meta/CMakeLists.txt new file mode 100644 index 00000000000..9b99b453996 --- /dev/null +++ b/Wrapping/Packaging/meta/CMakeLists.txt @@ -0,0 +1,6 @@ +# The itk meta-package is pure-Python (wheel.platlib=false): it builds no native +# code and installs nothing. This trivial CMake project lets scikit-build-core +# produce a metadata-only wheel whose dependencies (the 6 group wheels) come from +# [project] in pyproject.toml. +cmake_minimum_required(VERSION 3.26.6 FATAL_ERROR) +project(itk_meta NONE) diff --git a/Wrapping/Packaging/meta/pyproject.toml b/Wrapping/Packaging/meta/pyproject.toml new file mode 100644 index 00000000000..6b27e9c1870 --- /dev/null +++ b/Wrapping/Packaging/meta/pyproject.toml @@ -0,0 +1,30 @@ +# itk — the umbrella package users `pip install`. Ships no code; its only job is +# to depend on the six group wheels at the same version. +[build-system] +requires = ["scikit-build-core>=0.10"] +build-backend = "scikit_build_core.build" + +[project] +name = "itk" +version = "6.0b2" # the meta package's OWN distribution version +description = "ITK — N-dimensional scientific image processing (meta-package)." +readme = "../README.md" +requires-python = ">=3.9" +dependencies = [ + # Pinned to the group wheels' published version (ITK's version). + "itk-core==6.0b2", + "itk-numerics==6.0b2", + "itk-io==6.0b2", + "itk-filtering==6.0b2", + "itk-registration==6.0b2", + "itk-segmentation==6.0b2", + "numpy", +] + +[tool.scikit-build] +# Tag to the platform + ABI (not py3-none-any), matching the PyPI itk wheel, so +# `pip install itk` only resolves where the group wheels exist. Built per platform. +wheel.platlib = true +wheel.py-api = "cp39" +cmake.source-dir = "." +build-dir = "build/{wheel_tag}" diff --git a/Wrapping/Packaging/numerics/pyproject.toml b/Wrapping/Packaging/numerics/pyproject.toml new file mode 100644 index 00000000000..9e5d09e4f71 --- /dev/null +++ b/Wrapping/Packaging/numerics/pyproject.toml @@ -0,0 +1,22 @@ +# itk-numerics — built by slicing the pre-built ITK tree (see ../README.md). +[build-system] +requires = ["scikit-build-core>=0.10"] +build-backend = "scikit_build_core.build" + +[project] +name = "itk-numerics" +version = "6.0b2" +description = "ITK numerics modules (FFT, optimizers, statistics)." +readme = "../README.md" +requires-python = ">=3.9" +dependencies = [ + "itk-core==6.0b2", +] + +[tool.scikit-build] +cmake.source-dir = ".." +cmake.build-type = "Release" +cmake.version = ">=3.26.6" +build-dir = "build/{wheel_tag}" +wheel.platlib = true +install.strip = true diff --git a/Wrapping/Packaging/registration/pyproject.toml b/Wrapping/Packaging/registration/pyproject.toml new file mode 100644 index 00000000000..b17b37f231d --- /dev/null +++ b/Wrapping/Packaging/registration/pyproject.toml @@ -0,0 +1,22 @@ +# itk-registration — built by slicing the pre-built ITK tree (see ../README.md). +[build-system] +requires = ["scikit-build-core>=0.10"] +build-backend = "scikit_build_core.build" + +[project] +name = "itk-registration" +version = "6.0b2" +description = "ITK registration modules." +readme = "../README.md" +requires-python = ">=3.9" +dependencies = [ + "itk-filtering==6.0b2", +] + +[tool.scikit-build] +cmake.source-dir = ".." +cmake.build-type = "Release" +cmake.version = ">=3.26.6" +build-dir = "build/{wheel_tag}" +wheel.platlib = true +install.strip = true diff --git a/Wrapping/Packaging/segmentation/pyproject.toml b/Wrapping/Packaging/segmentation/pyproject.toml new file mode 100644 index 00000000000..a37335486d9 --- /dev/null +++ b/Wrapping/Packaging/segmentation/pyproject.toml @@ -0,0 +1,22 @@ +# itk-segmentation — built by slicing the pre-built ITK tree (see ../README.md). +[build-system] +requires = ["scikit-build-core>=0.10"] +build-backend = "scikit_build_core.build" + +[project] +name = "itk-segmentation" +version = "6.0b2" +description = "ITK segmentation modules (level sets, region growing, classifiers)." +readme = "../README.md" +requires-python = ">=3.9" +dependencies = [ + "itk-filtering==6.0b2", +] + +[tool.scikit-build] +cmake.source-dir = ".." +cmake.build-type = "Release" +cmake.version = ">=3.26.6" +build-dir = "build/{wheel_tag}" +wheel.platlib = true +install.strip = true From fad0552ffde5a962a9044a0b1e6257b958aef7a9 Mon Sep 17 00:00:00 2001 From: Cavan Riley Date: Sun, 21 Jun 2026 13:59:56 -0500 Subject: [PATCH 02/14] ENH: Add build_wheels.py driver and pixi wheel environment Add the local orchestration for the in-tree wheel build: * Utilities/Maintenance/build_wheels.py builds the wrapped ITK tree once then loops over the Wrapping/Packaging/ group definitions, slicing each wheel (and the meta wheel) by passing the ITK_BINARY_DIR reuse defines to scikit-build-core. It guards against an unbuilt ITK tree and accepts a subset of groups to build. * a [tool.pixi.feature.wheel] environment plus configure-wheels, build-wrapped-itk, and build-wheels tasks, so the full flow is "pixi run -e wheel build-wrapped-itk" then "pixi run -e wheel build-wheels". configure-wheels bakes in the three settings the slice requires (WRAP_ITK_INSTALL_COMPONENT_PER_MODULE, the PythonWheel component identifier, and a relative PY_SITE_PACKAGES_PATH so the install lands in the wheel). --- Utilities/Maintenance/build_wheels.py | 86 +++++++++++++++++++++++++++ pyproject.toml | 35 +++++++++++ 2 files changed, 121 insertions(+) create mode 100755 Utilities/Maintenance/build_wheels.py diff --git a/Utilities/Maintenance/build_wheels.py b/Utilities/Maintenance/build_wheels.py new file mode 100755 index 00000000000..8dbb03f26a4 --- /dev/null +++ b/Utilities/Maintenance/build_wheels.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +"""Build ITK Python wheels by slicing a pre-built, Python-wrapped ITK tree. + +Run via pixi: pixi run -e wheel build-wheels +Or directly: python Utilities/Maintenance/build_wheels.py [GROUP ...] + +The wheel *definitions* (per-group pyproject.toml, the slicing CMake, WHEEL_NAMES.txt) +live in Wrapping/Packaging/. This script is just the driver that loops over them. + +Prerequisite: a wrapped ITK build tree configured for slicing — produce it once +with `pixi run -e wheel build-wrapped-itk` (sets WRAP_ITK_INSTALL_COMPONENT_PER_MODULE, +the PythonWheel component identifier, and a relative PY_SITE_PACKAGES_PATH). +""" +from __future__ import annotations +import argparse +import os +import subprocess +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent # Utilities/Maintenance +REPO = HERE.parents[1] # ITK source root +PACKAGING = REPO / "Wrapping" / "Packaging" # wheel definitions live here + + +def wheel_groups() -> list[str]: + """Group names (e.g. 'core') from WHEEL_NAMES.txt, excluding the meta wheel.""" + names = (PACKAGING / "WHEEL_NAMES.txt").read_text().split() + return [n[len("itk-"):] for n in names if n.startswith("itk-") and n != "itk-meta"] + + +def run_build(src: Path, dist: Path, defines: list[str]) -> None: + cmd = [ + sys.executable, "-m", "build", "--wheel", "--no-isolation", + "--skip-dependency-check", "--outdir", str(dist), + *defines, str(src), + ] + print(f"\n=== building {src.name} ===", flush=True) + subprocess.run(cmd, check=True) + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("groups", nargs="*", help="group(s) to build (default: all + meta)") + ap.add_argument("--itk-build-dir", + default=os.environ.get("ITK_BINARY_DIR", str(REPO / "build-python")), + help="pre-built wrapped ITK tree to slice (default: /build-python)") + ap.add_argument("--dist", default=str(PACKAGING / "dist"), help="output directory for wheels") + ap.add_argument("--use-tbb", default="OFF", help="ITKPythonPackage_USE_TBB (ON/OFF)") + args = ap.parse_args() + + itk_build = Path(args.itk_build_dir).resolve() + if not (itk_build / "cmake_install.cmake").exists(): + sys.exit( + f"ERROR: '{itk_build}' is not a built ITK tree (no cmake_install.cmake).\n" + f" Build it once first: pixi run -e wheel build-wrapped-itk" + ) + dist = Path(args.dist) + dist.mkdir(parents=True, exist_ok=True) + + slice_defines = [ + f"--config-setting=cmake.define.ITK_BINARY_DIR={itk_build}", + f"--config-setting=cmake.define.ITK_SOURCE_DIR={REPO}", + "--config-setting=cmake.define.ITKPythonPackage_ITK_BINARY_REUSE=ON", + f"--config-setting=cmake.define.ITKPythonPackage_USE_TBB={args.use_tbb}", + ] + + groups = args.groups or wheel_groups() + for g in groups: + run_build( + PACKAGING / g, dist, + [f"--config-setting=cmake.define.ITKPythonPackage_WHEEL_NAME=itk-{g}", *slice_defines], + ) + + # Build the pure-Python meta wheel only on a full run (no explicit groups given). + if not args.groups: + run_build(PACKAGING / "meta", dist, []) + + print(f"\nWheels in {dist}:") + for whl in sorted(dist.glob("*.whl")): + print(f" {whl.name}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pyproject.toml b/pyproject.toml index a15907900d5..e77623e09e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -280,6 +280,40 @@ cmd = '''cp ./build-debug-python/Wrapping/Generators/Python/WrapITK.pth $(python description = "Run a Python executable with the development ITK Python Debug build package" depends-on = ["build-debug-python"] +[tool.pixi.feature.wheel.dependencies] +scikit-build-core = ">=0.10" +python-build = ">=1.2" +numpy = ">=2" + +# Compiler: defaults to the pixi env's cxx-compiler (gcc on Linux, clang on macOS). +# Override with WHEEL_CC / WHEEL_CXX to drive the build with Clang, the target for +# weekly ITK wheels (InsightSoftwareConsortium/ITK#4656, gated on #3093): +# WHEEL_CC=clang WHEEL_CXX=clang++ pixi run -e wheel build-wrapped-itk +[tool.pixi.feature.wheel.tasks.configure-wheels] +cmd = '''cmake \ + -Bbuild-python \ + -S. \ + -GNinja \ + -DITK_WRAP_PYTHON:BOOL=ON \ + -DCMAKE_BUILD_TYPE:STRING=Release \ + -DBUILD_TESTING:BOOL=OFF \ + -DWRAP_ITK_INSTALL_COMPONENT_PER_MODULE:BOOL=ON \ + -DWRAP_ITK_INSTALL_COMPONENT_IDENTIFIER:STRING=PythonWheel \ + ${WHEEL_CC:+-DCMAKE_C_COMPILER:STRING=$WHEEL_CC} \ + ${WHEEL_CXX:+-DCMAKE_CXX_COMPILER:STRING=$WHEEL_CXX} \ + -DPY_SITE_PACKAGES_PATH:STRING=.''' +description = "Configure ITK for wheel slicing (per-module components + wheel-relative install)" +outputs = ["build-python/CMakeCache.txt"] + +[tool.pixi.feature.wheel.tasks.build-wrapped-itk] +cmd = "cmake --build build-python" +description = "Build the wrapped ITK tree once — the reuse target every wheel slices" +depends-on = ["configure-wheels"] + +[tool.pixi.feature.wheel.tasks.build-wheels] +cmd = "python Utilities/Maintenance/build_wheels.py" +description = "Slice the pre-built ITK tree into all ITK Python wheels (run build-wrapped-itk first)" + [tool.pixi.environments] dev = ["dev"] cxx = ["dev", "cxx"] @@ -287,3 +321,4 @@ python = ["python", "dev"] pre-commit = ["pre-commit"] external-data-upload = ["external-data-upload"] doxygen = ["dev", "doxygen"] +wheel = ["python", "dev", "wheel"] From bab8e405d89d63aecaa866b08b80e0100db5dc3c Mon Sep 17 00:00:00 2001 From: Cavan Riley Date: Sun, 21 Jun 2026 14:00:09 -0500 Subject: [PATCH 03/14] ENH: Add cibuildwheel workflow sketch for in-tree wheels Sketch a GitHub Actions workflow that builds the in-tree ITK Python wheels with cibuildwheel. One job per platform compiles the wrapped ITK tree once inside the build container (CIBW_BEFORE_ALL, guarded so the remaining wheel builds reuse it) then slices each Wrapping/Packaging/ group, letting cibuildwheel handle the manylinux/macOS containers, wheel repair, the abi3 tag, and the import test. The ~8.5 GB wrapped ITK tree cannot be shared across runners as an artifact, so it is built and consumed in place within each platform job rather than in a per-wheel matrix. This is a starting point; image, arch, and path details (and a Windows variant) need iteration in real CI. --- .github/workflows/python-wheels.yml | 76 +++++++++++++++++++ .../workflows/wheel-compiler-benchmark.yml | 55 ++++++++++++++ .../Maintenance/cibw-build-wrapped-itk.sh | 46 +++++++++++ 3 files changed, 177 insertions(+) create mode 100644 .github/workflows/python-wheels.yml create mode 100644 .github/workflows/wheel-compiler-benchmark.yml create mode 100755 Utilities/Maintenance/cibw-build-wrapped-itk.sh diff --git a/.github/workflows/python-wheels.yml b/.github/workflows/python-wheels.yml new file mode 100644 index 00000000000..1d85d351d68 --- /dev/null +++ b/.github/workflows/python-wheels.yml @@ -0,0 +1,76 @@ +# Build ITK Python wheels with cibuildwheel (experimental). +# +# One job per platform: ITK is compiled once inside the build container +# (CIBW_BEFORE_ALL, guarded), then each group wheel is produced by pointing +# cibuildwheel at its definition under Wrapping/Packaging/. A per-wheel matrix is +# not used because the wrapped ITK tree is too large to share across runners. +# Images, arches, and a Windows variant still need iteration in real CI. +name: Python wheels + +on: + workflow_dispatch: + pull_request: + paths: ["Wrapping/Packaging/**", "Utilities/Maintenance/cibw-build-wrapped-itk.sh", ".github/workflows/python-wheels.yml"] + +jobs: + wheels: + name: wheels ${{ matrix.id }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - { id: linux-x86_64, runner: ubuntu-latest, archs: x86_64, itk_dir: /project/build-python } + - { id: linux-aarch64, runner: ubuntu-24.04-arm, archs: aarch64, itk_dir: /project/build-python } + - { id: macos-arm64, runner: macos-14, archs: arm64, itk_dir: $GITHUB_WORKSPACE/build-python } + - { id: macos-x86_64, runner: macos-13, archs: x86_64, itk_dir: $GITHUB_WORKSPACE/build-python } + # - { id: windows, runner: windows-2022, archs: AMD64, itk_dir: ... } # needs a .ps1 before-all variant + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # ITK version is derived from git describe + + - uses: actions/setup-python@v5 + with: { python-version: "3.11" } + + - run: python -m pip install "cibuildwheel>=2.20" + + - name: Build all ITK wheels (one ITK compile, sliced per group) + shell: bash + run: | + for group in core numerics io filtering registration segmentation; do + echo "::group::itk-$group" + python -m cibuildwheel --output-dir wheelhouse "Wrapping/Packaging/$group" + echo "::endgroup::" + done + # meta ships no code but is tagged per platform+ABI (like PyPI's itk wheel), + # so it is built PER PLATFORM here too (the guarded before-all is a no-op for it). + python -m cibuildwheel --output-dir wheelhouse "Wrapping/Packaging/meta" + env: + # One abi3 wheel per platform (requires wheel.py-api="cp39" in the pyprojects). + CIBW_BUILD: "cp39-*" + CIBW_ARCHS: ${{ matrix.archs }} + CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 + CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28 + + # Build wrapped ITK once, in-container, before the first wheel (guarded). + CIBW_BEFORE_ALL: bash Utilities/Maintenance/cibw-build-wrapped-itk.sh "{project}" + + # Every wheel slices the one ITK build. {project} resolves to the + # in-container/in-checkout root; itk_dir handles the Linux-vs-native path. + CIBW_ENVIRONMENT: >- + ITK_BINARY_DIR=${{ matrix.itk_dir }} + ITK_SOURCE_DIR={project} + CIBW_CONFIG_SETTINGS: >- + cmake.define.ITK_BINARY_DIR=${{ matrix.itk_dir }} + cmake.define.ITKPythonPackage_ITK_BINARY_REUSE=ON + cmake.define.ITKPythonPackage_USE_TBB=OFF + + # cibuildwheel auto-repairs (auditwheel/delocate) — light here since ITK is static. + CIBW_TEST_COMMAND: 'python -c "import itk; print(itk.Version.GetITKVersion())"' + + - uses: actions/upload-artifact@v4 + with: + name: itk-wheels-${{ matrix.id }} + path: wheelhouse/*.whl diff --git a/.github/workflows/wheel-compiler-benchmark.yml b/.github/workflows/wheel-compiler-benchmark.yml new file mode 100644 index 00000000000..5e59bb6c137 --- /dev/null +++ b/.github/workflows/wheel-compiler-benchmark.yml @@ -0,0 +1,55 @@ +# Benchmark: gcc vs Clang for the wrapped-ITK build (experimental). +# +# Answers "how much does Clang help the wheel build?" on Linux, the platform the +# weekly-wheel Clang switch targets (InsightSoftwareConsortium/ITK#4656, #3093). +# Two parallel jobs build the wrapped ITK tree with each compiler under +# `/usr/bin/time -v`, reporting wall-clock and peak memory. Manual trigger only. +# +# Tune before relying on it: a full wrapped build may exceed a runner's time +# limit, so this builds the default module set with wrapping (no heavy remote +# modules). Peak memory is the metric that most distinguishes the compilers on +# ITK's large generated wrapping translation units. +name: Wheel compiler benchmark + +on: + workflow_dispatch: + +jobs: + build: + name: wrapped-ITK (${{ matrix.compiler }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - { compiler: gcc, cc: gcc, cxx: g++ } + - { compiler: clang, cc: clang, cxx: clang++ } + steps: + - uses: actions/checkout@v4 + with: { fetch-depth: 0 } + + - run: sudo apt-get update && sudo apt-get install -y ${{ matrix.compiler }} ninja-build cmake + + - name: Configure (${{ matrix.compiler }}) + run: | + cmake -B build-bench -S . -G Ninja \ + -DITK_WRAP_PYTHON:BOOL=ON \ + -DCMAKE_BUILD_TYPE:STRING=Release \ + -DBUILD_TESTING:BOOL=OFF \ + -DCMAKE_C_COMPILER=${{ matrix.cc }} \ + -DCMAKE_CXX_COMPILER=${{ matrix.cxx }} + + - name: Build (timed + peak memory) + run: /usr/bin/time -v cmake --build build-bench 2> time-${{ matrix.compiler }}.log || true + + - name: Report + run: | + { + echo "### ${{ matrix.compiler }}" + grep -E 'Elapsed \(wall clock\)|Maximum resident set size' time-${{ matrix.compiler }}.log || true + } >> "$GITHUB_STEP_SUMMARY" + + - uses: actions/upload-artifact@v4 + with: + name: time-${{ matrix.compiler }} + path: time-${{ matrix.compiler }}.log diff --git a/Utilities/Maintenance/cibw-build-wrapped-itk.sh b/Utilities/Maintenance/cibw-build-wrapped-itk.sh new file mode 100755 index 00000000000..e6daade7f02 --- /dev/null +++ b/Utilities/Maintenance/cibw-build-wrapped-itk.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# +# cibuildwheel CIBW_BEFORE_ALL hook (SKETCH). +# +# Builds the Python-wrapped ITK tree ONCE, *inside* the build container +# (e.g. manylinux), configured for wheel slicing. Guarded so that the +# 2nd..Nth wheel builds in the same cibuildwheel job reuse it instead of +# recompiling ITK 7x. This is the "build ITK in the same image" step that +# every wheel then slices. +# +# The toolchain (cmake, ninja, C++ compiler) and the exact configure flags come +# from the `wheel` pixi environment defined in pyproject.toml, so CI uses the +# same pinned tools and the same build definition as the local +# `pixi run -e wheel build-wrapped-itk` flow. cibuildwheel runs CIBW_BEFORE_ALL +# at the project root: +# * Linux: inside the manylinux container, project mounted at /project +# (pixi is not preinstalled there, so we bootstrap it) +# * macOS/Windows: natively, in the checked-out repo +# +set -euo pipefail + +PROJECT="${1:-$PWD}" +BUILD="${PROJECT}/build-python" + +if [ -f "${BUILD}/cmake_install.cmake" ]; then + echo "[cibw-before-all] wrapped ITK already present at ${BUILD} — reusing." + exit 0 +fi + +# Use pixi if present; otherwise bootstrap it into the container (manylinux). +if ! command -v pixi >/dev/null 2>&1; then + echo "[cibw-before-all] pixi not found — installing it..." + export PIXI_HOME="${PIXI_HOME:-$HOME/.pixi}" + curl -fsSL https://pixi.sh/install.sh | bash + export PATH="${PIXI_HOME}/bin:${PATH}" +fi + +echo "[cibw-before-all] building wrapped ITK into ${BUILD} via the pixi wheel env ..." +cd "${PROJECT}" + +# configure-wheels + build-wrapped-itk carry the slice flags +# (per-module components, the PythonWheel identifier, relative +# PY_SITE_PACKAGES_PATH) and pull cmake/ninja/compiler from the env. +pixi run -e wheel build-wrapped-itk + +echo "[cibw-before-all] wrapped ITK build complete." From 0289aad0615178490217ee735f72833ce6341dd9 Mon Sep 17 00:00:00 2001 From: Cavan Riley Date: Sun, 21 Jun 2026 14:00:22 -0500 Subject: [PATCH 04/14] DOC: Document the in-tree ITK Python wheel build Add Wrapping/Packaging/README.md describing the in-tree wheel build: the slicing architecture, the one-time wrapped-ITK build plus per-wheel slice commands, the per-group module placement, and platform findings (static linking, the runtime location, the relative install prefix, and a stale-ninja-log gotcha). Add an "In-Tree Wheel Build" section to the contributing Python packaging guide so the in-tree mechanism is discoverable alongside the existing ITKPythonPackage workflow, pointing at the pixi tasks and the Wrapping/Packaging/ README. --- .../docs/contributing/python_packaging.md | 27 ++++++++ Wrapping/Packaging/README.md | 65 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 Wrapping/Packaging/README.md diff --git a/Documentation/docs/contributing/python_packaging.md b/Documentation/docs/contributing/python_packaging.md index b29a54f5fc5..5f273dc59d5 100644 --- a/Documentation/docs/contributing/python_packaging.md +++ b/Documentation/docs/contributing/python_packaging.md @@ -22,6 +22,33 @@ Building ITK Python wheels requires the following: - C++ Compiler (see [scikit-build platform specific requirements](https://scikit-build.readthedocs.io/en/latest/generators.html)) - Python >= 3.10 +## In-Tree Wheel Build (experimental) + +ITK can also build its Python wheels directly from this repository, without the +external [ITKPythonPackage] project. The wheel definitions live in +[`Wrapping/Packaging/`](https://github.com/InsightSoftwareConsortium/ITK/tree/main/Wrapping/Packaging), +which slices a single pre-built, Python-wrapped ITK tree into the per-group +wheels (`itk-core`, `itk-numerics`, `itk-io`, `itk-filtering`, +`itk-registration`, `itk-segmentation`) plus the `itk` meta-package. ITK is +compiled once; each wheel is produced by installing only its modules' wrapping +components, with no per-wheel recompilation. + +The `wheel` pixi environment provides the full flow: + +```bash +# Build the wrapped ITK tree once (the reuse target every wheel slices) +pixi run -e wheel build-wrapped-itk + +# Slice that tree into all ITK Python wheels +pixi run -e wheel build-wheels +``` + +See [`Wrapping/Packaging/README.md`](https://github.com/InsightSoftwareConsortium/ITK/blob/main/Wrapping/Packaging/README.md) +for the architecture, the per-group module placement, and platform notes. A +[cibuildwheel](https://cibuildwheel.pypa.io/) workflow that builds ITK once +inside the manylinux container and slices the wheels per platform is sketched in +`.github/workflows/python-wheels.yml`. + ## Automated Platform Scripts The following sections outline how to use the ITKPythonPackage project to build wheels on Linux, macOS, and Windows. Each script will fetch tagged ITK sources, build ITK with Python wrappings, and package binaries into the Python wheel archive format for distribution. diff --git a/Wrapping/Packaging/README.md b/Wrapping/Packaging/README.md new file mode 100644 index 00000000000..332e663b2e2 --- /dev/null +++ b/Wrapping/Packaging/README.md @@ -0,0 +1,65 @@ +# Wrapping/Packaging — in-tree Python wheel build + +Build ITK's Python wheels from this repository by slicing a single pre-built, +Python-wrapped ITK tree into per-group wheels, with no per-wheel recompilation. + +ITK already emits a per-module install component +(`PythonWheelRuntimeLibraries`) when wrapping is configured with +`-DWRAP_ITK_INSTALL_COMPONENT_PER_MODULE=ON` — that is the slicing seam. This +directory adds only the wheel definitions on top of it: a per-group +`pyproject.toml` plus the placement and per-component install CMake under +`cmake/` (relocated from ITKPythonPackage). Each wheel build is a standard +PEP 517 build that runs `cmake --install` of only its group's components. + +> Experimental. Nothing here is wired into ITK's default build. + +## Layout + +``` +Wrapping/Packaging/ +├── CMakeLists.txt shared scikit-build-core source for all groups +├── WHEEL_NAMES.txt the wheel names +├── cmake/ module->wheel placement + per-component install +├── core/ numerics/ io/ filtering/ registration/ segmentation/ +│ one pyproject.toml per group wheel +└── meta/ the "itk" umbrella package +``` + +Group wheels are versioned at ITK's version (they are ITK, sliced). The `itk` +meta-package depends on the six group wheels pinned at that version. + +## Build + +The `wheel` pixi environment provides cmake, ninja, the compiler, and the build +frontend: + +```sh +pixi run -e wheel build-wrapped-itk # compile wrapped ITK once (the reuse target) +pixi run -e wheel build-wheels # slice it into every wheel +``` + +`build-wrapped-itk` configures with the flags the slice requires: +`WRAP_ITK_INSTALL_COMPONENT_PER_MODULE`, `WRAP_ITK_INSTALL_COMPONENT_IDENTIFIER`, +and a relative `PY_SITE_PACKAGES_PATH` (so the install lands in the wheel, not +the environment). `build-wheels` runs `Utilities/Maintenance/build_wheels.py`, +which slices each group plus the meta wheel. + +For CI, `.github/workflows/python-wheels.yml` drives the same flow with +cibuildwheel. + +## Design notes + +- **Self-contained wheels.** ITK is built static (`BUILD_SHARED_LIBS=OFF`), so + each extension embeds the ITK code it needs and links only system libraries. + Wheels carry no ITK shared libs and wheel-repair is nearly a no-op. +- **Dependency-driven placement.** Each module is assigned to the lowest wheel + group that all of its dependents share ("common ancestor"), so a module used + across layers sinks toward Core. Placement needs `ITK_SOURCE_DIR` for group + membership and `find_package(ITK)` for the dependency graph. +- **Meta wheel is platform-tagged.** The `itk` package ships no code but is + tagged to the platform + ABI (not `py3-none-any`), matching the PyPI `itk` + wheel, so `pip install itk` only resolves where the group wheels exist. +- **Compiler.** The build uses the pixi env compiler by default (gcc on Linux, + clang on macOS). Set `WHEEL_CC` / `WHEEL_CXX` to override — e.g. to drive the + Linux build with Clang, the target for weekly wheels (ITK#4656, gated on + #3093). `.github/workflows/wheel-compiler-benchmark.yml` times gcc vs Clang. From 99b526665ddaa1d66fe3a7dca72b21c02ea77e9a Mon Sep 17 00:00:00 2001 From: Cavan Riley Date: Thu, 2 Jul 2026 23:05:09 -0500 Subject: [PATCH 05/14] COMP: Fix pixi configure-wheels parse error in deno_task_shell The configure-wheels task used bash conditional expansion (${WHEEL_CC:+...}) to add the compiler defines only when set. Pixi parses task scripts with deno_task_shell, which accepts only bare $VAR and rejects every brace form, so the task failed to parse before running cmake and every Python-wheels CI job died in the CIBW_BEFORE_ALL hook. Move the conditional-compiler logic into a Python driver, Utilities/Maintenance/configure_wrapped_itk.py, mirroring build_wheels.py in the same directory. The WHEEL_CC / WHEEL_CXX override interface is preserved exactly: the C/C++ compiler defines are appended only when the variables are set. --- .../Maintenance/configure_wrapped_itk.py | 39 +++++++++++++++++++ pyproject.toml | 13 +------ 2 files changed, 40 insertions(+), 12 deletions(-) create mode 100755 Utilities/Maintenance/configure_wrapped_itk.py diff --git a/Utilities/Maintenance/configure_wrapped_itk.py b/Utilities/Maintenance/configure_wrapped_itk.py new file mode 100755 index 00000000000..02ca7428916 --- /dev/null +++ b/Utilities/Maintenance/configure_wrapped_itk.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Configure the wrapped ITK tree for wheel slicing (the `configure-wheels` task). + +A Python driver rather than an inline pixi task: pixi's task shell parses only +bare ``$VAR`` — it rejects the ``${WHEEL_CC:+...}`` form needed to add the +compiler defines only when set. Here that stays conditional. WHEEL_CC / WHEEL_CXX +override the pixi env compiler (e.g. Clang for the weekly wheel build, +InsightSoftwareConsortium/ITK#4656); unset leaves CMake on the env default. +""" +from __future__ import annotations +import os +import subprocess +import sys + +CMAKE = [ + "cmake", + "-Bbuild-python", + "-S.", + "-GNinja", + "-DITK_WRAP_PYTHON:BOOL=ON", + "-DCMAKE_BUILD_TYPE:STRING=Release", + "-DBUILD_TESTING:BOOL=OFF", + "-DWRAP_ITK_INSTALL_COMPONENT_PER_MODULE:BOOL=ON", + "-DWRAP_ITK_INSTALL_COMPONENT_IDENTIFIER:STRING=PythonWheel", + "-DPY_SITE_PACKAGES_PATH:STRING=.", +] + + +def main() -> int: + cmd = list(CMAKE) + if cc := os.environ.get("WHEEL_CC"): + cmd.append(f"-DCMAKE_C_COMPILER:STRING={cc}") + if cxx := os.environ.get("WHEEL_CXX"): + cmd.append(f"-DCMAKE_CXX_COMPILER:STRING={cxx}") + return subprocess.run(cmd).returncode + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pyproject.toml b/pyproject.toml index e77623e09e8..5bea8e076c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -290,18 +290,7 @@ numpy = ">=2" # weekly ITK wheels (InsightSoftwareConsortium/ITK#4656, gated on #3093): # WHEEL_CC=clang WHEEL_CXX=clang++ pixi run -e wheel build-wrapped-itk [tool.pixi.feature.wheel.tasks.configure-wheels] -cmd = '''cmake \ - -Bbuild-python \ - -S. \ - -GNinja \ - -DITK_WRAP_PYTHON:BOOL=ON \ - -DCMAKE_BUILD_TYPE:STRING=Release \ - -DBUILD_TESTING:BOOL=OFF \ - -DWRAP_ITK_INSTALL_COMPONENT_PER_MODULE:BOOL=ON \ - -DWRAP_ITK_INSTALL_COMPONENT_IDENTIFIER:STRING=PythonWheel \ - ${WHEEL_CC:+-DCMAKE_C_COMPILER:STRING=$WHEEL_CC} \ - ${WHEEL_CXX:+-DCMAKE_CXX_COMPILER:STRING=$WHEEL_CXX} \ - -DPY_SITE_PACKAGES_PATH:STRING=.''' +cmd = "python Utilities/Maintenance/configure_wrapped_itk.py" description = "Configure ITK for wheel slicing (per-module components + wheel-relative install)" outputs = ["build-python/CMakeCache.txt"] From d3d04e9fbe68c620af682763bcc8547f03185bb1 Mon Sep 17 00:00:00 2001 From: Cavan Riley Date: Fri, 3 Jul 2026 11:09:55 -0500 Subject: [PATCH 06/14] COMP: Default wheel name from the scikit-build project name Each group wheel is built from the shared Wrapping/Packaging source, which requires ITKPythonPackage_WHEEL_NAME to pick the module group. The build_wheels.py driver passes it per group, but the cibuildwheel CI drives a bare `python -m build` whose one shared CIBW_CONFIG_SETTINGS cannot vary per group, so configure failed with "ITKPythonPackage_WHEEL_NAME must be defined". Default the name from SKBUILD_PROJECT_NAME when the driver did not set it. Each group's pyproject already declares name = itk-; scikit-build-core normalizes that to itk_, so convert underscores back to the itk- form ipp_wheel_to_group expects. An explicit define still wins via the DEFINED guard, and a plain `pip wheel` of a group now configures without the driver. --- Wrapping/Packaging/CMakeLists.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Wrapping/Packaging/CMakeLists.txt b/Wrapping/Packaging/CMakeLists.txt index beb1498e641..24461952db4 100644 --- a/Wrapping/Packaging/CMakeLists.txt +++ b/Wrapping/Packaging/CMakeLists.txt @@ -32,5 +32,13 @@ endif() message(STATUS "ITK_SOURCE_DIR = ${ITK_SOURCE_DIR}") message(STATUS "ITK_BINARY_DIR = ${ITK_BINARY_DIR} (reused, not rebuilt)") +# Which module group this build packages. build_wheels.py passes it explicitly; +# a plain `pip wheel` / cibuildwheel build defaults it from the scikit-build +# project name (itk-core, itk-io, ...). scikit-build-core normalizes that to +# underscores (itk_core), so restore the itk- form ipp_wheel_to_group wants. +if(NOT DEFINED ITKPythonPackage_WHEEL_NAME AND DEFINED SKBUILD_PROJECT_NAME) + string(REPLACE "_" "-" ITKPythonPackage_WHEEL_NAME "${SKBUILD_PROJECT_NAME}") +endif() + include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/ITKPythonPackage_Utils.cmake) include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/ITKPythonPackage_BuildWheels.cmake) From e5d87d236c061970eb2201c497e0a5c86457690f Mon Sep 17 00:00:00 2001 From: Cavan Riley Date: Fri, 3 Jul 2026 13:22:53 -0500 Subject: [PATCH 07/14] COMP: Resolve wheel ITK_BINARY_DIR from the environment under cibuildwheel The macOS matrix sets ITK_BINARY_DIR to $GITHUB_WORKSPACE/build-python. cibuildwheel shell-expands CIBW_ENVIRONMENT but passes CIBW_CONFIG_SETTINGS verbatim, so the cmake.define carried the literal string $GITHUB_WORKSPACE and configure failed with "ITK_BINARY_DIR must point at a pre-built ITK build tree". Drop the config-settings define and read ITK_BINARY_DIR from the environment in the shared wheel CMakeLists when the driver did not pass it. build_wheels.py still passes it as a define, which wins via the DEFINED guard; Linux keeps working because its /project path needs no expansion. --- .github/workflows/python-wheels.yml | 3 ++- Wrapping/Packaging/CMakeLists.txt | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-wheels.yml b/.github/workflows/python-wheels.yml index 1d85d351d68..6f73f727d16 100644 --- a/.github/workflows/python-wheels.yml +++ b/.github/workflows/python-wheels.yml @@ -62,8 +62,9 @@ jobs: CIBW_ENVIRONMENT: >- ITK_BINARY_DIR=${{ matrix.itk_dir }} ITK_SOURCE_DIR={project} + # ITK_BINARY_DIR is passed via CIBW_ENVIRONMENT, not here: config + # settings are not shell-expanded, so $GITHUB_WORKSPACE would stay literal. CIBW_CONFIG_SETTINGS: >- - cmake.define.ITK_BINARY_DIR=${{ matrix.itk_dir }} cmake.define.ITKPythonPackage_ITK_BINARY_REUSE=ON cmake.define.ITKPythonPackage_USE_TBB=OFF diff --git a/Wrapping/Packaging/CMakeLists.txt b/Wrapping/Packaging/CMakeLists.txt index 24461952db4..5e8bd2c3a92 100644 --- a/Wrapping/Packaging/CMakeLists.txt +++ b/Wrapping/Packaging/CMakeLists.txt @@ -12,7 +12,14 @@ if(NOT DEFINED ITK_SOURCE_DIR) ) endif() -# Pre-built, Python-wrapped ITK tree to reuse; supplied by the driver. +# Pre-built, Python-wrapped ITK tree to reuse. build_wheels.py passes it as a +# define; under cibuildwheel it arrives only in the environment, because +# CIBW_ENVIRONMENT expands $GITHUB_WORKSPACE whereas a cmake.define config-setting +# would not. +if(NOT DEFINED ITK_BINARY_DIR AND DEFINED ENV{ITK_BINARY_DIR}) + set(ITK_BINARY_DIR "$ENV{ITK_BINARY_DIR}") +endif() + if( NOT DEFINED From 190c5a9efa21cb55e1d0d1b983ed5c20f3a79708 Mon Sep 17 00:00:00 2001 From: Cavan Riley Date: Fri, 3 Jul 2026 13:23:09 -0500 Subject: [PATCH 08/14] COMP: Build wheels' ITK with the manylinux toolchain auditwheel rejected the Linux wheels ("too-recent versioned symbols") because the CIBW_BEFORE_ALL hook compiled ITK's static libraries with the pixi conda-forge compiler (g++ 14.3.0), whose libstdc++/glibc symbols are newer than the manylinux_2_28 baseline, while cibuildwheel compiles each extension with the image's gcc-toolset (g++ 14.2.1). The mismatched symbols propagated from the static libs into the extension. In the manylinux container, build the wrapped ITK tree with the image's own gcc-toolset (via WHEEL_CC/WHEEL_CXX and pip-provided cmake/ninja) instead of pixi, so ITK and the extensions share one ABI baseline. Native macOS builds keep using the pixi env, where delocate handles the dylib ABI. --- .../Maintenance/cibw-build-wrapped-itk.sh | 66 +++++++++++++------ Wrapping/Packaging/README.md | 11 ++-- 2 files changed, 52 insertions(+), 25 deletions(-) diff --git a/Utilities/Maintenance/cibw-build-wrapped-itk.sh b/Utilities/Maintenance/cibw-build-wrapped-itk.sh index e6daade7f02..efdd57e6ddb 100755 --- a/Utilities/Maintenance/cibw-build-wrapped-itk.sh +++ b/Utilities/Maintenance/cibw-build-wrapped-itk.sh @@ -8,14 +8,16 @@ # recompiling ITK 7x. This is the "build ITK in the same image" step that # every wheel then slices. # -# The toolchain (cmake, ninja, C++ compiler) and the exact configure flags come -# from the `wheel` pixi environment defined in pyproject.toml, so CI uses the -# same pinned tools and the same build definition as the local -# `pixi run -e wheel build-wrapped-itk` flow. cibuildwheel runs CIBW_BEFORE_ALL -# at the project root: -# * Linux: inside the manylinux container, project mounted at /project -# (pixi is not preinstalled there, so we bootstrap it) -# * macOS/Windows: natively, in the checked-out repo +# Two toolchain paths, because the per-wheel extension (compiled later by +# scikit-build-core) must link ITK's static libs without pulling in symbols +# newer than the wheel's ABI baseline: +# * manylinux container: build ITK with the image's own gcc-toolset. The +# conda-forge compiler (via pixi) links a newer libstdc++/glibc, so +# auditwheel rejects the wheel with "too-recent versioned symbols". The +# manylinux gcc-toolset matches the compiler cibuildwheel uses for the +# extension, keeping every symbol within the manylinux_2_28 baseline. +# * macOS/native: the pixi `wheel` env supplies clang, cmake, and ninja +# (delocate handles the dylib ABI, so no glibc concern applies). # set -euo pipefail @@ -27,20 +29,42 @@ if [ -f "${BUILD}/cmake_install.cmake" ]; then exit 0 fi -# Use pixi if present; otherwise bootstrap it into the container (manylinux). -if ! command -v pixi >/dev/null 2>&1; then - echo "[cibw-before-all] pixi not found — installing it..." - export PIXI_HOME="${PIXI_HOME:-$HOME/.pixi}" - curl -fsSL https://pixi.sh/install.sh | bash - export PATH="${PIXI_HOME}/bin:${PATH}" -fi - -echo "[cibw-before-all] building wrapped ITK into ${BUILD} via the pixi wheel env ..." cd "${PROJECT}" -# configure-wheels + build-wrapped-itk carry the slice flags -# (per-module components, the PythonWheel identifier, relative -# PY_SITE_PACKAGES_PATH) and pull cmake/ninja/compiler from the env. -pixi run -e wheel build-wrapped-itk +# /opt/python is the manylinux image layout; its absence means a native build. +if [ -d /opt/python ]; then + echo "[cibw-before-all] manylinux container — building ITK with the image gcc-toolset ..." + + # Newest gcc-toolset on the image (manylinux_2_28 ships gcc-toolset-14). Its + # binaries stay compatible with the image's baseline glibc/libstdc++. + gts_enable="" + for f in /opt/rh/gcc-toolset-*/enable; do + [ -e "${f}" ] && gts_enable="${f}" + done + if [ -n "${gts_enable}" ]; then + # shellcheck disable=SC1090 + source "${gts_enable}" + fi + + # cp39 interpreter + pip-provided cmake/ninja; no pixi/conda toolchain here. + export PATH="/opt/python/cp39-cp39/bin:${PATH}" + python -m pip install --upgrade cmake ninja + + export WHEEL_CC=gcc WHEEL_CXX=g++ + python "${PROJECT}/Utilities/Maintenance/configure_wrapped_itk.py" + cmake --build "${BUILD}" +else + echo "[cibw-before-all] native build via the pixi wheel env ..." + if ! command -v pixi >/dev/null 2>&1; then + echo "[cibw-before-all] pixi not found — installing it..." + export PIXI_HOME="${PIXI_HOME:-$HOME/.pixi}" + curl -fsSL https://pixi.sh/install.sh | bash + export PATH="${PIXI_HOME}/bin:${PATH}" + fi + # configure-wheels + build-wrapped-itk carry the slice flags (per-module + # components, the PythonWheel identifier, relative PY_SITE_PACKAGES_PATH) and + # pull cmake/ninja/compiler from the env. + pixi run -e wheel build-wrapped-itk +fi echo "[cibw-before-all] wrapped ITK build complete." diff --git a/Wrapping/Packaging/README.md b/Wrapping/Packaging/README.md index 332e663b2e2..543a8234186 100644 --- a/Wrapping/Packaging/README.md +++ b/Wrapping/Packaging/README.md @@ -59,7 +59,10 @@ cibuildwheel. - **Meta wheel is platform-tagged.** The `itk` package ships no code but is tagged to the platform + ABI (not `py3-none-any`), matching the PyPI `itk` wheel, so `pip install itk` only resolves where the group wheels exist. -- **Compiler.** The build uses the pixi env compiler by default (gcc on Linux, - clang on macOS). Set `WHEEL_CC` / `WHEEL_CXX` to override — e.g. to drive the - Linux build with Clang, the target for weekly wheels (ITK#4656, gated on - #3093). `.github/workflows/wheel-compiler-benchmark.yml` times gcc vs Clang. +- **Compiler.** Local builds use the pixi env compiler (gcc on Linux, clang on + macOS). Set `WHEEL_CC` / `WHEEL_CXX` to override — e.g. to drive the Linux + build with Clang, the target for weekly wheels (ITK#4656, gated on #3093). + `.github/workflows/wheel-compiler-benchmark.yml` times gcc vs Clang. In the + manylinux CI container the wrapped-ITK build instead uses the image's own + gcc-toolset (see `cibw-build-wrapped-itk.sh`) so ITK's static libraries share + the extension's ABI baseline and pass `auditwheel`. From af955490c52a285f41ab910b4c48485484bab44f Mon Sep 17 00:00:00 2001 From: Cavan Riley Date: Fri, 3 Jul 2026 19:26:16 -0500 Subject: [PATCH 09/14] COMP: Target Python 3.10 for the wheels, matching ITK 6 The wheels targeted cp39, but ITK 6 requires Python >= 3.10 (CMake/ITKSetPython3Vars.cmake sets PYTHON_VERSION_MIN 3.10) and its Python support uses 3.10-only syntax: itkConfig.py annotates with PEP 604 unions (message: Warning | str), which evaluate at import and raise TypeError on 3.9. Both platforms built, repaired, and installed the wheel, then failed the `import itk` smoke test on the cp39 runner. Raise the floor to 3.10 everywhere: CIBW_BUILD cp310-*, the manylinux interpreter (/opt/python/cp310-cp310), the abi3 tag (wheel.py-api cp310), and requires-python >=3.10 in every group and the meta package. --- .github/workflows/python-wheels.yml | 5 +++-- Utilities/Maintenance/cibw-build-wrapped-itk.sh | 5 +++-- Wrapping/Packaging/core/pyproject.toml | 2 +- Wrapping/Packaging/filtering/pyproject.toml | 2 +- Wrapping/Packaging/io/pyproject.toml | 2 +- Wrapping/Packaging/meta/pyproject.toml | 4 ++-- Wrapping/Packaging/numerics/pyproject.toml | 2 +- Wrapping/Packaging/registration/pyproject.toml | 2 +- Wrapping/Packaging/segmentation/pyproject.toml | 2 +- 9 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.github/workflows/python-wheels.yml b/.github/workflows/python-wheels.yml index 6f73f727d16..7609f425324 100644 --- a/.github/workflows/python-wheels.yml +++ b/.github/workflows/python-wheels.yml @@ -48,8 +48,9 @@ jobs: # so it is built PER PLATFORM here too (the guarded before-all is a no-op for it). python -m cibuildwheel --output-dir wheelhouse "Wrapping/Packaging/meta" env: - # One abi3 wheel per platform (requires wheel.py-api="cp39" in the pyprojects). - CIBW_BUILD: "cp39-*" + # One abi3 wheel per platform (requires wheel.py-api="cp310"); ITK 6 + # requires Python >= 3.10 (CMake/ITKSetPython3Vars.cmake). + CIBW_BUILD: "cp310-*" CIBW_ARCHS: ${{ matrix.archs }} CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28 diff --git a/Utilities/Maintenance/cibw-build-wrapped-itk.sh b/Utilities/Maintenance/cibw-build-wrapped-itk.sh index efdd57e6ddb..bdc92ac2710 100755 --- a/Utilities/Maintenance/cibw-build-wrapped-itk.sh +++ b/Utilities/Maintenance/cibw-build-wrapped-itk.sh @@ -46,8 +46,9 @@ if [ -d /opt/python ]; then source "${gts_enable}" fi - # cp39 interpreter + pip-provided cmake/ninja; no pixi/conda toolchain here. - export PATH="/opt/python/cp39-cp39/bin:${PATH}" + # cp310 interpreter (ITK 6 needs Python >= 3.10) + pip cmake/ninja; no + # pixi/conda toolchain here. + export PATH="/opt/python/cp310-cp310/bin:${PATH}" python -m pip install --upgrade cmake ninja export WHEEL_CC=gcc WHEEL_CXX=g++ diff --git a/Wrapping/Packaging/core/pyproject.toml b/Wrapping/Packaging/core/pyproject.toml index d5df7118e8a..7e995194003 100644 --- a/Wrapping/Packaging/core/pyproject.toml +++ b/Wrapping/Packaging/core/pyproject.toml @@ -9,7 +9,7 @@ name = "itk-core" version = "6.0b2" # tracks the ITK version this wheel slices description = "ITK core modules — image classes, the NumPy bridge, and runtime." readme = "../README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "numpy", # core depends on nothing else in the ITK set ] diff --git a/Wrapping/Packaging/filtering/pyproject.toml b/Wrapping/Packaging/filtering/pyproject.toml index 0acb3ba8ac2..404e32aa5c2 100644 --- a/Wrapping/Packaging/filtering/pyproject.toml +++ b/Wrapping/Packaging/filtering/pyproject.toml @@ -8,7 +8,7 @@ name = "itk-filtering" version = "6.0b2" description = "ITK filtering modules (image filters, smoothing, thresholding)." readme = "../README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "itk-numerics==6.0b2", ] diff --git a/Wrapping/Packaging/io/pyproject.toml b/Wrapping/Packaging/io/pyproject.toml index d4379b4e256..47748094bb7 100644 --- a/Wrapping/Packaging/io/pyproject.toml +++ b/Wrapping/Packaging/io/pyproject.toml @@ -8,7 +8,7 @@ name = "itk-io" version = "6.0b2" description = "ITK I/O modules (image and mesh file formats)." readme = "../README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "itk-core==6.0b2", ] diff --git a/Wrapping/Packaging/meta/pyproject.toml b/Wrapping/Packaging/meta/pyproject.toml index 6b27e9c1870..cac12ee4fac 100644 --- a/Wrapping/Packaging/meta/pyproject.toml +++ b/Wrapping/Packaging/meta/pyproject.toml @@ -9,7 +9,7 @@ name = "itk" version = "6.0b2" # the meta package's OWN distribution version description = "ITK — N-dimensional scientific image processing (meta-package)." readme = "../README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ # Pinned to the group wheels' published version (ITK's version). "itk-core==6.0b2", @@ -25,6 +25,6 @@ dependencies = [ # Tag to the platform + ABI (not py3-none-any), matching the PyPI itk wheel, so # `pip install itk` only resolves where the group wheels exist. Built per platform. wheel.platlib = true -wheel.py-api = "cp39" +wheel.py-api = "cp310" cmake.source-dir = "." build-dir = "build/{wheel_tag}" diff --git a/Wrapping/Packaging/numerics/pyproject.toml b/Wrapping/Packaging/numerics/pyproject.toml index 9e5d09e4f71..df988cc8165 100644 --- a/Wrapping/Packaging/numerics/pyproject.toml +++ b/Wrapping/Packaging/numerics/pyproject.toml @@ -8,7 +8,7 @@ name = "itk-numerics" version = "6.0b2" description = "ITK numerics modules (FFT, optimizers, statistics)." readme = "../README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "itk-core==6.0b2", ] diff --git a/Wrapping/Packaging/registration/pyproject.toml b/Wrapping/Packaging/registration/pyproject.toml index b17b37f231d..c12514ce515 100644 --- a/Wrapping/Packaging/registration/pyproject.toml +++ b/Wrapping/Packaging/registration/pyproject.toml @@ -8,7 +8,7 @@ name = "itk-registration" version = "6.0b2" description = "ITK registration modules." readme = "../README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "itk-filtering==6.0b2", ] diff --git a/Wrapping/Packaging/segmentation/pyproject.toml b/Wrapping/Packaging/segmentation/pyproject.toml index a37335486d9..5127136a46f 100644 --- a/Wrapping/Packaging/segmentation/pyproject.toml +++ b/Wrapping/Packaging/segmentation/pyproject.toml @@ -8,7 +8,7 @@ name = "itk-segmentation" version = "6.0b2" description = "ITK segmentation modules (level sets, region growing, classifiers)." readme = "../README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "itk-filtering==6.0b2", ] From 7bffd277cde3100a431de2040004e1f51f66d651 Mon Sep 17 00:00:00 2001 From: Cavan Riley Date: Sun, 5 Jul 2026 18:03:49 -0500 Subject: [PATCH 10/14] COMP: Target Python 3.11 for the wheels, matching ITK 6 The PR-triggered build compiles the branch merged into main, so the wheels run main's Python support, which requires 3.11: itk/support/types.py does an unconditional `from typing import Self` (Self is 3.11+), and main sets PYTHON_VERSION_MIN 3.11. The cp310 wheels built, repaired, and installed, then failed the `import itk` smoke test with "cannot import name 'Self' from typing". A stale 3.10 floor in the branch's own CMake had misled the earlier bump. Raise the floor to 3.11: CIBW_BUILD cp311-*, the manylinux interpreter (/opt/python/cp311-cp311), the abi3 tag (wheel.py-api cp311), and requires-python >=3.11 across every group and the meta package. --- .github/workflows/python-wheels.yml | 6 +++--- Utilities/Maintenance/cibw-build-wrapped-itk.sh | 4 ++-- Wrapping/Packaging/core/pyproject.toml | 2 +- Wrapping/Packaging/filtering/pyproject.toml | 2 +- Wrapping/Packaging/io/pyproject.toml | 2 +- Wrapping/Packaging/meta/pyproject.toml | 4 ++-- Wrapping/Packaging/numerics/pyproject.toml | 2 +- Wrapping/Packaging/registration/pyproject.toml | 2 +- Wrapping/Packaging/segmentation/pyproject.toml | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/python-wheels.yml b/.github/workflows/python-wheels.yml index 7609f425324..8af1015cf8f 100644 --- a/.github/workflows/python-wheels.yml +++ b/.github/workflows/python-wheels.yml @@ -48,9 +48,9 @@ jobs: # so it is built PER PLATFORM here too (the guarded before-all is a no-op for it). python -m cibuildwheel --output-dir wheelhouse "Wrapping/Packaging/meta" env: - # One abi3 wheel per platform (requires wheel.py-api="cp310"); ITK 6 - # requires Python >= 3.10 (CMake/ITKSetPython3Vars.cmake). - CIBW_BUILD: "cp310-*" + # One abi3 wheel per platform (requires wheel.py-api="cp311"); ITK 6 + # requires Python >= 3.11 (CMake/ITKSetPython3Vars.cmake). + CIBW_BUILD: "cp311-*" CIBW_ARCHS: ${{ matrix.archs }} CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28 diff --git a/Utilities/Maintenance/cibw-build-wrapped-itk.sh b/Utilities/Maintenance/cibw-build-wrapped-itk.sh index bdc92ac2710..d1474ff7315 100755 --- a/Utilities/Maintenance/cibw-build-wrapped-itk.sh +++ b/Utilities/Maintenance/cibw-build-wrapped-itk.sh @@ -46,9 +46,9 @@ if [ -d /opt/python ]; then source "${gts_enable}" fi - # cp310 interpreter (ITK 6 needs Python >= 3.10) + pip cmake/ninja; no + # cp311 interpreter (ITK 6 needs Python >= 3.11) + pip cmake/ninja; no # pixi/conda toolchain here. - export PATH="/opt/python/cp310-cp310/bin:${PATH}" + export PATH="/opt/python/cp311-cp311/bin:${PATH}" python -m pip install --upgrade cmake ninja export WHEEL_CC=gcc WHEEL_CXX=g++ diff --git a/Wrapping/Packaging/core/pyproject.toml b/Wrapping/Packaging/core/pyproject.toml index 7e995194003..29e2bbef8b1 100644 --- a/Wrapping/Packaging/core/pyproject.toml +++ b/Wrapping/Packaging/core/pyproject.toml @@ -9,7 +9,7 @@ name = "itk-core" version = "6.0b2" # tracks the ITK version this wheel slices description = "ITK core modules — image classes, the NumPy bridge, and runtime." readme = "../README.md" -requires-python = ">=3.10" +requires-python = ">=3.11" dependencies = [ "numpy", # core depends on nothing else in the ITK set ] diff --git a/Wrapping/Packaging/filtering/pyproject.toml b/Wrapping/Packaging/filtering/pyproject.toml index 404e32aa5c2..10a3032f3f5 100644 --- a/Wrapping/Packaging/filtering/pyproject.toml +++ b/Wrapping/Packaging/filtering/pyproject.toml @@ -8,7 +8,7 @@ name = "itk-filtering" version = "6.0b2" description = "ITK filtering modules (image filters, smoothing, thresholding)." readme = "../README.md" -requires-python = ">=3.10" +requires-python = ">=3.11" dependencies = [ "itk-numerics==6.0b2", ] diff --git a/Wrapping/Packaging/io/pyproject.toml b/Wrapping/Packaging/io/pyproject.toml index 47748094bb7..78012a8b61f 100644 --- a/Wrapping/Packaging/io/pyproject.toml +++ b/Wrapping/Packaging/io/pyproject.toml @@ -8,7 +8,7 @@ name = "itk-io" version = "6.0b2" description = "ITK I/O modules (image and mesh file formats)." readme = "../README.md" -requires-python = ">=3.10" +requires-python = ">=3.11" dependencies = [ "itk-core==6.0b2", ] diff --git a/Wrapping/Packaging/meta/pyproject.toml b/Wrapping/Packaging/meta/pyproject.toml index cac12ee4fac..1d33aba203f 100644 --- a/Wrapping/Packaging/meta/pyproject.toml +++ b/Wrapping/Packaging/meta/pyproject.toml @@ -9,7 +9,7 @@ name = "itk" version = "6.0b2" # the meta package's OWN distribution version description = "ITK — N-dimensional scientific image processing (meta-package)." readme = "../README.md" -requires-python = ">=3.10" +requires-python = ">=3.11" dependencies = [ # Pinned to the group wheels' published version (ITK's version). "itk-core==6.0b2", @@ -25,6 +25,6 @@ dependencies = [ # Tag to the platform + ABI (not py3-none-any), matching the PyPI itk wheel, so # `pip install itk` only resolves where the group wheels exist. Built per platform. wheel.platlib = true -wheel.py-api = "cp310" +wheel.py-api = "cp311" cmake.source-dir = "." build-dir = "build/{wheel_tag}" diff --git a/Wrapping/Packaging/numerics/pyproject.toml b/Wrapping/Packaging/numerics/pyproject.toml index df988cc8165..3814062b1ea 100644 --- a/Wrapping/Packaging/numerics/pyproject.toml +++ b/Wrapping/Packaging/numerics/pyproject.toml @@ -8,7 +8,7 @@ name = "itk-numerics" version = "6.0b2" description = "ITK numerics modules (FFT, optimizers, statistics)." readme = "../README.md" -requires-python = ">=3.10" +requires-python = ">=3.11" dependencies = [ "itk-core==6.0b2", ] diff --git a/Wrapping/Packaging/registration/pyproject.toml b/Wrapping/Packaging/registration/pyproject.toml index c12514ce515..d78217f4c20 100644 --- a/Wrapping/Packaging/registration/pyproject.toml +++ b/Wrapping/Packaging/registration/pyproject.toml @@ -8,7 +8,7 @@ name = "itk-registration" version = "6.0b2" description = "ITK registration modules." readme = "../README.md" -requires-python = ">=3.10" +requires-python = ">=3.11" dependencies = [ "itk-filtering==6.0b2", ] diff --git a/Wrapping/Packaging/segmentation/pyproject.toml b/Wrapping/Packaging/segmentation/pyproject.toml index 5127136a46f..ea4a947cf8e 100644 --- a/Wrapping/Packaging/segmentation/pyproject.toml +++ b/Wrapping/Packaging/segmentation/pyproject.toml @@ -8,7 +8,7 @@ name = "itk-segmentation" version = "6.0b2" description = "ITK segmentation modules (level sets, region growing, classifiers)." readme = "../README.md" -requires-python = ">=3.10" +requires-python = ">=3.11" dependencies = [ "itk-filtering==6.0b2", ] From 0ab487a15ca81015f2abe999f2851677014dedb4 Mon Sep 17 00:00:00 2001 From: Cavan Riley Date: Mon, 6 Jul 2026 17:15:11 -0500 Subject: [PATCH 11/14] COMP: Smoke-test the ITK wheel set together, not per wheel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cibuildwheel ran the import test after each group wheel in an isolated venv, but the group wheels depend on one another (itk-numerics needs itk-core==6.0b2) and those versions are not on PyPI, so the per-wheel install failed to resolve the sibling: "No matching distribution found for itk-core==6.0b2". Core passed because it only needs numpy. Drop CIBW_TEST_COMMAND and add a final step that installs the meta package and its six group wheels from the local wheelhouse (--no-index --find-links, with numpy from PyPI) and imports itk once — the correct integration test for a set of interdependent, unpublished wheels. --- .github/workflows/python-wheels.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-wheels.yml b/.github/workflows/python-wheels.yml index 8af1015cf8f..0acaf6fff4a 100644 --- a/.github/workflows/python-wheels.yml +++ b/.github/workflows/python-wheels.yml @@ -70,7 +70,19 @@ jobs: cmake.define.ITKPythonPackage_USE_TBB=OFF # cibuildwheel auto-repairs (auditwheel/delocate) — light here since ITK is static. - CIBW_TEST_COMMAND: 'python -c "import itk; print(itk.Version.GetITKVersion())"' + # No per-wheel CIBW_TEST_COMMAND: the group wheels depend on each other + # (itk-numerics needs itk-core) and are not on PyPI, so an isolated + # per-wheel install cannot resolve siblings. The whole set is smoke-tested + # together below instead. + + - name: Smoke-test the full wheel set together + shell: bash + run: | + python -m pip install numpy # only external dependency of the wheel set + # Resolve the itk meta-package and its six group wheels solely from the + # local build (--no-index); numpy is already satisfied above. + python -m pip install --no-index --find-links wheelhouse itk + python -c "import itk; print(itk.Version.GetITKVersion())" - uses: actions/upload-artifact@v4 with: From 36669556e19bc5afa57d5f3bfbb718f87de77b7e Mon Sep 17 00:00:00 2001 From: Cavan Riley Date: Tue, 7 Jul 2026 18:38:41 -0500 Subject: [PATCH 12/14] COMP: Build manylinux only and skip repair for the code-less meta wheel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cibuildwheel builds musllinux by default, but ITK ships glibc (manylinux) wheels only, and the musl/Alpine container cannot execute the glibc vendored swig binary — the before-all failed there with "Failed to run vendored swig" while the manylinux build succeeded. Skip musllinux with CIBW_SKIP. The meta package is pure Python (no binaries), so auditwheel/delocate abort with "Failed to find any binary with the required architecture". Build it with CIBW_REPAIR_WHEEL_COMMAND empty so its platform+ABI tag is kept without repair. --- .github/workflows/python-wheels.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-wheels.yml b/.github/workflows/python-wheels.yml index 0acaf6fff4a..452516e87f0 100644 --- a/.github/workflows/python-wheels.yml +++ b/.github/workflows/python-wheels.yml @@ -46,11 +46,16 @@ jobs: done # meta ships no code but is tagged per platform+ABI (like PyPI's itk wheel), # so it is built PER PLATFORM here too (the guarded before-all is a no-op for it). - python -m cibuildwheel --output-dir wheelhouse "Wrapping/Packaging/meta" + # It has no binaries, so skip wheel repair — auditwheel/delocate error out + # with "no binary with the required architecture" on a code-less wheel. + CIBW_REPAIR_WHEEL_COMMAND="" python -m cibuildwheel --output-dir wheelhouse "Wrapping/Packaging/meta" env: # One abi3 wheel per platform (requires wheel.py-api="cp311"); ITK 6 # requires Python >= 3.11 (CMake/ITKSetPython3Vars.cmake). CIBW_BUILD: "cp311-*" + # ITK ships glibc (manylinux) wheels only; musllinux is not a target and + # its Alpine/musl container cannot run the glibc vendored swig binary. + CIBW_SKIP: "*-musllinux*" CIBW_ARCHS: ${{ matrix.archs }} CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28 From ce41ef73a010b830a679d28c5330cf1fe05436bc Mon Sep 17 00:00:00 2001 From: Cavan Riley Date: Wed, 8 Jul 2026 09:28:54 -0500 Subject: [PATCH 13/14] COMP: Build wrapped ITK once on Linux by sharing it across wheel containers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each group wheel is a separate cibuildwheel invocation, so on Linux every one started a fresh container and the before-all recompiled ITK — seven builds that overran the 6h job limit. macOS reused the build because its native invocations share the workspace. Mount one named Docker volume into every Linux container (CIBW_CONTAINER_ENGINE) and build wrapped ITK into it (ITK_BINARY_DIR=/itk-build/build-python). The before-all guard then finds the tree the first wheel built and every later wheel reuses it. cibw-build-wrapped-itk.sh takes the build location from ITK_BINARY_DIR, and configure_wrapped_itk.py honors WHEEL_ITK_BUILD_DIR; macOS and local builds keep using build-python unchanged. --- .github/workflows/python-wheels.yml | 10 ++++++++-- Utilities/Maintenance/cibw-build-wrapped-itk.sh | 7 ++++++- Utilities/Maintenance/configure_wrapped_itk.py | 6 ++++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python-wheels.yml b/.github/workflows/python-wheels.yml index 452516e87f0..c4095ad465c 100644 --- a/.github/workflows/python-wheels.yml +++ b/.github/workflows/python-wheels.yml @@ -20,8 +20,8 @@ jobs: fail-fast: false matrix: include: - - { id: linux-x86_64, runner: ubuntu-latest, archs: x86_64, itk_dir: /project/build-python } - - { id: linux-aarch64, runner: ubuntu-24.04-arm, archs: aarch64, itk_dir: /project/build-python } + - { id: linux-x86_64, runner: ubuntu-latest, archs: x86_64, itk_dir: /itk-build/build-python } + - { id: linux-aarch64, runner: ubuntu-24.04-arm, archs: aarch64, itk_dir: /itk-build/build-python } - { id: macos-arm64, runner: macos-14, archs: arm64, itk_dir: $GITHUB_WORKSPACE/build-python } - { id: macos-x86_64, runner: macos-13, archs: x86_64, itk_dir: $GITHUB_WORKSPACE/build-python } # - { id: windows, runner: windows-2022, archs: AMD64, itk_dir: ... } # needs a .ps1 before-all variant @@ -60,6 +60,12 @@ jobs: CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28 + # Linux only: mount one named volume into every per-wheel container so the + # wrapped-ITK build (into /itk-build, ITK_BINARY_DIR) is compiled once and + # reused, instead of recompiled per wheel. Ignored on native macOS, which + # already reuses the build in the shared workspace. + CIBW_CONTAINER_ENGINE: "docker; create_args: --volume itk-build:/itk-build" + # Build wrapped ITK once, in-container, before the first wheel (guarded). CIBW_BEFORE_ALL: bash Utilities/Maintenance/cibw-build-wrapped-itk.sh "{project}" diff --git a/Utilities/Maintenance/cibw-build-wrapped-itk.sh b/Utilities/Maintenance/cibw-build-wrapped-itk.sh index d1474ff7315..6a81101bf4d 100755 --- a/Utilities/Maintenance/cibw-build-wrapped-itk.sh +++ b/Utilities/Maintenance/cibw-build-wrapped-itk.sh @@ -22,7 +22,11 @@ set -euo pipefail PROJECT="${1:-$PWD}" -BUILD="${PROJECT}/build-python" +# Build tree location. On Linux, ITK_BINARY_DIR points at a Docker volume shared +# across cibuildwheel's per-wheel containers (see CIBW_CONTAINER_ENGINE), so the +# one build here is reused by every wheel instead of recompiled per container. +# On macOS the shared workspace already persists it; locally it is build-python. +BUILD="${ITK_BINARY_DIR:-${PROJECT}/build-python}" if [ -f "${BUILD}/cmake_install.cmake" ]; then echo "[cibw-before-all] wrapped ITK already present at ${BUILD} — reusing." @@ -52,6 +56,7 @@ if [ -d /opt/python ]; then python -m pip install --upgrade cmake ninja export WHEEL_CC=gcc WHEEL_CXX=g++ + export WHEEL_ITK_BUILD_DIR="${BUILD}" # build into the shared volume, not /project python "${PROJECT}/Utilities/Maintenance/configure_wrapped_itk.py" cmake --build "${BUILD}" else diff --git a/Utilities/Maintenance/configure_wrapped_itk.py b/Utilities/Maintenance/configure_wrapped_itk.py index 02ca7428916..c55091281f1 100755 --- a/Utilities/Maintenance/configure_wrapped_itk.py +++ b/Utilities/Maintenance/configure_wrapped_itk.py @@ -14,7 +14,6 @@ CMAKE = [ "cmake", - "-Bbuild-python", "-S.", "-GNinja", "-DITK_WRAP_PYTHON:BOOL=ON", @@ -27,7 +26,10 @@ def main() -> int: - cmd = list(CMAKE) + # Build tree location. Defaults to build-python (local/pixi flow); CI points + # it at a container-shared volume so the one build is reused across wheels. + build_dir = os.environ.get("WHEEL_ITK_BUILD_DIR", "build-python") + cmd = ["cmake", f"-B{build_dir}", *CMAKE[1:]] if cc := os.environ.get("WHEEL_CC"): cmd.append(f"-DCMAKE_C_COMPILER:STRING={cc}") if cxx := os.environ.get("WHEEL_CXX"): From 24cb5e74fd5b37e5eb55e6ff46f584f853f3e3f5 Mon Sep 17 00:00:00 2001 From: Cavan Riley Date: Wed, 8 Jul 2026 12:42:17 -0500 Subject: [PATCH 14/14] COMP: Drop macos-x86_64 wheels and summarize the full set per job All target platforms now build green (Linux reuses the one ITK build in ~1.5h, macOS arm64 succeeds). Remove the macos-x86_64 (macos-13) job: Intel macOS runners are being retired and it routinely timed out waiting in the runner queue. Add a "Wheel build summary" step that writes a table of every wheel in the wheelhouse to the job's Summary page, so the whole set is visible at once rather than only the last cibuildwheel invocation (the meta wheel) at the end of the log. --- .github/workflows/python-wheels.yml | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-wheels.yml b/.github/workflows/python-wheels.yml index c4095ad465c..ab2163a8185 100644 --- a/.github/workflows/python-wheels.yml +++ b/.github/workflows/python-wheels.yml @@ -23,7 +23,8 @@ jobs: - { id: linux-x86_64, runner: ubuntu-latest, archs: x86_64, itk_dir: /itk-build/build-python } - { id: linux-aarch64, runner: ubuntu-24.04-arm, archs: aarch64, itk_dir: /itk-build/build-python } - { id: macos-arm64, runner: macos-14, archs: arm64, itk_dir: $GITHUB_WORKSPACE/build-python } - - { id: macos-x86_64, runner: macos-13, archs: x86_64, itk_dir: $GITHUB_WORKSPACE/build-python } + # macos-x86_64 (macos-13) dropped: Intel macOS runners are being retired and + # the job routinely timed out in the runner queue. Re-add if x86 macOS demand returns. # - { id: windows, runner: windows-2022, archs: AMD64, itk_dir: ... } # needs a .ps1 before-all variant steps: @@ -95,6 +96,29 @@ jobs: python -m pip install --no-index --find-links wheelhouse itk python -c "import itk; print(itk.Version.GetITKVersion())" + # One table per job on the run's Summary page listing every wheel produced, + # so the whole set is visible at a glance instead of only the last cibuildwheel + # invocation's output. Runs even on failure to show what was built. + - name: Wheel build summary + if: always() + shell: bash + run: | + shopt -s nullglob + wheels=(wheelhouse/*.whl) + { + echo "## ITK wheels — ${{ matrix.id }} (${#wheels[@]})" + echo "" + if [ ${#wheels[@]} -gt 0 ]; then + echo "| Wheel | Size |" + echo "|-------|------|" + for whl in "${wheels[@]}"; do + printf '| `%s` | %s |\n' "$(basename "$whl")" "$(du -h "$whl" | cut -f1)" + done + else + echo "_No wheels were produced._" + fi + } >> "$GITHUB_STEP_SUMMARY" + - uses: actions/upload-artifact@v4 with: name: itk-wheels-${{ matrix.id }}