# InVesalius -- 3D medical imaging reconstruction, segmentation and # neuronavigation, from Centro de Pesquisas Renato Archer (Brazil). # # Packaged as the "standalone app / E-field reference" row of # docs/open-neuronavigation.md: it is the most complete single open-source # neuronavigator, and its real-time E-field work is the reference for doing the # same in NaviNIBS. It is NOT the core of that suite -- its tracker wrappers are # Windows-only, its robot service is unpublished, and it is wxPython. # # Snapshot, not a release: the newest tag (v3.1.99998) is from April 2022 while # master moves continuously, so this follows the ndicapi precedent in this repo # and versions as ^git. %global commit e32395ec5fad1e83110f06df7ca95df39e9c22a1 %global shortcommit %(c=%{commit}; echo ${c:0:8}) %global snapdate 20260921 # The Python distribution version. It cannot be the RPM version: "^" is not # valid in a PEP 440 version, and upstream's own version machinery does not # work here -- pyproject.toml declares dynamic=["version"] with # [tool.setuptools_scm], but the build backend is maturin, not setuptools, so # setuptools_scm never runs and maturin would fall back to Cargo.toml's # version = "0.1.0". Set it explicitly in %%prep instead. %global distversion 3.1.99998 Name: invesalius Version: 3.1.99998^%{snapdate}git%{shortcommit} Release: 11%{?dist} Summary: 3D medical imaging reconstruction, segmentation and neuronavigation # GPL-2.0-only, not -or-later: every source header says "de acordo com a versao # 2 da Licenca" and no file offers a later version. Verified by grep -- zero # files say "version 2 or (any) later"; the "any later version" text in # LICENSE.txt is the GPL's own section 9 boilerplate, not a grant. # # Apache-2.0: invesalius/segmentation/deep_learning/fastsurfer_subpart/ is # FastSurfer code (Image Analysis Lab, DZNE Bonn, 2019/2023). # NOTE: Apache-2.0 is not compatible with GPL-2.0-only. That is an upstream # licence conflict, not a packaging one; see docs/COORDINATION.md. Reported # rather than worked around. # # CC-BY-4.0: navigation/objects/head.stl, per navigation/objects/LICENSE.txt -- # "Head" by Thingiverse user "figure", thing:979818. License: GPL-2.0-only AND Apache-2.0 AND CC-BY-4.0 URL: https://invesalius.github.io/ # NOT an upstream tarball. invesalius-clean.sh strips content Fedora cannot # ship, so a canonical github.com/invesalius URL would reintroduce exactly what # was removed: # # bundle_tools/ a 24 MB prebuilt vc_redist_2015_2022_x64.exe # ai/ brain_mri_t1/model.h5, Keras weights with NO licence at all, # and dead weight besides -- the code downloads # brain_mri_t1.pt/.onnx at runtime and never reads model.h5 # plugins/ a git submodule whose repo (inv3_plugins) declares no licence # # Same situation as opencv in this repo, and handled the same way: the cleaned # tarball is published in this project's COPR dist-git lookaside, which is public # and content-addressed by md5, so the package is SCM-buildable from git alone. # # Seeded 2026-09-27 at Morgan's direction by one uploaded-SRPM build (11042466), # which is how COPR populates its lookaside at dist-git import time. The package # was converted straight back to SCM afterwards, so it ends up built from git per # docs/PACKAGING-POLICY.md section 5; the upload was the bridge, not the state. # Verified live before pinning: HTTP 200, Content-Length 76651992 matching the # local tarball exactly, and a ranged GET returning 206 with real bytes. # # This is a bridge, not the destination -- same caveat opencv carries. The # intended fix per ci/lookaside-allowlist.txt is a real lookaside cache. %global copr_lookaside https://copr-dist-git.fedorainfracloud.org/repo/pkgs/mhough/neurofedora/%{name} %global cleanmd5 c868629a30ea7540272868ecbee6c309 Source0: %{copr_lookaside}/%{name}-clean-%{shortcommit}.tar.xz/md5/%{cleanmd5}/%{name}-clean-%{shortcommit}.tar.xz # Vendored crate tree for invesalius_rs, served from the same lookaside and pinned # by md5. COPR builders have no network during %%build, so the crates Cargo.lock # names must be on disk before the build starts. Generated reproducibly inside mock # (no toolchain installed on the host) with: # # cargo vendor --versioned-dirs vendor # tar -cJf invesalius-vendor-e32395ec.tar.xz vendor # # (the commit is spelled out rather than written as a macro on purpose: this is a # record of how THIS bundle was produced, and a macro in a comment is still # expanded by rpm, while escaping it to %%%% would break copy-pasting the command) # # 41 crates. Cargo.lock has 42 [[package]] entries; the 42nd is invesalius_rs # itself, a workspace member rather than a dependency, so its absence from vendor/ # is correct. Unpacked and wired up in %%prep. %global vendormd5 4645ea471ff67fe06778a16a7409ed24 Source1: %{copr_lookaside}/%{name}-vendor-%{shortcommit}.tar.xz/md5/%{vendormd5}/%{name}-vendor-%{shortcommit}.tar.xz # VTK 9.7 removed vtkIdFilter outright (renamed to vtkGenerateIds, no compat shim), so # InVesalius cannot start at all against it: # ImportError: cannot import name 'vtkIdFilter' from 'vtkmodules.vtkFiltersCore' # Found by Morgan running the installed package on Fedora 44 -- the first time anyone # had launched it. The build, the smoke gates and %%check all passed without touching # this path, which is why a headless import check is not a substitute for starting the # application. # # The patch also pins the output array name. VTK changed the default from # "vtkIdFilter_Ids" to "vtkPointIds", and this file reads the array back BY NAME at two # sites, so a bare class rename would yield vtk_to_numpy(None) inside the render loop -- # a later and more confusing failure than the ImportError it replaced. Patch0: %{name}-vtk97-generateids.patch # Python 3.13 removed imghdr from the standard library (PEP 594) and f44 ships # Python 3.14, so reader/bitmap_reader.py's module-level import aborts startup # before any window appears: # ModuleNotFoundError: No module named 'imghdr' # It sits on the unavoidable path -- app.py:712 load_neuronavigation -> # net/neuronavigation_api:24 -> data/markers/marker:7 -> data/imagedata_utils:42 # -> reader/bitmap_reader:19. Found by Morgan running -5 on Fedora 44, directly # behind the vtkIdFilter crash above: fixing that import exposed this one. # # Replaced with Pillow, already a hard Requires below. Image.format returns # 'BMP'/'TIFF'/'PNG'/'JPEG', so .lower() reproduces imghdr.what()'s vocabulary # exactly -- which is what the t == "bmp" / "tiff" / "png" / "jpeg" comparisons # at bitmap_reader.py:324-333 expect. PIL.UnidentifiedImageError subclasses # OSError, so VerifyDataType's existing handler still covers unreadable files. # # A sweep of all 25 modules dropped in Python 3.12/3.13 across this tree's 148 # .py files found imghdr to be the only one present, so this is the whole of # that class of breakage, not the first of several. Patch1: %{name}-py314-imghdr.patch # VTK 9.7 also removed vtkViewport's 2D-prop aliases, which is a separate purge # from the vtkIdFilter rename above but the same release. AddActor2D, # RemoveActor2D and RemoveAllProps are gone from vtkViewport (and so from # vtkRenderer); the survivors are AddViewProp, RemoveViewProp and # RemoveAllViewProps, all three verified present on vtkRenderer in 9.7.0-6. # # Third startup crash Morgan hit, immediately behind the imghdr one: # AttributeError: 'vtkOpenGLRenderer' object has no attribute 'AddActor2D'. # Did you mean: 'AddActor'? # at gui/widgets/canvas_renderer.py:171, reached from Frame.__init_aui -> # default_viewers -> viewer_volume.Viewer.__init__:178. Note the interpreter's # suggestion is WRONG here: AddActor takes a vtkProp3D, and these are 2D props, # so AddViewProp is the correct replacement, not AddActor. # # 6 call sites across 4 files, found by sweeping every CamelCase method call in # the tree against the installed VTK 9.7 with each receiver typed from its own # assignment, per file, dropping any name assigned something non-VTK in that # file. 1706 call sites checked against 330 typed receivers. The sweep carries a # positive control -- it must flag AddActor2D and must not flag AddViewProp -- # because an earlier union-of-all-classes version reported "present" for # AddActor2D (some other class still has it) and would have missed this # entirely. # # Everything else it flagged was either a pre-9.7 removal in code that is # already dead (SetInput, VTK 6; ImmediateModeRenderingOn, VTK 9.0; # vtkImageData.Update, VTK 6) or a false positive: commented-out lines, which # the regex also scans, and wx receivers that share a name with a VTK one. # Those are upstream reports, not packaging fixes, and are deliberately not # patched here. Patch2: %{name}-vtk97-viewprops.patch # Two downstream fixes that are not VTK-related. # # 1. utils.py UpdateCheck() POSTed to https://www.cti.gov.br/dt3d/invesalius/ # update/checkupdate.php on every launch -- from a thread started at # app.py:241, so unconditionally -- sending invesalius_version, platform, # architecture, language and a persistent random_id, with # requests.post(..., verify=False). That is telemetry with TLS certificate # verification switched off. Distribution packages are updated by the # distribution, so the check has no purpose here, and the unverified POST is # a defect regardless. The function keeps its name and signature and returns # immediately, so app.py's thread becomes a no-op and nothing else changes. # Observed by Morgan in the console on a real run: "Checking updates..." # followed by urllib3's InsecureRequestWarning. # # With this applied, a freshly launched InVesalius makes no network # connection and binds no port: the DICOM SCP at net/dicom.py:433 is inside # __RunCMove and is reached only by an explicit retrieval, with # scp.shutdown() at :446 and :485. # # 2. data/viewer_navigation.py's NavigationRenderer wrapper dispatches by # STRING -- getattr(self.renderer, "AddActor2D")(prop) -- so the VTK 9.7 # removals patched in Patch2 are invisible to any grep for call sites: the # method name exists only as a string literal. No in-tree caller exists # today, so this is not a crash, but the wrapper advertises AddActor2D and # RemoveActor2D as working API and its __getattr__ delegates to the renderer # besides. The dispatch strings now name AddViewProp/RemoveViewProp; the # wrapper's own method names are deliberately kept for API compatibility. # Found by neurofedora-t2 reading the installed tree, not by my sweep -- # a receiver-typed method sweep cannot see a string literal. Patch3: %{name}-fedora-no-phonehome-and-navprops.patch # Make the deep-learning stack optional, so python3-torch (309 MiB installed) # is not a hard dependency of an imaging application that mostly does not use # it. Two module-level imports made it mandatory, via two independent paths: # # control.py:48 -> segment -> torch # frame.py:33 -> default_tasks:30 -> task_navigator:62 # -> deep_learning_seg_dialog:15 -> segment -> torch # # (segment.py:19 imports fastsurfer_subpart/data_process.py, which does # "import torch" at module scope. segment.py's own torch uses are already # function-local, so only fastsurfer_subpart forces it.) # # The second one is an upstream bug in its own right: deep_learning_seg_dialog # already guards "import torch"/"import tinygrad" at lines 17-28 and exposes # HAS_TORCH/HAS_TINYGRAD, but the unguarded segment import at line 15 defeats # that design -- the module cannot be imported without torch despite being # written to run without it. The patch wraps both imports in try/except # ImportError, matching the idiom already in that file. # # Safe because every use is gated: control.py needs only # segment.run_cranioplasty_implant (one Publisher.subscribe, a command-line # path), and the dialog's five segment.* uses are all inside methods whose call # sites already test HAS_TORCH/HAS_TINYGRAD (frame.py:1180-1257, # task_navigator.py:946). # # Measured in a mock chroot with no torch installed -- before: control, frame # and deep_learning_seg_dialog all raise ModuleNotFoundError: torch; after: all # five startup modules import, and "torch" in sys.modules is False once # invesalius.gui.frame has been imported. Patch4: %{name}-optional-deep-learning.patch # Upstream's own fix for a defect we found and documented, carried until the pin # moves past it: invesalius3 ef797bb19a (#1482, 2026-10-02). NIfTI/Analyze/PAR-REC # import called LoadProject(create_default_mask=False), so no mask existed and # `--no-gui -o volume.nii.gz -t lo,hi -e out.stl` died with KeyError: 0 and wrote # nothing. Drop this patch when %%{commit} contains ef797bb19a. Patch5: %{name}-nifti-default-mask.patch # VTK's wxPython interactor ignores display scaling, so on a scaled display every # slice and volume pane renders into 1/scale of itself and picking is offset. # Measured on a real HiDPI laptop: image 0.499 of its pane at 200%. The defect is # in VTK (docs/upstream/vtk-wx-hidpi-render-window-size.md); this corrects the # VTK module in memory so no installed file changes, and it is inert -- never # raises, changes nothing -- if VTK already scales or its source is not the # expected shape. The only package here that imports the bridge is this one. Patch6: %{name}-vtk-wx-hidpi.patch # Rust extension (invesalius_rs, PyO3/maturin). Not noarch. BuildRequires: python3-devel BuildRequires: pyproject-rpm-macros BuildRequires: cargo BuildRequires: rust BuildRequires: maturin # The only application icon upstream ships is a .ico; hicolor needs a PNG. BuildRequires: icoutils BuildRequires: desktop-file-utils BuildRequires: libappstream-glib # For the MIME definition in %%install and its validation in %%check. BuildRequires: libxml2 BuildRequires: shared-mime-info # For %%check only. invesalius_rs/__init__.py imports numpy at module level # (line 5) before re-exporting the compiled _native module, so the extension # cannot be imported without it. The vtkmodules imports further down that file # are function-local and do not affect an import check. BuildRequires: python3-numpy # For the %%check of invesalius/vtk_wx_hidpi.py, which imports vtkmodules.wx (and # so wx) to prove the HiDPI correction can still be applied to this buildroot's VTK. BuildRequires: python3-wxpython4 BuildRequires: python3-vtk # Upstream's dependency metadata is pinned to versions that all predate # Fedora's, and %%pyproject_buildrequires is run with -R (build deps only), so # the runtime set is declared here explicitly against real Fedora packages. Requires: python3-wxpython4 Requires: python3-vtk Requires: python3-numpy Requires: python3-scipy Requires: python3-scikit-image Requires: python3-nibabel Requires: python3-imageio Requires: python3-pydicom Requires: python3-pynetdicom Requires: python3-gdcm Requires: python3-h5py Requires: python3-psutil Requires: python3-pypubsub Requires: python3-pyserial Requires: python3-matplotlib Requires: python3-pandas Requires: python3-pillow # Both are imported at module level by invesalius/data/brainmesh_handler.py # (lines 2 and 5), so both are hard runtime requirements. An earlier version of # this comment said pyvista "arrives only transitively via pyacvd" -- that was # wrong, it is imported in its own right. Neither is in upstream's dependency # metadata, which is why they are declared here by hand. # # python3-pyacvd is NOT in Fedora at all; it comes from this COPR, and it was # built only for aarch64 and f44-x86_64 until 2026-09-28, which made invesalius # unresolvable on rawhide-x86_64 and f45-x86_64 despite building green there. Requires: python3-pyacvd Requires: python3-pyvista Requires: python3-requests Requires: python3-lib3mf # Owns the .inv3 MIME definition installed in %%install. Requires: shared-mime-info # The deep-learning stack (torch, onnx, onnxruntime, tinygrad) moved to the -ai # subpackage; see Patch4 and %%package ai below. python3-torch alone is 309 MiB # installed -- measured from a depsolve, not the ~675 MB this comment used to # claim -- for a feature most users of an imaging application never touch. # # Dropping the manual Requires: lines is NOT sufficient, and the first attempt # at this split was cosmetic because of it. Upstream's own metadata lists all # four in invesalius-%%{distversion}.dist-info/METADATA: # # Requires-Dist: torch / tinygrad / onnx / onnxruntime # # and rpm's Python dependency generator turns those into # python3.14dist(torch) etc. on the BASE package at packaging time. The built # RPM still pulled torch with the manual lines removed. %%pyproject_buildrequires # -R governs build requires only; it has no say over the runtime set. # # %%__requires_exclude filters the generated runtime requires without editing # upstream's metadata or the wheel. Anchored so it matches these four exactly and # cannot catch a future torchvision/onnxscript. # # NOTE: there is no python-specific variant on this rpm -- # %%__pythondist_requires_exclude is undefined (rpm --eval returns nothing), and a # first attempt using that name was inert: the rebuild produced a byte-identical # 24-entry requires list. The generic %%__requires_exclude in /usr/lib/rpm/macros # is the real mechanism. Appended rather than assigned, following the idiom in # macros.perl, so an existing filter is not clobbered. # Backslashes MUST be doubled. rpm expands the macro body before the filter ever # sees it, so a single-backslash pattern arrives stripped: `rpm --eval` on the # first version returned ^python3(.[0-9]+)?dist((torch))$ -- parens turned into # groups, dots into any-char -- which cannot match python3.14dist(torch). Two # builds passed with the filter silently inert because of it. The same doubling # is visible in /usr/lib/rpm/fileattrs/pythondist.attr's own __pythondist_path. %global __requires_exclude ^python3(\\.[0-9]+)?dist\\((torch|tinygrad|onnx|onnxruntime)\\)$ # The Rust extension statically links 41 vendored crates, so the base package # carries them and must say so: licence and CVE tracking both key off # bundled(crate(NAME)). Generated from the vendor tree rather than by hand -- # every crate directory carrying a .cargo-checksum.json, 45 of 45. Licence audit # of the same tree: 28 'MIT OR Apache-2.0', 4 'Apache-2.0 OR MIT', 4 'Apache-2.0', # 3 'Zlib OR Apache-2.0 OR MIT', 2 'MIT/Apache-2.0', 1 'MIT', 1 'BSD-2-Clause', # 1 'Apache-2.0 WITH LLVM-exception', 1 '(MIT OR Apache-2.0) AND Unicode-3.0'; # no crate lacked a licence field. Fedora's %%cargo_vendor_manifest does this # automatically, but it needs cargo-rpm-macros, which this spec deliberately # avoids -- see the %%prep note on %%cargo_prep versus maturin. Provides: bundled(crate(approx)) = 0.5.1 Provides: bundled(crate(autocfg)) = 1.5.1 Provides: bundled(crate(bytemuck)) = 1.25.2 Provides: bundled(crate(crossbeam-deque)) = 0.8.8 Provides: bundled(crate(crossbeam-epoch)) = 0.9.21 Provides: bundled(crate(crossbeam-utils)) = 0.8.23 Provides: bundled(crate(either)) = 1.18.0 Provides: bundled(crate(heck)) = 0.5.0 Provides: bundled(crate(libc)) = 0.2.189 Provides: bundled(crate(matrixmultiply)) = 0.3.11 Provides: bundled(crate(nalgebra)) = 0.33.3 Provides: bundled(crate(nalgebra-macros)) = 0.2.2 Provides: bundled(crate(ndarray)) = 0.17.2 Provides: bundled(crate(num-bigint)) = 0.4.8 Provides: bundled(crate(num-complex)) = 0.4.6 Provides: bundled(crate(num-integer)) = 0.1.47 Provides: bundled(crate(numpy)) = 0.29.0 Provides: bundled(crate(num-rational)) = 0.4.2 Provides: bundled(crate(num-traits)) = 0.2.19 Provides: bundled(crate(once_cell)) = 1.21.4 Provides: bundled(crate(paste)) = 1.0.15 Provides: bundled(crate(portable-atomic)) = 1.15.0 Provides: bundled(crate(portable-atomic-util)) = 0.2.8 Provides: bundled(crate(proc-macro2)) = 1.0.107 Provides: bundled(crate(pyo3)) = 0.29.2 Provides: bundled(crate(pyo3-build-config)) = 0.29.2 Provides: bundled(crate(pyo3-ffi)) = 0.29.2 Provides: bundled(crate(pyo3-macros)) = 0.29.2 Provides: bundled(crate(pyo3-macros-backend)) = 0.29.2 Provides: bundled(crate(quote)) = 1.0.47 Provides: bundled(crate(rawpointer)) = 0.2.1 Provides: bundled(crate(rayon)) = 1.12.0 Provides: bundled(crate(rayon-core)) = 1.13.0 Provides: bundled(crate(rustc-hash)) = 2.1.3 Provides: bundled(crate(safe_arch)) = 0.7.4 Provides: bundled(crate(simba)) = 0.9.1 Provides: bundled(crate(syn)) = 2.0.119 Provides: bundled(crate(target-lexicon)) = 0.13.5 Provides: bundled(crate(typenum)) = 1.20.1 Provides: bundled(crate(unicode-ident)) = 1.0.26 Provides: bundled(crate(wide)) = 0.7.33 %global _description %{expand: InVesalius reconstructs 3D models from CT and MRI DICOM series. It covers DICOM/NIfTI/Analyze import, threshold and watershed segmentation with 2D editing tools, surface generation and export (STL, PLY, OBJ, VRML, 3MF), volume rendering with presets, measurement tools, and volume reorientation. It also includes a neuronavigation module -- subject registration, targets and markers, tracked-tool views and real-time E-field display -- which on Linux runs with the trackers reachable through its serial and network backends. The Optitrack, Claron MicronTracker and Polhemus wrappers are Windows-only builds and are not part of this package. Developed by Centro de Pesquisas Renato Archer, Brazil.} %description %_description %package doc Summary: User guides for InVesalius BuildArch: noarch %description doc The InVesalius user guide in English and Brazilian Portuguese. %package data Summary: Sample dataset for InVesalius BuildArch: noarch Requires: %{name} = %{version}-%{release} %description data A sample cranium project (Cranium.inv3) for trying InVesalius without a DICOM series to hand. %package ai Summary: Deep-learning segmentation for InVesalius Requires: %{name} = %{version}-%{release} Requires: python3-torch Requires: python3-onnxruntime Requires: python3-onnx Requires: python3-tinygrad %description ai Deep-learning segmentation: brain, trachea, mandible, cranioplasty implant, and the FastSurfer subpart models. Split out because python3-torch alone is 309 MiB installed, and the rest of InVesalius -- DICOM import, masks, surfaces, volume rendering, neuronavigation -- does not use it. Without this subpackage the base install works fully and the segmentation dialogs report the feature as unavailable, which is what HAS_TORCH/HAS_TINYGRAD in invesalius/gui/deep_learning_seg_dialog.py already exist to signal. %prep %autosetup -n %{name}-%{version} -p1 # --------------------------------------------------------------------------- # pyproject.toml: make the metadata describe something Fedora can build. # --------------------------------------------------------------------------- python3 - pyproject.toml <<'PYFIX' import re import sys path = sys.argv[1] with open(path, encoding="utf-8") as fh: src = fh.read() # 1. A static version. maturin is the backend, so [tool.setuptools_scm] never # runs and the version would come from Cargo.toml (0.1.0). old = 'dynamic = ["version"]' new = 'version = "@DISTVERSION@"' assert src.count(old) == 1, "dynamic version declaration not found exactly once" src = src.replace(old, new) # 2. requires-python. Upstream pins the interpreter exactly: # requires-python = "==3.12.*" # Fedora has zero packages built for python(abi) = 3.12 (against 6279 for # 3.14), so building against 3.12 would mean packaging the entire GUI and # scientific stack for a non-default interpreter. Widen it instead. old = 'requires-python = "==3.12.*"' new = 'requires-python = ">=3.12"' assert src.count(old) == 1, "requires-python pin not found exactly once" src = src.replace(old, new) # 3. The console script is declared as "inv", but /usr/bin/inv is already owned # by python3-invoke. Rename it. old = "inv = 'invesalius.app:main'" new = "invesalius = 'invesalius.app:main'" assert src.count(old) == 1, "gui-scripts entry point not found exactly once" src = src.replace(old, new) # 4. Drop every version constraint from [project] dependencies. All of them sit # BELOW Fedora's versions -- numpy 1.26.4 vs 2.4.6, pydicom 2.4.5 vs 3.0.2, # pyacvd 0.2.11 vs 0.4.0, vtk 9.3.0 vs 9.7.0, wxpython 4.2.5 vs 4.2.4, # setuptools<68 (a stale wxPython 4.2.2 workaround) -- so pinning them is # exactly wrong here. Environment markers are preserved, which is what keeps # the Windows-only tracker wrappers (optitrack, pyclaron, polhemus*, # pypolaris) out of the Linux dependency set. def relax(match): body = match.group(1) out = [] for raw in re.findall(r'"([^"]*)"', body): if ";" in raw: name, marker = raw.split(";", 1) marker = ";" + marker else: name, marker = raw, "" name = re.split(r"[=<>!~]", name, 1)[0].strip() # Fedora's onnxruntime is the CPU build; there is no onnxruntime-gpu. if name == "onnxruntime-gpu": name = "onnxruntime" # Drop two names that would generate UNSATISFIABLE python3dist() # requires. Both are covered by explicit Requires: on the real RPM # names in the preamble, which do resolve. # # python-gdcm : Fedora's python3-gdcm provides NO python3dist() # capability whatsoever -- not python3dist(gdcm) and not # python3dist(python-gdcm) -- only python3-gdcm, # python3.14-gdcm and python-gdcm. So renaming this to # "gdcm" (the obvious move, by analogy with the # onnxruntime-gpu case above) fails identically. # Measured, not assumed. # # lib3mf : supplied by this repo's own python-lib3mf, which does # provide python3dist(lib3mf). Requiring it by RPM name # is more robust for a COPR-local package, and avoids # the built RPM being uninstallable for as long as # python-lib3mf is unpublished. if name in ("python-gdcm", "lib3mf"): continue out.append('"%s%s"' % (name, marker)) return "dependencies = [\n " + ",\n ".join(out) + ",\n]" src, n = re.subn(r"dependencies = \[(.*?)\]", relax, src, count=1, flags=re.S) assert n == 1, "[project] dependencies array not found" with open(path, "w", encoding="utf-8") as fh: fh.write(src) print("pyproject.toml: static version, requires-python >=3.12, script renamed, pins relaxed") PYFIX sed -i 's/@DISTVERSION@/%{distversion}/' pyproject.toml # --------------------------------------------------------------------------- # The entry point points at invesalius.app:main, but there is no # invesalius/app.py -- app.py sits at the repository root, and # [tool.maturin] python-packages installs only the invesalius and invesalius_rs # packages. So upstream's own console script cannot import its target. Move it # into the package; every import in it is already absolute # (import invesalius.i18n, from invesalius import inv_paths, ...) and there is # no sys.path manipulation, so it relocates unchanged. # --------------------------------------------------------------------------- test -f app.py || { echo "ERROR: app.py missing at the tree root" >&2; exit 1; } test ! -e invesalius/app.py || { echo "ERROR: invesalius/app.py now exists upstream -- drop this move" >&2; exit 1; } mv app.py invesalius/app.py # --------------------------------------------------------------------------- # inv_paths.py: find the data where Fedora installs it. # # INV_TOP_DIR is derived from __file__, which after installation resolves to # %%{python3_sitearch} -- so icons, presets, locale, samples, docs and the # navigation data would all be looked for next to the Python package. Every # other path in the file is derived from INV_TOP_DIR (including the five that # already honour INV_*_DIR environment overrides), so retargeting this one # constant fixes all of them at once. # --------------------------------------------------------------------------- python3 - invesalius/inv_paths.py <<'PYFIX' import sys path = sys.argv[1] with open(path, encoding="utf-8") as fh: src = fh.read() old = "INV_TOP_DIR = pathlib.Path(__file__).parent.parent.resolve()" new = 'INV_TOP_DIR = pathlib.Path("@DATADIR@/invesalius")' assert src.count(old) == 1, "INV_TOP_DIR definition not found exactly once" src = src.replace(old, new) with open(path, "w", encoding="utf-8") as fh: fh.write(src) print("inv_paths.py: INV_TOP_DIR -> the installed data directory") PYFIX sed -i 's|@DATADIR@|%{_datadir}|' invesalius/inv_paths.py # Unpack the vendored crate tree and point cargo at it, for a fully offline build. # COPR builders have no network during %%build, so every crate Cargo.lock names has # to be on disk already. 41 crates are vendored here; Cargo.lock has 42 entries, the # 42nd being invesalius_rs itself, which is the workspace member rather than a # dependency and so is correctly absent from vendor/. # # This is deliberately the hand-rolled form (as leangz.spec uses) and NOT # %%cargo_prep -v vendor, even though elan.spec uses the macro. Read the macro in # cargo-rpm-macros: it does `mkdir -p target/rpm` then `ln -s rpm target/release`, # creating a target/ inside the source tree -- exactly what CARGO_TARGET_DIR below # exists to prevent, because maturin packages the invesalius_rs directory and a # target/ underneath it blows past the zip crate's 4 GiB ZIP64 limit. %%cargo_prep # also writes a [profile.rpm] that maturin does not use, and %%cargo_build / # %%cargo_install assume a plain cargo binary rather than a wheel build. The macro # form is right for a plain binary crate like elan; the distinguishing factor is # maturin, not Rust. # # Note tar -xJf, not leangz's -xzf: this bundle is .tar.xz. tar -xJf %{SOURCE1} # The bundle ships the Cargo.lock it was resolved with, and it must replace # upstream's. This bump moves versions (pyo3 0.27.2 -> 0.29.2, numpy 0.27.1 -> # 0.29.0), so upstream's in-tree lockfile pins crates the vendor tree no longer # contains, and cargo fails before compiling anything: # error: failed to select a version for the requirement `openssl = "^0.10"` # was exactly this, one package over, in elan. A vendor tree and the lockfile that # describes it are one artifact. test -f Cargo.lock mv -f Cargo.lock invesalius_rs/Cargo.lock grep -q '^version = "0.29.2"' invesalius_rs/Cargo.lock # Raise the manifest pins to match the bundle. Python 3.15 is newer than pyo3 # 0.27's maximum supported 3.14, so f45 and rawhide could not build at all: # error: the configured Python version (3.15) is newer than PyO3's maximum # supported version (3.14) # 3.15 support arrives in pyo3 0.29.0, and rust-numpy tracks pyo3 in exact minor # lockstep (numpy 0.29.0 bumps PyO3 to v0.29.0), so both move together or neither # resolves. NOT using PYO3_USE_ABI3_FORWARD_COMPATIBILITY: that silences the check # while still building against an interpreter whose module init changed (PEP 820 / # PyModExport in 0.29), turning a build failure into an import failure. python3 - <<'PINS' import pathlib p = pathlib.Path("invesalius_rs/Cargo.toml") s = p.read_text() for old, new in ( ('pyo3 = { version = "0.27", features = ["extension-module"] }', 'pyo3 = { version = "0.29", features = ["extension-module"] }'), ('numpy = "0.27"', 'numpy = "0.29"'), ): assert s.count(old) == 1, (old, s.count(old)) s = s.replace(old, new) p.write_text(s) print("invesalius_rs/Cargo.toml: pyo3 and numpy pinned to 0.29") PINS mkdir -p .cargo cat > .cargo/config.toml <<'EOF' [source.crates-io] replace-with = "vendored-sources" [source.vendored-sources] directory = "vendor" # maturin invokes cargo itself, so --offline cannot be passed through the wheel # macro the way leangz passes it to a bare `cargo build`. Setting it here makes the # offline guarantee structural: if a crate is missing from vendor/, the build fails # loudly instead of reaching for crates.io and timing out. # # Do NOT name the %%pyproject_wheel macro in this heredoc. rpm expands macros in # spec text before the shell ever sees it, quoted <<'EOF' delimiter or not, so the # macro body landed in config.toml and cargo rejected it with # "key with no value, expected `=`" on a bare backslash. rpmspec -P emits no # warning for this: it is not a # comment at spec level, just text to expand. [net] offline = true EOF %generate_buildrequires # -R: build-system requires only. The runtime set is declared by hand above -- # upstream's names do not all map onto Fedora's python3dist() provides, and # resolving them here would fail the build before it reaches anything real. %pyproject_buildrequires -R %build # Keep cargo's build tree OUT of the source directory. # # [tool.maturin] python-packages = ["invesalius", "invesalius_rs"] makes maturin # package the invesalius_rs DIRECTORY as a Python package -- and after a build # that directory also contains target/, the entire cargo build tree. Fedora's # RUSTFLAGS force -Cdebuginfo=2 -Ccodegen-units=1 -Cstrip=none, so those # artifacts are very large, and the wheel writer dies on one of them with: # # Failed to write to zip archive for "invesalius_rs/target/wheels/...whl" # Caused by: Large file option has not been set # # which is the zip crate refusing an entry at the 4 GiB ZIP64 threshold. The # Rust build itself is fine (42 crates, "Finished release profile in 54.32s", # zero errors); it is only the packaging step that fails. Redirect the target # directory so maturin never sees it. export CARGO_TARGET_DIR=%{_builddir}/invesalius-rust-target # Cap cargo's parallelism. Fedora's RUSTFLAGS force -Ccodegen-units=1 # -Cdebuginfo=2 -Cstrip=none, and nalgebra/ndarray/pyo3 under those settings # are memory-hungry: one rustc per core (8 here) was enough to put a 31 GB # machine under memory pressure and get the build killed mid-compile at 9 of # 42 crates. Two jobs keeps the peak well inside budget and costs about a # minute of wall time on a build that takes under one. export CARGO_BUILD_JOBS=2 # Make cargo read the vendored-sources config written in %%prep. Unlike # %%cargo_prep's target/release symlink, CARGO_HOME and CARGO_TARGET_DIR compose # cleanly: the config lives in the source tree, the build artifacts do not. export CARGO_HOME=.cargo %pyproject_wheel %install %pyproject_install %pyproject_save_files invesalius invesalius_rs # Data that is not part of the Python package. inv_paths.py was retargeted at # this directory in %%prep. for d in icons presets locale navigation samples docs; do install -d %{buildroot}%{_datadir}/%{name}/$d cp -a $d/. %{buildroot}%{_datadir}/%{name}/$d/ done # docs/user_guide_figures/ is the SOURCE material for the two user-guide PDFs, # not something the application or the reader needs -- the PDFs already contain # these images. Shipping both nearly doubles the -doc subpackage for no benefit. # Measured in a staged buildroot: docs/ is 56 MB, of which user_guide_figures is # 25 MB, leaving 31 MB (almost entirely the two PDFs, 15.8 MB + 16.3 MB). rm -rf %{buildroot}%{_datadir}/%{name}/docs/user_guide_figures # hicolor icon. Upstream ships only icons/invesalius.ico -- a single 256x256 # 24bpp BMP-encoded frame -- and invesalius/gui/frame.py loads that .ico # directly at runtime from ICON_DIR, so it stays where it is and this is an # additional extracted copy for the desktop environment. icotool -x -o . icons/invesalius.ico install -D -m 0644 invesalius_1_256x256x24.png \ %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/%{name}.png cat > %{name}.desktop <: a real .inv3 is a gzip stream (samples/Cranium.inv3 is # "gzip compressed data"), so the obvious worry is that application/gzip magic # outranks the glob. Measured rather than assumed -- a temporary MIME database # built from this file resolved the real Cranium.inv3 to # application/x-invesalius-project, with a non-gzip file named .inv3 as a # positive control and a .txt as the negative. install -d %{buildroot}%{_datadir}/mime/packages cat > %{buildroot}%{_datadir}/mime/packages/%{name}.xml < InVesalius 3 project EOF install -d %{buildroot}%{_metainfodir} cat > %{buildroot}%{_metainfodir}/%{name}.metainfo.xml < io.github.invesalius.InVesalius CC0-1.0 GPL-2.0-only InVesalius Reconstruct 3D models from CT and MRI scans

InVesalius reconstructs three-dimensional models from CT and MRI DICOM series. It provides DICOM import, threshold and watershed segmentation with 2D editing tools, surface generation and export for 3D printing, volume rendering, and measurement tools.

It also provides a neuronavigation module with subject registration, targets and markers, and real-time E-field display.

%{name}.desktop https://invesalius.github.io/ https://github.com/invesalius/invesalius3/issues Centro de Pesquisas Renato Archer
EOF appstream-util validate-relax --nonet %{buildroot}%{_metainfodir}/%{name}.metainfo.xml %check desktop-file-validate %{buildroot}%{_datadir}/applications/%{name}.desktop # The MIME definition, checked the way neuro-mime-types checks its own. # 1. Well-formed XML -- necessary and nowhere near sufficient. xmllint --noout %{buildroot}%{_datadir}/mime/packages/%{name}.xml # 2. The real gate. update-mime-database exits 0 while printing errors, and a # rejected element does not stop the others loading, so the exit status is # not the signal -- stderr is. It also writes a benign "not in the search # path set by XDG_DATA_HOME" notice, which is why this greps for diagnostics # rather than for any output at all. mimedir=$(mktemp -d) install -Dpm0644 %{buildroot}%{_datadir}/mime/packages/%{name}.xml \ "$mimedir/packages/%{name}.xml" update-mime-database "$mimedir" 2>"$mimedir/err.log" >/dev/null || : if grep -qiE '^error|Error in type' "$mimedir/err.log"; then echo "update-mime-database rejected part of the definition:" >&2 cat "$mimedir/err.log" >&2 exit 1 fi # 3. A type that fails to compile leaves no trace in the install tree, so # assert it arrived instead of trusting step 2's silence. test -f "$mimedir/application/x-invesalius-project.xml" # 4. The desktop entry must declare the type it just defined -- a # [Default Applications] entry is silently ignored otherwise. grep -q '^MimeType=.*application/x-invesalius-project' \ %{buildroot}%{_datadir}/applications/%{name}.desktop rm -rf "$mimedir" # The GUI modules need wx and a display, so do not import them here. The Rust # extension is the part most likely to be mis-built, and it imports cleanly # without a display. # # Run from outside the unpacked source. "python -c" puts the current directory # first on sys.path, and %%check starts inside the source tree -- which contains # an invesalius_rs/ directory. Importing from there would test the SOURCE copy # and never touch the installed extension, which is the whole point of the # check. Assert the resolved path really is under the buildroot. cd %{_builddir} PYTHONPATH=%{buildroot}%{python3_sitearch} %{python3} -c " import invesalius_rs path = invesalius_rs.__file__ print('invesalius_rs:', path) assert path.startswith('%{buildroot}'), 'imported the source tree, not the built package: ' + path from invesalius_rs import _native print('_native:', _native.__file__) " # The HiDPI correction must still be applicable to the VTK in this buildroot. Its # STATUS is 'applied' against today's vtk, and 'already-fixed' once vtk carries the # fix; anything else ('skipped: ...') means VTK's source drifted from what the # shim was written against, and InVesalius would silently lose the correction. No # DISPLAY is needed: importing wx does not open one. env -u DISPLAY -u WAYLAND_DISPLAY PYTHONPATH=%{buildroot}%{python3_sitearch} %{python3} -c " import invesalius.vtk_wx_hidpi as m print('vtk_wx_hidpi STATUS:', m.STATUS) assert m.STATUS in ('applied', 'already-fixed'), m.STATUS " # ...and app.py must actually import it, after wx and before any VTK widget exists. grep -q '^import invesalius.vtk_wx_hidpi' %{buildroot}%{python3_sitearch}/invesalius/app.py %files -f %{pyproject_files} # %%pyproject_save_files globs the whole invesalius package, so the deep-learning # tree would be claimed here as well as by -ai. rpm fails on duplicate file # ownership, so it has to be excluded from the base package explicitly. %exclude %{python3_sitearch}/invesalius/segmentation/deep_learning %license LICENSE.txt LICENSE.pt.txt %doc README.md changelog.md AUTHORS.md %{_bindir}/invesalius %{_datadir}/applications/%{name}.desktop %{_datadir}/mime/packages/%{name}.xml %{_metainfodir}/%{name}.metainfo.xml %{_datadir}/icons/hicolor/256x256/apps/%{name}.png %dir %{_datadir}/%{name} %{_datadir}/%{name}/icons %{_datadir}/%{name}/presets %{_datadir}/%{name}/locale # navigation/objects/head.stl is CC-BY-4.0; its LICENSE.txt travels with it. %{_datadir}/%{name}/navigation %files doc %license LICENSE.txt %{_datadir}/%{name}/docs %files data %license LICENSE.txt %{_datadir}/%{name}/samples %files ai %license LICENSE.txt # Apache-2.0 inside a GPL-2.0-only tree: fastsurfer_subpart/ is FastSurfer code. # That conflict is upstream's and is reported, not worked around here -- but it # now lives in a subpackage a user can decline, which it did not before. %{python3_sitearch}/invesalius/segmentation/deep_learning %changelog * Mon Oct 05 2026 Morgan Hough - 3.1.99998^20260921gite32395ec-11 - Scaled (HiDPI) displays: every slice and volume pane rendered into 1/scale of itself. Measured on a real laptop at 200%: the axial image box was 376x260 inside a 754x521 pane, 0.499 in each direction -- exactly the scale factor -- with the rest of the pane never painted, and mouse picking offset by the same factor. With GDK_SCALE=1 the same project fills its panes, which isolates it from InVesalius's own fitting code (Reposition fits the image perfectly to the size VTK reports; VTK's size is what is wrong). - The defect is VTK's, not InVesalius's: vtkmodules.wx.wxVTKRenderWindowInteractor hands wxWidgets' LOGICAL size and mouse position to VTK, which wants PHYSICAL pixels. Patch6 adds invesalius/vtk_wx_hidpi.py, imported by app.py right after wx, which corrects that VTK module in memory. No installed VTK file changes, and no other package is touched: of the 19 RPMs in this COPR that require python3-vtk, none but this one imports the wx bridge (scanned ~1,300 Python files; the same regex finds 7 bridge files in InVesalius, so the zeros mean something). - Inert when it should be: VTK already scaling -> 'already-fixed'; VTK's source not the shape it was written against -> 'skipped'; any exception -> 'skipped'. It never raises, so the worst case is the old behaviour. %%check asserts STATUS is 'applied' or 'already-fixed', so a VTK change that would silently disable the correction fails the build instead. - Tested against the unmodified installed VTK 9.7.0 under Xvfb at GDK_SCALE=1 and 2: VTK window 800x600 where 400x300 was delivered, a click at logical (100,50) delivered as (200,499) where it was (100,249); identical at scale 1. Confirmed on the real laptop with the same logic loaded by a launcher script. - Patch5 carries upstream's own fix for the NIfTI import defect we documented: invesalius3 ef797bb19a (#1482, 2026-10-02) makes both call sites take LoadProject()'s default, so a NIfTI import creates a mask and `--export` has the mask 0 it hardcodes. The upstream report is withdrawn as superseded. Drop Patch5 when the pin moves past that commit. - A second upstream commit ahead of the pin, bdd93e49ff, only changes dependency pins in pyproject.toml/environment.yml/uv.lock; this spec declares its runtime requirements by hand, so it is irrelevant here and not carried. - BuildRequires python3-wxpython4 and python3-vtk are new, for the %%check only. * Tue Sep 29 2026 Morgan Hough - 3.1.99998^20260921gite32395ec-10 - Desktop integration: Exec= gains a field code and the entry declares a MIME type, so "Open With" can hand InVesalius a file and a [Default Applications] entry is no longer silently ignored. Ship application/x-invesalius-project for *.inv3, which nothing in the repo or in shared-mime-info defined. - Narrower than the desktop-integration handoff asked for, and deliberately: the handoff listed invesalius as needing %%F. The entry point takes ONE positional (app.py:400, nargs="?") and dispatches it exactly one way -- app.py:501-511, "try opening as it was a inv3", sending "Open project". DICOM and NIfTI arrive only via the -i / -o flags, never as a bare path. So %%f is correct and %%F is not, and declaring application/dicom or application/x-nifti here would advertise InVesalius for files it would then feed to the project loader, which fails. Those belong on viewers that take them positionally, which is where they already are. - glob only, no , and measured rather than assumed: a real .inv3 is a gzip stream, so gzip magic could plausibly outrank the glob. A temporary MIME database built from this definition resolved samples/Cranium.inv3 to application/x-invesalius-project, with a non-gzip file named .inv3 as a positive control and a .txt as the negative. - %%check gains four assertions for this: well-formed XML, a real update-mime-database run whose stderr is grepped (it exits 0 while printing errors, so the exit status is not the signal), proof the compiled type reaches the install tree, and that the desktop entry declares the type it defines. * Mon Sep 28 2026 Morgan Hough - 3.1.99998^20260921gite32395ec-9 - Split the deep-learning stack into an -ai subpackage, so python3-torch is no longer a hard dependency of the base package. Measured, not estimated: torch is 309 MiB installed (from a depsolve) while the deep-learning code itself is 500 KB across 31 files. The comment this replaces claimed ~675 MB. - Dropping the manual Requires: lines was NOT enough, and three rebuilds shipped a base package that still required torch before this was caught. Upstream's own dist-info/METADATA lists torch/tinygrad/onnx/onnxruntime as Requires-Dist, and rpm's dependency generator turns those into python3.14dist(...) on the base package regardless of the spec. %%pyproject_buildrequires -R governs build requires only. %%__requires_exclude is the mechanism, and the escaping is the trap: backslashes must be DOUBLED in the spec, because rpm expands the macro body before the filter sees it -- the single-backslash form arrives as ^python3(.[0-9]+)?dist((torch|...))$, with the parens degraded to groups, and silently matches nothing. - Verified on the built RPMs, not the spec: base python3dist requires 24 -> 20, with exactly those four removed and nothing else; -ai carries all four; base ships 0 deep-learning files and -ai owns 35. - Two module-level imports made torch mandatory, by independent paths: control.py:48 -> segment -> torch, and frame.py:33 -> default_tasks:30 -> task_navigator:62 -> deep_learning_seg_dialog:15 -> segment -> torch. Only fastsurfer_subpart/data_process.py forces it; segment.py's own torch imports are already function-local. - The second path is an upstream bug: deep_learning_seg_dialog guards "import torch"/"import tinygrad" at lines 17-28 and exposes HAS_TORCH/ HAS_TINYGRAD, but the unguarded segment import at line 15 means the module cannot be imported without torch at all -- defeating its own design. Patch4 wraps both imports in try/except ImportError, matching that file's idiom. - Safe because every consumer is already gated: control.py uses only segment.run_cranioplasty_implant (a command-line Publisher.subscribe), and the dialog's five segment.* uses sit in methods whose call sites test HAS_TORCH or HAS_TINYGRAD (frame.py:1180-1257, task_navigator.py:946). - Verified in a mock chroot with no torch installed. Before: control, frame and deep_learning_seg_dialog all raise ModuleNotFoundError: torch. After: all five startup-path modules import, and "torch" in sys.modules is False once invesalius.gui.frame has been imported. - The Apache-2.0-inside-GPL-2.0-only fastsurfer_subpart conflict is unchanged and still upstream's, but it now ships in a subpackage a user can decline. * Sun Sep 27 2026 Morgan Hough - 3.1.99998^20260921gite32395ec-8 - Stop the application phoning home on every launch. utils.py UpdateCheck(), started from a thread at app.py:241, POSTed invesalius_version, platform, architecture, language and a persistent random_id to cti.gov.br with requests.post(..., verify=False) -- telemetry with TLS verification disabled. The distribution updates the package, so the check has no purpose here. With this applied a freshly launched InVesalius opens no network connection and binds no port; the DICOM SCP is inside __RunCMove and needs an explicit retrieval. - data/viewer_navigation.py: NavigationRenderer dispatched by string literal, getattr(self.renderer, "AddActor2D")(prop), so the VTK 9.7 removals fixed in -7 were invisible to a grep for call sites. No in-tree caller exists, so this was latent rather than a crash, but the wrapper advertises the removed names as working API. Found by neurofedora-t2. - Verified on real hardware by Morgan: -7 starts, loads ds000114, and renders axial/sagittal/coronal slices. Three startup crashes in a row (vtkIdFilter, imghdr, AddActor2D) were all found by launching the installed package, which is the rung our build/depsolve/install/ldd checks do not reach. * Sun Sep 27 2026 Morgan Hough - 3.1.99998^20260921gite32395ec-7 - Fix the third startup crash: VTK 9.7 removed vtkViewport's 2D-prop aliases, so 'vtkOpenGLRenderer' object has no attribute 'AddActor2D' at gui/widgets/canvas_renderer.py:171 during Frame construction. AddActor2D -> AddViewProp, RemoveActor2D -> RemoveViewProp, RemoveAllProps -> RemoveAllViewProps; 6 call sites across 4 files. The interpreter suggests AddActor, which is wrong -- that takes a vtkProp3D and these are 2D props. - Found by sweeping every CamelCase method call in the tree against installed VTK 9.7, typing each receiver from its own per-file assignment: 1706 call sites against 330 typed receivers, with a positive control (must flag AddActor2D, must not flag AddViewProp). Remaining flags are pre-9.7 removals in already-dead code, or false positives from commented-out lines and wx receivers sharing a name with a VTK one; left for upstream, not patched. * Sun Sep 27 2026 Morgan Hough - 3.1.99998^20260921gite32395ec-6 - Fix startup crash on Python 3.13+: imghdr was removed from the standard library (PEP 594) and f44 ships 3.14, so reader/bitmap_reader.py's module-level import aborted before any window appeared. Replaced with Pillow, already a hard dependency; Image.format.lower() reproduces imghdr.what()'s vocabulary exactly. A sweep of all 25 modules dropped in Python 3.12/3.13 across 148 .py files found imghdr to be the only one present. - This and the VTK fix below were both found by running the installed package rather than by any build or gate. The smoke override now enters the import graph at net.neuronavigation_api, the way /usr/bin/invesalius does, which reaches both failures; an earlier version imported gui.dialogs directly and FAILED a working package, because dialogs.py:95 pulls in polydata_utils while class ErrorMessageBox is still 795 lines away from being defined. - Records releases 2 through 5, which were built but never given changelog entries: pyacvd and the remaining runtime dependencies declared explicitly (-2, -3); pyo3 0.27 -> 0.29 and rust-numpy 0.27 -> 0.29 with a regenerated 41-crate vendor bundle, clearing the Python 3.15 ceiling (-4); VTK 9.7 removed vtkIdFilter with no compatibility shim, so nothing started at all -- renamed to vtkGenerateIds and pinned the output array name, since VTK also changed the default from vtkIdFilter_Ids to vtkPointIds and this file reads it back by name at two sites (-5). * Sat Sep 26 2026 Morgan Hough - 3.1.99998^20260921gite32395ec-1 - Initial package. Net-new to Fedora; the standalone-application row of docs/open-neuronavigation.md. - Snapshot of master: the newest tag is v3.1.99998 from April 2022 while upstream moves continuously, so this uses the ndicapi ^date-git-sha scheme. - Source is repacked by invesalius-clean.sh, which removes a 24 MB prebuilt vc_redist .exe, an unlicensed Keras weights file that the code never loads (weights are downloaded at runtime), and an unlicensed empty plugin submodule. - requires-python was "==3.12.*"; Fedora has zero packages built for python(abi) = 3.12 against 6279 for 3.14, so the cap is widened rather than worked around. Every dependency pin sat below Fedora's version and is relaxed. - The declared entry point (inv = invesalius.app:main) could not work: there is no invesalius/app.py, and /usr/bin/inv belongs to python3-invoke. app.py is moved into the package and the script renamed to invesalius. - INV_TOP_DIR is retargeted at %%{_datadir}/invesalius so the data directories resolve after installation instead of next to the Python package. - torch is a hard dependency for now: control.py imports the deep-learning tree at module level. An -ai split needs an upstream-worthy try/except guard first.