FYI: This is the draft PR for Slicer development on Nix that I mentioned being a… WIP around two team meetings ago.
## Background & motivation
This PR adds a [Nix flake](https://zero-to-nix.com/concepts/flakes) (`flake.nix`) that provides a one-command, fully reproducible development environment for building Slicer from source on [NixOS](https://nixos.org), plus the upstream-friendly fixes required to make that build succeed against system-provided dependencies.
The flake itself is opt-in: it lives at the repository root and has zero effect on existing Linux/macOS/Windows build paths. The accompanying CMake fixes are not Nix-specific — they make Slicer's superbuild robust on any system whose dependencies don't follow the [Filesystem Hierarchy Standard](https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard) (FHS), or that mixes system and bundled libraries.
<details>
<summary><b>Why a Nix flake?</b> (click to expand)</summary>
[Nix](https://nixos.org/) is a purely functional package manager with a [25-year history](https://edolstra.github.io/pubs/phd-thesis.pdf) and an [80,000-package](https://search.nixos.org/packages) standard library ([Nixpkgs](https://github.com/NixOS/nixpkgs)). A *flake* is just a `flake.nix` + `flake.lock` pair at a project root that declares the complete set of inputs (compilers, libraries, tools) and outputs (development shells, packages, container images) for that project.
For an existing C++/CMake project like Slicer, three properties of flakes are interesting:
1. **Pinned, bit-for-bit reproducible inputs.** The companion `flake.lock` records the exact Git commit of every dependency tree (Nixpkgs, third-party flakes) plus content hashes for any prefetched sources. Two developers running `nix develop` six months apart get byte-identical Qt 6.10.2, GCC 15.2.0, CMake 3.31, system Python 3.12 + scientific stack, etc. ([reproducibility](https://zero-to-nix.com/concepts/reproducibility), [pinning](https://zero-to-nix.com/concepts/pinning))
2. **Hermetic dev shells.** `nix develop` drops you into a shell where `PATH`, `CMAKE_PREFIX_PATH`, `PKG_CONFIG_PATH`, and friends point only at the dependencies declared in `flake.nix`. Tools accidentally installed in `/usr/local` can't leak in, eliminating "works on my machine" drift. ([hermeticity](https://zero-to-nix.com/concepts/hermeticity), [dev environments](https://zero-to-nix.com/concepts/dev-env))
3. **Declarative, one-file dependency manifest.** Compare the [current Linux build instructions](https://slicer.readthedocs.io/en/latest/developer_guide/build_instructions/linux.html) — separate `apt-get install` lines for Ubuntu, Fedora `dnf` lines, manual notes about Qt path layout, version compatibility tables — to the equivalent Nix expression: a single attribute set listing `qt6.qtbase`, `cmake`, `gcc15`, `python312`, etc. The flake serves as both documentation and an executable build environment. ([declarative programming](https://zero-to-nix.com/concepts/declarative))
This is the same value proposition that has led projects like [llvm/llvm-project](https://github.com/llvm/llvm-project/blob/72d4ce9889a0bae9645de1a07cb051d0205cb964/lld/utils/speed-test-reproducers/collect.nix), [openzfs/zfs](https://github.com/NixOS/nixpkgs/blob/nixos-25.11/pkgs/os-specific/linux/zfs/generic.nix) to ship Nix flakes alongside their conventional build systems.
</details>
<details>
<summary><b>Why NixOS specifically? — and why the CMake fixes are not NixOS-specific</b> (click to expand)</summary>
[NixOS](https://nixos.org/manual/nixos/stable/) is a Linux distribution that takes Nix's reproducibility model and applies it to the whole operating system. Critically for this PR, NixOS deliberately rejects the [Filesystem Hierarchy Standard](https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard): there is no `/usr/include`, no `/usr/lib`, no single Qt prefix. Every package lives at its own content-addressed path under `/nix/store/`, e.g.:
```
/nix/store/w4q31b93w262q2b75ri3jc7m3xd4i31h-qtbase-6.10.2/
/nix/store/abc...-qttools-6.10.2/
/nix/store/def...-qtdeclarative-6.10.2/
```
The Qt 6 installation that on Ubuntu lives under `/usr/lib/qt6/{bin,include,lib}` is, on NixOS, **split across separate prefixes per Qt module**. This breaks several assumptions in Slicer's CMake:
- `find_program(lconvert PATHS ${qt_root_dir}/bin)` only knows about qtbase, but `lconvert` lives in qttools.
- `QLibraryInfo::HeadersPath()` returns qtbase's include dir, so PythonQt's generator can't find `QtUiTools` headers in qttools.
- Slicer's `External_QtTesting.cmake` and friends assume `${Qt6_DIR}/../../..` is the install root.
These are not NixOS curiosities — the same split-prefix layout shows up in:
- **[Conan](https://conan.io/)** package manager (each library in its own cache directory)
- **[vcpkg](https://github.com/microsoft/vcpkg)** with manifest mode (per-feature subpaths)
- **[Spack](https://spack.io/)** in HPC environments (per-package install prefixes by design)
- **[Homebrew](https://brew.sh/)** with `--prefix` overrides
- **Container images** assembled from multiple base layers (Qt from one layer, ITK from another)
- **Distro packages** with non-standard layouts (Debian's multiarch, Fedora modules)
Each CMake fix in this PR (`find_program` fallback for Qt tools, `find_package(Qt6 ... CONFIG)` auto-detection, system header path discovery for PythonQt, etc.) makes Slicer build on *any* of these systems, not just NixOS. Validation on NixOS happens to be the most rigorous test for FHS-independence because NixOS will catch any hard-coded path assumption immediately.
</details>
<details>
<summary><b>Why "system packages" (<code>Slicer_USE_SYSTEM_*</code>) is the force multiplier</b> (click to expand)</summary>
Slicer's superbuild builds **VTK, ITK, DCMTK, Python, libffi, TBB, JsonCpp, RapidJSON, HDF5, LibArchive, zlib, OpenSSL, curl, SQLite,** and dozens of other third-party libraries from source. On a current laptop a clean build from cold cache takes hours of CPU time even with `ccache`. This is fine for release engineering but painful for the inner loop of "edit a CMake file, see if it broke anything."
Nixpkgs already ships pre-built binaries for almost every one of these libraries, served from [cache.nixos.org](https://cache.nixos.org/) via Nix's [content-addressed binary cache](https://zero-to-nix.com/concepts/caching). When the flake declares (e.g.) `pkgs.vtkWithQt6` as a buildInput, `nix develop` substitutes a cached binary download for what would otherwise be a 45-minute VTK rebuild — and the `flake.lock` still pins the *exact* VTK version, so reproducibility is preserved.
The [`Slicer_USE_SYSTEM_*` cmake flags](https://github.com/Slicer/Slicer/blob/main/SuperBuild.cmake) already exist for many libraries but are sparsely implemented — several were `FATAL_ERROR` stubs (`message(FATAL_ERROR "Slicer_USE_SYSTEM_TBB is not yet supported")`) and several superbuild children (CTK in particular) didn't propagate the flags to their own inner builds. This PR fills in those gaps for **VTK, ITK, DCMTK, TBB, JsonCpp, RapidJSON, libffi, and Python**, then exposes them in the flake via a `useSystem` toggle map:
```nix
useSystem = {
VTK = true; # use nixpkgs vtkWithQt6
ITK = false; # build from source — needs Slicer/ITK fork patches
DCMTK = false; # build from source — must match ITK's bundled copy
tbb = true;
JsonCpp = true;
# ...
};
```
This per-library granularity matters because Slicer's superbuild pins forks of VTK and ITK with downstream patches that aren't yet in any released upstream version (e.g. ITK's `AxesReorder` API used in `vtkITKImageSequenceReader`, merged upstream as [InsightSoftwareConsortium/ITK#5476](https://github.com/InsightSoftwareConsortium/ITK/pull/5476) but not in any 5.4.x release). For those libraries we keep the superbuild path; for everything else we get the cache.
</details>
<details>
<summary><b>Scope & non-goals</b> (click to expand)</summary>
This PR **does**:
- Add a `flake.nix` + `flake.lock` opt-in development environment
- Fix CMake assumptions about FHS layout in Slicer's superbuild and CTK
- Add real `find_package` paths where `Slicer_USE_SYSTEM_*` flags were `FATAL_ERROR` stubs
- Apply two NixOS+NVIDIA runtime workarounds via environment variables in the flake's `shellHook` (no source patches)
- Fix two GCC 15 / C23 compatibility issues that affect any modern toolchain, not just Nix
This PR **does not**:
- Change the default build path on Ubuntu, macOS, or Windows (verified by reading every changed `External_*.cmake` to confirm new code paths are guarded by `if(Slicer_USE_SYSTEM_*)`)
- Require any reviewer to install Nix to evaluate the changes — the CMake fixes stand on their own and can be reviewed as ordinary CMake patches
- Add a new CI target (a follow-up PR could add a `nix flake check` job to `.github/workflows/`)
- Ship pre-built binaries or vendor any third-party source
- Modify Slicer's existing Linux build documentation (a follow-up could add a Nix section)
</details>
## Summary
Adds a Nix flake providing a reproducible development environment for building Slicer from source on NixOS, plus the upstream-friendly fixes required to make that build succeed using system-provided packages instead of the superbuild's bundled copies.
The flake exposes a `useSystem` toggle map that drives `Slicer_USE_SYSTEM_*` cmake flags, allowing developers to opt into system VTK, DCMTK, TBB, JsonCpp, RapidJSON, libffi, and Python — substituting cached binary downloads for hours of compilation while still preserving full reproducibility via the `flake.lock`.
## Changes
### Build system fixes (upstream-friendly)
These fixes make Slicer's superbuild work on any system whose dependencies don't follow FHS layout, or that mixes system and bundled libraries. **None are Nix-specific** — they benefit Conan, vcpkg, Spack, Homebrew, multi-layer container builds, and distro packages with non-standard layouts equally.
- **`ENH: Auto-detect Qt6 on CMAKE_PREFIX_PATH when Qt6_DIR is not set`** — Probe for Qt6 via `find_package(Qt6 ... QUIET CONFIG)` before falling back to Qt5, enabling package managers (Nix, Conan, vcpkg) to work without explicit `-DQt6_DIR` or `-DSlicer_REQUIRED_QT_VERSION`.
- **`BUG: Fall back to find_program for Qt tools on split-prefix systems`** — On systems where Qt modules are installed in separate prefixes, `lconvert`/`lrelease`/`lupdate` live under `qttools` rather than `qtbase`. Add a `find_program` fallback when the tool is not found at the expected qtbase-relative path.
- **`ENH: Add support for building against system TBB / RapidJSON / JsonCpp`** — Replace `FATAL_ERROR` stubs in `External_tbb.cmake`, `External_RapidJSON.cmake`, and `External_JsonCpp.cmake` with real `find_package` paths and target-property extraction.
- **`ENH: Propagate system VTK/ITK/DCMTK flags to CTK superbuild`** — CTK is itself a superbuild with an outer and inner build; matching `CTK_USE_SYSTEM_*` flags must be forwarded, plus a `VTK_VERSION` propagation fix in CTK's `VTK.cmake`.
- **`BUG: Guard vtkWin32OutputWindow.h include behind _WIN32 in test drivers`** — The test driver template unconditionally included a Windows-only header (`vtkWin32OutputWindow.h`), breaking the build whenever VTK was compiled without Windows support. Use `vtkOutputWindow.h` as the primary include and conditionally include the Win32 variant.
- **`COMP: Fix ITK C flags for GCC 15 compatibility`** — GCC 15 defaults to C23, where empty `()` declarations mean *zero* parameters, not unspecified. ITK's bundled MINC library uses old-style `()` declarations with arguments, causing build failures. Force `-std=gnu17` for ITK's C code as a workaround. (Affects any GCC 15 user, not Nix-specific.)
- **`COMP: Disable PythonQt uitools wrapping for Qt 6`** + **`WIP: Disable multimedia support to work around PythonQt Qt 6 generator bug`** — The PythonQt code generator silently fails to produce uitools/multimedia wrappers under Qt6 due to unported Qt5-era class references in `typesystem_*.xml`. No Slicer code uses these bindings.
- **`ENH: Re-enable PythonQt uitools wrapping and fix NixOS Qt header paths`** — Patches CTK's `PythonQt.cmake` to pass `--include-paths` to the generator with extra Qt module include dirs discovered via `find_path(HINTS ENV CMAKE_PREFIX_PATH)`. Restores `qt.QUiLoader` availability on split-prefix systems.
- **`ENH: Build CTK AppLauncher from source with Qt6 for native Wayland support`** — The pre-built CTKAppLauncher binary is statically linked against Qt5 and cannot load Qt6 platform plugins, producing `Could not find the Qt platform plugin 'wayland'` warnings on Wayland sessions. When building with Qt6, build the launcher from source so it dynamically links against the system Qt6.
- **`BUG: Fix CTKAPPLAUNCHER clone failure after Qt6 branch merged into main`** — Pin to a commit hash instead of the deleted `add-qt6-support` branch.
- **`ENH: Remove unused couchdb dependency from extension manager requirements`** + **`ENH: Remove deprecated nose dependency from python-numpy system check`** — Drop dead/archived Python dependencies (couchdb v1.2 archived 2017, nose deprecated 2015) that block system Python use.
### Symbol-conflict fixes
When Slicer mixes system and bundled copies of the same library, ELF symbol interposition can cause runtime crashes during static initialization or destruction. These fixes pick a single source of truth per library.
- **`BUG: Fix HDF5 dual-library symbol conflict by using system HDF5 for ITK`** — When system VTK loads `libhdf5_cpp.so` alongside ITK's bundled HDF5, C++ symbol interposition causes `H5::PredType` static-initialization warnings on stderr at startup. Setting `ITK_USE_SYSTEM_HDF5=ON` ensures a single HDF5 copy.
- **`BUG: Disable system DCMTK to fix crash on exit`** — System DCMTK and ITK's bundled DCMTK create dual `DcmCodecList` global registries; during shutdown the dynamic linker destroys them in undefined order, causing a `free(): invalid size` abort in `libCTKDICOMCore` triggered from `libITKIODCMTK`'s static destructors. Building DCMTK from source via the superbuild ensures a single copy.
- **`ENH: Disable system ITK in Nix flake`** — nixpkgs ITK 5.4.5 lacks Slicer-specific patches required by `vtkITKImageSequenceReader` (the `AxesReorder` enum on `NrrdImageIO`, [merged upstream as InsightSoftwareConsortium/ITK#5476](https://github.com/InsightSoftwareConsortium/ITK/pull/5476) but not in any 5.4.x release) and Slicer-specific remote modules (MGHIO, IOScanco). ITK must be built from source via the superbuild until ITK 5.5 ships and the remaining custom-namespace patch is upstreamed.
### NixOS runtime workarounds
These two fixes address NVIDIA proprietary driver bugs that surface when running Slicer on NixOS. They are wired through environment variables in the flake's `shellHook` and **make no changes to Slicer source code**, so they have no effect on any other platform.
- **`BUG: Fix QtWebEngine crash caused by NVIDIA kernel driver bug`** — Chromium (inside QtWebEngine) scans `/proc/self/mem` at startup, reading 64 bytes from every VMA. On systems with NVIDIA GPU mappings (driver 595.x Open Kernel Module, kernel 6.18+), this triggers a NULL pointer dereference in the kernel's `nvidia_vma_access` callback, which oopses and SIGKILL's the process. Fixed with a two-layer `LD_PRELOAD` interposer (`blockProcMem`) that returns `EACCES` from `open`/`openat` and `EIO` from `pread`/`pread64` for `/proc/self/mem` (Chromium handles both errors gracefully), combined with `QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu --no-zygote"`. VTK's 3D OpenGL rendering is unaffected.
- **`BUG: Fix Volume Rendering crash caused by GLX context conflict on NVIDIA`** — VTK's `vtkXOpenGLRenderWindow` calls `glXMakeCurrent` during initialization (in `vtkRenderViewBase` → `ctkVTKChartView` setup for the Volume Rendering tab), which fails with X11 `BadAccess` on NVIDIA when another GL context is already current on the thread. Setting `VTK_DEFAULT_OPENGL_WINDOW=vtkEGLRenderWindow` causes VTK to instantiate `vtkEGLRenderWindow` instead, avoiding GLX entirely.
### Flake (`flake.nix`)
The flake provides:
- A `devShell` with all build-time dependencies (CMake, GCC 15, Qt 6.10, libXt, OpenSSL, etc.)
- All runtime dependencies (`xcb-util`, `pcre2`, `qt6.qtwayland`, `pipewire`, `libgbm`, Vulkan ICDs)
- A `useSystem` toggle map driving `Slicer_USE_SYSTEM_*` flags
- `ccache` wired via `CMAKE_C_COMPILER_LAUNCHER` and `CMAKE_CXX_COMPILER_LAUNCHER`
- A `slicer-cmake` wrapper script for shell-agnostic flag passing
- `gdb` for debugging
- Python 3.12 via `python312.withPackages` with all packages required by the superbuild's `python-*` system-package checks pre-bundled
- The two NixOS runtime workarounds (`blockProcMem` `LD_PRELOAD`, `VTK_DEFAULT_OPENGL_WINDOW`, `QTWEBENGINE_CHROMIUM_FLAGS`) wired into the `shellHook`
## Test plan
### Existing-platform regression (proves we didn't break anything)
- [ ] Ubuntu 24.04 / Qt 5 build with default cmake flags completes and tests pass
- [ ] Ubuntu 24.04 / Qt 6 build with default cmake flags completes and tests pass
- [ ] macOS Qt 6 build with default cmake flags completes and tests pass
- [ ] Windows Qt 6 build with default cmake flags completes (vtkWin32OutputWindow include guard verified harmless)
- [ ] PythonQt uitools wrapping still produces correct bindings on Ubuntu (unchanged code path when `--include-paths` is not needed)
- [ ] CTKAppLauncher pre-built binary path still used for Qt5 builds
### NixOS validation (proves the new path works)
- [x] `nix develop` enters a working build environment on NixOS unstable
- [x] `slicer-cmake` configures the superbuild without errors using the `useSystem` defaults
- [x] `make -j$(nproc)` completes the full superbuild
- [x] Slicer launches and the main 3D viewer renders
- [x] Loading a sample volume (`MRHead`) and switching to the Volume Rendering tab does not crash
- [x] Opening the Extension Manager (QtWebEngine) does not freeze the process
- [x] Wayland session: no `Could not find the Qt platform plugin 'wayland'` warnings on stderr
- [ ] `qt.QUiLoader` is importable from Slicer's Python console