%define debug_package %{nil} Name: plastimatch Version: 1.10.0 Release: 16%{?dist} Summary: Medical image registration and segmentation for radiation therapy License: BSD-3-Clause URL: https://plastimatch.org/ Source0: https://gitlab.com/plastimatch/plastimatch/-/archive/%{version}/%{name}-%{version}.tar.gz # ITK 5.4.7-10 removed the legacy API (ITK's CMake forces ITK_LEGACY_REMOVE=ON # when ITK_WRAP_PYTHON is on). plastimatch trips over it twice. # # 1. RUNTIME: it calls itk::PlatformMultiThreader::SpawnThread, so the shipped # binary does not start at all: # plastimatch: symbol lookup error: /lib64/libplmregister.so.1.10: # undefined symbol: _ZN3itk21PlatformMultiThreader11SpawnThreadEPFPvS1_ES1_ # Like c3d and unlike ANTs, a rebuild is not enough -- readelf shows one # R_X86_64_JUMP_SLOT, a real call site. ITK's own header says of both # SpawnThread and TerminateThread: "Deprecated. Use C++11 thread support # instead." The patch does exactly that. # # 2. COMPILE: itk::ImageIOBase::IOPixelType and ::IOComponentType, and the # enumerators reached through ImageIOBase (UCHAR, FLOAT, SCALAR, ...), are # all backwards-compatibility aliases inside #if !defined(ITK_LEGACY_REMOVE). # 38 references across 6 files; rewritten to itk::IOPixelEnum / # itk::IOComponentEnum, which the aliases point at. Verified the modern # spellings compile against the installed ITK with a standalone probe before # touching the tree. # NB itk::Image<>::IOPixelType is a DIFFERENT alias (= PixelType), is not # legacy, and is deliberately left alone -- ss_img_extract uses it. The # rewrite is anchored on "itk::ImageIOBase::" for exactly that reason. # # 3. COMPILE: plm_warp.cxx assigns a Vector expression (Matrix*Vector + Vector) # to an itk::Point. Point's implicit construction from a Vector is also # guarded on ITK_LEGACY_REMOVE, so build the Point from the components # explicitly. Same arithmetic, no implicit conversion. # 4. COMPILE: the vendored ESMDemonsRegistrationWithMask function calls # SpatialObject::IsInside(), which ITK 5 renamed IsInsideInWorldSpace() and # removed under ITK_LEGACY_REMOVE. Masks here are world-space, which is what # the old IsInside() meant. # 5. COMPILE: the vendored RANSAC (libs/ransac) drives itk::PlatformMultiThreader # with SetNumberOfThreads() and ThreadInfoStruct, both legacy-only. Use # SetMaximumNumberOfThreads()+SetNumberOfWorkUnits() (what the removed # SetNumberOfThreads() did) and WorkUnitInfo. # 6. COMPILE: standalone dicom_info calls GDCMImageIO::SetMaxSizeLoadEntry(), # which ITK documents as a GDCM 1.x-only option with no effect on GDCM 2.x # and which is gone now; the call is deleted. Patch0: plastimatch-itk-legacy-spawnthread.patch BuildRequires: cmake BuildRequires: gcc-c++ BuildRequires: InsightToolkit5-devel >= 5.4 BuildRequires: dcmtk-devel BuildRequires: fftw-devel BuildRequires: sqlite-devel BuildRequires: hdf5-devel # VTK transitive deps (ITK5 cmake config loads ITKVtkGlue → find_package(VTK)) BuildRequires: python3-devel BuildRequires: qt6-qtdeclarative-devel %if 0%{?fedora} BuildRequires: libminc-devel %endif # Pin the exact DCMTK snapshot this package was built against. dcmtk is packaged from git # HEAD and upstream's DCMTK_ABI_VERSION stays 20 across snapshots, so every snapshot installs # libdcm*.so.20 and rpm cannot tell them apart: a consumer built against one installs cleanly # against another and fails at load with undefined symbols (dcmqi on f45, with OverlapUtil). # %%dcmtk_abi comes from dcmtk-devel's macros.dcmtk, so it re-pins on every rebuild. The # missing-macro check is in %%build, not at parse time, for the reason spelled out at the vtk # pin in 3dslicer.spec: an SRPM parse has no buildroot. Requires: dcmtk(abi) = %{?dcmtk_abi}%{!?dcmtk_abi:UNSET} %description Plastimatch is an open-source software for medical image registration, segmentation, and computational geometry. It is primarily used in radiation therapy research and supports deformable registration, DRR generation, dose computation, and DICOM-RT structure processing. The single 'plastimatch' command provides 40+ subcommands for image operations. %package devel Summary: Development files for plastimatch Requires: %{name}%{?_isa} = %{version}-%{release} Requires: InsightToolkit5-devel >= 5.4 Requires: dcmtk-devel %description devel Headers, shared-library symlinks and CMake package config for building against the plastimatch registration/segmentation libraries. Added for SlicerRT: its SuperBuild otherwise ExternalProject_Add()s plastimatch from git at build time, which cannot work without network access in mock/COPR. %prep %autosetup -n %{name}-%{version} -p1 # Drop the bundled FindOpenMP.cmake. CMakeLists.txt:64 prepends cmake/ to # CMAKE_MODULE_PATH, so this module SHADOWS CMake's builtin for every consumer in # the configure -- and it only ever sets the pre-3.x all-caps OPENMP_FOUND, never # OpenMP_FOUND, and defines no OpenMP::OpenMP_CXX targets. # plastimatch's own probe therefore succeeds ("OpenMP flags -fopenmp") while # Viskores's find_dependency(OpenMP), reached via # find_package(ITK) -> ITKVtkGlue -> vtk-config -> ViskoresConfig.cmake:151 # gets no OpenMP_FOUND and aborts with # "Viskores could not be found because dependency OpenMP could not be found". # Latent for years; it only became fatal when Viskores was split out of VTK into # its own package after this package last built green (2026-06-02). rm -f cmake/FindOpenMP.cmake test ! -e cmake/FindOpenMP.cmake # ...but plastimatch's own build reads the legacy all-caps spellings that only # the deleted module ever set -- 25 OPENMP_FLAGS, 15 OPENMP_LDFLAGS and 13 # OPENMP_LIBRARIES references across the tree. CMake's builtin exports the # modern OpenMP_CXX_* names instead (OPENMP_FOUND does still come through, via # find_package_handle_standard_args), so with the module gone # set_source_files_properties (x PROPERTIES COMPILE_FLAGS ${OPENMP_FLAGS}) # is called with a missing argument and configure dies in 14 places. # Map the modern names onto the legacy ones at the find_package call site. sed -i 's|^ find_package (OpenMP)$| find_package (OpenMP)\n set (OPENMP_FLAGS "${OpenMP_CXX_FLAGS}")\n set (OPENMP_LDFLAGS "${OpenMP_CXX_FLAGS}")\n set (OPENMP_LIBRARIES "${OpenMP_CXX_LIBRARIES}")|' CMakeLists.txt grep -q 'set (OPENMP_LDFLAGS "${OpenMP_CXX_FLAGS}")' CMakeLists.txt %build # dcmtk_abi comes from dcmtk-devel's macros.dcmtk. Absent means the buildroot has a dcmtk-devel # that predates the snapshot, and the Requires above would carry the UNSET placeholder. test -n "%{?dcmtk_abi}" || { echo "ERROR: dcmtk_abi undefined - needs the dcmtk-devel that ships macros.dcmtk" >&2; exit 1; } export CXXFLAGS="%{optflags} -std=c++17 -include cstdint -fpermissive" export CFLAGS="%{optflags} -std=gnu17 -Wno-error=implicit-function-declaration -Wno-error=int-conversion -Wno-error=incompatible-pointer-types" %cmake \ -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ -DCMAKE_CXX_STANDARD:STRING=17 \ -DBUILD_SHARED_LIBS:BOOL=ON \ -DPLM_SYSTEM_ITK:STRING=YES \ -DITK_DIR:PATH=%{_prefix}/lib/cmake/ITK-5.4 \ -DPLM_SYSTEM_DCMTK:STRING=YES \ -DPLM_CONFIG_ENABLE_CUDA:BOOL=OFF \ -DPLM_CONFIG_ENABLE_OPENCL:BOOL=OFF \ -DPLM_CONFIG_ENABLE_QT:BOOL=OFF \ -DPLM_CONFIG_ENABLE_PYTHON:BOOL=OFF \ -DPLM_CONFIG_ENABLE_CSHARP:BOOL=OFF \ -DPLM_CONFIG_ENABLE_SUPERBUILD:BOOL=OFF \ -DPLM_CONFIG_INSTALL_LIBRARIES:BOOL=ON \ -DPLM_INSTALL_LIB_DIR:PATH=%{_lib} \ -DPLM_BUILD_TESTING:BOOL=OFF \ -DCMAKE_SKIP_INSTALL_RPATH:BOOL=ON %cmake_build %install %cmake_install # Keep the bundled static helper archives. They are NOT optional: plastimatch's # exported cmake package imports each one as a target with an # IMPORTED_LOCATION_RELEASE pointing at %%{_libdir}/lib.a, so deleting # them makes PlastimatchConfig.cmake fail outright for every consumer -- # "The imported target \"lbfgs\" references the file /usr/lib64/liblbfgs.a # but this file does not exist" # (hit by 3dslicer-rt's PlmCommon/CMakeLists.txt:12). Upstream builds these six # small third-party/internal helpers static by design; there are no shared # equivalents to link instead. for _a in devillard inih lbfgs nkidecompress plmclp specfun; do test -f %{buildroot}%{_libdir}/lib${_a}.a done # Upstream installs a curated "public API" subset of the headers -- 189 of the # 451 in src/ -- but that subset #includes headers outside itself, so it does not # stand alone. plm_image.h needs plmbase_config.h; itk_image.h needs # itk_direction_type.h; and so on. Chasing them one at a time is whack-a-mole # (3dslicer-rt found two before this), so install the lot. # # Flattening is safe and is what the sources expect: plastimatch's own includes # are unqualified ("itk_image.h", not "base/itk_image.h") and all 451 basenames # are unique -- the guard below fails the build if that ever stops being true. _dupes=$(find src -name '*.h' -exec basename {} \; | sort | uniq -d) if [ -n "$_dupes" ]; then echo "ERROR: header basename collision would occur when flattening:" >&2 echo "$_dupes" >&2 exit 1 fi find src -name '*.h' -exec install -m 0644 -t %{buildroot}%{_includedir}/plastimatch/ {} + # plm_image.h is the header SlicerRT includes first; its chain must resolve or # the -devel package is not consumable regardless of what else shipped. test -f %{buildroot}%{_includedir}/plastimatch/plmbase_config.h test -f %{buildroot}%{_includedir}/plastimatch/itk_direction_type.h # PLM_INSTALL_LIB_DIR puts the libraries in %%{_libdir}, but the cmake package # config still lands under %%{_prefix}/lib/cmake. Move it so find_package works # on a 64-bit multilib layout. if [ -d %{buildroot}%{_prefix}/lib/cmake ] && [ "%{_lib}" != "lib" ]; then mkdir -p %{buildroot}%{_libdir}/cmake cp -a %{buildroot}%{_prefix}/lib/cmake/. %{buildroot}%{_libdir}/cmake/ rm -rf %{buildroot}%{_prefix}/lib/cmake fi %ldconfig_scriptlets %files %license LICENSE.TXT %{_bindir}/plastimatch %{_bindir}/dicom_uid %{_libdir}/lib*.so.* %{_datadir}/doc/plastimatch/ %files devel %{_includedir}/plastimatch/ %{_libdir}/lib*.so # Bundled static helpers imported by PlastimatchLibraryDepends-release.cmake. %{_libdir}/lib*.a %{_libdir}/cmake/plastimatch/ %changelog * Mon Oct 05 2026 Morgan Hough - 1.10.0-16 - Pin the exact DCMTK snapshot: Requires: dcmtk(abi) = %%{dcmtk_abi}, from dcmtk-devel's macros.dcmtk, plus a %%build guard against the UNSET placeholder. dcmtk is now packaged from git HEAD (policy in dcmtk.spec) and upstream's DCMTK_ABI_VERSION stays 20 across snapshots, so rpm cannot tell snapshots apart by soname; this is the same mechanism as vtk(abi). - This release exists to be rebuilt against the snapshot; the pin means it cannot be installed against a different one. * Tue Sep 29 2026 Morgan Hough - 1.10.0-15 - plastimatch does not run: `plastimatch --version` dies in the loader on itk::PlatformMultiThreader::SpawnThread, removed by the ITK legacy purge in 5.4.7-10. Fourth package hit by that change, and the third distinct symbol. - Port the one call site to std::thread (Patch0), which is what ITK's own header recommends in place of SpawnThread/TerminateThread. The conversion is contained: Registration used the threader for nothing else, TerminateThread was never called, and the returned thread id was stored and never read, so the whole itk::PlatformMultiThreader member becomes a std::thread. The ITK_VERSION_MAJOR branches go with it -- ITK 5 requires C++11 regardless. - Local mock then found a SECOND legacy dependency, this one at compile time: itk::ImageIOBase::IOPixelType / ::IOComponentType and the enumerators reached through ImageIOBase are all aliases behind !ITK_LEGACY_REMOVE. 38 references across 6 files, rewritten to itk::IOPixelEnum / itk::IOComponentEnum. Note itk::Image<>::IOPixelType is a different, non-legacy alias (= PixelType) that ss_img_extract relies on, so the rewrite is anchored on "itk::ImageIOBase::" and leaves it untouched. - And a third: plm_warp.cxx assigns a Vector expression to an itk::Point, whose implicit construction from a Vector is guarded the same way. Built explicitly from the components now. - Also add -p1 to %%autosetup, without which Patch0 was silently skipped -- rpm reported "No file to patch. Skipping patch. 5 out of 5 hunks ignored" and failed %%prep, which is the good outcome; a fuzzier setup would have built a package with none of the fix in it. - A fourth: the vendored itkESMDemonsRegistrationWithMaskFunction.txx calls SpatialObject::IsInside(), renamed IsInsideInWorldSpace() in ITK 5 (same semantics: a world-space point test). Three call sites. - A fifth: the vendored RANSAC (libs/ransac/RANSAC.txx) uses the legacy threader API (SetNumberOfThreads, ThreadInfoStruct). Ported to SetMaximumNumberOfThreads+SetNumberOfWorkUnits and WorkUnitInfo. - A sixth: dicom_info.cxx calls GDCMImageIO::SetMaxSizeLoadEntry(), documented by ITK as having no effect on GDCM 2.x; removed. - Six legacy dependencies, found one per build, is the argument for doing this in local mock: each fix only reveals the next. Patch verified to apply cleanly against a pristine tarball before each build. - It also fixes a latent bug the old code left as a comment. The private destructor said "do something more here ... wait for running threads to exit" and did not; it now joins. start_registration joins before relaunching, after the existing worker_running.grab(), which by then guarantees the previous worker has released and so the join cannot block. - Caught by tests/smoke.bats only after the generator was hardened on 2026-09-29: the row expects /plastimatch/ and the crash message contains "plastimatch", so the test had been passing on a binary that cannot start. * Sat Sep 19 2026 Morgan Hough - 1.10.0-14 - 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. * Sat Aug 22 2026 Morgan Hough - 1.10.0-13 - Rebuild for aarch64 against dcmtk 3.7.0-3 (SONAME 19 -> 20). The previous aarch64 build was compiled against Fedora's dcmtk 3.6.9 (.so.19) before the COPR dcmtk reached that arch, so it still requires libdcm*.so.19 -- which nothing provides now, leaving it uninstallable. x86_64 is already correct and is not rebuilt. * Mon Aug 03 2026 Morgan Hough - 1.10.0-8 - Rebuild with the FindOpenMP removal actually in the SRPM (the -7 SRPM predated the %prep edit) * Sun May 24 2026 Morgan Hough - 1.10.0-7 - Add -DCMAKE_POLICY_VERSION_MINIMUM=3.5 to %%cmake for cmake 4.x compat. * Wed Mar 18 2026 Morgan Hough - 1.10.0-4 - Switch to shared libs (upstream has proper SOVERSION) - Enable PLM_CONFIG_INSTALL_LIBRARIES (required for cmake export set) - Add VTK transitive BuildRequires (python3-devel, qt6-qtdeclarative-devel) * Wed Mar 18 2026 Morgan Hough - 1.10.0-1 - Initial package of plastimatch 1.10.0 - Built against system InsightToolkit5 and DCMTK - CPU-only build (CUDA/OpenCL/Qt disabled), static internal libraries