# Trekker is one executable. It is built on nibrary, the author's neuroimaging # library, which upstream only distributes as a pinned source archive that # trekker's CMake downloads at configure time (external/cmake/ExternalNibrary.cmake) # and links statically (build_linux.sh: BUILD_SHARED_LIBS=OFF). nibrary has no # release cadence of its own, installs into versioned private directories # (lib/nibrary_v0.4.4) with no SONAME, and nothing else in this stack uses it, so # it is built here, statically, the way upstream does -- declared below as # bundled(nibrary) with the third-party code nibrary itself embeds. A separate # nibrary package is the right shape once a second consumer appears. # # Upstream version is 1.0.0-rc6: a pre-release, hence the tilde. %global upver 1.0.0-rc6 %global nibver 0.4.4 Name: trekker Version: 1.0.0~rc6 Release: 1%{?dist} Summary: Parallel transport tractography for diffusion MRI # Reference: (Debian DEP-12 upstream/metadata field names; founding paper) # Author: Aydogan DB, Shi Y # Title: Parallel Transport Tractography # Journal: IEEE Transactions on Medical Imaging # Year: 2021 # Volume: 40 # Pages: 635-647 # DOI: 10.1109/TMI.2020.3034038 # PMID: 33104507 # Reference: (real-time use with InVesalius; see invesalius.spec) # Author: Aydogan DB, Souza VH, Matsuda RH, Lioumis P, Ilmoniemi RJ # Title: Real-Time Tractography-Assisted Neuronavigation for Transcranial Magnetic Stimulation # Journal: Human Brain Mapping # Year: 2025 # DOI: 10.1002/hbm.70122 # trekker and nibrary: BSD-3-Clause. Compiled in from nibrary/external/ (its # LICENSE.md lists each): geogram BSD-3-Clause; dcm2niix BSD-2-Clause with # BSD-3-Clause, MIT and public-domain parts (Fedora's dcm2niix tag); # proxsuite BSD-2-Clause; fast-marching-method, triangleVoxelIntersection and # unordered_dense MIT; gifticlib and marchingCubes public domain. # Eigen, libigl, SIMDe, zlib, expat and nifticlib come from Fedora # (below), so their licences are not part of this binary. License: BSD-3-Clause AND BSD-2-Clause AND MIT AND LicenseRef-Fedora-Public-Domain URL: https://dmritrekker.github.io/ # Upstream's tag archive minus tests/testData (HCP subject 100307 derivatives, # WU-Minn HCP Open Access terms, 101 MB) and doc/ (32 MB of the same subject's # renderings). trekker-clean.sh in this repository makes the tarball at SRPM # time (COPR make_srpm method, ci/make-srpm.sh); the md5 pins what it produced. %global copr_lookaside https://copr-dist-git.fedorainfracloud.org/repo/pkgs/mhough/neurofedora/%{name} %global cleanmd5 efc95f873399f20c9fec2efcabe8a445 Source0: %{copr_lookaside}/%{name}-%{version}-clean.tar.gz/md5/%{cleanmd5}/%{name}-%{version}-clean.tar.gz # The nibrary version trekker 1.0.0-rc6 pins (NIBRARY_MIN_VERSION in # external/cmake/ExternalNibrary.cmake); unpacked to where that file looks for a # local copy, so nothing is downloaded at build time. Source1: https://github.com/nibrary/nibrary/releases/download/v%{nibver}/nibrary_v%{nibver}.zip BuildRequires: cmake BuildRequires: ninja-build BuildRequires: gcc-c++ # dcm2niix's SuperBuild refuses to configure without git (find_package(Git) at # its first line), although nothing here is fetched. BuildRequires: git-core # nibrary's USE_SYSTEM_* switches, plus expat and nifticlib (no # switch upstream; patched in %%prep: the embedded expat is 2.4.8, from 2022). BuildRequires: eigen3-devel BuildRequires: pkgconfig(zlib) BuildRequires: simde-devel BuildRequires: libigl-devel BuildRequires: expat-devel BuildRequires: nifticlib-devel # %%check BuildRequires: python3-nibabel BuildRequires: python3-numpy # Compiled in; see the License comment. Versions read from the sources. Provides: bundled(nibrary) = %{nibver} Provides: bundled(geogram) = 1.9.7 Provides: bundled(dcm2niix) = 1.0.20241211 Provides: bundled(proxsuite) = 0.7.2 Provides: bundled(gifticlib) = 1.0.9 Provides: bundled(unordered_dense) = 4.5.0 Provides: bundled(fast-marching-method) Provides: bundled(marchingCubes) Provides: bundled(triangleVoxelIntersection) %description Trekker reconstructs white-matter fiber pathways from diffusion MRI with parallel transport tractography (PTT), a probabilistic streamline method that propagates geometrically smooth curves through a fiber orientation distribution (FOD) field. It reads FODs from MRtrix3 or ExploreDTI as spherical harmonics or sampled spherical functions, supports seeding from and pathway rules on images and surfaces, and writes, filters, resamples, converts and maps tractograms (VTK, TCK, TRK). It is the tractography engine behind InVesalius Navigator's real-time tractography for TMS. %prep %autosetup -n %{name}-%{upver} -a 1 # The tag archive ships documentation with the executable bit set. chmod -x README.md LICENSE.md # Where ExternalNibrary.cmake looks for a local nibrary before downloading one. mv nibrary_v%{nibver} external/ cd external/nibrary_v%{nibver} # Expat: compile against Fedora's instead of the embedded 2.4.8 copy. nibrary # globs external/expat/*.c into its own objects and has no USE_SYSTEM_EXPAT, so # drop the sources, their include path and their header install, and link the # system library; gifticlib's include is repointed at /usr/include/expat.h. rm -r external/expat sed -i -e 's|^include_directories(${CMAKE_SOURCE_DIR}/external/expat)|include_directories(%{_includedir}/nifti)|' \ -e '/"external\/expat\/\*\.c\*"/d' \ -e '/install(DIRECTORY ${CMAKE_SOURCE_DIR}\/external\/expat /d' \ -e 's|target_link_libraries(${libNibrary} PUBLIC ${ZLIB_LIBRARIES} ${GEOGRAM_LIBRARY})|target_link_libraries(${libNibrary} PUBLIC ${ZLIB_LIBRARIES} ${GEOGRAM_LIBRARY} expat)|' \ CMakeLists.txt grep -q 'GEOGRAM_LIBRARY} expat)' CMakeLists.txt ! grep -q 'external/expat' CMakeLists.txt grep -q 'include_directories(%{_includedir}/nifti)' CMakeLists.txt # NIfTI: nibrary compiles its own nifti2_io and znzlib sources, which match # Fedora's nifticlib (the headers differ only in the include path of znzlib.h). # Drop them, include Fedora's headers and link Fedora's libraries. Fedora's # nifti2_io.h includes , which is why %%{_includedir}/nifti is on the # include path. gifticlib stays embedded: Fedora's gifti_io.h is built on the # NIfTI-1 header, whose types clash with the NIfTI-2 header nibrary uses. rm -r external/niftilib sed -i -e '/"external\/niftilib\/\*\.c\*"/d' \ -e '/install(DIRECTORY ${CMAKE_SOURCE_DIR}\/external\/niftilib /d' \ -e 's|GEOGRAM_LIBRARY} expat)|GEOGRAM_LIBRARY} nifti2 znz expat)|' \ CMakeLists.txt ! grep -q 'external/niftilib' CMakeLists.txt grep -q 'nifti2 znz expat)' CMakeLists.txt sed -i -e 's|#include "niftilib/znzlib/znzlib.h"|#include |' \ -e 's|#include "niftilib/nifti2/nifti2_io.h"|#include |' \ -e 's|#include "expat/expat.h"|#include |' external/gifticlib/gifti_io.h ! grep -q 'niftilib' external/gifticlib/gifti_io.h # dcm2niix's nibrary wrapper header takes the NIfTI-1 header the same way; it # is copied over the dcm2niix tree from dcm2niix_patch/, so change the source. sed -i 's|#include "niftilib/nifti2/nifti1.h"|#include |' \ external/dcm2niix_patch/dcm2niix++.h external/dcm2niix/console/dcm2niix++.h ! grep -rq 'niftilib/nifti2' external/dcm2niix_patch external/dcm2niix/console src # Fedora builds with -Werror=format-security. nibrary passes its signature # string as the format of sprintf (surface_io.cpp); give it a "%%s" so a "%%" in # it can never be read as a conversion. sed -i 's|sprintf(buffer, SGNTR().c_str());|sprintf(buffer, "%%s", SGNTR().c_str());|' src/surface/surface_io.cpp grep -q 'sprintf(buffer, "%%s", SGNTR().c_str());' src/surface/surface_io.cpp grep -q '#include ' external/gifticlib/gifti_io.h sed -i 's|#include "niftilib/nifti2/nifti2_io.h"|#include |' src/image/image.h grep -q '' src/image/image.h # System libigl: Fedora's (this COPR's) libigl-devel ships headers without a # CMake config, and upstream probes with find_package(LIBIGL). Probe for the # headers instead. sed -i 's|find_package(LIBIGL ${LIBIGL_MIN_VERSION} QUIET)|find_path(LIBIGL_INCLUDE_DIRS NAMES igl/igl_inline.h)\n if(LIBIGL_INCLUDE_DIRS)\n set(LIBIGL_FOUND TRUE)\n endif()|' external/cmake/ExternalLibigl.cmake grep -q 'igl/igl_inline.h' external/cmake/ExternalLibigl.cmake # Version floors are 3.4.0 (Eigen; Fedora has 5.x, whose config is not # compatible with a 3.x request) and 1.3.1 (zlib; zlib-ng reports # "1.3.1.zlib-ng"). Both would silently fall back to the embedded copies. # Eigen 5's config no longer sets EIGEN3_INCLUDE_DIR; read the path off the # imported target instead. sed -i -e 's|find_package(Eigen3 ${EIGEN_MIN_VERSION} QUIET)|find_package(Eigen3 QUIET)|' \ -e 's|set(EIGEN_INCLUDE_DIR ${EIGEN3_INCLUDE_DIR} CACHE INTERNAL "")|get_target_property(EIGEN3_INCLUDE_DIR Eigen3::Eigen INTERFACE_INCLUDE_DIRECTORIES)\n set(EIGEN_INCLUDE_DIR ${EIGEN3_INCLUDE_DIR} CACHE INTERNAL "")|' \ external/cmake/ExternalEigen.cmake grep -q 'INTERFACE_INCLUDE_DIRECTORIES' external/cmake/ExternalEigen.cmake sed -i 's|find_package(ZLIB ${ZLIB_MIN_VERSION} QUIET)|find_package(ZLIB QUIET)|' external/cmake/ExternalZlib.cmake # geogram and dcm2niix are built as ExternalProjects, which do not inherit the # compiler flags of the parent configure (dcm2niix is even handed a bare -fPIC # and a static libstdc++, which Fedora does not ship). Forward Fedora's flags # so the embedded code gets the same hardening and -O2 as everything else, and # link the normal runtime. geogram would otherwise install into lib64, where # nibrary's own staging step (ExternalGeogram_aux.cmake) does not look. sed -i 's|^\(\s*\)-DBUILD_SHARED_LIBS=${BUILD_SHARED_LIBS}$|&\n\1"-DCMAKE_C_FLAGS=${CMAKE_C_FLAGS}"\n\1"-DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}"\n\1"-DCMAKE_EXE_LINKER_FLAGS=${CMAKE_EXE_LINKER_FLAGS}"\n\1-DCMAKE_INSTALL_LIBDIR=lib|' \ external/cmake/ExternalGeogram.cmake sed -i -e 's|-DUSE_STATIC_RUNTIME=$<$:OFF;ON>|-DUSE_STATIC_RUNTIME=OFF|' \ -e 's|^\(\s*\)-DCMAKE_C_FLAGS=-fPIC$|\1"-DCMAKE_C_FLAGS=${CMAKE_C_FLAGS} -fPIC"|' \ -e 's|^\(\s*\)-DCMAKE_CXX_FLAGS=-fPIC$|\1"-DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS} -fPIC"|' \ external/cmake/ExternalDcm2niix.cmake grep -q '"-DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}"' external/cmake/ExternalGeogram.cmake grep -q 'DCMAKE_INSTALL_LIBDIR=lib' external/cmake/ExternalGeogram.cmake %ifarch aarch64 # geogram's static Linux platform file adds -m64, which gcc on aarch64 rejects; # it ships a Linux64-gcc-aarch64 platform for this. sed -i 's|^\(\s*\)-DCMAKE_INSTALL_LIBDIR=lib$|&\n\1-DVORPALINE_PLATFORM=Linux64-gcc-aarch64|' external/cmake/ExternalGeogram.cmake grep -q 'VORPALINE_PLATFORM=Linux64-gcc-aarch64' external/cmake/ExternalGeogram.cmake %endif test "$(grep -c -- '-DUSE_STATIC_RUNTIME=OFF' external/cmake/ExternalDcm2niix.cmake)" -eq 2 test "$(grep -c -- '"-DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS} -fPIC"' external/cmake/ExternalDcm2niix.cmake)" -eq 2 # Embedded copies that are now unused: remove them so the tarball-to-binary # story is the one in the License comment (zlib, Eigen, libigl, SIMDe come from # Fedora; the globs above only compile what is left). rm -r external/zlib external/eigen external/libigl external/simde cd ../.. # trekker links the static nibrary set by name; the system libraries the # archive needs are named after it, and nibrary's headers reach Fedora's NIfTI # header through . sed -i -e 's|target_link_libraries(${CMDNAME} ${NIBRARY_LIBRARIES})|target_link_libraries(${CMDNAME} ${NIBRARY_LIBRARIES} nifti2 znz expat)|' \ -e 's|^include_directories(${NIBRARY_INCLUDE_DIR})|&\ninclude_directories(%{_includedir}/nifti)|' CMakeLists.txt grep -q 'NIBRARY_LIBRARIES} nifti2 znz expat)' CMakeLists.txt grep -q 'include_directories(%{_includedir}/nifti)' CMakeLists.txt %build # 1. nibrary, static, into its own build tree. Its CMakeLists forces # CMAKE_INSTALL_PREFIX to /install, so "install" is a staging step. # BUILD_NATIVE=OFF: upstream defaults to -march=native, which would bake # the builder's CPU into every user's binary. %global _vpath_srcdir external/nibrary_v%{nibver} %global __cmake_builddir nibrary-build %cmake \ -DBUILD_SHARED_LIBS:BOOL=OFF \ -DBUILD_NATIVE:BOOL=OFF \ -DUSE_SYSTEM_EIGEN:BOOL=ON \ -DUSE_SYSTEM_ZLIB:BOOL=ON \ -DUSE_SYSTEM_SIMDE:BOOL=ON \ -DUSE_SYSTEM_LIBIGL:BOOL=ON \ -DBUILD_DCM2NIIX:BOOL=ON \ -DENABLE_CUDA:BOOL=OFF # The system-library switches are "use if found"; a silent fallback to an # embedded copy is exactly what this build must not do. Each External*.cmake # records its decision in the cache. for lib in EIGEN ZLIB SIMDE LIBIGL; do grep -q "^BUILDING_${lib}_FROM_SOURCE:INTERNAL=FALSE" nibrary-build/CMakeCache.txt \ || { echo "ERROR: nibrary fell back to its embedded $lib" >&2; grep "BUILDING_${lib}" nibrary-build/CMakeCache.txt >&2; exit 1; } done %cmake_build %cmake_build --target install stage=$PWD/nibrary-build/install test -f "$stage/lib/nibrary_v%{nibver}/libNibrary.a" test -f "$stage/lib/nibrary_v%{nibver}/libgeogram.a" test -f "$stage/lib/nibrary_v%{nibver}/libdcm2niix++.a" test -f "$stage/include/nibrary_v%{nibver}/nibrary.h" ! test -d nibrary-build/external/download # 2. trekker against that staging tree. # nibrary's headers include Eigen and libigl, whose headers include ; # trekker adds CMAKE_INCLUDE_PATH to its include directories. Everything is # linked statically, so no run path is wanted: the build-tree one that CMake # adds for the staged nibrary directory is rejected by check-rpaths. %global _vpath_srcdir . %global __cmake_builddir %{_vpath_builddir} %cmake \ -DBUILD_SHARED_LIBS:BOOL=OFF \ -DBUILD_NATIVE:BOOL=OFF \ -DUSE_SYSTEM_NIBRARY:BOOL=ON \ -DNIBRARY_INCLUDE_DIR:PATH="$stage/include/nibrary_v%{nibver}" \ -DNIBRARY_LIBRARY_DIR:PATH="$stage/lib/nibrary_v%{nibver}" \ -DCMAKE_INCLUDE_PATH:PATH=%{_includedir}/eigen3 \ -DCMAKE_SKIP_RPATH:BOOL=ON %cmake_build %install # Upstream names the binary trekker_v1.0.0-rc6 and installs it under # /install; ship it as trekker. install -Dpm 0755 %{_vpath_builddir}/trekker_v%{upver} %{buildroot}%{_bindir}/%{name} %check # No upstream unit tests; the shipped test script needs the HCP-derived data # the tarball no longer carries. Run the tracker on a synthetic field instead: # a 24 mm cube whose FOD peaks along x everywhere (spherical-function input on # an explicit set of directions), seeded in the centre. Every accepted # streamline must run along x, so the output must contain streamlines and # every one of them must be long: this exercises image I/O, the sphere-domain # FOD path, seeding, propagation and VTK output. b=%{buildroot}%{_bindir}/%{name} "$b" >/dev/null # help text, exit 0 "$b" track 2>&1 | grep -q -- --seed # subcommand help work=$PWD/check; rm -rf "$work"; mkdir "$work"; cd "$work" %{python3} - <<'EOF' import numpy as np, nibabel as nib rng = np.random.default_rng(7) d = rng.normal(size=(120, 3)); d /= np.linalg.norm(d, axis=1)[:, None] d = np.vstack([d, [[1, 0, 0], [-1, 0, 0]]]) # make sure the peak is sampled np.savetxt("dirs.txt", d, fmt="%.8f") f = np.exp(12.0 * (d[:, 0] ** 2 - 1.0)).astype(np.float32) # peaked along +-x n = 24 fod = np.broadcast_to(f, (n, n, n, len(f))).copy() aff = np.diag([1.0, 1.0, 1.0, 1.0]) nib.save(nib.Nifti1Image(fod, aff), "fod.nii.gz") seed = np.zeros((n, n, n), np.uint8); seed[11:13, 11:13, 11:13] = 1 nib.save(nib.Nifti1Image(seed, aff), "seed.nii.gz") EOF "$b" info fod.nii.gz | grep -qiE 'nifti|dim|size' "$b" track fod.nii.gz dirs.txt sym \ --seed seed.nii.gz --seed_count 200 --seed_trials 20 \ --minlength 6 --output out.vtk --verbose quiet test -s out.vtk # Legacy VTK keeps an ASCII header even in binary files: "LINES ". lines=$(grep -a -m1 '^LINES ' out.vtk | awk '{print $2}') echo "streamlines: $lines" test "${lines:-0}" -ge 100 "$b" info out.vtk | grep -qiE 'streamline|lines|points' %files %license LICENSE.md %doc README.md %{_bindir}/%{name} %changelog * Thu Oct 22 2026 Morgan Hough - 1.0.0~rc6-1 - Initial package. nibrary 0.4.4 built in, statically, as upstream does, with Fedora's Eigen, zlib, SIMDe, libigl, expat and nifticlib substituted for the embedded copies and the rest declared as bundled(). Tarball cleaned of the HCP-derived test data and docs (trekker-clean.sh); %%check tracks a synthetic FOD. - Patches applied in %%prep, none changing behavior: system library wiring, geogram installed into lib, a format-security fix in nibrary's sprintf.