diff --git a/.github/workflows/python-wheels.yml b/.github/workflows/python-wheels.yml new file mode 100644 index 00000000000..ab2163a8185 --- /dev/null +++ b/.github/workflows/python-wheels.yml @@ -0,0 +1,125 @@ +# 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: /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 } + # 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: + - 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). + # 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 + + # 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}" + + # 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} + # 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.ITKPythonPackage_ITK_BINARY_REUSE=ON + cmake.define.ITKPythonPackage_USE_TBB=OFF + + # cibuildwheel auto-repairs (auditwheel/delocate) — light here since ITK is static. + # 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())" + + # 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 }} + 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/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/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/Utilities/Maintenance/cibw-build-wrapped-itk.sh b/Utilities/Maintenance/cibw-build-wrapped-itk.sh new file mode 100755 index 00000000000..6a81101bf4d --- /dev/null +++ b/Utilities/Maintenance/cibw-build-wrapped-itk.sh @@ -0,0 +1,76 @@ +#!/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. +# +# 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 + +PROJECT="${1:-$PWD}" +# 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." + exit 0 +fi + +cd "${PROJECT}" + +# /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 + + # cp311 interpreter (ITK 6 needs Python >= 3.11) + pip cmake/ninja; no + # pixi/conda toolchain here. + export PATH="/opt/python/cp311-cp311/bin:${PATH}" + 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 + 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/Utilities/Maintenance/configure_wrapped_itk.py b/Utilities/Maintenance/configure_wrapped_itk.py new file mode 100755 index 00000000000..c55091281f1 --- /dev/null +++ b/Utilities/Maintenance/configure_wrapped_itk.py @@ -0,0 +1,41 @@ +#!/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", + "-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: + # 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"): + cmd.append(f"-DCMAKE_CXX_COMPILER:STRING={cxx}") + return subprocess.run(cmd).returncode + + +if __name__ == "__main__": + raise SystemExit(main()) 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..5e8bd2c3a92 --- /dev/null +++ b/Wrapping/Packaging/CMakeLists.txt @@ -0,0 +1,51 @@ +# 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. 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 + 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)") + +# 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) diff --git a/Wrapping/Packaging/README.md b/Wrapping/Packaging/README.md new file mode 100644 index 00000000000..543a8234186 --- /dev/null +++ b/Wrapping/Packaging/README.md @@ -0,0 +1,68 @@ +# 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.** 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`. 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..29e2bbef8b1 --- /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.11" +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..10a3032f3f5 --- /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.11" +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..78012a8b61f --- /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.11" +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..1d33aba203f --- /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.11" +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 = "cp311" +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..3814062b1ea --- /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.11" +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..d78217f4c20 --- /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.11" +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..ea4a947cf8e --- /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.11" +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/pyproject.toml b/pyproject.toml index a15907900d5..5bea8e076c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -280,6 +280,29 @@ 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 = "python Utilities/Maintenance/configure_wrapped_itk.py" +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 +310,4 @@ python = ["python", "dev"] pre-commit = ["pre-commit"] external-data-upload = ["external-data-upload"] doxygen = ["dev", "doxygen"] +wheel = ["python", "dev", "wheel"]