%define debug_package %{nil} %global pypi_name charm-gems %global pkg_name charm_gems Name: python-%{pypi_name} Version: 1.3.3 Release: 9%{?dist} Summary: Python bindings for the GEMS segmentation package License: GPL-3.0-only URL: https://github.com/simnibs/charm-gems Source0: %{url}/archive/v%{version}/%{pypi_name}-%{version}.tar.gz # Restore what ITK_LEGACY_REMOVE takes away. ITK_LEGACY_REMOVE is FORCED ON by # ITK's own cmake whenever ITK_WRAPPING is ON (python3-itk), so this is not a # flag anyone can turn off -- read the patch header before touching it. Patch0: charm-gems-itk-legacy-remove.patch BuildRequires: gcc-c++ # nm and c++filt, used by %%check to prove the ITK legacy symbols are gone BuildRequires: binutils BuildRequires: cmake >= 3.9 BuildRequires: make BuildRequires: python3-devel BuildRequires: python3-setuptools BuildRequires: python3-wheel BuildRequires: python3-pip BuildRequires: python3-numpy BuildRequires: python3-pybind11 BuildRequires: pybind11-devel BuildRequires: InsightToolkit5-devel >= 5.4.5 BuildRequires: zlib-devel # ITK 5.4.5 VtkGlue cmake config transitively requires VTK at find_package time # (was pinned to <9.3 for the retired system Qt5 VTK 9.2.6; F44 no longer ships # that, so use COPR VTK 9.5/9.6 — charm-gems doesn't use VTK directly) BuildRequires: vtk-devel BuildRequires: java-devel BuildRequires: libtheora-devel %description charm-gems provides Python bindings for the GEMS (Generative Model for brain MRI segmentation) library. It is used by SimNIBS's CHARM tool for creating individualized head models from MRI scans. %package -n python3-%{pkg_name} Summary: %{summary} Requires: python3-numpy # Pin the exact VTK ABI this package was built against. VTK freezes its soname at # libvtk*.so.1 across all of 9.x, so rpm cannot tell 9.5 from 9.6 from 9.7: a consumer # built against one installs cleanly against another and fails at LOAD with undefined # symbols. A >= floor does not help -- 9.7.0 satisfies >= 9.6.2 -- so this is an exact # match. %%vtk_abi comes from vtk-devel's macros.vtk, so it re-pins on every rebuild. # # The missing-macro check lives in %%build, deliberately, NOT at parse time. A # parse-time %%error cannot distinguish "COPR is generating the SRPM" from "this is a # real buildroot": rpkg srpm parses the spec with no buildroot and therefore no # vtk-devel, so a parse-time guard killed every SCM build of every guarded package # before any chroot existed (python-simpleitk 10980296 failed in 0 minutes with # "can't parse specfile"). Expanding to UNSET keeps the SRPM parse alive, and the # %%build test still fails loudly in a buildroot lacking vtk-devel, so no RPM carrying # the placeholder can ever be produced. # NOTE: this pin lives on the SUBPACKAGE, not in the preamble. The preamble # belongs to the main "python-*" package, which this spec never builds -- so a # pin there ships on nothing at all. Verified with rpmspec -q --builtrpms. Requires: vtk(abi) = %{?vtk_abi}%{!?vtk_abi:UNSET} # AND PIN ITK, for a reason measured on 2026-09-28 while verifying that SimNIBS # `charm` finally runs. From a fresh COPR install it did not, and this is why. # # ITK freezes its soname at libITK*-5.4.so.1 across the whole 5.4 series, and # this package's ONLY dependency on ITK was 97 soname requires with no version # anywhere. The free COPR publishes TEN copies of InsightToolkit5 5.4.7 for # f44-x86_64 (-2, -4, -5, -6, -7, -8, -9, -10, -11, -13), every one of which # satisfies every one of those sonames, so the depsolver was free to pick any -- # and on a fresh chroot it picked 5.4.7-2, the OLDEST. Measured, both ways: # # InsightToolkit5-5.4.7-2 import fails: "double free or corruption # (!prev)", SIGABRT; charm --help exits 134 # InsightToolkit5-5.4.7-13 import ok; charm --help exits 0 # # Nothing failed at install time and nothing failed at link time. The GEMS tree # passes allocations across the ITK boundary, so a build against -13's # unbundled Eigen/VXL layout aborts in the allocator against -2's bundled one. # # A FLOOR, not an equality: the ITK changes involved are additive, so a newer # ITK is fine and a rebuild is not forced on every ITK bump. Same two-part # expansion and same %%build guard as c3d.spec and quit.spec, which carry the # identical floor for the identical frozen soname -- ">= UNSET" is SATISFIED, # because rpm ranks a numeric segment above an alphabetic one, so an undefined # macro makes the floor vacuous in silence and only the %%build test catches it. # %%itk_ver comes from InsightToolkit5-devel's macros.itk, so it re-pins on # every rebuild. # # On the SUBPACKAGE, like the vtk(abi) pin above: the preamble belongs to the # main "python-*" package, which this spec never builds. Requires: InsightToolkit5%{?_isa} >= %{?itk_ver}%{!?itk_ver:UNSET} %description -n python3-%{pkg_name} Python bindings for the GEMS segmentation package used by SimNIBS. %prep %autosetup -p1 -n %{pypi_name}-%{version} # Remove bundled ITK and pybind11 submodule stubs (use system packages) rm -rf ITK pybind11 # Patch CMakeLists.txt to use system pybind11 via find_package sed -i 's|add_subdirectory(pybind11)|find_package(pybind11 REQUIRED)|' CMakeLists.txt # Relax -Werror (GCC 15 triggers new warnings on old code) sed -i 's/-Werror//' CMakeLists.txt # Fix x86-only SSE flags for aarch64 compatibility sed -i 's/-msse2 -mfpmath=sse//' gems/CMakeLists.txt sed -i 's/-msse2 -mfpmath=sse//' gems_python/CMakeLists.txt # Patch setup.py to pass ITK_DIR and pybind11_DIR to cmake sed -i "s|'-H.'|'-H.', '-DITK_DIR=%{_prefix}/lib/cmake/ITK-5.4', '-Dpybind11_DIR=%{_datadir}/cmake/pybind11'|" setup.py # Fix C++11 → C++17 for ITK 5.4 compatibility sed -i 's/CMAKE_CXX_STANDARD 11/CMAKE_CXX_STANDARD 17/' CMakeLists.txt sed -i 's/CMAKE_CXX_STANDARD 11/CMAKE_CXX_STANDARD 17/' gems/CMakeLists.txt # ---- ITK 4.13 → 5.4 API migration ---- # itkExceptionObject.h must not be included directly find . \( -name '*.h' -o -name '*.cxx' -o -name '*.cpp' \) \ -exec grep -l 'itkExceptionObject' {} + | \ xargs -r sed -i 's|itkExceptionObject\.h|itkMacro.h|g' # Threading API changes find . \( -name '*.h' -o -name '*.cxx' \) -exec sed -i \ -e 's|ITK_THREAD_RETURN_TYPE|itk::ITK_THREAD_RETURN_TYPE|g' \ -e 's|ITK_THREAD_RETURN_VALUE|itk::ITK_THREAD_RETURN_DEFAULT_VALUE|g' \ -e 's|itk::MultiThreader::ThreadInfoStruct|itk::PlatformMultiThreader::WorkUnitInfo|g' \ -e 's|->ThreadID|->WorkUnitID|g' \ -e 's|->NumberOfThreads|->NumberOfWorkUnits|g' \ -e 's|itk::SimpleFastMutexLock|std::mutex|g' \ -e 's|\.Lock()|.lock()|g' \ -e 's|\.Unlock()|.unlock()|g' \ -e 's|itk::MultiThreader::GetGlobalDefaultNumberOfThreads|itk::MultiThreaderBase::GetGlobalDefaultNumberOfThreads|g' \ -e 's|itk::MultiThreader::Pointer|itk::PlatformMultiThreader::Pointer|g' \ -e 's|itk::MultiThreader::New|itk::PlatformMultiThreader::New|g' \ {} + # Add missing ITK 5 threading headers sed -i '/#include "kvlAtlasMesh.h"/a #include "itkPlatformMultiThreader.h"' gems/kvlAtlasMeshRasterizor.h sed -i '1i #include ' gems/kvlAtlasMeshRasterizor.cxx sed -i '1i #include "itkMultiThreaderBase.h"' gems/kvlAtlasParameterEstimator.cxx # ITK 5.x SmartPointer: convert '= 0' to '= nullptr' for known Pointer members # Whitelist approach avoids false positives on int/double member assignments for m in m_CostFunction m_BinnedImage m_Image m_MeshCollection \ m_CompressionLookupTable m_LabelImage m_ProbabilityImage \ m_PositionGradient m_TargetPoints m_LabelStatistics \ m_PointParameters m_Cells m_ReferenceTetrahedronInfos \ m_ReferencePosition m_CellLinks m_Mesh m_Calculator \ m_Position m_Gradient m_OldGradient m_OldSearchDirection; do find gems gems_python \( -name '*.cxx' -o -name '*.h' \) \ -exec sed -i "s/${m} = 0;/${m} = nullptr;/g" {} + done # Convert 'Pointer/ConstPointer varname = 0' local variable declarations find gems gems_python \( -name '*.cxx' -o -name '*.h' \) \ -exec sed -i -E 's/(Pointer\s+\w+)\s*=\s*0;/\1 = nullptr;/g' {} + # Convert 'return 0;' → 'return nullptr;' (all instances are in Pointer-returning functions) find gems gems_python \( -name '*.cxx' -o -name '*.h' \) \ -exec sed -i 's/return 0;/return nullptr;/g' {} + %build # vtk_abi comes from vtk-devel's macros.vtk. Absent means BuildRequires: vtk-devel is # missing, and the Requires above would ship as the vacuous "vtk(abi) = UNSET". test -n "%{?vtk_abi}" || { echo "ERROR: vtk_abi undefined - add 'BuildRequires: vtk-devel'" >&2; exit 1; } # itk_ver comes from InsightToolkit5-devel's macros.itk. Absent means the ITK # floor above would ship as the vacuous "InsightToolkit5 >= UNSET", which any # ITK satisfies -- see the long comment beside that Requires. test -n "%{?itk_ver}" || { echo "ERROR: itk_ver undefined - add 'BuildRequires: InsightToolkit5-devel'" >&2; exit 1; } export ITK_DIR=%{_prefix}/lib/cmake/ITK-5.4 export CXXFLAGS="%{optflags} -std=c++17 -include cstdint -fpermissive" %pyproject_wheel %install %pyproject_install %pyproject_save_files %{pkg_name} %check # Prove the ITK legacy-API break is FIXED, do not assume the rebuild did it. # # python3-charm_gems-1.3.3-7 installed cleanly on top of InsightToolkit5 # 5.4.7-13 and then died at import with # undefined symbol: _ZN3itk35ObjectToObjectOptimizerBaseTemplateIdE18SetNumberOfThreadsEj # Nothing rpm can express would have caught that: the soname # libITKOptimizersv4-5.4.so.1 is unchanged across the ITK rebuild that removed # the symbols, so the stale build satisfies every dependency it declares. # See charm-gems-itk-legacy-remove.patch for why the symbols went away. # # Find the module FIRST and make its absence fatal. A check that inspects a # path which has moved must FAIL, not pass having looked at nothing. _so=$(find %{buildroot}%{python3_sitearch}/%{pkg_name} -name 'gemsbindings*.so' -print -quit) if [ -z "$_so" ]; then echo "CHECK FAILED: no gemsbindings*.so under %{buildroot}%{python3_sitearch}/%{pkg_name}" >&2 echo " The extension module was not built, or it moved. Without it the" >&2 echo " symbol assertions below would inspect nothing and report success." >&2 exit 1 fi echo "checking $_so" _undef=$(mktemp) nm -D --undefined-only "$_so" | c++filt > "$_undef" # Positive control on the instrument itself. A Python extension module always # has a long list of undefined symbols (Py*, libstdc++, ITK). A short or empty # list means nm/c++filt produced nothing useful and every grep below is vacuous. if [ "$(wc -l < "$_undef")" -lt 10 ]; then echo "CHECK FAILED: nm -D --undefined-only returned almost nothing for $_so;" >&2 echo " the symbol assertions below would be meaningless." >&2 cat "$_undef" >&2 exit 1 fi # 1. No undefined legacy NumberOfThreads accessor on an ITK optimizer type. # Scoped to ObjectToObjectOptimizer* on purpose: MultiThreaderBase's # SetGlobalDefaultNumberOfThreads is a DIFFERENT, non-legacy API that this # package legitimately uses, and must not be caught here. if grep -E 'ObjectToObjectOptimizer[A-Za-z]*<[^>]*>::(Set|Get)NumberOfThreads' "$_undef"; then echo "CHECK FAILED: the module still needs ITK's legacy ObjectToObjectOptimizer" >&2 echo " Set/GetNumberOfThreads accessors. They are NOT in" >&2 echo " libITKOptimizersv4-5.4.so.1 any more, so this RPM would install" >&2 echo " cleanly and then fail at 'import charm_gems'." >&2 exit 1 fi # 2. Positive control for (1): the optimizer base must still be referenced, # through the modern accessor. If GEMS ever stopped instantiating ITK's # optimizers, test 1 would pass while proving nothing at all. if ! grep -qE 'ObjectToObjectOptimizerBaseTemplate<[^>]*>::SetNumberOfWorkUnits' "$_undef"; then echo "CHECK FAILED: $_so has no undefined" >&2 echo " ObjectToObjectOptimizerBaseTemplate<...>::SetNumberOfWorkUnits, so it" >&2 echo " no longer references ITK's optimizer base at all and test 1 above is" >&2 echo " vacuous. Either the code changed or the assertion needs updating." >&2 echo " Undefined ITK optimizer symbols actually present:" >&2 grep 'ObjectToObjectOptimizer' "$_undef" >&2 || echo " (none)" >&2 exit 1 fi rm -f "$_undef" # 3. And it must actually import. export PYTHONPATH=%{buildroot}%{python3_sitearch} %{python3} -c "import charm_gems; print('charm_gems imported successfully')" %files -n python3-%{pkg_name} -f %{pyproject_files} %license LICENSE %changelog * Mon Sep 28 2026 Morgan Hough - 1.3.3-9 - Pin ITK. This package had NO version dependency on ITK at all -- 97 soname requires and nothing else -- and ITK freezes its soname at libITK*-5.4.so.1 across the whole 5.4 series. The free COPR publishes ten copies of InsightToolkit5 5.4.7 for f44-x86_64 (-2 through -13) and every one satisfies every one of those sonames, so on a fresh chroot the depsolver chose 5.4.7-2, the OLDEST. Measured both directions on 2026-09-28: over 5.4.7-2, `import charm_gems` dies with "double free or corruption (!prev)" and SIGABRT and `charm --help` exits 134; over 5.4.7-13 both are clean and charm --help exits 0. Nothing failed at install time or at link time -- only in the allocator, at import. - The fix is PACKAGING-POLICY rule 1: an explicit floor InsightToolkit5%%{?_isa} >= %%{?itk_ver}%%{!?itk_ver:UNSET} on the SUBPACKAGE (rule 2 -- the preamble ships on nothing in a python spec), plus the test -n "%%{?itk_ver}" guard in %%build, because ">= UNSET" is satisfied by any version and would go vacuous in silence. Same shape as c3d.spec and quit.spec, which carry the identical floor for the same frozen soname. - Found while verifying that the brainsynth -> brainnet chain makes SimNIBS charm work. It does -- but a user installing from the COPR could still get an ITK old enough to abort at import, and no instrument in the repo could see it. * Sun Sep 27 2026 Morgan Hough - 1.3.3-8 - Port GEMS off ITK's legacy API (charm-gems-itk-legacy-remove.patch). 1.3.3-7 installed cleanly over InsightToolkit5 5.4.7-13 and then failed at "import charm_gems" with undefined itk::ObjectToObjectOptimizerBaseTemplate::Set/GetNumberOfThreads. The soname libITKOptimizersv4-5.4.so.1 did not change, so nothing in rpm could see it -- same class as the c3d/quit FFTW-symbol rows. - The cause is structural, not a mistake in the ITK package: ITK declares ITK_LEGACY_REMOVE with cmake_dependent_option(... OFF "NOT ITK_WRAPPING" ON), which FORCES it ON when the Python wrapping is built. python3-itk exists, so ITK_LEGACY_REMOVE is on and cannot be turned off from here. Every ITK consumer still using the ITK 4 API needs a change like this one; a plain rebuild FTBFSes. - Six distinct legacy constructs had to go, not just the optimizer accessors: ImageIOBase's unscoped IOComponent/IOByteOrder enumerators, CellInterface's cell-geometry names (43 sites), itkEventMacro, ImageIOFactory::ReadMode, ImageRegistrationMethodv4's MetricSamplingStrategyType/RANDOM, and itkMultiThreader.h, which is now nothing but an #error. - MultiThreaderBase::SetGlobalDefaultNumberOfThreads is deliberately NOT renamed -- it is a different, non-legacy API. tests/simnibs.bats pins that. - %%check now proves the fix instead of assuming it: nm -D --undefined-only | c++filt must show no legacy optimizer accessor AND must still show SetNumberOfWorkUnits, so the assertion cannot pass by inspecting nothing. * Sat Sep 19 2026 Morgan Hough - 1.3.3-7 - Rebuild against InsightToolkit5 5.4.7-8, which unbundles Eigen3, VXL and double-conversion. No source or spec change; the Release bump is what makes the rebuild reach anyone. COPR adds a sibling at an already-published NVR rather than replacing it, and dnf then serves the older copy -- so a rebuild at the same NVR would leave this package linked against the bundled ITK while appearing to succeed. - Validated against -8 in mhough/neurofedora-itk5-test before this bump. * Tue Sep 15 2026 Morgan Hough - 1.3.3-6 - The vtk(abi) pin was in the preamble, which belongs to the main "python-*" package this spec never builds -- so it shipped on nothing. Moved onto the python3-* subpackage, where the VTK linkage actually is. Verified with rpmspec -q --builtrpms rather than by reading the spec. - The package does link VTK despite the older note saying it does not: three libvtk*.so.1 sonames, pulled in transitively through ITK's VtkGlue. * Sun Aug 23 2026 Morgan Hough - 1.3.3-5 - Rebuild against VTK 9.7.0. VTK here installs unversioned sonames (libvtk*.so.1), so 9.7 replaces 9.6 in place and every consumer must be rebuilt or it fails at runtime with undefined symbols. * Fri Jun 12 2026 Morgan Hough - 1.3.3-4 - Rebuild for rawhide's Python 3.15 transition (the -3 build was built against python 3.14, stale on rawhide). Refreshes the SimNIBS chain so python-samseg and simnibs resolve and install on rawhide. * Sat May 23 2026 Morgan Hough - 1.3.3-3 - Drop vtk-devel <9.3 pin (system VTK 9.2.6 Qt5 retired on F44+); charm-gems pulls vtk transitively via ITK VtkGlue, doesn't use it directly * Wed Mar 11 2026 Morgan Hough - 1.3.3-2 - Fix SmartPointer nullptr conversion: use whitelist instead of blanket regex * Tue Mar 10 2026 Morgan Hough - 1.3.3-1 - Initial package