%global pypi_name navinibs_simulated_tools # The importable name, and the directory the NaviNIBS addon loader looks for. %global module NaviNIBS_Simulated_Tools # The binary RPM name, minus the pythonX- prefix. %global srcname NaviNIBS-Simulated-Tools Name: python-NaviNIBS-Simulated-Tools Version: 0.1.6 Release: 3%{?dist} Summary: NaviNIBS addon for simulating tracked tool poses License: LGPL-3.0-only URL: https://github.com/PrecisionNeuroLab/NaviNIBS_Simulated_Tools Source0: %{pypi_source %{pypi_name} %{version} tar.gz} # The 'Simulated' tool positions server that SimulatedToolPositionsClient # requires and nothing provided, registered through the ToolPositionsServers # addon element that python-NaviNIBS 0.2.4-6 carries. Without both, the # simulated tools cannot drive a NaviNIBS session. Not yet upstream. Patch0: navinibs-simulated-tools-positions-server.patch BuildArch: noarch BuildRequires: python3-devel # %%check imports NaviNIBS, which imports pylsl, which dlopens the native liblsl # at import time; python3-pylsl does not pull liblsl in. Same reason as the # BuildRequires in python-NaviNIBS.spec. BuildRequires: liblsl %global _description %{expand: An addon for NaviNIBS that simulates the poses of tracked tools, so that a NaviNIBS session can be driven end to end with no tracking camera and no stimulator attached. It adds a "Simulated tools" main view panel for placing and dragging simulated trackers, and a session attribute that persists the simulated poses, and it feeds them to NaviNIBS over the same ZMQ tool-positions channel a real tracker would use.} %description %_description %package -n python3-%{srcname} Summary: %{summary} # 0.2.4-6 carries the pluggable positions-server patch Patch0 registers with. Requires: python3-NaviNIBS >= 0.2.4-6 %description -n python3-%{srcname} %_description %prep %autosetup -n %{pypi_name}-%{version} -p1 # --------------------------------------------------------------------------- # The sdist is not a sdist. Three separate problems, all verified against # navinibs_simulated_tools-0.1.6.tar.gz as published on PyPI: # # 1. It ships upstream's entire .git directory -- 232 entries, 264 KiB of # loose objects -- plus build/lib/ and a nested dist/ containing an older # 0.0.1 sdist (which itself contains a further nested .tar.gz). None of # that belongs in an RPM, and the wheel upstream publishes installs all of # it into site-packages. # # 2. The declared build backend cannot build this tree at all: # # [tool.poetry] # packages = [{ include = "NaviNIBS_Simulated_Tools", from = ".."}] # # "from .." is upstream's development layout, where the addon repo is # cloned next to a NaviNIBS checkout. Inside the extracted sdist, ".." is # outside the source tree, so poetry-core has nothing to include. The # module's contents are the sdist root itself, not a subdirectory of it. # Confirmed by running the declared backend against the extracted sdist: # "Backend 'poetry.core.masonry.api' is not available" / no such package. # The file also carries a *commented-out* setuptools backend plus a live # [tool.setuptools] table, so the intent to switch exists upstream but the # 0.1.6 sdist still selects poetry. # # 3. requires-python is capped at <3.14 by poetry convention. f44 is Python # 3.14 and f45 is 3.15, so pip refuses the build outright -- the same wall # python-NaviNIBS hit at 0.2.4-2: "Package 'navinibs' requires a different # Python: 3.15.0 not in '<3.15,>=3.12'". # # So: drop the junk, move the module's contents into a directory actually # named after the module, and replace the build config with a setuptools one # that describes the real layout. Rewriting rather than patching is deliberate # -- there is no minimal edit that makes "from .." mean the sdist root. # --------------------------------------------------------------------------- # Fail if upstream has since switched backends: this rewrite was written # against the poetry form, and should be re-examined, not silently reapplied, # if the published sdist changes shape. grep -q '^build-backend = "poetry.core.masonry.api"' pyproject.toml || { echo "ERROR: pyproject.toml no longer selects the poetry backend." >&2 echo "The %%prep rewrite below was written against that form; re-check it." >&2 exit 1 } rm -rf .git .gitignore build dist # These two must be gone, or the RPM ships a git repository and a stale 0.0.1 # copy of itself. A test that cannot find what it checks has to fail. test ! -e .git || exit 1 test ! -e dist || exit 1 mkdir -p %{module} mv Devices Navigator addon_configuration.json __init__.py %{module}/ test -f %{module}/Devices/SimulatedToolPositionsServer.py || exit 1 # The addon loader (NaviNIBS/Navigator/Model/Addons.py) opens # /addon_configuration.json and then imports # "%%{module}.Navigator...." by name, so both have to end up under a directory # called exactly %%{module}. Assert it, rather than trusting the mv above. test -f %{module}/addon_configuration.json || exit 1 test -f %{module}/Navigator/Model/SimulatedToolsConfiguration.py || exit 1 test -f %{module}/Devices/SimulatedToolPositionsClient.py || exit 1 test -f %{module}/__init__.py || exit 1 cat > pyproject.toml <<'EOF' [build-system] requires = ["setuptools >= 77"] build-backend = "setuptools.build_meta" [project] name = "NaviNIBS_Simulated_Tools" version = "0.1.6" description = "NaviNIBS addon for simulating tool poses" readme = "README.md" license = "LGPL-3.0-only" authors = [ {name = "Christopher C. Cline", email = "chriscline@users.noreply.github.com"}, ] # Upstream's <3.14 cap is a poetry default with no code behind it; see note 3 # in %%prep. The floor is upstream's. requires-python = ">=3.10" # navinibs is upstream's only declared dependency. The rest are imported by # the shipped modules and declared nowhere upstream -- an upstream metadata # bug, reported in docs/upstream/navinibs-addons-packaging.md. They are listed # here rather than as bare Requires: so that one list feeds both the generated # BuildRequires (%%check has to import them) and the generated Requires. dependencies = [ "navinibs >= 0.1.5", "attrs", "numpy", "pyzmq", "pyvista", "qtawesome", "qtpy", ] [project.urls] Homepage = "https://github.com/PrecisionNeuroLab/NaviNIBS" Repository = "https://github.com/PrecisionNeuroLab/NaviNIBS_Simulated_Tools" Documentation = "https://precisionneurolab.github.io/navinibs-docs" [tool.setuptools.packages.find] include = ["NaviNIBS_Simulated_Tools*"] # Devices/, Navigator/, Navigator/GUI/ and Navigator/Model/ carry no # __init__.py upstream -- they are implicit namespace portions inside a regular # package, which imports fine but is invisible to plain find_packages. Without # namespaces=true the wheel would contain only __init__.py and the RPM would # install an addon with no code in it, and still build green. namespaces = true [tool.setuptools.package-data] "*" = ["*.json"] EOF %generate_buildrequires %pyproject_buildrequires %build %pyproject_wheel %install %pyproject_install %pyproject_save_files -l %{module} # The whole point of the %%prep rewrite is that the module's code is present. # Check the installed tree, not the build log: 3 .py files besides __init__.py, # plus addon_configuration.json. test -f %{buildroot}%{python3_sitelib}/%{module}/addon_configuration.json || exit 1 test -f %{buildroot}%{python3_sitelib}/%{module}/Navigator/Model/SimulatedToolsConfiguration.py || exit 1 test -f %{buildroot}%{python3_sitelib}/%{module}/Navigator/GUI/ViewPanels/SimulatedToolsPanel.py || exit 1 test -f %{buildroot}%{python3_sitelib}/%{module}/Devices/SimulatedToolPositionsClient.py || exit 1 test -f %{buildroot}%{python3_sitelib}/%{module}/Devices/SimulatedToolPositionsServer.py || exit 1 # And that none of the sdist junk survived. test ! -e %{buildroot}%{python3_sitelib}/%{module}/.git || exit 1 test ! -e %{buildroot}%{python3_sitelib}/%{module}/dist || exit 1 test ! -e %{buildroot}%{python3_sitelib}/%{module}/build || exit 1 %check # Import-check every shipped module. The generated module list is filtered # directly because the %%pyproject_check_import -e option is not reliably # forwarded by the macro on f44 (same workaround as python-NaviNIBS.spec). # # Until -1, Navigator.GUI.ViewPanels.SimulatedToolsPanel was excluded, and the # exclusion was asserted to still be necessary, because importing it through # NaviNIBS raised "TypeError: metaclass conflict". python-NaviNIBS 0.2.4-4 # found the cause (qtpy and pyqtgraph binding to two different Qt wrappers) and # fixed it by pinning QT_API/PYQTGRAPH_QT_LIB in NaviNIBS/__init__.py, so the # panel now imports and the exclusion is gone: the GUI panel is the part of this # addon a user looks at, and it was never import-tested before. test -s %{_pyproject_modules} || { echo "ERROR: %{_pyproject_modules} is missing or empty -- there is nothing" >&2 echo "to import-check, so %%check would pass by testing nothing." >&2 exit 1 } n=$(wc -l < %{_pyproject_modules}) echo "import-checking $n modules:"; cat %{_pyproject_modules} test "$n" -ge 4 || { echo "ERROR: only $n modules listed; expected at least 4 (package, configuration," >&2 echo "positions client, SimulatedToolsPanel)." >&2 exit 1 } grep -q '^%{module}\.Navigator\.GUI\.ViewPanels\.SimulatedToolsPanel$' %{_pyproject_modules} || { echo "ERROR: SimulatedToolsPanel is not in the module list; the GUI panel would go" >&2 echo "untested." >&2 exit 1 } %pyproject_check_import # The point of Patch0, through NaviNIBS's own addon loader: loading the addon # registers type 'Simulated', the server starts the way CameraPanel starts it # and stays up, and a client gets back a pose it pushed. Loopback ZMQ only. PYTHONPATH=%{buildroot}%{python3_sitelib} timeout 120 %{python3} - <<'PYEOF' import asyncio, multiprocessing as mp, os, time import numpy as np from NaviNIBS.Devices import (getToolPositionsServerTypes, getToolPositionsServerClass, TimestampedToolPosition) from NaviNIBS.Devices.ToolPositionsClient import ToolPositionsClient from NaviNIBS.Navigator.Model.Addons import Addon import %{module} Addon.fromDict({'addonInstallPath': os.path.dirname(%{module}.__file__)}) assert 'Simulated' in getToolPositionsServerTypes(), getToolPositionsServerTypes() Server = getToolPositionsServerClass('Simulated') proc = mp.get_context('fork').Process(target=Server.createAndRun, daemon=True, kwargs={'pubPort': 18970, 'cmdPort': 18971}) proc.start() async def go(): c = ToolPositionsClient(serverPubPort=18970, serverCmdPort=18971) t0 = time.time() while not c.isConnected and time.time() - t0 < 30: await asyncio.sleep(0.1) assert c.isConnected, 'client never connected' assert c.getServerType() == 'Simulated', c.getServerType() await c.recordNewPosition_async(key='Coil', position=TimestampedToolPosition(time=time.time(), transf=np.eye(4))) t0 = time.time() while c.getLatestTransf('Coil', None) is None and time.time() - t0 < 10: await asyncio.sleep(0.05) assert c.getLatestTransf('Coil', None) is not None, 'pose did not come back' asyncio.run(go()) assert proc.is_alive(), 'server exited' proc.kill() print('Simulated positions server: ok') PYEOF %files -n python3-%{srcname} -f %{pyproject_files} %doc README.md %changelog * Fri Oct 23 2026 Morgan Hough - 0.1.6-3 - Add the 'Simulated' tool positions server the addon's client requires and register it through the ToolPositionsServers addon element (Patch0; needs python3-NaviNIBS >= 0.2.4-6). Until now no server of that type existed, so the simulated tools could not drive a session. %%check starts the server through NaviNIBS's addon loader and round-trips a pose. * Thu Oct 22 2026 Morgan Hough - 0.1.6-2 - %%check imports SimulatedToolsPanel too. It was excluded for a "metaclass conflict" that python-NaviNIBS 0.2.4-4 traced to qtpy and pyqtgraph choosing different Qt bindings and fixed; the assertion that the exclusion was still necessary would have failed the next rebuild by design. Requires python3-NaviNIBS >= 0.2.4-4 for the fix. Found by the 2026-10-22 readiness review. * Sun Sep 27 2026 Morgan Hough - 0.1.6-1 - Initial package. - Rewrite the build config in %%prep: the published sdist selects poetry-core with packages=[{include=..., from=".."}], which points outside the extracted tree and cannot build. Replaced with a setuptools config that describes the real layout (the module's contents ARE the sdist root). - find_packages needs namespaces=true: Devices/ and Navigator/ ship no __init__.py, so a plain discovery installs an addon containing nothing but __init__.py and still builds green. %%install asserts the files are there. - Strip .git/ (232 entries), build/lib/ and a nested dist/ holding an older 0.0.1 sdist -- all three are in the published sdist and the published wheel. - Drop the <3.14 requires-python cap (poetry convention, no code reason); f44 is 3.14 and f45 is 3.15. - Declare the six dependencies the code imports and upstream's metadata omits. - %%check import-tests everything but SimulatedToolsPanel, which is blocked by the PySide6/py3.14 metaclass conflict already documented in python-NaviNIBS.spec.