Resolve Eigen once, before Ceres, and refuse a build that mixes two

Ceres asks for Eigen with a version range - find_package(Eigen3 3.3.4...5
NO_MODULE) - and Eigen's own Eigen3ConfigVersion.cmake rejects any range whose
endpoints differ in major version. A 3.4 Eigen therefore declares itself
INCOMPATIBLE with Ceres' query, the search falls through, and Ceres binds whatever
older Eigen comes next on the prefix path. Where a distro eigen3-devel 3.3.4 is
installed alongside a 3.4 one, Ceres created Eigen3::Eigen first, in its own
directory scope, pointing at the older headers, and exported it publicly; the
project's own find_package then ran afterwards and made a second target pointing at
the newer ones. Targets linking both - the geometry refinement and the scale/merge
libraries - took the older Eigen first.

The result was a binary holding Eigen 3.3.4 and 3.4.90 template instantiations at
once. Identically mangled, they are merged at link time with disagreeing evaluator
layouts, so the program is undefined: at -O2 it segfaulted inside an Eigen product
under the lattice reduction, nine runs out of nine, and at -O3 it happened not to,
which is luck rather than correctness.

Resolve Eigen before Ceres is added. The first find_package to run creates the
imported target and later ones leave it alone, so Ceres inherits ours. Then assert
it: if Ceres ever creates an Eigen3::Eigen of its own, the configure fails with an
explanation rather than producing a binary that is quietly ill-formed. The guard
fires only on that condition, not merely because two Eigens are installed.

After the change no translation unit sees the older headers - 0 of 227 flags files,
against 30 before - and Ceres reports the Eigen it actually compiled against. The
same nine runs that all crashed now all complete. Release output is unaffected:
eight datasets give byte-identical reflection files and identical merging
statistics either way, so nothing previously measured is invalidated.

Eigen and ZLIB stay external find_package dependencies on every platform, and
OVERRIDE_FIND_PACKAGE is not reintroduced. Where only one Eigen is installed - the
Windows and macOS case - Ceres' non-range fallback honours the same Eigen3_DIR and
the change is a no-op.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016NNnL26LAvruQ9eLUUWvrJ
This commit is contained in:
2026-08-24 19:39:58 +02:00
co-authored by Claude Opus 5
parent 2f54a1189d
commit d19409f4a1
3 changed files with 25 additions and 3 deletions
+1
View File
@@ -18,6 +18,7 @@ This is an UNSTABLE release. It includes many experimental features, as well as
* HDF5 and image stream: `mirror_y` records whether the assembled image is mirrored in Y relative to the detector's raw readout.
* rugnux: an image integrated in pyFAI through the `.poni` file written by `--mode calibration` now comes out with the correct azimuth. Radial integration is unchanged.
* rugnux: the `.poni` file declares pyFAI's `orientation`, which needs pyFAI 2024.01 or newer.
* The build resolves a single Eigen for the whole project, and refuses to configure if Ceres picks up a different one; a build that mixed two Eigen versions was undefined behaviour and crashed at -O2.
* rugnux: scaling and merging are faster, with identical output.
* The per-image resolution estimate now predicts the resolution the merged data reach, rather than reporting the highest-resolution spot found; rugnux reports the run's value as `SPOT_RESOLUTION_ESTIMATE` in its report.
* rugnux: fixing the space group with `-S` no longer prevents the lattice from being found; the group is applied to scaling and merging rather than to the indexing search.
+23 -2
View File
@@ -1,3 +1,9 @@
# Eigen has to be resolved BEFORE Ceres is added below. Whichever find_package(Eigen3) runs first
# creates the Eigen3::Eigen imported target, and every later one leaves an existing target alone --
# so the first one wins for the whole build, Ceres included. See the guard after Ceres for why that
# matters here.
FIND_PACKAGE(Eigen3 3.4 REQUIRED NO_MODULE) # provides Eigen3::Eigen
SET(PROVIDE_UNINSTALL_TARGET OFF)
# Force Ceres to build WITHOUT its own CUDA support: Jungfraujoch never uses Ceres' GPU solvers, so
# this just drops unused code and build time (Ceres would otherwise enable CUDA whenever a toolkit is
@@ -17,6 +23,23 @@ FetchContent_Declare(
FetchContent_MakeAvailable(ceres)
# Ceres asks for Eigen with a version RANGE -- find_package(Eigen3 3.3.4...5 NO_MODULE) -- and
# Eigen's own Eigen3ConfigVersion.cmake rejects any range whose two endpoints differ in major
# version. A 3.4 Eigen therefore reports itself INCOMPATIBLE with Ceres' query and the search falls
# through to whatever older Eigen comes next on the prefix path (a distro eigen3-devel, say). Having
# resolved Eigen above, Ceres inherits our Eigen3::Eigen instead, because a generated targets file
# returns early when its targets already exist. If that ever stops holding, Ceres compiles against
# one Eigen and the rest of the project against another; the two versions' identically mangled
# template instantiations are then merged at link time -- an ODR violation whose symptom is a crash
# inside Eigen. Refuse to build it.
GET_PROPERTY(CERES_IMPORTED_TARGETS DIRECTORY ${ceres_SOURCE_DIR} PROPERTY IMPORTED_TARGETS)
IF ("Eigen3::Eigen" IN_LIST CERES_IMPORTED_TARGETS)
MESSAGE(FATAL_ERROR "Ceres created an Eigen3::Eigen of its own instead of using the Eigen this "
"project resolved. Two Eigen versions in one binary is undefined behaviour. Leave a "
"single Eigen on the find_package path, or point CMAKE_PREFIX_PATH / Eigen3_DIR at the "
"one to use.")
ENDIF()
ADD_LIBRARY(JFJochImageAnalysis STATIC
MXAnalysisWithoutFPGA.cpp
MXAnalysisWithoutFPGA.h
@@ -42,8 +65,6 @@ ADD_LIBRARY(JFJochImageAnalysis STATIC
rotation_indexer/RotationIndexerCounter.cpp
rotation_indexer/RotationIndexerCounter.h)
FIND_PACKAGE(Eigen3 3.4 REQUIRED NO_MODULE) # provides Eigen3::Eigen
ADD_SUBDIRECTORY(spot_finding)
ADD_SUBDIRECTORY(bragg_integration)
ADD_SUBDIRECTORY(bragg_prediction)
+1 -1
View File
@@ -21,7 +21,7 @@ TARGET_LINK_LIBRARIES(JFJochIndexing JFJochCommon JFJochLatticeSearch)
IF (JFJOCH_CUDA_AVAILABLE)
# GIT_SUBMODULES "" -> do not fetch ffbidx's bundled eigen submodule; it would add_subdirectory
# a second Eigen3::Eigen target. ffbidx instead resolves Eigen via the project-level Eigen3
# (OVERRIDE_FIND_PACKAGE in the top-level CMakeLists).
# (the find_package(Eigen3) at the top of image_analysis/CMakeLists.txt).
FetchContent_Declare(
fast-indexer
GIT_REPOSITORY https://github.com/paulscherrerinstitute/fast-feedback-indexer/