%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: 1%{?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} 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} %description -n python3-%{srcname} %_description %prep %autosetup -n %{pypi_name}-%{version} # --------------------------------------------------------------------------- # 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}/ # 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 # 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 except the ones that genuinely cannot be # imported in a build root. The 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). # # Excluded, with the reason: # - Navigator.GUI.ViewPanels.SimulatedToolsPanel: it imports # NaviNIBS.Navigator.GUI.ViewPanels.MainViewPanelWithDockWidgets and # NaviNIBS.util.pyvista, and those are exactly the NaviNIBS modules that # python-NaviNIBS.spec already excludes from its own import check for a # "metaclass conflict" under PySide6 6.9 + Python 3.14. The panel cannot # import until that upstream incompatibility is fixed in NaviNIBS itself; # it is not a defect in this addon. # # What remains is the top-level package, the session-attribute model # (SimulatedToolsConfiguration -- the class NaviNIBS instantiates from # addon_configuration.json) and the ZMQ positions client. That covers both # non-GUI entry points and exercises navinibs, attrs, numpy and pyzmq. 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 } grep -vE '^%{module}\.Navigator\.GUI\.ViewPanels\.SimulatedToolsPanel$' \ %{_pyproject_modules} > %{_pyproject_modules}.filtered mv %{_pyproject_modules}.filtered %{_pyproject_modules} # Guard the filter itself: if the module naming ever changes, the grep above # could strip everything and %%check would silently become a no-op. n=$(wc -l < %{_pyproject_modules}) echo "import-checking $n modules:"; cat %{_pyproject_modules} test "$n" -ge 3 || { echo "ERROR: only $n modules left after filtering; expected at least 3." >&2 exit 1 } %pyproject_check_import # Now prove the exclusion above is still NECESSARY, and necessary for the # stated reason. An exclusion that nobody revisits is how a %%check quietly # shrinks to testing nothing, and "I assume it fails" is not a finding. # If NaviNIBS's PySide6/Python-3.14 metaclass conflict gets fixed, the import # starts working and this fails, telling the next packager to drop the filter. # If it fails for some *other* reason, that is a new defect and this also # fails rather than absorbing it. if PYTHONPATH=%{buildroot}%{python3_sitearch}:%{buildroot}%{python3_sitelib} \ %{python3} -c 'import %{module}.Navigator.GUI.ViewPanels.SimulatedToolsPanel' \ > panel-import.log 2>&1; then echo "ERROR: %{module}.Navigator.GUI.ViewPanels.SimulatedToolsPanel now" >&2 echo "imports cleanly. Remove it from the grep filter above." >&2 exit 1 fi grep -q 'metaclass conflict' panel-import.log || { echo "ERROR: SimulatedToolsPanel failed to import, but NOT with the" >&2 echo "metaclass conflict this exclusion is documented against. That is a" >&2 echo "new defect; do not widen the exclusion to cover it. Output was:" >&2 cat panel-import.log >&2 exit 1 } echo "verified: SimulatedToolsPanel is excluded for a metaclass conflict:" grep -F 'metaclass conflict' panel-import.log %files -n python3-%{srcname} -f %{pyproject_files} %doc README.md %changelog * 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.