%define debug_package %{nil} %define _python_dist_allow_version_zero 1 %global pypi_name samseg %global commit e86d8762c9140d5e3d4250e90d7ec947a575c355 %global shortcommit %(c=%{commit}; echo ${c:0:7}) Name: python-%{pypi_name} Version: 0.5a0 Release: 8%{?dist} Summary: Sequence Adaptive Multimodal SEGmentation License: MIT URL: https://github.com/freesurfer/samseg Source0: %{url}/archive/%{commit}/%{pypi_name}-%{shortcommit}.tar.gz Patch0: samseg-system-deps.patch # 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. Patch1: samseg-itk-legacy-remove.patch BuildRequires: python3-devel BuildRequires: python3-setuptools BuildRequires: python3-wheel BuildRequires: python3-pip BuildRequires: python3-versioneer BuildRequires: python3-pybind11 BuildRequires: pybind11-devel BuildRequires: cmake BuildRequires: gcc-c++ # nm and c++filt, used by %%check to prove the ITK legacy symbols are gone BuildRequires: binutils 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; samseg doesn't # use VTK directly so the COPR VTK 9.5/9.6 is fine) BuildRequires: vtk-devel BuildRequires: java-devel BuildRequires: libtheora-devel BuildRequires: qt5-qtbase-devel # Needed for %check (samseg __init__.py imports surfa and sklearn at top level) BuildRequires: python3-surfa BuildRequires: python3-scikit-learn BuildRequires: python3-scipy BuildRequires: python3-nibabel BuildRequires: python3-pillow BuildRequires: python3-xxhash %description SAMSEG (Sequence Adaptive Multimodal SEGmentation) is a tool for robustly segmenting various brain structures from head MRI scans without requiring extensive preprocessing. %package -n python3-%{pypi_name} Summary: %{summary} Requires: python3-numpy Requires: python3-scipy Requires: python3-surfa Requires: python3-scikit-learn Requires: python3-numba # 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} # itk(abi): ITK_LEGACY_REMOVE changes class layouts at unchanged sonames, so the itk_ver floor alone let a # consumer built against the removed state install over the present one (python3-simpleitk -11 on ITK -23 # returned all-zero images). Equality on the legacy-state token; see InsightToolkit5.spec. Requires: itk(abi) = %{?itk_abi}%{!?itk_abi:UNSET} %description -n python3-%{pypi_name} SAMSEG (Sequence Adaptive Multimodal SEGmentation) is a tool for robustly segmenting various brain structures from head MRI scans without requiring extensive preprocessing. %prep %setup -q -n %{pypi_name}-%{commit} %patch -P 0 -p0 %patch -P 1 -p1 # Remove bundled dirs to ensure we use system libraries rm -rf pybind11 ITK # Fix C++11 -> C++17 for ITK 5.4 / GCC 15 compatibility sed -i 's/CMAKE_CXX_STANDARD 11/CMAKE_CXX_STANDARD 17/' CMakeLists.txt # Remove -Werror flags from subdirectory cmake files find . -name CMakeLists.txt -exec sed -i 's/-Werror//' {} + # Fix x86-only SSE flags for aarch64 find . -name CMakeLists.txt -exec sed -i 's/-msse2 -mfpmath=sse//' {} + # itkExceptionObject.h must not be included directly in ITK 5 find . \( -name '*.h' -o -name '*.cxx' -o -name '*.cpp' \) \ -exec grep -l 'itkExceptionObject' {} + | \ xargs -r sed -i 's|itkExceptionObject\.h|itkMacro.h|g' # ITK 5.x SmartPointer: convert '= 0' to '= nullptr' for known Pointer members 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 \( -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 \( -name '*.cxx' -o -name '*.h' \) \ -exec sed -i -E 's/(Pointer\s+\w+)\s*=\s*0;/\1 = nullptr;/g' {} + # Convert 'return 0;' -> 'return nullptr;' in Pointer-returning functions find gems \( -name '*.cxx' -o -name '*.h' \) \ -exec sed -i 's/return 0;/return nullptr;/g' {} + # Remove pinned ml-dtypes version and git-pinned surfa URL from setup.cfg sed -i '/ml-dtypes==/d' setup.cfg sed -i '/surfa @ git+/d' setup.cfg # Relax numpy pinning sed -i 's/numpy >= 2.0/numpy/' setup.cfg %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; } test -n "%{?itk_abi}" || { echo "ERROR: itk_abi undefined - needs InsightToolkit5-devel >= 5.4.7-24 (macros.itk)" >&2; exit 1; } export ITK_DIR=%{_prefix}/lib/cmake/ITK-5.4 export pybind11_DIR=%{_datadir}/cmake/pybind11 export CXXFLAGS="%{optflags} -std=c++17 -include cstdint -fpermissive" %pyproject_wheel %install %pyproject_install %pyproject_save_files %{pypi_name} %check # Prove the ITK legacy-API break is FIXED, do not assume the rebuild did it. # # python3-samseg-0.5a0-5 installed cleanly on top of InsightToolkit5 5.4.7-13 # and then died at import with # undefined symbol: _ZN3itk35ObjectToObjectOptimizerBaseTemplateIdE18SetNumberOfThreadsEj # which took SimNIBS charm down with it, because charm_main.py imports samseg # at module scope. Nothing rpm can express would have caught it: 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 samseg-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}/%{pypi_name}/gems -name 'gemsbindings*.so' -print -quit) if [ -z "$_so" ]; then echo "CHECK FAILED: no gemsbindings*.so under %{buildroot}%{python3_sitearch}/%{pypi_name}/gems" >&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 samseg' -- and take charm with it." >&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 load. The .so is loaded directly rather than through # "import samseg", because samseg/__init__.py pulls in numba, which is not # guaranteed to be in the buildroot. cd / %{python3} -c " import importlib.util, sys, os so_dir = '%{buildroot}%{python3_sitearch}/samseg/gems' loaded = False for f in os.listdir(so_dir): if f.startswith('gemsbindings') and f.endswith('.so'): spec = importlib.util.spec_from_file_location('gemsbindings', os.path.join(so_dir, f)) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) print('gemsbindings loaded successfully') loaded = True break if not loaded: sys.exit('no gemsbindings*.so found in %s -- nothing was loaded' % so_dir) " %files -n python3-%{pypi_name} -f %{pyproject_files} %{_bindir}/computeTissueConcentrations %{_bindir}/gems_compute_atlas_probs %{_bindir}/merge_add_mesh_alphas %{_bindir}/prepareAtlasDirectory %{_bindir}/run_samseg %{_bindir}/run_samseg_long %{_bindir}/sbtiv %{_bindir}/segment_subregions %changelog * Tue Oct 06 2026 Morgan Hough - 0.5a0-8 - Pin the ITK layout state: Requires: itk(abi) = %%{itk_abi}, from InsightToolkit5-devel's macros.itk, plus a %%build guard. ITK -23 flipped ITK_LEGACY_REMOVE back off, which changes class layouts at unchanged sonames; this package was built against -10..-21 (legacy removed) and the itk_ver floor let it install over -23, giving silent wrong results or crashes. Rebuild against ITK >= 5.4.7-24. * Mon Sep 28 2026 Morgan Hough - 0.5a0-7 - 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 samseg` 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 - 0.5a0-6 - Port GEMS off ITK's legacy API (samseg-itk-legacy-remove.patch). 0.5a0-5 installed cleanly over InsightToolkit5 5.4.7-13 and then failed at "import samseg" with undefined itk::ObjectToObjectOptimizerBaseTemplate::Set/GetNumberOfThreads -- which took SimNIBS charm down with it, because charm_main.py imports samseg at module scope. The soname libITKOptimizersv4-5.4.so.1 did not change, so nothing in rpm could see it. - 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. - Upstream samseg (branch dev) had already fixed the threading half under "#if ITK_VERSION_MAJOR >= 5"; what remains is ImageIOBase's unscoped IOComponent/IOByteOrder enumerators, CellInterface's cell-geometry names (58 sites), itkEventMacro, ImageIOFactory::ReadMode/WriteMode and ImageRegistrationMethodv4's MetricSamplingStrategyType/RANDOM. - MultiThreaderBase::SetGlobalDefaultNumberOfThreads is deliberately NOT renamed -- it is a different, non-legacy API. - %%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. The .so is also located first, and its absence is fatal. * Sat Sep 19 2026 Morgan Hough - 0.5a0-5 - 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 - 0.5a0-4 - 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 - 0.5a0-3 - 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. * Sat May 23 2026 Morgan Hough - 0.5a0-2 - Drop vtk-devel <9.3 pin (system VTK 9.2.6 Qt5 retired on F44+) * Thu Apr 23 2026 Morgan Hough - 0.5a0-1 - Update to 0.5a (new commit e86d876) - Remove pinned ml-dtypes and git-pinned surfa from setup.cfg - Relax numpy version requirement * Wed Mar 11 2026 Morgan Hough - 0.4a0-3 - Add VTK transitive BuildRequires (ITK VtkGlue cmake config) - Add ITK 5 SmartPointer nullptr conversion and itkExceptionObject fix - Suppress empty debug package * Tue Mar 10 2026 Morgan Hough - 0.4a0-2 - Fix: use InsightToolkit5-devel instead of nonexistent InsightToolkit6-devel - Fix: use C++17 for ITK 5.4 / GCC 15 compatibility - Add surfa, scikit-learn, numba runtime Requires * Wed Feb 25 2026 Morgan Hough - 0.4a0-1 - Initial package for SimNIBS dependency