%global commit 6565932c434827a2e2b103bb4f80e46f449cba40 %global shortcommit %(c=%{commit}; echo ${c:0:7}) %global snapdate 20251104 # CLI shape-analysis tools; strip the debug subpackage to keep it lean. %global debug_package %{nil} Name: SPHARM-PDM Version: 2.0.0 Release: 0.7.%{snapdate}git%{shortcommit}%{?dist} Summary: Spherical harmonic point distribution model shape analysis tools License: Apache-2.0 URL: https://github.com/NIRALUser/SPHARM-PDM Source0: %{url}/archive/%{commit}/%{name}-%{commit}.tar.gz BuildRequires: gcc-c++ BuildRequires: cmake BuildRequires: ninja-build BuildRequires: chrpath BuildRequires: git-core # The three superbuild dependencies — all provided as system packages, so we # disable the (offline-fatal) superbuild and build the inner project directly. BuildRequires: InsightToolkit5-devel BuildRequires: vtk-devel # Defines %%vtk_abi on Fedora's vtk (via vtk-abi-compat); our vtk provides it itself. BuildRequires: vtk(abi) BuildRequires: SlicerExecutionModel-devel # VTK cmake config runs find_package for all its module deps at configure time. BuildRequires: freetype-devel BuildRequires: pugixml-devel BuildRequires: fmt-devel BuildRequires: utf8cpp-devel BuildRequires: PEGTL-devel BuildRequires: jsoncpp-devel BuildRequires: nlohmann-json-devel BuildRequires: hdf5-devel BuildRequires: double-conversion-devel BuildRequires: expat-devel BuildRequires: glew-devel BuildRequires: lz4-devel BuildRequires: libxml2-devel BuildRequires: libtiff-devel BuildRequires: libjpeg-turbo-devel BuildRequires: libpng-devel # 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. Requires: vtk(abi) = %{?vtk_abi}%{!?vtk_abi: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 SPHARM-PDM is a set of command-line tools for spherical-harmonic-based shape analysis. It computes correspondent point-based shape models (PDMs) from binary segmentations by parameterising the surface with spherical harmonics and sampling a corresponding icosahedral mesh, enabling group statistical shape analysis of brain structures and other objects. Part of the NIRAL / SlicerSALT shape-analysis ecosystem. This build uses the system ITK, VTK and SlicerExecutionModel (no superbuild). %prep %autosetup -n %{name}-%{commit} # ITK_LEGACY_REMOVE. Lifted VERBATIM from 3dslicer-spharm-pdm.spec, which is # the same upstream project (NIRALUser/SPHARM-PDM) at a different commit -- # 6565932 here, 522de7b there -- and which took three releases discovering # these three constructs one build at a time. # # This port did NOT repeat that. ci/prep-tdd.sh ran the assertions against this # commit's unpatched source first (RED: 7 + 12 + 1 hits), then against the # patched source (GREEN: zero). The counts match the sibling's exactly, so the # three blocks transfer unchanged. # # Worth recording why the harness earned its keep: my own inventory of this # tarball reported only TWO constructs, because the inline grep pattern left # `()` unescaped and silently matched nothing for ComputeBoundingBox. The RED # step found the third before a builder did. # ITK_LEGACY_REMOVE. InsightToolkit5 5.4.7-19 ships with ITK_LEGACY_REMOVE # defined, which deletes the class-scope aliases for the pre-5.4 enum names, so # MeshType::CellsAllocatedDynamicallyCellByCell stopped existing and 7 call # sites here stopped compiling. Nothing in this package changed; the buildroot # did. That is not reversible from our side either: ITK declares # # cmake_dependent_option(ITK_LEGACY_REMOVE "..." OFF "NOT ITK_WRAPPING" ON) # # so enabling ITK_WRAP_PYTHON FORCES it on, and -DITK_LEGACY_SILENT=ON is # forced back off by the same mechanism. python3-itk and the legacy ITK API # cannot both be had from one build. # # The modern spelling is what upstream ITK wants anyway and compiles against # both settings, since the enum class itself is unconditional in # itkCommonEnums.h. Worth sending to NIRAL rather than carrying forever. python3 - <<'PYEOF' import pathlib, re old = re.compile(r"\b(?:Output)?MeshType::CellsAllocatedDynamicallyCellByCell\b") new = "itk::MeshEnums::MeshClassCellsAllocationMethod::CellsAllocatedDynamicallyCellByCell" total = 0 touched = [] for f in sorted(pathlib.Path(".").rglob("*")): if f.suffix not in (".cxx", ".txx", ".h", ".hxx") or not f.is_file(): continue src = f.read_text(encoding="utf-8", errors="surrogateescape") n = len(old.findall(src)) if n: f.write_text(old.sub(new, src), encoding="utf-8", errors="surrogateescape") total += n touched.append("%s (%d)" % (f, n)) assert total == 7, "expected 7 legacy enum uses, replaced %d: %s" % (total, touched) # Zero-residual: a substitution that ran is not the same as one that finished. left = sum(len(old.findall(f.read_text(encoding="utf-8", errors="surrogateescape"))) for f in pathlib.Path(".").rglob("*") if f.suffix in (".cxx", ".txx", ".h", ".hxx") and f.is_file()) assert left == 0, "%d legacy enum uses remain" % left print("ITK legacy enum: replaced %d uses in %d files" % (total, len(touched))) for t in touched: print(" ", t) PYEOF # Second ITK_LEGACY_REMOVE casualty in this package (the first is the mesh # allocation enum above). The cell-geometry enumerators were class-scope aliases # too: CellType::LINE_CELL and friends. Found only after the first fix let the # build get far enough to reach them -- legacy removal does not fail once, it # fails one layer at a time, so a clean build of the first fix proved nothing # about the second. python3 - <<'PYEOF' import pathlib, re pat = re.compile(r"\bCellType::((?:VERTEX|LINE|POLYGON|TRIANGLE)_CELL)\b") exts = (".cxx", ".txx", ".h", ".hxx") total, touched = 0, [] for f in sorted(pathlib.Path(".").rglob("*")): if f.suffix not in exts or not f.is_file(): continue src = f.read_text(encoding="utf-8", errors="surrogateescape") n = len(pat.findall(src)) if n: f.write_text(pat.sub(r"itk::CommonEnums::CellGeometry::\1", src), encoding="utf-8", errors="surrogateescape") total += n touched.append("%s (%d)" % (f, n)) assert total == 12, "expected 12 legacy cell-geometry uses, replaced %d: %s" % (total, touched) left = sum(len(pat.findall(f.read_text(encoding="utf-8", errors="surrogateescape"))) for f in pathlib.Path(".").rglob("*") if f.suffix in exts and f.is_file()) assert left == 0, "%d legacy cell-geometry uses remain" % left print("ITK legacy cell geometry: replaced %d uses in %d files" % (total, len(touched))) PYEOF # THIRD ITK_LEGACY_REMOVE casualty. SpatialObject::ComputeBoundingBox() was a # legacy shim and is gone. # # gcc suggests ComputeMyBoundingBox and that suggestion is WRONG, twice over. # itkSpatialObject.h:669-677 shows what the removed method actually was: # # #if !defined(ITK_LEGACY_REMOVE) # itkLegacyMacro(void ComputeBoundingBox()) # { # this->Update(); /* Update() should be used instead of outdated ComputeBoundingBox() */ # } # # so the behaviour-preserving replacement is Update(), full stop. And # ComputeMyBoundingBox() is declared at :697, under the `protected:` at :688 -- # not callable from this call site at all. Taking the compiler's hint would # either fail to compile or, from inside a derived class, silently compute only # the object's own box and skip the transform/family update Update() performs. # # One call site. The line after it is `this->Update()` on a DIFFERENT object # (this, vs m_CoefsMeshSpatialObject), so it is not redundant and must stay. python3 - <<'PYEOF' import pathlib, re pat = re.compile(r"->ComputeBoundingBox\(\)") exts = (".cxx", ".txx", ".h", ".hxx") total, touched = 0, [] for f in sorted(pathlib.Path(".").rglob("*")): if f.suffix not in exts or not f.is_file(): continue src = f.read_text(encoding="utf-8", errors="surrogateescape") n = len(pat.findall(src)) if n: f.write_text(pat.sub("->Update()", src), encoding="utf-8", errors="surrogateescape") total += n touched.append("%s (%d)" % (f, n)) assert total == 1, "expected 1 legacy ComputeBoundingBox use, replaced %d: %s" % (total, touched) rest = "".join(f.read_text(encoding="utf-8", errors="surrogateescape") for f in pathlib.Path(".").rglob("*") if f.suffix in exts and f.is_file()) assert "ComputeBoundingBox()" not in rest, "legacy ComputeBoundingBox() remains" # Assert no CALL to ComputeMyBoundingBox was introduced. The declaration and # definition are legitimate and must survive: this package OVERRIDES the # protected virtual (SphericalHarmonicSpatialObject.{h:107,cxx:109}). That # override is also why gcc's suggestion was dangerous rather than merely wrong # -- ComputeMyBoundingBox() IS accessible from inside this class, so the hint # would have compiled and silently skipped the transform update Update() does. import re as _re calls = _re.findall(r"[.>]ComputeMyBoundingBox\s*\(", rest) assert not calls, "a CALL to ComputeMyBoundingBox was introduced: %r" % calls print("ITK legacy ComputeBoundingBox -> Update: %d use in %s" % (total, touched)) PYEOF # CMake/CMakeLists.txt does nothing but file(DOWNLOAD) a Windows-only # FindVcvars.cmake from GitHub at configure time (fatal offline). FindVcvars is # never used on Linux — neutralise the whole sub-CMakeLists. echo '# FindVcvars download removed: Windows-only, unused on Linux, offline build' \ > CMake/CMakeLists.txt # GCC 16 rejects this legacy code's unqualified std names (e.g. MeshMath.cxx # uses bare `cout`/`endl`, relying on a `using namespace std` that older # standard headers pulled in transitively). Force-include a small compat header # that brings the std names into scope for every translation unit. cat > std-compat.h <<'EOF' #pragma once #include #include #include #include #include #include #include using namespace std; EOF # Fedora's vtk (9.7, rawhide) builds the Java wrapping and exports VTK::Java with # the JDK's /usr/lib/jvm/java/lib/libjava.so in its link interface. SPHARM-PDM links # VTK modules that pull VTK::Java in, so that file lands on the link line: without # a JDK ninja has no rule for it, and with one ld cannot resolve its libjvm.so. # SPHARM-PDM never calls into Java: right after find_package(VTK) in # SPHARM-PDM.cmake, drop the JDK library from the imported target, and once every # directory is configured take VTK::Java off the link lists of every target # SPHARM-PDM defines (same fix as 3dslicer.spec). python3 - SPHARM-PDM.cmake <<'PYEOF' import re, sys f = sys.argv[1]; s = open(f).read() inj = r''' if(TARGET VTK::Java) get_target_property(_pkgvtk_java VTK::Java INTERFACE_LINK_LIBRARIES) list(FILTER _pkgvtk_java EXCLUDE REGEX "/libjava[.]so$") set_property(TARGET VTK::Java PROPERTY INTERFACE_LINK_LIBRARIES "${_pkgvtk_java}") function(_pkgvtk_drop_java _dir) get_property(_tgts DIRECTORY "${_dir}" PROPERTY BUILDSYSTEM_TARGETS) foreach(_t IN LISTS _tgts) foreach(_p LINK_LIBRARIES INTERFACE_LINK_LIBRARIES) get_property(_l TARGET ${_t} PROPERTY ${_p}) if(_l MATCHES "VTK::Java") list(FILTER _l EXCLUDE REGEX "VTK::Java([^A-Za-z0-9_]|$)") set_property(TARGET ${_t} PROPERTY ${_p} "${_l}") endif() endforeach() endforeach() get_property(_subs DIRECTORY "${_dir}" PROPERTY SUBDIRECTORIES) foreach(_s IN LISTS _subs) _pkgvtk_drop_java("${_s}") endforeach() endfunction() cmake_language(DEFER CALL _pkgvtk_drop_java "${CMAKE_CURRENT_SOURCE_DIR}") endif() ''' m = re.search(r'^[ \t]*find_package[ \t]*\([ \t]*VTK\b[^)]*\)[^\n]*\n', s, re.M | re.I) if not m: sys.exit("find_package(VTK) not found in " + f) open(f, 'w').write(s[:m.end()] + inj + s[m.end():]) PYEOF grep -q '_pkgvtk_drop_java' SPHARM-PDM.cmake %build # Pin the ITK layout state this build sees (see the Requires above). test -n "%{?itk_abi}" || { echo "ERROR: itk_abi undefined - needs InsightToolkit5-devel >= 5.4.7-24 (macros.itk)" >&2; exit 1; } # 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; } export CXXFLAGS="%{optflags} -include ${PWD}/std-compat.h -fpermissive" %cmake -GNinja \ -DCMAKE_BUILD_TYPE=Release \ -DSPHARM-PDM_SUPERBUILD:BOOL=OFF \ -DUSE_SYSTEM_ITK:BOOL=ON \ -DUSE_SYSTEM_VTK:BOOL=ON \ -DUSE_SYSTEM_SlicerExecutionModel:BOOL=ON \ -DBUILD_TESTING:BOOL=OFF \ -DCMAKE_SKIP_INSTALL_RPATH:BOOL=ON # %%prep strips the JDK's libjava.so from VTK::Java and VTK::Java from every target; # if either still reaches a link line, stop here instead of hours into the build. if grep -qE '/libjava[.]so|libvtkJava[.]so' %{_vpath_builddir}/build.ninja; then echo 'ERROR: libjava.so or libvtkJava.so is on a link line; the VTK::Java fixes in %%prep did not apply' >&2 exit 1 fi %cmake_build %install %cmake_install # This is a CLI-tools package: drop the static dev libraries and headers the # build also installs (the tools are statically linked and self-contained). rm -rf %{buildroot}%{_includedir}/Shape find %{buildroot} -name '*.a' -delete # Strip any residual build-tree RPATHs the SEM macros may leave. find %{buildroot} -type f -perm -111 \ -exec chrpath --delete {} \; 2>/dev/null || : %files %license License.txt %{_bindir}/* %changelog * Fri Oct 30 2026 Morgan Hough - 2.0.0-0.7.20251104git6565932 - Fedora's vtk 9.7 (rawhide) exports VTK::Java with the JDK's libjava.so in its link interface; the rawhide build failed at the first link ("libjava.so, needed by bin/STL2Meta, missing and no known rule"). %%prep now strips libjava.so from VTK::Java right after find_package(VTK) in SPHARM-PDM.cmake and, via cmake_language(DEFER), drops VTK::Java from every target's link lists (the 3dslicer.spec fix). %%build stops after configure if libjava.so or libvtkJava.so is still in build.ninja. * Tue Oct 06 2026 Morgan Hough - 2.0.0-0.6.20251104git6565932 - 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 Oct 05 2026 Morgan Hough - 2.0.0-0.5.20251104git6565932 - Port 3dslicer-spharm-pdm.spec's three ITK_LEGACY_REMOVE blocks. Same upstream project at a different commit (6565932 here, 522de7b there), and the construct counts match exactly: 7 mesh-allocation enums, 12 cell-geometry enums, 1 SpatialObject::ComputeBoundingBox call. - Surfaced by medInria failing on fedora-rawhide-x86_64, where nothing provides libitkvnl-5.4.so.1: InsightToolkit5 5.4.7-21 moved to the system vxl and ships no bundled vnl. Eight packages on that chroot still link it and every one carries an .fc45 tag -- builds never redone for fc46. Only three are genuinely stale rather than superseded leftovers, and this is one. - Verified with ci/prep-tdd.sh rather than by building: RED on unpatched source (7 + 12 + 1), GREEN after. That caught an error in my own inventory, which reported only two constructs because an inline grep left `()` unescaped and matched nothing for ComputeBoundingBox. The sibling spent three builds finding its three layers serially; this found all three in about thirty seconds. * Sat Sep 19 2026 Morgan Hough - 2.0.0-0.4.git - 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. * Sun Aug 23 2026 Morgan Hough - 2.0.0-0.2.20251104git6565932 - 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. * Mon Jun 15 2026 Morgan Hough - 2.0.0-0.1.20251104git6565932 - Initial package: SPHARM-PDM spherical-harmonic shape-analysis CLI tools (master snapshot 6565932). Built against system ITK/VTK/SlicerExecutionModel with the superbuild disabled (Fedora no-superbuild policy). Complements the FSL FIRST mesh shape-analysis workflow.