COMP: Move ITK Python wrapping to SWIG 4.4.1#6484
Conversation
|
Pushed a fix for the vendored-SWIG Python CI failure (ARMBUILD-Python / ITK.*.Python): the FetchContent refactor removed the |
78951a1 to
b91641b
Compare
Content-addressed (CID) archives repackaged from the official SWIG 4.4.1 PyPI wheels (macOS 10.9 universal2, manylinux2010/2014, Windows), used as the IPFS/CDN mirror for ITK's vendored SWIG download. See InsightSoftwareConsortium/ITK#6484.
CID-addressed swig 4.4.1 source used by the ITK_USE_SYSTEM_SWIG=OFF source-build fallback when no prebuilt binary exists for the host. See InsightSoftwareConsortium/ITK#6484.
CID-addressed swig 4.4.1 source used by the ITK_USE_SYSTEM_SWIG=OFF source-build fallback when no prebuilt binary exists for the host. See InsightSoftwareConsortium/ITK#6484.
ITK packs several SWIG modules per shared library and injects C that calls PyInit__<X>Python() and stores the return in sys.modules. SWIG 4.4 (PEP 489 multi-phase init) makes PyInit_* return a raw PyModuleDef rather than a populated module, so the proxy fails at import with 'moduledef' object has no attribute 'delete_SwigPyIterator'. Detect a returned def at runtime and instantiate it via PyModule_FromDefAndSpec + PyModule_ExecDef; the single-phase (<=4.3) path is unchanged. All APIs used are Limited-API-safe (>=3.5). Pin the abi3 floor to a single cached source of truth (_ITK_MINIMUM_SUPPORTED_LIMITED_API_VERSION = 3.11) shared by the Limited-API auto-detect and the USE_SABI setting, so 3.11+ builds load on Python 3.11; 3.10 keeps the non-Limited-API path. Assisted-by: Claude Code — root-cause analysis and local 3.11/3.13 validation
Replace the prebuilt-binary ExternalProject_Add download branches with a single FetchContent_Declare/MakeAvailable that downloads and extracts the binary at configure time, so SWIG_EXECUTABLE exists immediately and is checked against swig_version_min by itk_assert_swig_version() (runs 'swig -version'). A wrong or corrupt upload now fails at configure instead of deep in the build. The source-build fallback keeps ExternalProject_Add (it compiles SWIG + PCRE2). Point the download at SWIG 4.4.1 binaries repackaged from the official, broadly portable SWIG PyPI wheels (Linux manylinux2010/2014, macOS 10.9 universal2, Windows) and uploaded to data.kitware.com, which resolves each by its SHA512. Every archive shares one bin/swig[.exe] + share/swig/4.4.1 layout, so the per-platform swig_dir_subpath collapses to a single value. swig_version_min is raised 4.2.0 -> 4.3.0 (4.2.x lacks the SWIG_Py_DECREF runtime); too-old system swig is now a FATAL_ERROR. Assisted-by: Claude Code — FetchContent refactor and configure-time download+assert verification
Add swig (>=4.4.1) and castxml to the python pixi feature and pass ITK_USE_SYSTEM_SWIG/ITK_USE_SYSTEM_CASTXML in configure-python and configure-debug-python, so local 'pixi run build-python' uses the conda-forge toolchain instead of the internal download/build. CI continues to exercise the vendored 4.4.1 binaries via the standard Azure/ARM Python pipelines.
swig_archive was only read by the IPFS mirror URLs, which were dropped when the vendored download moved to data.kitware.com (resolved by SHA512). The macOS per-architecture branch existed solely to set it (the universal2 archive has one hash for both), so it collapses too.
swig_cmake_version and ITK_SWIG_VERSION were both 4.4.1 after the consolidation to PyPI-wheel archives (all platforms share the share/swig/<version> layout). Drop the duplicate so SWIG_DIR's version segment derives from a single source on both the prebuilt and source-build paths.
The data.kitware.com + ITKTestingData + IPFS gateway mirror list was duplicated across the prebuilt FetchContent download and the source-build ExternalProject. Emit it from a single itk_swig_mirror_urls() helper so the gateway set stays consistent across both swig acquisition paths.
|
| Filename | Overview |
|---|---|
| Wrapping/Generators/SwigInterface/CMakeLists.txt | Migrates prebuilt SWIG download from ExternalProject to FetchContent for configure-time availability; adds version assertion, but version check is bypassed when SWIG_EXECUTABLE is already cached. Source-build fallback hash changed but PR description calls the version bump a followup, creating ambiguity. |
| Wrapping/Generators/Python/CMakeLists.txt | Adds PEP 489 multi-phase init handling for SWIG 4.4; reference counting looks correct but errors during module instantiation are silently swallowed with PyErr_Clear(). |
| CMake/ITKSetPython3Vars.cmake | Promotes _ITK_MINIMUM_SUPPORTED_LIMITED_API_VERSION to CACHE INTERNAL so itk_end_wrap_module.cmake can share it; removes the local unset() that previously prevented cross-file use. |
| Wrapping/macro_files/itk_end_wrap_module.cmake | USE_SABI floor pinned from Python3_VERSION_MAJOR.MINOR to _ITK_MINIMUM_SUPPORTED_LIMITED_API_VERSION (3.11), producing a genuine abi3 wheel that loads on any 3.11+ interpreter. |
| pyproject.toml | Adds swig >=4.4.1 and castxml >=0.7.0 to the python pixi feature; configure-python and configure-debug-python tasks pass ITK_USE_SYSTEM_SWIG/ITK_USE_SYSTEM_CASTXML to use the conda-forge packages. |
| pixi.lock | Lock file updated to add swig-4.4.1 and castxml-0.7.0 plus their transitive dependencies (libllvm20, libclang-cpp20.1, icu, libxml2-16, pcre2) across all five platforms. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[CMake configure starts] --> B{ITK_USE_SYSTEM_SWIG?}
B -- yes --> C[find_package SWIG - Version >= 4.3.0 check - FATAL_ERROR if old]
B -- no --> D{swig_prebuilt? Win/Linux/macOS}
D -- yes --> E{SWIG_EXECUTABLE cached and exists?}
E -- no --> F[FetchContent_Declare itkswig SHA512 - FetchContent_MakeAvailable]
F --> G[Set SWIG_EXECUTABLE - Set SWIG_DIR - Verify swig.swg]
G --> H[itk_assert_swig_version >= 4.3.0]
E -- yes --> I[Skip fetch and assertion - stale cache risk]
D -- no --> J[ExternalProject_Add swig - source build at build-time - SWIG_DIR share/swig/4.4.1]
C & H & I & J --> K[CMake wrapping macros run]
K --> L{ITK_USE_PYTHON_LIMITED_API? Python >= 3.11}
L -- yes --> M[Python3_add_library USE_SABI 3.11 - produces .abi3.so]
L -- no --> N[Python3_add_library non-Limited-API]
M & N --> O[Injected C init code runs at Python import time]
O --> P{PyInit_* returns PyModule_Check?}
P -- yes SWIG 4.3 single-phase --> Q[PyDict_SetItemString sys.modules]
P -- no SWIG 4.4 multi-phase PyModuleDef --> R[importlib.machinery.ModuleSpec - PyModule_FromDefAndSpec - PyModule_ExecDef]
R --> S{Success?}
S -- yes --> Q
S -- no --> T[PyErr_Clear - error silently dropped]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[CMake configure starts] --> B{ITK_USE_SYSTEM_SWIG?}
B -- yes --> C[find_package SWIG - Version >= 4.3.0 check - FATAL_ERROR if old]
B -- no --> D{swig_prebuilt? Win/Linux/macOS}
D -- yes --> E{SWIG_EXECUTABLE cached and exists?}
E -- no --> F[FetchContent_Declare itkswig SHA512 - FetchContent_MakeAvailable]
F --> G[Set SWIG_EXECUTABLE - Set SWIG_DIR - Verify swig.swg]
G --> H[itk_assert_swig_version >= 4.3.0]
E -- yes --> I[Skip fetch and assertion - stale cache risk]
D -- no --> J[ExternalProject_Add swig - source build at build-time - SWIG_DIR share/swig/4.4.1]
C & H & I & J --> K[CMake wrapping macros run]
K --> L{ITK_USE_PYTHON_LIMITED_API? Python >= 3.11}
L -- yes --> M[Python3_add_library USE_SABI 3.11 - produces .abi3.so]
L -- no --> N[Python3_add_library non-Limited-API]
M & N --> O[Injected C init code runs at Python import time]
O --> P{PyInit_* returns PyModule_Check?}
P -- yes SWIG 4.3 single-phase --> Q[PyDict_SetItemString sys.modules]
P -- no SWIG 4.4 multi-phase PyModuleDef --> R[importlib.machinery.ModuleSpec - PyModule_FromDefAndSpec - PyModule_ExecDef]
R --> S{Success?}
S -- yes --> Q
S -- no --> T[PyErr_Clear - error silently dropped]
Reviews (1): Last reviewed commit: "COMP: Factor swig vendored-download mirr..." | Re-trigger Greptile
| if (${base_name}Module == NULL) | ||
| { | ||
| // Clear any pending exception so it does not poison later submodule inits. | ||
| PyErr_Clear(); | ||
| } |
There was a problem hiding this comment.
Silent failure swallows module-init errors
When PyModule_FromDefAndSpec or PyModule_ExecDef fails (e.g., a missing symbol, ABI mismatch, or SWIG runtime issue), ${base_name}Module is left NULL, PyErr_Clear() drops the active exception, and execution continues. The submodule is never inserted into sys.modules, so users see a confusing AttributeError: module 'itk' has no attribute '...' rather than the actual initialization error. At minimum, the pending exception should be printed with PyErr_WriteUnraisable or converted to a warning before being cleared, so the root cause is visible in the Python traceback.
| if(NOT SWIG_EXECUTABLE OR NOT EXISTS "${SWIG_EXECUTABLE}") | ||
| set(SWIG_VERSION ${ITK_SWIG_VERSION}) | ||
|
|
||
| if(ITK_BINARY_DIR) | ||
| itk_download_attempt_check(SWIG) | ||
| endif() | ||
|
|
||
| include(FetchContent) | ||
| # DOWNLOAD_EXTRACT_TIMESTAMP requires CMake >= 3.24. | ||
| if(${CMAKE_VERSION} VERSION_LESS 3.24) | ||
| set(swig_extract_timestamp) | ||
| else() | ||
| set( | ||
| swig_extract_timestamp | ||
| DOWNLOAD_EXTRACT_TIMESTAMP | ||
| TRUE | ||
| ) | ||
| endif() | ||
|
|
||
| itk_swig_mirror_urls(swig_urls "${swig_archive_hash}" "${swig_archive_cid}") | ||
| FetchContent_Declare( | ||
| itkswig | ||
| URL | ||
| ${swig_urls} | ||
| URL_HASH SHA512=${swig_archive_hash} | ||
| ${swig_extract_timestamp} | ||
| ) | ||
| FetchContent_MakeAvailable(itkswig) | ||
|
|
||
| set( | ||
| SWIG_EXECUTABLE | ||
| "${itkswig_SOURCE_DIR}/${swig_exe_subpath}" | ||
| CACHE FILEPATH | ||
| "swig executable." | ||
| FORCE | ||
| ) | ||
| set( | ||
| SWIG_DIR | ||
| "${itkswig_SOURCE_DIR}/${swig_dir_subpath}" | ||
| CACHE FILEPATH | ||
| "swig directory." | ||
| FORCE | ||
| ) | ||
| mark_as_advanced(SWIG_DIR) | ||
|
|
||
| if(NOT EXISTS "${SWIG_DIR}/swig.swg") | ||
| message( | ||
| FATAL_ERROR | ||
| "Vendored swig library not found at \"${SWIG_DIR}\" (expected swig.swg). The archive layout may not match share/swig/${ITK_SWIG_VERSION}." | ||
| ) | ||
| endif() | ||
|
|
||
| itk_assert_swig_version("${SWIG_EXECUTABLE}" ${swig_version_min}) | ||
| endif() |
There was a problem hiding this comment.
Version assertion skipped when
SWIG_EXECUTABLE is already cached
The entire FetchContent_Declare / itk_assert_swig_version block is guarded by if(NOT SWIG_EXECUTABLE OR NOT EXISTS "${SWIG_EXECUTABLE}"). If a developer is upgrading from the previous 4.3.x prebuilt (binary still present in _deps/) without deleting CMakeCache.txt, SWIG_EXECUTABLE passes both conditions and the stale 4.3.x binary is silently used. The version assertion is never reached, so the incompatibility (4.3.x lacking PEP 489 multi-phase init) is not caught at configure time; the build proceeds and fails at import with the same obscure 'moduledef' object error this PR is fixing.
| if(NOT TARGET swig) | ||
| # Install swig ourselves | ||
| set(SWIG_VERSION ${ITK_SWIG_VERSION}) | ||
|
|
||
| set( | ||
| swig_hash | ||
| "5ca61bd3eac0b3c6a3196afdfc2c4f40d3dce5d626b29543ba5f16560efada1c38b7f53184f79e983b32a0c0ebc26e63fe035b5680fcea0ba9a1de6aa33550c7" | ||
| ) | ||
| set(swig_cid "bafybeignhqa2zcb3x4dykjgw6h3ghxxkkd3jh5jn4yxd3uhu7u6mxbf5mi") | ||
| set(swig_install_dir ${CMAKE_CURRENT_BINARY_DIR}/swig) | ||
| set(swig_ep "${swig_install_dir}/bin/swig") |
There was a problem hiding this comment.
Source-build archive version may not match
SWIG_DIR path
SWIG_VERSION is set to ITK_SWIG_VERSION (4.4.1), so SWIG_DIR resolves to share/swig/4.4.1/. Both swig_hash and swig_cid changed from the prior values, yet the PR description lists "bump source-build fallback swig-4.0.2 to 4.4.1" as a followup, implying the archive behind the new hash might not be the SWIG 4.4.1 tarball. If the extracted archive installs its standard library to share/swig/4.0.2/, SWIG_DIR will point to a non-existent path and the build will fail on any unsupported platform (non-x86_64/aarch64 Linux, non-Darwin) with a cryptic error. Can you confirm whether the new swig_hash corresponds to a SWIG 4.4.1 source tarball? Does the new swig_hash/swig_cid in the source-build fallback correspond to a SWIG 4.4.1 source tarball? The PR description lists this bump as a followup, but both the hash and CID changed in this PR.
Move ITK Python wrapping to SWIG 4.4.1. Fixes the import regression in #6479; supersedes the temporary #6480 (4.3.x baseline) and #6481 (WIP CI).
Three focused commits, no WIP — standard GHA + Azure CI run on this PR. The Azure/ARM Python pipelines exercise the vendored 4.4.1 binaries; local
pixi run build-pythonuses conda-forge SWIG/CastXML.What changed
PyInit__<X>Python()and stores the return insys.modules. SWIG 4.4 (PEP 489 multi-phase init) makesPyInit_*return a rawPyModuleDef, so the proxy failed at import with'moduledef' object has no attribute 'delete_SwigPyIterator'. The injection now detects a returned def at runtime and instantiates it viaPyModule_FromDefAndSpec+PyModule_ExecDef; the single-phase (≤4.3) path is unchanged; all APIs are Limited-API-safe (≥3.5)._ITK_MINIMUM_SUPPORTED_LIMITED_API_VERSION(3.11), shared by the Limited-API auto-detect andUSE_SABI. 3.11+ build abi3 and load on 3.11; 3.10 keeps the non-Limited-API path.ExternalProject_AddtoFetchContent, soSWIG_EXECUTABLEexists at configure time anditk_assert_swig_version()confirms it reports ≥swig_version_min(now 4.3.0; too-old system swig is aFATAL_ERROR). Binaries are repackaged from the official, broadly-portable SWIG PyPI wheels and hosted on data.kitware.com (resolved by SHA512). Source-build fallback stays on ExternalProject.configure-python/configure-debug-pythonpassITK_USE_SYSTEM_SWIG/ITK_USE_SYSTEM_CASTXML;swig (>=4.4.1)+castxmladded to thepythonpixi feature.How the vendored binaries were produced (reproducible)
The
swig<os>-<arch>-4.4.1.ziparchives are repackaged from the officialswigPyPI package (https://pypi.org/project/swig/, maintained by the SWIG team, built with cibuildwheel). These wheels are the broadest-portability prebuilt SWIG available — Linux manylinux2010 (glibc 2.12) / manylinux2014, macOS 10.9 universal2 (x86_64+arm64 in one file), and Windows — far more portable than building on a dev box. Each wheel containsswig/data/bin/swig[.exe]+swig/data/share/swig/4.4.1/, i.e. ITK's exact layout, so repackaging is just extract-and-rezip (platform-independent — no cross-compiling):The resulting archives were uploaded to data.kitware.com and are fetched by SHA512:
macosx_10_9_universal217c4637d…1ce3cef9manylinux2010_x86_646447a7b9…d88ca11manylinux2014_aarch6444a6d174…c8902d24win_amd64700bd4de…e338f3892itk_assert_swig_version()runsswig -versionon the downloaded binary at configure time and fails if it is not ≥swig_version_min, so a wrong/corrupt upload is caught immediately. For a future SWIG bump, rerun the recipe with the new version, re-upload, and updateITK_SWIG_VERSION+ the per-platform SHA512s.Validation
Builds and imports on Python 3.11.15 and 3.13.9 (same abi3 build); the
std::vector/SwigPyIteratorpath and 10/10 scoped Python ctests pass. The vendored FetchContent download from data.kitware.com was verified at configure time to fetch 4.4.1, pass the assertion, and build_ITKCommonPython.abi3.socleanly.Download mirrors
data.kitware.com(keyed by SHA512) is the primary;dweb.link,ipfs.io, andgateway.pinata.cloudserve the same bytes by content id as IPFS fallbacks, matching ITK's ExternalData gateway set.URL_HASH SHA512validates whichever mirror responds. The content ids are the reproducibleipfs-car/IPIP-0499 CIDs produced by the ExternalData upload pipeline:bafybeifxjnbydus4eq46bocwb2hcj5ufpbj4etocia5ims426gfiw7krfibafybeiep2wbecrcii7xzfe2cekhfco6mlxqym4jhfdl6xi3p4aieblgz6mbafybeifaqaus3yzp5oujxupxk422hfmcg2idf3oyvdtsppzq7ymn64nvzebafybeidllsbllkqwoby6r5w4ueqzky2o7r6qkjbh45q57mczyh2fwfwd3qThe archives are pinned to IPFS and mirrored into ITKTestingData via the ExternalData upload pipeline.