# SPHARM-PDM — Spherical Harmonics Point Distribution Models: the flagship shape # analysis pipeline of SlicerSALT. Computes spherical-harmonic parameterizations # of segmented 3D objects and the corresponding point-based shape models, the # basis for group statistical shape analysis (e.g. subcortical structures). C++ # extension: a set of SlicerExecutionModel CLI modules (GenParaMesh, # ParaToSPHARMMesh, SegPostProcess, MetaMeshTools, SpharmTool) + scripted modules, # built NON-superbuild against the system 3dslicer foundation (>= -50). # # Standalone: the inner build (SPHARM-PDM.cmake) find_package()s only # ITK / VTK / SlicerExecutionModel / Slicer — all system. The GROUPS extension # is FetchContent'd ONLY in the superbuild phase, so SUPERBUILD=OFF needs no # network. First of the SlicerSALT (Shape AnaLysis Toolbox) extensions. %global commit 522de7b54ff0748d57d04bd70d0f7048e48361e2 %global shortcommit %(c=%{commit}; echo ${c:0:8}) %global snapdate 20260601 %global slicer_inst %{_prefix}/lib/slicer %global slicer_subdir Slicer-5.12 Name: 3dslicer-spharm-pdm Version: 2.0.0 Release: 0.13.%{snapdate}git%{shortcommit}%{?dist} Summary: Spherical-harmonics point-distribution shape analysis for 3D Slicer (SlicerSALT) License: Apache-2.0 URL: https://github.com/slicersalt/SPHARM-PDM Source0: %{url}/archive/%{commit}/SPHARM-PDM-%{shortcommit}.tar.gz # The "/!\ " ASCII warning marker, written in non-raw strings, makes "\ " an # unrecognised escape: 30 warnings across 24 lines on every Slicer startup, and a # SyntaxError once Python makes the change it has announced. 48 occurrences doubled; # the printed output is byte identical. Found by sweeping the installed tree rather # than from a report -- this file was not in fedora-framework's two and has fifteen # times as many. Patch20: 3dslicer-spharm-pdm-escape-backslash.patch BuildRequires: cmake >= 3.20.6 BuildRequires: ninja-build BuildRequires: gcc-c++ BuildRequires: python3-devel # Common.cmake does find_package(Git REQUIRED) at configure time. BuildRequires: git-core BuildRequires: 3dslicer-devel >= 5.12.1 # Full Qt6 set find_package(Slicer)'s Qt check demands. BuildRequires: cmake(Qt6Core) BuildRequires: cmake(Qt6Gui) BuildRequires: cmake(Qt6Widgets) BuildRequires: cmake(Qt6Network) BuildRequires: cmake(Qt6Sql) BuildRequires: cmake(Qt6Svg) BuildRequires: cmake(Qt6Xml) BuildRequires: cmake(Qt6OpenGL) BuildRequires: cmake(Qt6OpenGLWidgets) BuildRequires: cmake(Qt6PrintSupport) BuildRequires: cmake(Qt6Multimedia) BuildRequires: cmake(Qt6MultimediaWidgets) BuildRequires: cmake(Qt6Core5Compat) BuildRequires: cmake(Qt6StateMachine) BuildRequires: cmake(Qt6WebEngineCore) BuildRequires: cmake(Qt6WebEngineWidgets) BuildRequires: cmake(Qt6WebChannel) BuildRequires: cmake(Qt6Qml) BuildRequires: cmake(Qt6Quick) BuildRequires: cmake(Qt6QuickWidgets) BuildRequires: cmake(Qt6Test) BuildRequires: cmake(Qt6UiTools) BuildRequires: qt6-qttools-devel BuildRequires: InsightToolkit5-devel >= 5.4.6-7 BuildRequires: InsightToolkit5-vtk-devel BuildRequires: vtk-devel >= 9.6 BuildRequires: ctk-devel BuildRequires: qttesting-devel BuildRequires: vtkAddon-devel BuildRequires: qRestAPI-devel BuildRequires: python-pythonqt-devel BuildRequires: commontk-applauncher-devel BuildRequires: dcmtk-devel BuildRequires: SlicerExecutionModel-devel BuildRequires: rapidjson-devel BuildRequires: libcurl-devel BuildRequires: jsoncpp-devel # Loading the foundation SlicerConfig.cmake triggers find_package(Teem) (config # mode); Fedora ships TeemConfig.cmake under %%{_libdir}/cmake. Provide it + point # Teem_DIR there so the config resolves (SPHARM itself doesn't use Teem). BuildRequires: teem-devel Requires: 3dslicer # 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 the spherical-harmonics point-distribution-model shape analysis toolbox, the flagship pipeline of SlicerSALT. From binary segmentations it computes a spherical-harmonic surface parameterization and a corresponding point-based shape model with consistent point correspondence across a population, enabling statistical shape analysis of anatomical structures. This package provides the SPHARM-PDM command-line modules (GenParaMesh, ParaToSPHARMMesh, SegPostProcess, and the MetaMeshTools utilities) plus the scripted Shape Analysis modules, built non-superbuild against the system 3D Slicer foundation. Known limitations: the pipeline accepts label maps (.nrrd, .nii, .gipl, .mhd, .hdr, .mgh) only. Mesh inputs (.vtk, .vtp) need the MeshToLabelMap extension and display of the results needs ShapePopulationViewer; neither is packaged yet. Rigid alignment needs the GROUPS extension, also not packaged. %prep %autosetup -p1 -n SPHARM-PDM-%{commit} # Post-condition for Patch20. autosetup shipped without -p1 once, at which point # patch could not find the file at all; the only reason that was caught is that it # also failed the build. Assert the EFFECT rather than the application: the whole # point of this patch is that the file compiles clean, so compile it here and # require zero SyntaxWarnings. python3 - <<'PYEOF' import pathlib, warnings files = ["CommandLineTool/SPHARM-PDM.py"] bad = [] for f in files: src = pathlib.Path(f).read_text(encoding="utf-8") with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") compile(src, f, "exec") msgs = [str(x.message) for x in w if issubclass(x.category, SyntaxWarning)] if msgs: bad.append((f, sorted(set(msgs)))) assert not bad, "SyntaxWarnings remain after the patch: " + str(bad) print("compile-clean, zero SyntaxWarnings:", ", ".join(files)) PYEOF # 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 # Distro build: install modules directly, not as an Extension-Manager bundle, so # skip SlicerExtensionCPack (wants README/license + s4ext template the install # tree doesn't ship). Guarded by if(DEFINED Slicer_DIR), which we set. sed -i '/include(${Slicer_EXTENSION_CPACK})/d' SPHARM-PDM.cmake # CMake/CMakeLists.txt does a configure-time file(DOWNLOAD) of FindVcvars.cmake # (a Windows-MSVC helper that is never used on Linux) — the build env has no # network, so the download fails the whole configure. Neutralize it. : > CMake/CMakeLists.txt # GCC 16 (Fedora 44) rejects unqualified std names. Several old NIRAL sources use # bare cout/cerr/endl assuming `using namespace std` was in effect. Inject the # directive after each affected file's last #include (namespace scope, post- # includes) so they compile unmodified otherwise. for f in Modules/CLI/MetaMeshTools/MeshMath.cxx \ Modules/CLI/MetaMeshTools/BYU2VTK.cxx \ Modules/CLI/MetaMeshTools/STL2VTK.cxx \ Libraries/Shape/Algorithms/ParametricMeshToSPHARMSpatialObjectFilter.cxx \ Libraries/SparseLibMVIml/mvvcio.cxx \ Libraries/SparseLibMVIml/mvvi.cxx \ Libraries/SparseLibMVIml/mvvd.cxx \ Libraries/SparseLibMVIml/mvvc.cxx \ Libraries/SparseLibMVIml/mvvf.cxx; do [ -f "$f" ] || continue awk 'NR==FNR{if($0 ~ /^[[:space:]]*#[[:space:]]*include/) last=FNR; next} {print; if(FNR==last) print "using namespace std;"}' "$f" "$f" > "$f.tmp" \ && mv "$f.tmp" "$f" done # ShapeAnalysisModule.py hard-imports RigidAlignmentModule at module scope, which # makes the WHOLE module fail to instantiate when it is absent -- the packaged # Slicer logged "Fail to instantiate module ShapeAnalysisModule" and dropped it. # RigidAlignmentModule comes from the GROUPS extension (NIRALUser/GROUPS), which # SPHARM-PDM does NOT declare in EXTENSION_DEPENDS and which the Slicer # ExtensionsIndex does not carry at all; it only resolves inside a SlicerSALT # build where GROUPS is co-installed. So this is an UNDECLARED SOFT dependency, # and upstream already treats the feature as optional: RigidAlignmentModuleLogic # is referenced at exactly one site (improveCorrespondence), behind # `if self.parameters.rigidAlignmentEnabled:`, which defaults to False. # Make the import match that intent -- degrade the one optional step instead of # losing the entire shape-analysis pipeline -- and raise a message that names the # missing extension if a user does enable it. Revisit if GROUPS is ever packaged # (it is a superbuild over MeshLib + RigidAlignment + SurfRemesh and depends on # ShapePopulationViewer, so it is a chain of its own). python3 - <<'PYEOF' p = "Modules/Scripted/ShapeAnalysisModule/ShapeAnalysisModule.py" s = open(p, encoding="utf-8").read() old_import = "from RigidAlignmentModule import RigidAlignmentModuleLogic\n" new_import = ( "try:\n" " from RigidAlignmentModule import RigidAlignmentModuleLogic\n" "except ImportError: # GROUPS extension not installed (optional)\n" " RigidAlignmentModuleLogic = None\n" ) assert old_import in s, "RigidAlignmentModule import not found (upstream changed?)" s = s.replace(old_import, new_import, 1) old_use = " logic = RigidAlignmentModuleLogic()\n" new_use = ( " if RigidAlignmentModuleLogic is None:\n" " raise RuntimeError(\n" " 'Rigid alignment requires the GROUPS extension, which provides the '\n" " 'RigidAlignmentModule and is not installed. Disable rigid alignment '\n" " 'to run the rest of the pipeline.')\n" " logic = RigidAlignmentModuleLogic()\n" ) assert old_use in s, "RigidAlignmentModuleLogic() call site not found (upstream changed?)" s = s.replace(old_use, new_use, 1) # Upstream writes the warning-dialog title as ' /!\ WARNING /!\ ' in a non-raw # string. "\ " is not a valid escape, so every import emits five SyntaxWarnings # and a future Python turns them into a SyntaxError. Escape the backslashes. bad, good = "' /!\\ WARNING /!\\ '", "' /!\\\\ WARNING /!\\\\ '" n = s.count(bad) assert n, "warning-title pattern not found (upstream changed?)" s = s.replace(bad, good) # (no printf-style formatting here: rpm expands macros in this heredoc first and # would eat a bare "%d") print("escaped " + str(n) + " invalid backslash-space sequences in the dialog title") open(p, "w", encoding="utf-8").write(s) print("guarded optional RigidAlignmentModule (GROUPS) import in ShapeAnalysisModule") PYEOF %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; } %cmake -G Ninja \ -DSPHARM-PDM_SUPERBUILD:BOOL=OFF \ -DSlicer_DIR:PATH=%{slicer_inst}/lib/%{slicer_subdir} \ -DTeem_DIR:PATH=%{_libdir}/cmake \ -DBUILD_TESTING:BOOL=OFF \ -DSlicer_SKIP_SlicerBlockAdditionalLauncherSettings:BOOL=TRUE \ -DCMAKE_INSTALL_PREFIX:PATH=%{slicer_inst} %cmake_build %install %cmake_install # Put the three SEM CLIs where Slicer looks for them. Slicer scans only # lib//cli-modules, but they install to %%{slicer_inst}/bin: UseSlicer.cmake # sets SlicerExecutionModel_DEFAULT_CLI_INSTALL_RUNTIME_DESTINATION only on APPLE, and # on Linux upstream relies on its superbuild's own SlicerExecutionModel to pass it. # Non-superbuild against the system SlicerExecutionModel, the default is bin. So # ShapeAnalysisModule registered, but slicer.modules.segpostprocessclp, # genparameshclp and paratospharmmeshclp did not exist, and the pipeline failed at # its first step. The other tools (MeshMath, SpharmTool, the *2VTK converters) are # plain executables; upstream installs them to the third-party bin dir, which is # bin, so they stay there. clidir=%{buildroot}%{slicer_inst}/lib/%{slicer_subdir}/cli-modules install -d "$clidir" for m in GenParaMeshCLP ParaToSPHARMMeshCLP SegPostProcessCLP; do for f in "$m" "$m.xml"; do test -e "%{buildroot}%{slicer_inst}/bin/$f" || { echo "ERROR: $f is not in %{slicer_inst}/bin" >&2; exit 1; } mv "%{buildroot}%{slicer_inst}/bin/$f" "$clidir/$f" done done # The CLIs are EXECUTABLE_ONLY; their libCLPLib.a archives are intermediate # build products that the install rules copy out anyway. Nothing links against them, # and Fedora does not ship static libraries without a reason, so drop them. _ar=$(find %{buildroot}%{slicer_inst} -name 'lib*CLPLib.a' | wc -l) test "$_ar" = 3 || { echo "ERROR: expected 3 lib*CLPLib.a, found $_ar" >&2; exit 1; } find %{buildroot}%{slicer_inst} -name 'lib*CLPLib.a' -delete # Catch-all file list (files + symlinks only, so the shared module dirs that # 3dslicer owns are never claimed). Tightened later. ( cd %{buildroot} && find . \( -type f -o -type l \) | sed 's|^\.||' ) \ > %{_builddir}/spharm.files %files -f %{_builddir}/spharm.files %license License.txt %doc README.md %changelog * Fri Oct 09 2026 Morgan Hough - 2.0.0-0.13.20260601git522de7b5 - Install GenParaMeshCLP, ParaToSPHARMMeshCLP and SegPostProcessCLP (and their .xml descriptors) into lib/Slicer-5.12/cli-modules, not %%{slicer_inst}/bin. Slicer scans only cli-modules, so none of the three registered, and ShapeAnalysisModule's pipeline (slicer.modules.segpostprocessclp and friends) could not run. ShapeAnalysisModule itself already registered on 0.12; the selftest's KNOWN_BROKEN note about it was out of date. - Stop shipping lib{GenParaMesh,ParaToSPHARMMesh,SegPostProcess}CLPLib.a. The CLIs are EXECUTABLE_ONLY, nothing links against these archives, and they are not something Fedora ships without a reason. - %%description: state that mesh (.vtk/.vtp) input and ShapePopulationViewer display need extensions that are not packaged. * Tue Oct 06 2026 Morgan Hough - 2.0.0-0.12.20260601git522de7b5 - 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. * Sat Oct 03 2026 Morgan Hough - 2.0.0-0.11.20260601git522de7b5 - Third ITK_LEGACY_REMOVE casualty: SpatialObject::ComputeBoundingBox(), one call site in SphericalHarmonicSpatialObject.cxx. Replaced with Update(), NOT with the ComputeMyBoundingBox() gcc suggests. itkSpatialObject.h:675 shows the removed method's entire body was "this->Update();", so Update() is behaviour-identical; ComputeMyBoundingBox() is declared under "protected:" at :688 and computes only the object's own box without the transform update. The compiler's hint would have been uncompilable here and semantically wrong where it did compile. - Third layer, and the pattern is worth naming: legacy removal does not fail once, it fails one layer at a time. -0.9 fixed the mesh allocation enum and exposed the cell-geometry enums; -0.10 fixed those and exposed this. A clean %%prep proves nothing about the next layer, so this is not claimed fixed until a build is green. * Sat Oct 03 2026 Morgan Hough - 2.0.0-0.10.20260601git522de7b5 - Second ITK_LEGACY_REMOVE fix: CellType::{VERTEX,LINE,POLYGON,TRIANGLE}_CELL, 12 uses in the three itkMeshTovtkPolyData.cxx copies (GenParaMeshCLP, ParaToSPHARMMeshCLP, MetaMeshTools), rewritten to itk::CommonEnums::CellGeometry::*. Compile-checked as a standalone switch over GetType() before committing. Asserted 12 replaced, 0 residual. - The 0.8 enum fix worked -- those errors are gone -- and that is exactly how this one surfaced: legacy removal fails one layer at a time, and each fix only exposes the next. - NOTE: a COPR build numbered 0.9 (11065867) was submitted from a spec that never reached git; it failed on this same cell-geometry error and produced no rpm. This release is 0.10, not a reuse of 0.9, so no NVR ever means two different things. * Tue Sep 29 2026 Morgan Hough - 2.0.0-0.8.20260601git522de7b5 - Build against an ITK with ITK_LEGACY_REMOVE. 7 call sites used the pre-5.4 class-scope enum alias MeshType::CellsAllocatedDynamicallyCellByCell, which InsightToolkit5 5.4.7-19 no longer defines; %%prep rewrites them to itk::MeshEnums::MeshClassCellsAllocationMethod::CellsAllocatedDynamicallyCellByCell, which is unconditional in itkCommonEnums.h and so compiles either way. - Nothing in this package changed to cause that: the buildroot did. ITK declares ITK_LEGACY_REMOVE with cmake_dependent_option(... OFF "NOT ITK_WRAPPING" ON), so turning on ITK_WRAP_PYTHON to get python3-itk FORCES legacy removal, and forces ITK_LEGACY_SILENT back off in the same breath. The two cannot coexist in one ITK build, which makes this the first of a class rather than a one-off. - Asserted both ways: exactly 7 replacements, then zero residual uses. * Tue Sep 29 2026 Morgan Hough - 2.0.0-0.7.20260601git522de7b5 - Apply Patch20 with -p1. autosetup was declared without it, so patch defaulted to -p0, could not strip the a/ prefix and reported "No file to patch. Skipping patch. 5 out of 5 hunks ignored" -- the build failed in %prep on both chroots (11047459). The patch itself was always correct; the path it names exists in the tarball. - Assert the post-condition too: compile CommandLineTool/SPHARM-PDM.py in %prep and require zero SyntaxWarnings. A patch step that runs is not the same as a patch that had an effect, and this checks the thing the patch exists for. * Mon Sep 28 2026 Morgan Hough - 2.0.0-0.6.20260601git522de7b5 - Patch20 doubles the backslash in the "/!\ " ASCII warning marker that SPHARM-PDM.py prints in 24 messages. Written in non-raw strings, "\ " is an unrecognised escape: 30 warnings across those 24 lines on every Slicer startup, and a SyntaxError once Python makes the change it has announced, at which point the command-line tool stops working. 48 occurrences doubled; printed output is byte identical and the file now compiles with zero escape-sequence warnings. - Found by sweeping the whole installed Slicer tree rather than from a report. fedora-framework reported two such warnings in 3dslicer-pipelines; this file was not in that report and has fifteen times as many, which is the argument for sweeping instead of fixing what you are handed. * Sat Sep 19 2026 Morgan Hough - 2.0.0-0.5.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.4.20260601git522de7b5 - 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 Jul 27 2026 Morgan Hough - 2.0.0-0.3.20260601git522de7b5 - Make ShapeAnalysisModule instantiate without the GROUPS extension. Its bare module-scope 'from RigidAlignmentModule import RigidAlignmentModuleLogic' made the entire module fail to load (Slicer logged "Fail to instantiate module ShapeAnalysisModule" and dropped it), because RigidAlignmentModule ships with GROUPS -- which SPHARM-PDM does not declare in EXTENSION_DEPENDS and which the Slicer ExtensionsIndex does not carry. Upstream already treats the feature as optional (one call site, behind rigidAlignmentEnabled which defaults to False), so guard the import and raise a message naming the missing extension only if a user enables rigid alignment. Also escape the invalid "backslash-space" sequences in the warning-dialog title (5 SyntaxWarnings today, SyntaxError in a future Python). Caught by the new CDash modules-all-instantiated check. * Wed Jul 22 2026 Morgan Hough - 2.0.0-0.2.20260601git522de7b5 - Retarget from Slicer-5.11 to Slicer-5.12 core (3dslicer 5.12.1); bump 3dslicer-devel floor to 5.12.1. * Thu Jun 18 2026 Morgan Hough - 2.0.0-0.1.20260601git522de7b5 - Initial package. SPHARM-PDM (spherical-harmonics PDM shape analysis CLIs + scripted modules) built non-superbuild against the system 3dslicer foundation >= -50. First SlicerSALT (Shape AnaLysis Toolbox) extension.