From d19409f4a191fb7050d824294f81ecba4021b1cd Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Mon, 24 Aug 2026 19:39:58 +0200 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_016NNnL26LAvruQ9eLUUWvrJ --- docs/CHANGELOG.md | 1 + image_analysis/CMakeLists.txt | 25 +++++++++++++++++++++++-- image_analysis/indexing/CMakeLists.txt | 2 +- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 026525a3..306b4aab 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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. diff --git a/image_analysis/CMakeLists.txt b/image_analysis/CMakeLists.txt index 342a7e76..ca925e09 100644 --- a/image_analysis/CMakeLists.txt +++ b/image_analysis/CMakeLists.txt @@ -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) diff --git a/image_analysis/indexing/CMakeLists.txt b/image_analysis/indexing/CMakeLists.txt index c639d36a..9b26e87e 100644 --- a/image_analysis/indexing/CMakeLists.txt +++ b/image_analysis/indexing/CMakeLists.txt @@ -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/