# PlusLib -- the Plus Toolkit's data acquisition library: the layer that turns # a tracker or an imaging device into a stream of calibrated, timestamped # transforms and frames, and serves them over OpenIGTLink. # # This is the missing link in the open neuronavigation stack. NaviNIBS ships # PlusServer configuration files for the NDI Polaris Vega and talks to it over # OpenIGTLink; 3D Slicer's SlicerIGT does the same. Without Plus, NaviNIBS on # Fedora runs only with its simulated-tools addon and no real hardware. # See docs/open-neuronavigation.md and docs/neuronavigation-hardware.md. # # Chain: ndicapi + aruco -> PlusLib -> PlusApp (PlusServer) -> NaviNIBS / SlicerIGT # # Superbuild OFF, everything from system packages. Upstream's normal build is # the PlusBuild superbuild, which clones and builds VTK, ITK, OpenIGTLink, # OpenIGTLinkIO, IGSIO, ndicapi and aruco itself. We have all seven packaged, # so this builds against them -- the same rule applied to igsio and Pulse. # # DEVICE BACKENDS: only the ones buildable from free software are enabled. # Roughly sixty exist; most are ultrasound and most need a vendor SDK under # NDA. Enabled here: NDI Polaris/Aurora (ndicapi), optical marker tracking # (aruco, any camera), Intel RealSense, V4L2, OpenCV capture, OpenVR/SteamVR # (Vive trackers), OpenIGTLink, and the simulated/replay sources that make # hardware-free testing possible. %global commit c796218916c2854c3fd8d2286facac8a5821ceb5 %global shortcommit %(c=%{commit}; echo ${c:0:8}) %global snapdate 20260820 Name: PlusLib Version: 2.9.0 Release: 0.3.%{snapdate}git%{shortcommit}%{?dist} Summary: Data acquisition and calibration library for image-guided intervention # LICENCE, and this one needs a human decision recorded rather than a guess. # # License.txt is the "Plus Contribution and Software License Agreement" v1.0, # which the file itself calls "a BSD style license, with extensions to cover # contributions and other issues specific to Plus". Part B is the grant we # rely on and it is permissive: # # B.1 "a royalty-free, non-exclusive license to use, reproduce, make # derivative works of, display and distribute the Software" # B.2 "includes without limitation the right to (i) incorporate the # Software into proprietary programs" # # with BSD-shaped conditions: carry the licence text along (B.1.b), preserve # attributions (B.1.c), mark modified versions (B.1.d), and an explicitly # non-binding request to publish modifications (B.1.e, "under no obligation"). # # The clause to think about is B.4: "The Software has been designed for # research purposes only and has not been reviewed or approved by the Food and # Drug Administration ... CLINICAL APPLICATIONS ARE NEITHER RECOMMENDED NOR # ADVISED." # # We read that as a DISCLAIMER, not a field-of-use restriction, and therefore # free. Three reasons: it describes what the software was designed for rather # than limiting the grant; the grant in B.1/B.2 has no field limitation and # expressly permits proprietary use; and B.4 itself goes on to contemplate # commercialisation ("Any commercialization of the Software is at the sole # risk of the party ... engaged in such commercialization"), which a # non-commercial restriction would forbid outright. # # Contrast mni_autoreg, which we put in the nonfree repository: its notice # says "This package is provided for research use only" as a flat condition on # use. That is a restriction. This is an FDA non-approval notice of the same # kind ITK, DCMTK and 3D Slicer carry, and Fedora ships all three. # # No SPDX identifier exists for it and it is not in fedora-license-data, so it # is expressed as a LicenseRef. **Morgan should confirm this reading before the # package is promoted anywhere that matters.** License: LicenseRef-Plus-Toolkit-BSD-style URL: https://github.com/PlusToolkit/PlusLib Source0: %{url}/archive/%{commit}/%{name}-%{shortcommit}.tar.gz # Two dependency-location problems. # # VTK and ITK are found with # FIND_PACKAGE(VTK NO_MODULE REQUIRED PATHS ${VTK_DIR} NO_DEFAULT_PATH) # FIND_PACKAGE(ITK REQUIRED PATHS ${ITK_DIR} NO_DEFAULT_PATH) # The PATHS + NO_DEFAULT_PATH form skips the standard config search that # modern VTK/ITK config packages rely on, so Fedora's are not found even with # VTK_DIR/ITK_DIR set. Exactly the same patch igsio needed. # # And PLUS_USE_V4L2 does FIND_PACKAGE(V4L2 REQUIRED), but there is no # FindV4L2.cmake in the tree and V4L2 ships no CMake config, so that option # cannot succeed for anyone -- it is unusable as shipped. The backend really # needs and libv4l2, so locate those with # CHECK_INCLUDE_FILE and FIND_LIBRARY instead. Patch0: PlusLib-find-system-deps.patch BuildRequires: cmake >= 3.16 BuildRequires: gcc-c++ BuildRequires: ninja-build BuildRequires: vtk-devel BuildRequires: InsightToolkit5-devel BuildRequires: igsio-devel # igsio's installed headers include from vtkAddon, # and IGSIOConfig.cmake does not carry that include directory, so name it here # and add the path in %%build. igsio-devel gained the matching Requires in # 1.0-0.9, but being explicit is right for a build dependency. BuildRequires: vtkAddon-devel BuildRequires: openigtlink-devel BuildRequires: openigtlinkio-devel BuildRequires: ndicapi-devel BuildRequires: aruco-devel BuildRequires: opencv-devel BuildRequires: librealsense-devel BuildRequires: openvr-devel BuildRequires: zlib-devel # V4L2 video capture: the header comes from kernel-headers, the library from # libv4l-devel. See Patch0 -- upstream's FIND_PACKAGE(V4L2) cannot work. BuildRequires: libv4l-devel BuildRequires: kernel-headers # OpenVR/SteamVR: Fedora ships only openvr.pc, no CMake config, so Patch0 # falls back to pkg-config. BuildRequires: pkgconfig(openvr) # 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 PlusLib is the data acquisition, pre-processing and calibration library of the Plus Toolkit (Public software Library for UltraSound). It provides a uniform interface over tracking and imaging hardware: devices are described in an XML configuration, their streams are timestamped and temporally aligned, spatial calibrations are applied, and the result is published over OpenIGTLink for consumers such as 3D Slicer, SlicerIGT and NaviNIBS. This build enables the device backends that need no proprietary SDK: * NDI Polaris and Aurora trackers, via ndicapi * optical marker tracking with any camera, via aruco * Intel RealSense depth cameras * Video4Linux2 and OpenCV video capture * OpenVR / SteamVR devices, including Vive trackers * OpenIGTLink client and server devices * replay from recorded sequences and simulated tools, for testing without hardware %package devel Summary: Development files for %{name} Requires: %{name}%{?_isa} = %{version}-%{release} Requires: igsio-devel%{?_isa} Requires: openigtlink-devel%{?_isa} Requires: vtk-devel%{?_isa} %description devel Headers and CMake package configuration for building against %{name}. %package server Summary: PlusServer -- publishes tracked tool poses over OpenIGTLink Requires: %{name}%{?_isa} = %{version}-%{release} %description server PlusServer reads a device configuration in XML, opens the tracking and imaging hardware it describes, and publishes the calibrated, timestamped streams over OpenIGTLink. It is the process that NaviNIBS and 3D Slicer's SlicerIGT connect to, and it is how a neuronavigation setup gets tool poses on Linux. Also includes PlusServerRemoteControl, which drives a running server (start/stop recording, reconstruct volumes, update transforms) over the same protocol. NaviNIBS ships PlusServer configurations for the NDI Polaris Vega -- serial, network, and with the Axilum Cobot -- in its Devices directory. %package tools Summary: Command line tools from the Plus toolkit Requires: %{name}%{?_isa} = %{version}-%{release} %description tools Utilities shipped with PlusLib: sequence file inspection and editing (EditSequenceFile, ViewSequenceFile), volume reconstruction, calibration (ProbeCalibration, TemporalCalibration), ultrasound scan conversion and scan line tools, fiducial list conversion, model rendering, and PlusVersion. Most of these serve ultrasound-guided intervention workflows rather than neuronavigation; PlusServer is packaged separately as %{name}-server. %prep %autosetup -p1 -n %{name}-%{commit} %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; } # IGSIOConfig.cmake does not export vtkAddon's include directory, but igsio's # own installed headers include from it. # igsio's installed headers include from vtkAddon, # but IGSIOConfig.cmake does not export that include directory. (The # librealsense2 include path is handled properly by Patch0 instead.) export CXXFLAGS="%{build_cxxflags} -I%{_includedir}/vtkAddon" # PLUSBUILD_BUILD_PlusLib_TOOLS is never declared as an option in PlusLib -- # the PlusBuild superbuild passes it down -- but it is what gates the command # line tools, and among them **PlusServer**: the daemon that publishes tracked # tool poses over OpenIGTLink, which is precisely what NaviNIBS and SlicerIGT # connect to. Without it PlusLib is only libraries and there is nothing to run. # It also builds PlusServerRemoteControl and the per-module diagnostic tools. # # Rendering and the Qt widgets are PlusApp's business, not the library's. # ITK_NO_IO_FACTORY_REGISTER_MANAGER: ITK's register manager generates a list # of every ImageIO factory to register at static-init time, and Fedora's ITK # config lists optional modules whose libraries it does not ship -- so the # generated code references itk::PhilipsRECImageIOFactoryRegister__Private and # itk::ScancoImageIOFactoryRegister__Private, which nothing defines. (Same # class of mismatch as the FFTW factory symbol that makes python3-simpleitk # unimportable; see docs/COORDINATION.md.) PlusLib handles the manager being # off explicitly, and ITK still finds its installed IO libraries through the # ordinary runtime factory lookup. # # CMAKE_BUILD_TYPE must be set explicitly. PlusLibInstallLibrary() installs # with `CONFIGURATIONS ${CONFIG}`, where CONFIG falls back to "Release" when # neither CMAKE_CONFIGURATION_TYPES nor CMAKE_BUILD_TYPE is set -- and Fedora's # %%cmake sets neither. The configuration name then never matches the build, so # every INSTALL(TARGETS) is skipped and the headers install without a single # library. RelWithDebInfo keeps debuginfo and makes the rules fire. %cmake -G Ninja \ -DCMAKE_BUILD_TYPE:STRING=RelWithDebInfo \ -DITK_NO_IO_FACTORY_REGISTER_MANAGER:BOOL=ON \ -DBUILD_SHARED_LIBS:BOOL=ON \ -DBUILD_TESTING:BOOL=OFF \ -DBUILD_DOCUMENTATION:BOOL=OFF \ -DPLUS_BUILD_WIDGETS:BOOL=OFF \ -DPLUS_USE_INTEL_MKL:BOOL=OFF \ -DPLUSBUILD_BUILD_PlusLib_TOOLS:BOOL=ON \ -DPLUS_TEST_HIGH_ACCURACY_TIMING:BOOL=OFF \ -DVTK_DIR:PATH=%{_libdir}/cmake/vtk \ -DITK_DIR:PATH=%{_prefix}/lib/cmake/ITK-5.4 \ -DPLUS_USE_OpenIGTLink:BOOL=ON \ -DPLUS_USE_NDI:BOOL=ON \ -DPLUS_USE_POLARIS:BOOL=ON \ -DPLUS_USE_aruco:BOOL=ON \ -DPLUS_USE_OPTICAL_MARKER_TRACKER:BOOL=ON \ -DPLUS_USE_OpenCV:BOOL=ON \ -DPLUS_USE_INTELREALSENSE:BOOL=ON \ -DPLUS_USE_V4L2:BOOL=ON \ -DPLUS_USE_STEAMVR:BOOL=ON \ -Daruco_DIR:PATH=%{_libdir}/cmake/aruco \ -Dndicapi_DIR:PATH=%{_libdir}/cmake/ndicapi \ -DIGSIO_DIR:PATH=%{_libdir}/cmake/IGSIO \ -DOpenIGTLink_DIR:PATH=%{_libdir}/cmake/OpenIGTLink \ -DOpenIGTLinkIO_DIR:PATH=%{_libdir}/cmake/igtlio \ -DPLUSLIB_LIBRARY_INSTALL:STRING=%{_lib} \ -DPLUS_INSTALL_EXTERNAL_LIBRARIES:BOOL=OFF \ -DCMAKE_SKIP_INSTALL_RPATH:BOOL=ON %cmake_build %install %cmake_install # PlusLib's bundled xio helper installs a static library (and to /usr/lib, via # a PLUSLIB_ARCHIVE_INSTALL that is hardcoded like the others). Its objects are # already inside the shared libraries that use it, and Fedora does not ship # static libraries without a reason, so drop it. rm -f %{buildroot}%{_prefix}/lib/libvtkxio.a %{buildroot}%{_libdir}/libvtkxio.a # Nothing else should have landed in a bare /usr/lib on a 64-bit build. if [ -d %{buildroot}%{_prefix}/lib ]; then rmdir --ignore-fail-on-non-empty %{buildroot}%{_prefix}/lib if [ -d %{buildroot}%{_prefix}/lib ]; then echo "unexpected files left in /usr/lib:" find %{buildroot}%{_prefix}/lib | head exit 1 fi fi %check # No hardware in a build root. What can be asserted is that the libraries # resolve, that the device backends we asked for were actually compiled in # (a CMake option can be silently ignored, which is how a build looks green # while supporting nothing), and that the configuration headers agree. # ldd -r must be able to find the SIBLING libraries, which are still only in # the buildroot -- without LD_LIBRARY_PATH every cross-library symbol looks # undefined and this check fails on a correct package. (The libraries do record # each other: objdump -p shows NEEDED libvtkPlusCommon.so.) export LD_LIBRARY_PATH=%{buildroot}%{_libdir} nlibs=0 for so in %{buildroot}%{_libdir}/libvtkPlus*.so; do [ -e "$so" ] || continue nlibs=$((nlibs+1)) # VTK's module autoinit emits references to *_AutoInit_Construct for # optional modules (vtkRenderingVRModels, vtkFiltersParallelDIY2, ...). # By VTK's design those are satisfied when the final consumer links the # module -- Fedora's VTK does provide both libraries -- so an intermediate # library legitimately carries them unresolved. Everything else must # resolve. bad=$(ldd -r "$so" 2>/dev/null | grep '^undefined symbol' \ | grep -v '_AutoInit_Construct' || :) if [ -n "$bad" ]; then echo "undefined symbols in $so:"; echo "$bad"; exit 1 fi done # A loop that iterated nothing would pass silently, which is how a check stops # being a check. test "$nlibs" -ge 8 || { echo "only $nlibs Plus libraries found, expected 8+"; exit 1; } echo "checked $nlibs Plus libraries for undefined symbols" cfg=%{buildroot}%{_includedir}/PlusConfigure.h test -f "$cfg" || cfg=$(find %{buildroot} -name PlusConfigure.h | head -1) test -n "$cfg" -a -f "$cfg" || { echo "PlusConfigure.h not installed"; exit 1; } # Each backend we enabled must appear as a #define, not merely as a cache # variable we passed in. for want in PLUS_USE_OpenIGTLink PLUS_USE_NDI PLUS_USE_OPTICAL_MARKER_TRACKER \ PLUS_USE_INTELREALSENSE PLUS_USE_V4L2; do grep -qE "^#define +$want" "$cfg" \ || { echo "backend not compiled in: $want"; grep -c . "$cfg"; exit 1; } done # And the tracker backends must really be linked against their drivers, # otherwise the device exists in name only. if [ -e %{buildroot}%{_libdir}/libvtkPlusDataCollection.so ]; then objdump -p %{buildroot}%{_libdir}/libvtkPlusDataCollection.so \ | grep -E 'NEEDED.*(ndicapi|aruco)' \ || { echo "DataCollection links neither ndicapi nor aruco"; exit 1; } fi # PlusServer is the point of the package, and a built executable that cannot # start is worth nothing. Better still, PlusVersion prints the device backends # that were actually COMPILED IN, with the driver version each resolved # against -- so this checks the thing that matters rather than that a binary # produces some output. # # Note on where Plus writes: it creates PlusConfig.xml and an Output/ directory # next to its EXECUTABLE, not in the current directory, so running it out of # the buildroot leaves files behind (and on an installed system it will try to # write into /usr/bin, which an ordinary user cannot). Clean up after the # check, and see %%{name}-server's description. export LD_LIBRARY_PATH=%{buildroot}%{_libdir} devices=$(%{buildroot}%{_bindir}/PlusVersion 2>&1 || :) rm -rf %{buildroot}%{_bindir}/Output %{buildroot}%{_bindir}/PlusConfig.xml echo "$devices" | grep -q 'Software version: Plus-%{version}' \ || { echo "PlusVersion did not report its version:"; echo "$devices"; exit 1; } # Each enabled backend must appear as a usable device. NDICAPI-backed ones must # also show the ndicapi version, which proves the driver linked rather than the # device merely being registered. for dev in 'PolarisTracker (ver: NDICAPI' 'AuroraTracker (ver: NDICAPI' \ 'OpticalMarkerTracker' 'IntelRealSense' 'V4L2Video' \ 'SteamVRTracker' 'OpenIGTLinkTracker (ver: OpenIGTLink' \ 'FakeTracker' 'SavedDataSource'; do echo "$devices" | grep -qF "$dev" \ || { echo "device backend missing from PlusVersion: $dev"; echo "$devices"; exit 1; } done # And PlusServer itself must start far enough to describe its own usage. _plus_scratch=$(mktemp -d) out=$(cd "$_plus_scratch" && %{buildroot}%{_bindir}/PlusServer --help 2>&1 || :) rm -rf "$_plus_scratch" %{buildroot}%{_bindir}/Output %{buildroot}%{_bindir}/PlusConfig.xml echo "$out" | grep -qiE 'config-file|usage' \ || { echo "PlusServer --help produced nothing usable:"; echo "$out"; exit 1; } # Nothing may be left in the buildroot, or the unpackaged-files check fails. leftover=$(find %{buildroot}%{_bindir} -maxdepth 1 \ \( -name 'PlusConfig.xml' -o -name 'Output' \) -print) test -z "$leftover" || { echo "check left files behind: $leftover"; exit 1; } echo "PlusLib: 10 libraries resolve, 9 device backends compiled in, PlusServer runs" %files %license License.txt %doc README.md %{_libdir}/libvtkPlus*.so %{_datadir}/PlusLib-2.9/ %files devel %{_includedir}/PlusLib-2.9/ %{_libdir}/cmake/PlusLib-2.9/ %files server %{_bindir}/PlusServer %{_bindir}/PlusServerRemoteControl %files tools %{_bindir}/ConvertFcsvToXml %{_bindir}/ConvertXmlToFcsv %{_bindir}/CreateSliceModels %{_bindir}/DrawClipRegion %{_bindir}/DrawScanLines %{_bindir}/EditSequenceFile %{_bindir}/EnhanceUsTrpSequence %{_bindir}/ExtractScanLines %{_bindir}/ModelRenderer %{_bindir}/PlusVersion %{_bindir}/ProbeCalibration %{_bindir}/RfProcessor %{_bindir}/ScanConvert %{_bindir}/TemporalCalibration %{_bindir}/ViewSequenceFile %{_bindir}/VolumeReconstructor %{_bindir}/planeEstimation %{_bindir}/sphereEstimation %changelog * Tue Oct 06 2026 Morgan Hough - 2.9.0-0.3.20260820gitc7962189 - 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. * Sun Sep 27 2026 Morgan Hough - 2.9.0-0.2.20260820gitc7962189 - Build the command line tools (PLUSBUILD_BUILD_PlusLib_TOOLS, which PlusLib never declares itself -- the PlusBuild superbuild passes it down). That is what produces **PlusServer**, the process NaviNIBS and SlicerIGT connect to; without it the package was libraries with nothing to run. New subpackages PlusLib-server (PlusServer, PlusServerRemoteControl) and PlusLib-tools. - PlusRendering applied vtk_module_autoinit over ${VTK_LIBRARIES} but linked only a handful, so every executable using it failed with "undefined reference to vtkRenderingVRModels_AutoInit_Construct". Link publicly what autoinit was told about. - %%check now asserts the compiled-in device list from PlusVersion -- PolarisTracker, AuroraTracker and NDITracker each reporting NDICAPI-3.7, OpticalMarkerTracker, IntelRealSense, V4L2Video, SteamVRTracker, OpenIGTLinkTracker on OpenIGTLink v3.1.0, FakeTracker and SavedDataSource -- which proves the drivers linked rather than that a binary produced output. - Noted, because it affects use: Plus writes PlusConfig.xml and an Output/ directory next to its EXECUTABLE, so on an installed system it tries to write into /usr/bin. An earlier version of this %%check tripped over exactly that by leaving files in the buildroot. * Sat Sep 26 2026 Morgan Hough - 2.9.0-0.1.20260820gitc7962189 - Initial package: the hardware layer for the open neuronavigation stack. Superbuild off, built against system VTK, ITK, IGSIO, OpenIGTLink, OpenIGTLinkIO, ndicapi and aruco. Only the device backends that need no proprietary SDK are enabled. - rpmlint reports invalid-soname for each library: PlusLib versions none of them (libvtkPlusCommon.so with no .so.N), which is upstream's choice and matches how igsio ships. Not worth patching a soname scheme onto a library set whose consumers are all built alongside it. - Licence expressed as a LicenseRef with the reasoning recorded in the spec: the "research purposes only" wording is in the FDA disclaimer, not the grant, and the grant expressly permits proprietary use and contemplates commercialisation. Needs Morgan's confirmation.