Files
Jungfraujoch/CMakeLists.txt
T
leonarski_fandFilip Leonarski 19b135186d
Build Packages / Create release (push) Successful in 7s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 4m56s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m10s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 13m51s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 16m56s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 20m59s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m25s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 21m17s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 14m14s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m59s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 25m48s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 15m52s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 19m33s
Build Packages / Generate python client (push) Successful in 29s
Build Packages / Build documentation (push) Successful in 1m19s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 20m49s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 25m31s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 25m24s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 26m5s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 22m19s
Build Packages / Unit tests (push) Failing after 2h28m24s
Build Packages / Validation Battery (push) Successful in 40m33s
v1.0.0-rc.175 (#85)
* Rugnux: Processes a collection live while jfjoch_broker writes it (`rugnux <master.h5> <broker URL>`).
* Rugnux: Processing starts on the first images and confirms its choices once all are in, so analysis can begin during collection.
* Rugnux: Faster processing, especially on servers with several GPUs and CPU sockets and on computers without a GPU (`--no-gpu`, macOS).
* Rugnux: Uses much less GPU memory, so large data sets stay on the GPU of smaller cards, and a data set too large for the card is processed on the CPU instead.
* Rugnux: On macOS, Bragg integration runs on the Apple GPU.
* Rugnux: Better handling of twinning and pseudo-symmetry, with the twin hypothesis written as `<prefix>_twin_<SG>.mtz`, and a wrong `--model` no longer confirms the data or decides the hand.
* Rugnux: `--model` accepts predicted (AlphaFold) models and falls back to molecular replacement when the model is not placed as a rigid body (experimental `--mr` forces it), settling space group, hand and twinning and writing a different answer as `<prefix>_mr.mtz`.
* Rugnux: Molecular replacement is faster and settles the space group in more cases with several copies in the asymmetric unit.
* Rugnux: Deposition-ready mmCIF, a richer report, and numbered steps on the terminal with the full log in `<prefix>_log.txt`; `_P1.mtz`, `--estimate-beam-center` and `--beam-center-search` are removed.
* Rugnux: The beam-stop shadow is found correctly on tilted detectors.
* Rugnux: Stills files that record a stationary goniometer axis get the default stills geometry refinement.
* jfjoch_viewer: "Analyze dataset" uses the same defaults as a bare `rugnux <file>`, and shows the results of processed CBF, marCCD and SMV sweeps.
* jfjoch_viewer: The Files panel refreshes on F5 and every few seconds, and the plot type and other selections stay put during live collection.
* jfjoch_writer: Memory stays bounded and old data files are closed efficiently on long runs, any failure ends the run in an error state the broker is told about, and the master file appears only after its data files; the ZeroMQ pusher logs when a stalled writer makes it drop images.
* Fixes: The TCP image pusher no longer blocks the broker's receiver, GPU out-of-memory failures stop with a clear message, and the XDS plugin masks module and chip edges.

---------

Co-authored-by: Filip Leonarski <jungfrau@filips-mac-mini.home>
Reviewed-on: #85
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-10-16 15:34:43 +02:00

978 lines
52 KiB
CMake

CMAKE_MINIMUM_REQUIRED(VERSION 3.26)
# The VERSION file is the single source of truth for the version, and JFJOCH_VERSION carries it
# verbatim - including a pre-release suffix such as "-rc.161" - into the package names, the DKMS
# install path and the version every binary reports. PROJECT(VERSION) cannot hold that string: it
# only accepts numeric major.minor.patch, so the numeric part is cut out of the same file rather
# than written out a second time, and PROJECT_VERSION can never drift from VERSION.
FILE(STRINGS VERSION JFJOCH_VERSION)
STRING(REGEX MATCH "^[0-9]+\\.[0-9]+\\.[0-9]+" JFJOCH_VERSION_NUMERIC "${JFJOCH_VERSION}")
# Oldest macOS the binaries run on (ignored elsewhere). Without it CMake takes the version of the
# build machine, and a .dmg would refuse to start on anything older. 13 is what the Qt the .dmg
# bundles (6.11) is built for, so a lower value only makes the linker warn about every framework;
# our own needs (<latch>, <filesystem>) stop at 11. Has to be set before PROJECT().
SET(CMAKE_OSX_DEPLOYMENT_TARGET "13.0" CACHE STRING "Minimum macOS version")
PROJECT(jfjoch VERSION ${JFJOCH_VERSION_NUMERIC} LANGUAGES C CXX)
SET(CMAKE_POLICY_DEFAULT_CMP0077 NEW)
INCLUDE(GNUInstallDirs)
SET(CMAKE_CXX_STANDARD 20)
SET(CMAKE_CXX_STANDARD_REQUIRED True)
# The Metal Bragg integration engine (image_analysis/bragg_integration) is Objective-C++. Enabled here,
# above every target that links it, as enable_language asks.
IF (APPLE)
ENABLE_LANGUAGE(OBJCXX)
SET(CMAKE_OBJCXX_STANDARD 20)
SET(CMAKE_OBJCXX_STANDARD_REQUIRED True)
ENDIF()
# MSVC gets C++ exception handling only from /EHsc, and CMake puts that flag in the DEFAULT value of
# CMAKE_CXX_FLAGS ("/DWIN32 /D_WINDOWS /GR /EHsc"), which is used only when the configure does not
# set CMAKE_CXX_FLAGS itself. CI does (-DCMAKE_CXX_FLAGS="/arch:AVX"), and that drops /EHsc: MSVC then
# does not unwind the stack on a throw (no destructors run) and does not define _CPPUNWIND, so
# Catch2 compiles itself with CATCH_CONFIG_DISABLE_EXCEPTIONS. Adding it here, before any
# FetchContent, reaches every C++ target whatever CMAKE_CXX_FLAGS holds. CUDA gets it from its own
# default CMAKE_CUDA_FLAGS, which nothing overrides.
#
# CUDA's CCCL headers (cub, thrust, libcu++) stop with an #error under MSVC's traditional
# preprocessor, which miscompiles their macros. nvcc preprocesses and compiles host code with cl.exe,
# so the standard-conforming preprocessor is switched on for .cu sources here, reaching every CUDA
# target, fetched ones included (VS 2019 16.5 or newer). ffbidx already sets the same for its own
# CUB-using kernel, which is why the other CUDA targets never needed it. C++ sources keep the
# default: none includes a CCCL header, and the switch would change the preprocessing of Qt, the
# Windows SDK and every fetched dependency with nothing to gain.
IF (MSVC)
ADD_COMPILE_OPTIONS($<$<COMPILE_LANGUAGE:CXX>:/EHsc>)
ADD_COMPILE_OPTIONS($<$<COMPILE_LANGUAGE:CUDA>:-Xcompiler=/Zc:preprocessor>)
ENDIF()
# No contraction of a*b+c into a fused multiply-add. GCC contracts whenever the target has FMA, so a
# -march=x86-64-v3 build (CI, production) and a baseline build rounded differently, and knife-edge
# decisions - which candidate cell a first pass indexes with, a resolution cut, a symmetry gate -
# came out differently from the same source and data. Off, every x86-64 level computes the same bits
# (measured: identical p.mtz on nine battery sets, no measurable time cost), and aarch64, where FMA is
# always there and GCC contracts by default, follows the same arithmetic. MSVC does not contract under
# its default /fp:precise. Host code of .cu sources gets it through nvcc, and the Metal kernels say so in
# their source (BraggIntegrationEngineMetal.mm).
IF (NOT MSVC)
ADD_COMPILE_OPTIONS($<$<COMPILE_LANGUAGE:C,CXX,OBJCXX>:-ffp-contract=off>)
ADD_COMPILE_OPTIONS($<$<COMPILE_LANGUAGE:CUDA>:-Xcompiler=-ffp-contract=off>)
ENDIF()
# The same for device code. nvcc contracts by default and does it separately for every architecture
# it compiles for, so a T4 (sm_75) and an RTX 5080 (sm_120) ran the same kernels to different bits
# and a run that hit a knife edge came out differently on the two machines. Off, the architectures
# compute the same bits (measured: identical p.hkl from sm_75 and sm_120 builds, no measurable time cost).
ADD_COMPILE_OPTIONS($<$<COMPILE_LANGUAGE:CUDA>:--fmad=false>)
SET(JFJOCH_WRITER_ONLY OFF CACHE BOOL "Compile HDF5 writer only")
SET(JFJOCH_INSTALL_DRIVER_SOURCE OFF CACHE BOOL "Install kernel driver source (ignored if building writer only; necessary for RPM building)")
SET(JFJOCH_USE_CUDA ON CACHE BOOL "Compile Jungfraujoch with CUDA")
SET(JFJOCH_VIEWER_BUILD OFF CACHE BOOL "Compile Jungfraujoch viewer")
# The portable products come out of two build modes; every artifact holds exactly one program:
#
# JFJOCH_VIEWER_ONLY jfjoch_viewer .tar.gz (Linux) / installer (Windows) / .dmg (macOS)
# and also rugnux (.tar.gz; .zip on Windows) and, on Linux only, the
# XDS plugin (.tar.gz)
# JFJOCH_RUGNUX_ONLY rugnux .tar.gz (Linux, incl. cross-built aarch64) / .zip (Windows)
#
# The viewer build compiles everything rugnux needs anyway, so one build packs all three; the
# rugnux-only mode remains for where there is no Qt (the aarch64 cross build).
#
# They used to be one package: the viewer tarball also carried rugnux and two CLI tools, which made
# the download confusing (a GUI package containing a batch processor) and meant a rugnux user had to
# take Qt with it. Splitting them is possible because cuFFT is now linked statically, so neither
# artifact has to ship a runtime library beside its executable.
SET(JFJOCH_RUGNUX_ONLY OFF CACHE BOOL "Compile only rugnux, the offline analysis CLI (no Qt)")
# macOS only: the Developer ID Application certificate to sign the shipped program with at install
# time - hardened runtime and a secure timestamp, which is what Apple's notarization demands. Given
# as the certificate's SHA-1 hash, the 40 hex digits `security find-identity -v -p codesigning`
# prints in front of the name: the name itself ("Developer ID Application: <name> (<team>)") has
# spaces, and Qt's generated deploy script would split it into separate macdeployqt arguments. Empty
# (the default) keeps the ad-hoc signature, which runs locally but is refused by Gatekeeper after a
# download. Notarizing the packed artefact is a separate step after cpack: tools/notarize_macos.sh.
SET(JFJOCH_MACOS_SIGNING_IDENTITY "" CACHE STRING "macOS: SHA-1 of the Developer ID Application certificate to codesign with (empty: ad-hoc)")
# Only the viewer (and its portable dependency tree) is supported on Windows and macOS -- the
# broker/receiver/FPGA/writer server stack is Linux-only. Force viewer-only on those platforms so a
# plain configure builds the right subset; on Linux it remains a user-togglable option. A rugnux-only
# build is portable too (it is the same subtree minus viewer/), so asking for it explicitly wins.
# TRUE for either portable product. Both build the same subtree of analysis libraries and neither
# wants any server-side target -- the broker service, the writer executables, the FPGA/detector
# tools or the frontend -- so the guards that exclude those key on this rather than on one product's
# option. Set below, once JFJOCH_VIEWER_ONLY has been resolved.
IF ((WIN32 OR APPLE) AND NOT JFJOCH_RUGNUX_ONLY)
SET(JFJOCH_VIEWER_ONLY ON CACHE BOOL "Compile only jfjoch_viewer and its dependencies" FORCE)
ELSEIF (JFJOCH_RUGNUX_ONLY)
SET(JFJOCH_VIEWER_ONLY OFF CACHE BOOL "Compile only jfjoch_viewer and its dependencies" FORCE)
ELSE()
SET(JFJOCH_VIEWER_ONLY OFF CACHE BOOL "Compile only jfjoch_viewer and its dependencies")
ENDIF()
IF (JFJOCH_VIEWER_ONLY OR JFJOCH_RUGNUX_ONLY)
SET(JFJOCH_PORTABLE_ONLY ON)
ELSE()
SET(JFJOCH_PORTABLE_ONLY OFF)
ENDIF()
# GPU architectures to generate device code for. Overridable so a build that targets one machine
# can pin its own set instead of paying for the full fat binary -- e.g. a cross build for
# GB10 (DGX Spark, sm_121) and GH200 (Grace Hopper, sm_90) passes -DJFJOCH_CUDA_ARCHITECTURES="90;121".
SET(JFJOCH_CUDA_ARCHITECTURES "" CACHE STRING "Override the CUDA architecture list (empty = the default set below)")
OPTION(SLS9 "Build with sls_detector_package v9.2.0" OFF)
SET(BUILD_SHARED_LIBS OFF)
SET(BUILD_TESTING OFF)
SET(ZSTD_LEGACY_SUPPORT OFF)
SET(ZSTD_MULTITHREAD_SUPPORT OFF)
SET(ZSTD_BUILD_PROGRAMS OFF)
SET(ZSTD_BUILD_SHARED OFF)
SET(SLS_USE_RECEIVER OFF)
SET(SLS_USE_RECEIVER_BINARIES OFF)
SET(SLS_BUILD_SHARED_LIBRARIES OFF)
SET(BUILD_FAST_INDEXER OFF)
SET(BUILD_FAST_INDEXER_STATIC ON)
INCLUDE(CheckLanguage)
INCLUDE(CheckIncludeFile)
# Locate nvcc ourselves when it isn't already pinned (-DCMAKE_CUDA_COMPILER / $CUDACXX). CHECK_LANGUAGE
# below only searches PATH, which is missed routinely on Windows and intermittently on Linux; the
# standard CUDA install locations (CUDA_PATH on Windows, /usr/local/cuda on Linux) cover both, so the
# compiler need not be passed by hand. Only set it when actually found, leaving CHECK_LANGUAGE to run
# its normal detection otherwise.
IF (JFJOCH_USE_CUDA AND NOT CMAKE_CUDA_COMPILER AND NOT DEFINED ENV{CUDACXX})
FIND_PROGRAM(_jfjoch_nvcc nvcc
HINTS ENV CUDA_PATH ENV CUDA_HOME ENV CUDA_ROOT /usr/local/cuda /opt/cuda
PATH_SUFFIXES bin)
IF (_jfjoch_nvcc)
SET(CMAKE_CUDA_COMPILER "${_jfjoch_nvcc}")
ENDIF()
ENDIF()
CHECK_LANGUAGE(CUDA)
# GPU architectures to generate device code for: Turing (T4), Ampere (A100; RTX A4000), Ada (L4),
# Hopper, and Blackwell in both its data-centre and consumer forms. A bare entry emits SASS *and*
# PTX for that architecture, so the newest one here also covers any future GPU: the driver
# JIT-compiles its PTX on first launch. Volta (V100) is added below, once the nvcc version is known.
IF (JFJOCH_CUDA_ARCHITECTURES)
SET(CMAKE_CUDA_ARCHITECTURES ${JFJOCH_CUDA_ARCHITECTURES})
ELSE()
SET(CMAKE_CUDA_ARCHITECTURES 75 80 86 89 90 100 120)
ENDIF()
SET(CMAKE_CUDA_STANDARD 20)
SET(CMAKE_CUDA_STANDARD_REQUIRED True)
SET(CMAKE_CUDA_FLAGS_RELEASE "-O3 -DNDEBUG -lineinfo")
SET(CMAKE_CUDA_RUNTIME_LIBRARY Static)
SET(JFJOCH_CUDA_AVAILABLE OFF)
IF (CMAKE_CUDA_COMPILER)
IF (JFJOCH_USE_CUDA)
ENABLE_LANGUAGE(CUDA)
MESSAGE(STATUS "CUDA VERSION: ${CMAKE_CUDA_COMPILER_VERSION}")
IF (CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL "12.8")
FIND_PACKAGE(CUDAToolkit REQUIRED)
ADD_COMPILE_DEFINITIONS(JFJOCH_USE_CUDA)
SET(JFJOCH_CUDA_AVAILABLE ON)
# Blackwell GB10 (DGX Spark) is sm_121, only known to nvcc >= 12.9; add it there so the
# binary runs natively on Spark instead of having the driver JIT sm_120 PTX for it.
IF (CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL "12.9" AND NOT JFJOCH_CUDA_ARCHITECTURES)
LIST(APPEND CMAKE_CUDA_ARCHITECTURES 121)
ENDIF()
# Volta (V100) is sm_70, and PTX only ever JITs *forwards* - without an sm_70 entry a
# V100 has no runnable code in the fatbin at all and every launch fails with "no kernel
# image is available for execution on the device". CUDA 13 removed offline compilation
# for Volta, so only a CUDA 12 toolkit can emit it; 12.8/12.9 still do, but warn on
# every .cu, hence -Wno-deprecated-gpu-targets. Volta is the floor - older GPUs are
# not in use here.
IF (CMAKE_CUDA_COMPILER_VERSION VERSION_LESS "13.0" AND NOT JFJOCH_CUDA_ARCHITECTURES)
LIST(APPEND CMAKE_CUDA_ARCHITECTURES 70)
SET(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -Wno-deprecated-gpu-targets")
ENDIF()
ELSE()
MESSAGE(WARNING "CUDA older than 12.8 not supported")
ENDIF()
ELSE()
MESSAGE(WARNING "CUDA Available, but disabled by user")
ENDIF()
ENDIF()
INCLUDE_DIRECTORIES(include)
include(FetchContent)
# Dependencies are fetched as PINNED TARBALLS, not git clones. GitHub throttles unauthenticated
# git-upload-pack and picks its targets by TLS fingerprint, so a clone answers 200 to the initial
# ref advertisement and then 401 to the fetch itself; git asks for a password, finds no terminal,
# and the configure stops dead with no error. Which images it hits follows their OpenSSL version,
# which is why it looks like a broken build image rather than a rate limit. The archive endpoint is
# not part of that campaign, and a tarball is a fraction of the transfer (hdf5: 40 MB against a
# 271 MB shallow clone), so it is both the fix and the cheaper fetch.
#
# Every URL is pinned by tag or commit and its bytes by URL_HASH, so a silently changed archive
# fails the configure instead of entering the build. To bump one: change the version in the URL and
# replace the hash with `sha256sum` of the new tarball.
#
# Ceres needs one extra step for its abseil submodule; see image_analysis/CMakeLists.txt.
# zlib, via zlib-ng in its zlib-compatible mode: same zlib.h, same API and symbol names, faster
# deflate. It used to be the one compression library the project asked the host for; providing it
# here is what makes a build need no zlib-devel.
#
# It is built HERE, during the configure, into a small install prefix in the build tree, and
# ZLIB_ROOT then points every find_package(ZLIB) at it -- ours and those of HDF5, libtiff,
# cpp-httplib. Two alternatives were rejected:
#
# * FetchContent OVERRIDE_FIND_PACKAGE is the mechanism banned by the Eigen note further down
# (it segfaults the CMake bundled with Visual Studio), and a subproject's find_package(ZLIB)
# resolving an override through a nested FetchContent_MakeAvailable is exactly the faulting
# pattern.
# * Adding zlib-ng with add_subdirectory and aliasing its target to ZLIB::ZLIB looks simpler, but
# ZLIB::ZLIB is then an alias of a target built in this project, and a consumer that puts
# ZLIB::ZLIB in CMAKE_REQUIRED_LIBRARIES for its feature checks (libcurl did, when the viewer used
# it) breaks: try_compile only accepts IMPORTED targets, so that is a hard configure error, not a
# degraded check (verified).
#
# Building it first and finding it as an ordinary installed library avoids both: every consumer
# runs the stock FindZLIB module against a real libz.a and gets a genuine imported target.
#
# Passing -DZLIB_ROOT=<prefix> skips the build below and uses the zlib found there instead.
IF (NOT ZLIB_ROOT)
SET(ZLIB_ROOT "${CMAKE_BINARY_DIR}/zlib-ng-install")
IF (NOT EXISTS "${ZLIB_ROOT}/include/zlib.h")
FetchContent_Declare(zlib
URL https://github.com/zlib-ng/zlib-ng/archive/refs/tags/2.3.3.tar.gz
URL_HASH SHA256=f9c65aa9c852eb8255b636fd9f07ce1c406f061ec19a2e7d508b318ca0c907d1
SOURCE_SUBDIR cmake) # no CMakeLists.txt there: populate only, do not add_subdirectory
FetchContent_MakeAvailable(zlib)
# The nested configure inherits the toolchain, not the environment: pass the compiler, the
# flags and the platform selection explicitly, or a cross build (or an MSVC build with a
# non-default platform/toolset/runtime) would silently produce a library for the wrong one.
SET(JFJOCH_ZLIB_CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${ZLIB_ROOT}
-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
-DCMAKE_C_FLAGS=${CMAKE_C_FLAGS}
-DCMAKE_POSITION_INDEPENDENT_CODE=ON # xds-plugin is a shared library
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
-DBUILD_SHARED_LIBS=OFF
-DBUILD_TESTING=OFF
-DINSTALL_UTILS=OFF
-DZLIB_COMPAT=ON # provide zlib.h and the zlib API, not zlib-ng.h
-DWITH_GZFILEOP=ON # gzopen/gzread: GEMMI reads gzipped MTZ and CIF
-DWITH_NATIVE_INSTRUCTIONS=OFF) # portable binary; SIMD is chosen at run time
SET(JFJOCH_ZLIB_FORWARD CMAKE_TOOLCHAIN_FILE CMAKE_MSVC_RUNTIME_LIBRARY
CMAKE_OSX_ARCHITECTURES CMAKE_OSX_SYSROOT CMAKE_OSX_DEPLOYMENT_TARGET)
# CMAKE_SYSTEM_NAME only when actually cross-compiling: setting it is what PUTS a build into
# cross mode, so forwarding it unconditionally would make even a native zlib-ng think it is
# being cross-compiled.
IF (CMAKE_CROSSCOMPILING)
LIST(APPEND JFJOCH_ZLIB_FORWARD CMAKE_SYSTEM_NAME CMAKE_SYSTEM_PROCESSOR)
ENDIF()
FOREACH (_var IN LISTS JFJOCH_ZLIB_FORWARD)
IF (${_var})
LIST(APPEND JFJOCH_ZLIB_CMAKE_ARGS "-D${_var}=${${_var}}")
ENDIF()
ENDFOREACH()
IF (CMAKE_GENERATOR_PLATFORM)
LIST(APPEND JFJOCH_ZLIB_CMAKE_ARGS -A "${CMAKE_GENERATOR_PLATFORM}")
ENDIF()
IF (CMAKE_GENERATOR_TOOLSET)
LIST(APPEND JFJOCH_ZLIB_CMAKE_ARGS -T "${CMAKE_GENERATOR_TOOLSET}")
ENDIF()
# Single-config generators pick the configuration at configure time, multi-config ones at
# build time; give each the one it reads and keep both on this build's configuration.
IF (CMAKE_CONFIGURATION_TYPES OR NOT CMAKE_BUILD_TYPE)
SET(JFJOCH_ZLIB_CONFIG Release)
ELSE()
SET(JFJOCH_ZLIB_CONFIG ${CMAKE_BUILD_TYPE})
ENDIF()
IF (NOT CMAKE_CONFIGURATION_TYPES)
LIST(APPEND JFJOCH_ZLIB_CMAKE_ARGS -DCMAKE_BUILD_TYPE=${JFJOCH_ZLIB_CONFIG})
ENDIF()
MESSAGE(STATUS "Building zlib-ng (zlib-compatible) into ${ZLIB_ROOT}")
EXECUTE_PROCESS(
COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" ${JFJOCH_ZLIB_CMAKE_ARGS}
-S "${zlib_SOURCE_DIR}" -B "${zlib_BINARY_DIR}"
RESULT_VARIABLE JFJOCH_ZLIB_RESULT
OUTPUT_VARIABLE JFJOCH_ZLIB_LOG ERROR_VARIABLE JFJOCH_ZLIB_LOG)
IF (JFJOCH_ZLIB_RESULT EQUAL 0)
EXECUTE_PROCESS(
COMMAND ${CMAKE_COMMAND} --build "${zlib_BINARY_DIR}" --config ${JFJOCH_ZLIB_CONFIG}
--target install
RESULT_VARIABLE JFJOCH_ZLIB_RESULT
OUTPUT_VARIABLE JFJOCH_ZLIB_LOG ERROR_VARIABLE JFJOCH_ZLIB_LOG)
ENDIF()
IF (NOT JFJOCH_ZLIB_RESULT EQUAL 0)
MESSAGE(FATAL_ERROR "Building the bundled zlib-ng failed:\n${JFJOCH_ZLIB_LOG}\n"
"Re-run the configure with -DZLIB_ROOT=<prefix> to use a zlib "
"already on this machine.")
ENDIF()
ENDIF()
ENDIF()
SET(ZLIB_USE_STATIC_LIBS TRUE)
FIND_PACKAGE(ZLIB REQUIRED)
SET(HDF5_ENABLE_SZIP_SUPPORT OFF CACHE BOOL "" FORCE)
SET(HDF5_ENABLE_SZIP_ENCODING OFF CACHE BOOL "" FORCE)
SET(HDF5_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
SET(HDF5_BUILD_CPP_LIB OFF CACHE BOOL "" FORCE)
SET(HDF5_ENABLE_ZLIB_SUPPORT ON CACHE BOOL "" FORCE)
SET(HDF5_EXTERNALLY_CONFIGURED 1 CACHE BOOL "" FORCE)
SET(SPDLOG_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
SET(SPDLOG_BUILD_TESTS OFF CACHE BOOL "" FORCE)
SET(SPDLOG_BUILD_BENCH OFF CACHE BOOL "" FORCE)
set(SPDLOG_BUILD_SHARED OFF CACHE BOOL "" FORCE)
SET(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
SET(HTTPLIB_USE_NON_BLOCKING_GETADDRINFO OFF CACHE BOOL "" FORCE)
SET(HTTPLIB_REQUIRE_ZLIB ON CACHE BOOL "" FORCE)
FetchContent_Declare(
spdlog
URL https://github.com/gabime/spdlog/archive/refs/tags/v1.17.0.tar.gz
URL_HASH SHA256=d8862955c6d74e5846b3f580b1605d2428b11d97a410d86e2fb13e857cd3a744
EXCLUDE_FROM_ALL
)
FetchContent_Declare(
zstd
URL https://github.com/facebook/zstd/archive/refs/tags/v1.5.7.tar.gz
URL_HASH SHA256=37d7284556b20954e56e1ca85b80226768902e2edabd3b649e9e72c0c9012ee3
SOURCE_SUBDIR build/cmake
EXCLUDE_FROM_ALL
)
FetchContent_Declare(hdf5
URL https://github.com/HDFGroup/hdf5/archive/refs/tags/2.2.0.tar.gz
URL_HASH SHA256=5b8d75125ae7b4fef55d3b39e8d3dbfd238cdf63b6518afd67fa69ac80f06542
EXCLUDE_FROM_ALL)
SET(SLS_DETECTOR_VERSION "8.0.2")
SET(SLS_DETECTOR_SHA256 "783b557ebff281a537ec70a20a775dd412dd345bfcefff85988ecfafe703222a")
IF(SLS9)
SET(SLS_DETECTOR_VERSION "9.2.0")
SET(SLS_DETECTOR_SHA256 "020a7791934e5a9d034cfa4c1c69df7109488ab2f80a4e7fca07b3e51978933f")
ENDIF()
FetchContent_Declare(
sls_detector_package
URL https://github.com/slsdetectorgroup/slsDetectorPackage/archive/refs/tags/${SLS_DETECTOR_VERSION}.tar.gz
URL_HASH SHA256=${SLS_DETECTOR_SHA256}
EXCLUDE_FROM_ALL
)
FetchContent_Declare(
catch2
URL https://github.com/catchorg/Catch2/archive/refs/tags/v3.16.0.tar.gz
URL_HASH SHA256=0957cae5821b17ce07f0833aaa52b5137643a8382203221f363a8303c109af34
EXCLUDE_FROM_ALL
)
FetchContent_Declare(
httplib
URL https://github.com/yhirose/cpp-httplib/archive/refs/tags/v0.56.0.tar.gz
URL_HASH SHA256=41f214d844e3b9117b734a8cc305b6141c5b47d63009681944e03bc31d92b9ca
EXCLUDE_FROM_ALL
)
# httplib enables Brotli content-encoding whenever it finds system Brotli; we don't use it
# (gzip/zlib is enough for the broker), so disable it to avoid linking libbrotli.
SET(HTTPLIB_USE_BROTLI_IF_AVAILABLE OFF CACHE BOOL "" FORCE)
# httplib auto-enables HTTPS by linking system OpenSSL whenever it finds it. The broker serves
# plain HTTP only (TLS, if ever wanted, is terminated by a front reverse proxy), so disable it --
# otherwise jfjoch_broker would pull in libssl/libcrypto for no reason.
SET(HTTPLIB_USE_OPENSSL_IF_AVAILABLE OFF CACHE BOOL "" FORCE)
# ZeroMQ (libzmq): fetch it here, at the top level, so that THIS project - not
# slsDetectorPackage - controls the version. slsDetectorPackage bundles its own libzmq
# archive, but only populates it behind `if(NOT libzmq_POPULATED)`; by making libzmq
# available before sls below, sls reuses this copy and a single libzmq-static target is
# built (no duplicate target / double-symbol clash). A future viewer-only / Windows build
# (which does not build sls) fetches the same libzmq standalone.
SET(BUILD_SHARED OFF CACHE BOOL "" FORCE) # libzmq: static only (matches sls)
SET(BUILD_TESTS OFF CACHE BOOL "" FORCE) # libzmq: no test build
SET(WITH_PERF_TOOL OFF CACHE BOOL "" FORCE) # libzmq: no perf tools
SET(WITH_DOCS OFF CACHE BOOL "" FORCE) # libzmq: no docs
SET(ENABLE_CPACK OFF CACHE BOOL "" FORCE) # libzmq: no CPack injection
FetchContent_Declare(
libzmq
URL https://github.com/zeromq/libzmq/archive/refs/tags/v4.3.5.tar.gz
URL_HASH SHA256=6c972d1e6a91a0ecd79c3236f04cf0126f2f4dfbbad407d72b4606a7ba93f9c6
EXCLUDE_FROM_ALL
)
# CMake >= 4.0 (e.g. the 4.x bundled with Visual Studio 2026) makes any
# cmake_minimum_required(VERSION < 3.5) a fatal error. Some fetched dependencies still
# declare such old floors (libzmq: 3.0.2), so raise the policy-version floor for the
# FetchContent subprojects. Ignored (harmless) on CMake < 3.30, which lacks this variable.
SET(CMAKE_POLICY_VERSION_MINIMUM 3.5)
# libzmq must be made available BEFORE sls_detector_package for the override above to take effect.
FetchContent_MakeAvailable(libzmq)
IF (JFJOCH_PORTABLE_ONLY)
# A portable build still needs zstd/hdf5/spdlog/httplib (JFJochReader uses httplib), and catch2
# for jfjoch_portable_test. Only sls_detector_package (detector) is not built here -- it does
# not configure under MSVC.
FetchContent_MakeAvailable(zstd catch2 hdf5 spdlog httplib)
ELSE()
FetchContent_MakeAvailable(zstd sls_detector_package catch2 hdf5 spdlog httplib)
ENDIF()
# libtiff (used by JFJochPreview in every build mode): build it ourselves. Its C++ binding
# (tiffxx) is packaged inconsistently across distros (missing on Rocky 9) and absent on
# Windows. Library only; the SET()s below are libtiff's own codec/tool switches.
# We only need DEFLATE (zlib) + the internal LZW codec, matching what Python tifffile writes
# by default. The remaining codecs default to ON whenever their library is found on the build
# host, so each must be explicitly turned OFF to keep the dependency set minimal -- otherwise
# webp pulls in libwebp + libsharpyuv and lerc pulls in libLerc.
SET(jbig OFF)
SET(zstd OFF)
SET(lzma OFF)
SET(jpeg OFF)
SET(old-jpeg OFF)
SET(webp OFF)
SET(lerc OFF)
SET(tiff-tools OFF)
SET(tiff-tests OFF)
FetchContent_Declare(tiff
URL https://gitlab.com/libtiff/libtiff/-/archive/v4.7.2/libtiff-v4.7.2.tar.gz
URL_HASH SHA256=62b15f44c10106065211efa22a2dc77937bd97ee1f6e3afd2c9659010de112eb
EXCLUDE_FROM_ALL)
FetchContent_MakeAvailable(tiff)
# FFTW single precision (target fftw3f): the CPU fallback indexer, enables JFJOCH_USE_FFTW.
# Built from the release tarball -- the git repo ships no pre-generated codelets (needs the
# OCaml genfft).
SET(ENABLE_FLOAT ON CACHE BOOL "" FORCE)
SET(BUILD_TESTS OFF CACHE BOOL "" FORCE)
# SIMD codelets are runtime-dispatched (cpuid), so enabling AVX2 does NOT require an AVX2 CPU
# -- FFTW falls back at runtime. The codelet sets are arch-specific, so guard by processor:
# x86 gets SSE2/AVX/AVX2, aarch64 (Apple Silicon, Grace on DGX Spark) gets NEON - added below the
# FetchContent, as FFTW's CMake build has no switch for it; other arches build scalar.
IF (CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|amd64")
SET(ENABLE_SSE2 ON CACHE BOOL "" FORCE)
SET(ENABLE_AVX ON CACHE BOOL "" FORCE)
SET(ENABLE_AVX2 ON CACHE BOOL "" FORCE)
ENDIF()
FetchContent_Declare(fftw
URL https://www.fftw.org/fftw-3.3.10.tar.gz
URL_HASH SHA256=56c932549852cddcfafdab3820b0200c7742675be92179e59e6215b340e26467
EXCLUDE_FROM_ALL)
FetchContent_MakeAvailable(fftw)
# FFTW exposes fftw3.h only via $<INSTALL_INTERFACE:include>, so consuming fftw3f straight from
# the build tree (FetchContent) leaves the header unreachable. On Linux a system-installed
# fftw3.h masks this; on Windows it does not. Add the source-tree api/ dir for build-tree use.
TARGET_INCLUDE_DIRECTORIES(fftw3f INTERFACE $<BUILD_INTERFACE:${fftw_SOURCE_DIR}/api>)
# NEON: only FFTW's autotools build knows it (--enable-neon); its CMake build silently builds scalar
# code on ARM (and ignores an ENABLE_NEON, as this file once set). So the NEON codelets are added
# the way its CMakeLists adds the SSE2/AVX ones: the sources, and HAVE_NEON so that dft/conf.c and
# rdft/conf.c register them - as a definition, because its config.h template carries HAVE_NEON only
# as a comment. NEON is part of every aarch64 CPU, so no compiler flag is needed. It is single
# precision, which is all that is built here. Measured on Apple Silicon against the scalar build
# (same results to 1e-8): the indexer's batched 1D r2c 1.8x faster at a power-of-two length and 1.3x
# at 730, the 2D r2c of the beam-centre search 1.9x, a 128^3 c2r map 2.6x.
IF (CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64")
FILE(GLOB _fftw_neon_sources ${fftw_SOURCE_DIR}/dft/simd/neon/*.c ${fftw_SOURCE_DIR}/rdft/simd/neon/*.c)
TARGET_SOURCES(fftw3f PRIVATE ${_fftw_neon_sources})
TARGET_COMPILE_DEFINITIONS(fftw3f PRIVATE HAVE_NEON=1)
ENDIF()
ADD_COMPILE_DEFINITIONS(JFJOCH_USE_FFTW)
# Eigen (header-only), consumed by the analysis libraries directly and by Ceres and ffbidx through
# their own find_package(Eigen3). The headers are fetched here and a two-file config package for
# them is written into the build tree; Eigen3_DIR then points at it, so every find_package(Eigen3)
# in the build -- ours, Ceres', ffbidx' -- resolves through the ordinary config path and gets this
# one copy. Nothing needs Eigen's own CMake, so the source is populated but never added.
#
# The obvious route, FetchContent_Declare(Eigen3 ... OVERRIDE_FIND_PACKAGE), IS BANNED and must not
# be reintroduced: it makes the CMake bundled with Visual Studio (cmake 4.x "-msvc") intermittently
# SEGFAULT during configure -- ~1 in 3 fresh configures, and 100% with Ceres CUDA on. The fault is
# in CMake's own FetchContent variable-stack cleanup, reached when Ceres' find_package(Eigen3)
# resolves the override through a nested FetchContent_MakeAvailable. Stock Kitware CMake runs the
# identical scripts fine, so it is a bug in the VS-bundled cmake binary. A generated config package
# never enters that machinery: find_package finds an ordinary Eigen3Config.cmake on disk, exactly as
# it would for an Eigen installed on the system.
#
# Passing -DEigen3_DIR=<dir> skips the fetch and uses the Eigen whose config package lives there.
SET(JFJOCH_EIGEN_VERSION 3.4.1)
IF (Eigen3_DIR)
MESSAGE(STATUS "Eigen3_DIR is set (${Eigen3_DIR}) -- using that Eigen instead of the bundled one")
ELSE()
FetchContent_Declare(eigen3
URL https://gitlab.com/libeigen/eigen/-/archive/${JFJOCH_EIGEN_VERSION}/eigen-${JFJOCH_EIGEN_VERSION}.tar.gz
URL_HASH SHA256=b93c667d1b69265cdb4d9f30ec21f8facbbe8b307cf34c0b9942834c6d4fdbe2
SOURCE_SUBDIR cmake) # no CMakeLists.txt there: populate only, do not add_subdirectory
FetchContent_MakeAvailable(eigen3)
SET(JFJOCH_EIGEN_CONFIG_DIR "${CMAKE_BINARY_DIR}/eigen3-config")
FILE(WRITE "${JFJOCH_EIGEN_CONFIG_DIR}/Eigen3Config.cmake"
"# Generated by the Jungfraujoch build -- do not edit.\n"
"if (NOT TARGET Eigen3::Eigen)\n"
" add_library(Eigen3::Eigen INTERFACE IMPORTED GLOBAL)\n"
" set_target_properties(Eigen3::Eigen PROPERTIES\n"
" INTERFACE_INCLUDE_DIRECTORIES \"${eigen3_SOURCE_DIR}\")\n"
"endif()\n"
"set(EIGEN3_INCLUDE_DIR \"${eigen3_SOURCE_DIR}\")\n"
"set(EIGEN3_INCLUDE_DIRS \"${eigen3_SOURCE_DIR}\")\n"
"set(EIGEN3_VERSION_STRING \"${JFJOCH_EIGEN_VERSION}\")\n"
"set(Eigen3_FOUND TRUE)\n")
# AnyNewerVersion, not Eigen's own SameMajorVersion rule: Ceres asks for a version RANGE,
# find_package(Eigen3 3.3.4...5 NO_MODULE), and a same-major file declines any range whose two
# endpoints differ in major version. Eigen's own config file does exactly that, which is how a
# build could end up compiling Ceres against one Eigen and everything else against another.
INCLUDE(CMakePackageConfigHelpers)
WRITE_BASIC_PACKAGE_VERSION_FILE("${JFJOCH_EIGEN_CONFIG_DIR}/Eigen3ConfigVersion.cmake"
VERSION ${JFJOCH_EIGEN_VERSION} COMPATIBILITY AnyNewerVersion ARCH_INDEPENDENT)
SET(Eigen3_DIR "${JFJOCH_EIGEN_CONFIG_DIR}")
ENDIF()
# getopt/getopt_long shim for Windows: the MSVC CRT has no <getopt.h>. Vendored OpenBSD/NetBSD
# implementation (BSD-licensed). Built only on Windows and defined here, before the subdirectories,
# so the portable CLI tools in tools/ can link it. A no-op on Linux/macOS, where getopt lives in
# libc (macOS uses the system getopt even though it is also a forced viewer-only platform).
IF (WIN32)
ADD_LIBRARY(wingetopt STATIC tools/wingetopt/getopt.c tools/wingetopt/getopt.h)
TARGET_INCLUDE_DIRECTORIES(wingetopt PUBLIC tools/wingetopt)
ENDIF()
# Mbed TLS: the TLS backend of cpp-httplib in the jfjoch_viewer HTTP client (viewer/JFJochHttpReader).
# Fetched and STATICALLY linked ONLY for viewer builds, so the broker/writer never pull in a TLS
# stack. One backend on every platform: it builds with MSVC and needs nothing from the host, unlike
# OpenSSL, which the macOS SDK does not have. cpp-httplib loads the trusted roots from the OS store
# (Windows certificate store, macOS Keychain, the CA bundle on Linux). The release tarball, not
# the GitHub archive: only it carries the tf-psa-crypto and framework submodules.
IF (JFJOCH_VIEWER_BUILD OR JFJOCH_VIEWER_ONLY)
SET(ENABLE_PROGRAMS OFF CACHE BOOL "" FORCE)
SET(ENABLE_TESTING OFF CACHE BOOL "" FORCE)
SET(MBEDTLS_FATAL_WARNINGS OFF CACHE BOOL "" FORCE)
SET(TF_PSA_CRYPTO_FATAL_WARNINGS OFF CACHE BOOL "" FORCE)
SET(USE_STATIC_MBEDTLS_LIBRARY ON CACHE BOOL "" FORCE)
SET(USE_SHARED_MBEDTLS_LIBRARY OFF CACHE BOOL "" FORCE)
SET(USE_SHARED_TF_PSA_CRYPTO_LIBRARY OFF CACHE BOOL "" FORCE)
FetchContent_Declare(mbedtls
URL https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-4.2.0/mbedtls-4.2.0.tar.bz2
URL_HASH SHA256=2bed9d713b4668f76553b097e72b8aa30bc8f112a940d7ae228d524bbde6ffea
EXCLUDE_FROM_ALL)
FetchContent_MakeAvailable(mbedtls)
ENDIF()
# The notices every package ships (installed at the end of this file). Set before the subdirectories
# because the macOS viewer puts them into its .app bundle itself - see viewer/CMakeLists.txt.
SET(JFJOCH_NOTICE_FILES
${CMAKE_SOURCE_DIR}/LICENSE
${CMAKE_SOURCE_DIR}/THIRD_PARTY_NOTICES.md
${CMAKE_SOURCE_DIR}/docs/ACKNOWLEDGEMENT.md)
IF (JFJOCH_RUGNUX_ONLY)
# The viewer-only subtree minus viewer/ and tools/: just rugnux and what it links. broker/ is
# here for JFJochAPI alone (JFJochReader links it); its service targets are gated out. preview/
# arrives through JFJochWriter -> JFJochPreview, so libjpeg-turbo and libtiff are built too.
ADD_SUBDIRECTORY(jungfrau)
ADD_SUBDIRECTORY(compression)
ADD_SUBDIRECTORY(common)
ADD_SUBDIRECTORY(gemmi_gph)
ADD_SUBDIRECTORY(frame_serialize)
ADD_SUBDIRECTORY(preview)
ADD_SUBDIRECTORY(writer)
ADD_SUBDIRECTORY(image_analysis)
ADD_SUBDIRECTORY(broker)
ADD_SUBDIRECTORY(reader)
ADD_SUBDIRECTORY(rugnux)
ADD_SUBDIRECTORY(tests) # jfjoch_portable_test only
ELSEIF (JFJOCH_VIEWER_ONLY)
# Minimal subtree: jfjoch_viewer and only the libraries it transitively links.
# (broker here provides JFJochAPI only; its service targets are gated out.)
ADD_SUBDIRECTORY(jungfrau)
ADD_SUBDIRECTORY(compression)
ADD_SUBDIRECTORY(common)
ADD_SUBDIRECTORY(gemmi_gph)
ADD_SUBDIRECTORY(frame_serialize)
ADD_SUBDIRECTORY(preview)
ADD_SUBDIRECTORY(writer)
ADD_SUBDIRECTORY(image_analysis)
ADD_SUBDIRECTORY(broker)
ADD_SUBDIRECTORY(reader)
ADD_SUBDIRECTORY(rugnux)
ADD_SUBDIRECTORY(viewer)
ADD_SUBDIRECTORY(tools) # builds only the portable analysis tools (rugnux/extract_hkl)
ADD_SUBDIRECTORY(tests) # jfjoch_portable_test only
IF (NOT WIN32 AND NOT APPLE) # no native XDS for Windows; the macOS plugin is not tested
ADD_SUBDIRECTORY(xds-plugin)
ENDIF()
ELSE()
ADD_SUBDIRECTORY(jungfrau)
ADD_SUBDIRECTORY(compression)
ADD_SUBDIRECTORY(common)
ADD_SUBDIRECTORY(writer)
ADD_SUBDIRECTORY(frame_serialize)
ADD_SUBDIRECTORY(reader)
ADD_SUBDIRECTORY(detector_control)
ADD_SUBDIRECTORY(image_puller)
ADD_SUBDIRECTORY(preview)
ADD_SUBDIRECTORY(gemmi_gph)
ADD_SUBDIRECTORY(xds-plugin)
IF (JFJOCH_WRITER_ONLY)
MESSAGE(STATUS "Compiling HDF5 writer only")
ELSE()
ADD_SUBDIRECTORY(image_pusher)
ADD_SUBDIRECTORY(broker)
ADD_SUBDIRECTORY(fpga)
ADD_SUBDIRECTORY(acquisition_device)
ADD_SUBDIRECTORY(receiver)
ADD_SUBDIRECTORY(image_analysis)
ADD_SUBDIRECTORY(rugnux)
ADD_SUBDIRECTORY(tests)
ADD_SUBDIRECTORY(tools)
ENDIF()
IF (JFJOCH_VIEWER_BUILD)
ADD_SUBDIRECTORY(viewer)
ENDIF()
ENDIF()
IF (NOT JFJOCH_WRITER_ONLY AND NOT JFJOCH_PORTABLE_ONLY)
ADD_CUSTOM_COMMAND(OUTPUT frontend/dist/index.html
COMMAND npm ci
COMMAND npm run build
COMMAND npm run licenses # write dist/THIRD_PARTY_LICENSES.txt (npm deps attribution)
COMMAND npm run redocly
COMMAND npm run docs # bundle Sphinx docs into dist/docs (served at /frontend/docs)
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/frontend)
ADD_CUSTOM_TARGET(frontend DEPENDS frontend/dist/index.html)
ADD_CUSTOM_TARGET(update_version
COMMAND bash update_version.sh
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
IF (JFJOCH_INSTALL_DRIVER_SOURCE)
INSTALL(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/fpga/pcie_driver/
DESTINATION /usr/src/jfjoch-${JFJOCH_VERSION}
COMPONENT driver-dkms
FILES_MATCHING PATTERN "dkms.conf")
INSTALL(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/fpga/pcie_driver/
DESTINATION /usr/src/jfjoch-${JFJOCH_VERSION}/src
COMPONENT driver-dkms
FILES_MATCHING PATTERN "*.c" PATTERN "*.h" PATTERN "Makefile")
ENDIF()
FILE(MAKE_DIRECTORY ${CMAKE_SOURCE_DIR}/frontend/dist/)
INSTALL(DIRECTORY ${CMAKE_SOURCE_DIR}/frontend/dist/ DESTINATION share/jfjoch/frontend COMPONENT jfjoch )
ENDIF()
# Windows has no static cuFFT (the toolkit ships no cufft_static.lib), so there the indexer links
# the import library and the self-contained products must carry the DLL beside the executable --
# the same idea the Linux builds no longer need, since they link libcufft_static.a. CUDA 13 keeps
# the redistributable DLLs in bin/x64, earlier toolkits in bin; glob both.
IF (WIN32 AND JFJOCH_CUDA_AVAILABLE AND JFJOCH_PORTABLE_ONLY AND NOT TARGET CUDA::cufft_static)
FILE(GLOB _cufft_dll
"${CUDAToolkit_BIN_DIR}/x64/cufft64_*.dll"
"${CUDAToolkit_BIN_DIR}/cufft64_*.dll")
IF (NOT _cufft_dll)
MESSAGE(FATAL_ERROR "cuFFT runtime DLL not found under ${CUDAToolkit_BIN_DIR}")
ENDIF()
INSTALL(FILES ${_cufft_dll} DESTINATION bin COMPONENT rugnux)
IF (JFJOCH_VIEWER_ONLY)
INSTALL(FILES ${_cufft_dll} DESTINATION bin COMPONENT viewer)
ENDIF()
ENDIF()
IF(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
SET(CMAKE_INSTALL_PREFIX /opt/jfjoch CACHE PATH "Default directory" FORCE)
ENDIF(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
# Set Package Name
SET(CPACK_PACKAGE_NAME "jfjoch")
# Select the components to package based on build mode
if (JFJOCH_RUGNUX_ONLY)
set(CPACK_COMPONENTS_ALL rugnux)
elseif (JFJOCH_VIEWER_ONLY AND (WIN32 OR APPLE))
set(CPACK_COMPONENTS_ALL viewer rugnux) # the XDS plugin is built on Linux only
elseif (JFJOCH_VIEWER_ONLY)
set(CPACK_COMPONENTS_ALL viewer rugnux xds_plugin)
else()
# rugnux is packaged separately from everything else, as "rugnux" rather than "jfjoch-rugnux"
# (see the DEB/RPM branches below). It is the offline analysis CLI: it links none of the
# hardware libraries and talks to no service, so a machine that only processes data can install
# it alone. Up to 1.0.0-rc.163 it rode inside the jfjoch-viewer package, which is why both
# branches below declare the file move.
set(CPACK_COMPONENTS_ALL jfjoch writer rugnux)
if (JFJOCH_INSTALL_DRIVER_SOURCE)
list(APPEND CPACK_COMPONENTS_ALL driver-dkms)
endif()
if (JFJOCH_VIEWER_BUILD)
list(APPEND CPACK_COMPONENTS_ALL viewer)
endif()
endif()
# Common metadata
set(CPACK_PACKAGE_CONTACT "Filip Leonarski <filip.leonarski@psi.ch>")
set(CPACK_PACKAGE_VENDOR "Paul Scherrer Institut")
set(CPACK_PACKAGE_VERSION ${JFJOCH_VERSION})
# Names of the portable .tar.gz archives (CI renames them to .tgz). rugnux and the plugin always
# carry their architecture, because rugnux is built for more than one: x86_64 natively and aarch64
# through the cross toolchain. CMAKE_SYSTEM_PROCESSOR is the TARGET architecture (the toolchain file
# declares it), never the build host's. The Linux viewer is x86_64-only and keeps its existing name.
# The plugin has no -cuda/-cpu tag: it does not use CUDA, so both variants build the same library.
if (APPLE)
set(_jfjoch_tgz_os "macos")
else()
set(_jfjoch_tgz_os "linux")
endif()
if (JFJOCH_CUDA_AVAILABLE)
set(_jfjoch_tgz_variant "cuda${CUDAToolkit_VERSION_MAJOR}")
else()
set(_jfjoch_tgz_variant "cpu")
endif()
set(CPACK_ARCHIVE_VIEWER_FILE_NAME "jfjoch_viewer-${JFJOCH_VERSION}-${_jfjoch_tgz_os}-${_jfjoch_tgz_variant}")
if (WIN32)
set(CPACK_ARCHIVE_RUGNUX_FILE_NAME "rugnux-${JFJOCH_VERSION}-win64-${_jfjoch_tgz_variant}")
else()
set(CPACK_ARCHIVE_RUGNUX_FILE_NAME "rugnux-${JFJOCH_VERSION}-${_jfjoch_tgz_os}-${CMAKE_SYSTEM_PROCESSOR}-${_jfjoch_tgz_variant}")
endif()
set(CPACK_ARCHIVE_XDS_PLUGIN_FILE_NAME "jfjoch_xds_plugin-${JFJOCH_VERSION}-linux-${CMAKE_SYSTEM_PROCESSOR}")
# macOS and Windows pack the viewer as a .dmg / NSIS installer and the other programs of the same
# build (rugnux) as an archive. CPack reads
# CPACK_PROJECT_CONFIG_FILE once per generator, which is where each generator gets its components.
file(WRITE ${CMAKE_BINARY_DIR}/CPackProjectConfig.cmake [=[
if (CPACK_GENERATOR STREQUAL "TGZ" OR CPACK_GENERATOR STREQUAL "ZIP")
list(REMOVE_ITEM CPACK_COMPONENTS_ALL viewer)
set(CPACK_ARCHIVE_COMPONENT_INSTALL ON)
set(CPACK_COMPONENTS_GROUPING IGNORE)
else()
set(CPACK_COMPONENTS_ALL viewer)
endif()
]=])
# OS-aware packaging: DragNDrop (.dmg) on macOS, NSIS installer on Windows, DEB on Debian/Ubuntu,
# RPM on RHEL/Rocky. macOS/Windows are checked first because the /etc/* probes below are Linux-only.
if (APPLE AND NOT JFJOCH_RUGNUX_ONLY)
# .dmg containing jfjoch_viewer.app (Qt runtime already deployed into the bundle). Named like the
# Windows and Linux artifacts; CMAKE_SYSTEM_PROCESSOR is arm64 on Apple Silicon.
# plus rugnux as .tar.gz, see CPackProjectConfig.cmake above
set(CPACK_GENERATOR "DragNDrop;TGZ")
set(CPACK_PROJECT_CONFIG_FILE ${CMAKE_BINARY_DIR}/CPackProjectConfig.cmake)
set(CPACK_PACKAGE_FILE_NAME "jfjoch-viewer-${JFJOCH_VERSION}-macos-${CMAKE_SYSTEM_PROCESSOR}")
set(CPACK_DMG_VOLUME_NAME "Jungfraujoch Viewer ${JFJOCH_VERSION}")
elseif (WIN32 AND NOT JFJOCH_RUGNUX_ONLY)
# NSIS installer .exe (Qt runtime deployed next to the binary by windeployqt), plus rugnux as
# a .zip, see CPackProjectConfig.cmake above.
set(CPACK_GENERATOR "NSIS;ZIP")
set(CPACK_PROJECT_CONFIG_FILE ${CMAKE_BINARY_DIR}/CPackProjectConfig.cmake)
# GPLv3 text shown as the click-through license page of the installer.
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE")
# Branding is split across three CPack knobs so the CUDA/CPU variant surfaces exactly where we
# want it and nowhere else:
# - Install folder + Start Menu group come from CPACK_PACKAGE_INSTALL_DIRECTORY and
# CPACK_NSIS_PACKAGE_NAME (the latter feeds $(^Name), the Start Menu group's default folder).
# Both stay plain "Jungfraujoch", so the Start Menu group carries no variant tag and the two
# builds install to the same place (CUDA is a strict superset -- they replace, not coexist).
# - CPACK_NSIS_DISPLAY_NAME is the Add/Remove Programs entry -- tagged "(CUDA)"/"(CPU)".
# - CPACK_PACKAGE_FILE_NAME is the installer .exe filename -- tagged "-cuda<major>"/"-cpu", so
# the (much larger) CUDA download is self-identifying, with the CUDA major version baked in.
# The tag follows JFJOCH_CUDA_AVAILABLE automatically; CUDAToolkit_VERSION_MAJOR is set whenever
# it is ON (find_package(CUDAToolkit) ran in the same guard above).
set(CPACK_PACKAGE_INSTALL_DIRECTORY "Jungfraujoch")
set(CPACK_NSIS_PACKAGE_NAME "Jungfraujoch")
if (JFJOCH_CUDA_AVAILABLE)
set(CPACK_NSIS_DISPLAY_NAME "Jungfraujoch (CUDA)")
set(CPACK_PACKAGE_FILE_NAME "jfjoch-viewer-${JFJOCH_VERSION}-win64-cuda${CUDAToolkit_VERSION_MAJOR}")
else()
set(CPACK_NSIS_DISPLAY_NAME "Jungfraujoch (CPU)")
set(CPACK_PACKAGE_FILE_NAME "jfjoch-viewer-${JFJOCH_VERSION}-win64-cpu")
endif()
# Start Menu shortcut for the viewer ("<exe basename>;<label>"). The shortcut and the
# Add/Remove Programs entry both use the .exe's embedded icon.
set(CPACK_PACKAGE_EXECUTABLES "jfjoch_viewer;Jungfraujoch Viewer")
# Forward slash on purpose: CPack writes this value verbatim into CPackConfig.cmake, and a
# backslash there ("bin\jfjoch...") is an invalid escape that cmake 4.x (CMP0010 strict, e.g. the
# VS-bundled cmake) rejects when cpack re-parses it. Windows accepts the forward slash at runtime.
set(CPACK_NSIS_INSTALLED_ICON_NAME "bin/jfjoch_viewer.exe")
elseif (WIN32)
# rugnux on Windows is a single self-contained rugnux.exe (cuFFT, cudart and the fast-feedback
# indexer are all static, so nothing rides alongside it). An NSIS installer would be ceremony
# around one file that a user drops wherever they like, so ship a .zip: the .exe plus the
# licence notices the component installs.
set(CPACK_GENERATOR "ZIP")
set(CPACK_ARCHIVE_COMPONENT_INSTALL ON)
set(CPACK_COMPONENTS_GROUPING ALL_COMPONENTS_IN_ONE)
set(CPACK_PACKAGE_FILE_NAME ${CPACK_ARCHIVE_RUGNUX_FILE_NAME})
elseif (JFJOCH_VIEWER_ONLY OR JFJOCH_RUGNUX_ONLY)
# Linux portable products: self-contained .tar.gz archives (Qt is linked statically, so there is
# nothing external to ship alongside a binary). Built on the oldest supported distro (RHEL 8) for
# a low glibc floor, so an archive runs on any newer Linux -- unlike the per-distro .rpm/.deb
# these replace in the release.
set(CPACK_GENERATOR "TGZ")
# Package only the components in CPACK_COMPONENTS_ALL. Without this the archive generator falls
# back to a monolithic install and walks every install() rule in the tree -- including the
# fetched HDF5 command-line tools and abseil's EXCLUDE_FROM_ALL sub-libraries -- which the
# portable build does not produce, so cpack aborts with "cannot find <artifact>". This mirrors
# CPACK_DEB_COMPONENT_INSTALL / CPACK_RPM_COMPONENT_INSTALL in the DEB/RPM branches below.
set(CPACK_ARCHIVE_COMPONENT_INSTALL ON)
if (JFJOCH_RUGNUX_ONLY)
# A single tarball named CPACK_PACKAGE_FILE_NAME (no per-component suffix).
set(CPACK_COMPONENTS_GROUPING ALL_COMPONENTS_IN_ONE)
set(CPACK_PACKAGE_FILE_NAME ${CPACK_ARCHIVE_RUGNUX_FILE_NAME})
else()
# One tarball per program -- viewer, rugnux, XDS plugin -- named by CPACK_ARCHIVE_<C>_FILE_NAME.
set(CPACK_COMPONENTS_GROUPING IGNORE)
endif()
elseif (EXISTS "/etc/debian_version")
set(CPACK_PACKAGE_LICENSE "GPL-3.0-only")
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE")
# Debian/Ubuntu: .deb with components
set(CPACK_GENERATOR "DEB")
set(CPACK_DEB_COMPONENT_INSTALL ON)
# Every component package is named "<CPACK_PACKAGE_NAME>-<component>" unless the component
# overrides it, so the broker component came out as "jfjoch-jfjoch". Name it plain "jfjoch", to
# match what the RPM generator produces for CPACK_RPM_MAIN_COMPONENT below. (There is no
# CPACK_DEBIAN_MAIN_COMPONENT: only CPackRPM.cmake has that variable, and the DEB spelling this
# used to set was silently ignored.)
set(CPACK_DEBIAN_JFJOCH_PACKAGE_NAME "jfjoch")
set(CPACK_DEBIAN_FILE_NAME "DEB-DEFAULT")
# Up to 1.0.0-rc.160 that package was "jfjoch-jfjoch". It owns the same files as "jfjoch" does
# now, so dpkg would refuse to unpack over it; declare the rename so an existing installation
# upgrades instead of conflicting.
set(CPACK_DEBIAN_JFJOCH_PACKAGE_REPLACES "jfjoch-jfjoch")
set(CPACK_DEBIAN_JFJOCH_PACKAGE_CONFLICTS "jfjoch-jfjoch")
# rugnux ships as plain "rugnux", not "jfjoch-rugnux": it is a separately named product (see
# docs/NAMING.md), the released tarball is called rugnux-<version>-..., and it is what a user
# types. Overriding the name is the only way to drop the CPACK_PACKAGE_NAME prefix.
set(CPACK_DEBIAN_RUGNUX_PACKAGE_NAME "rugnux")
# /usr/bin/rugnux moved out of jfjoch-viewer into this package. Breaks+Replaces, NOT Conflicts:
# the two packages are meant to be co-installable from 1.0.0-rc.164 on, and this only has to
# stop an OLD viewer (which still owns the file) from being unpacked alongside. Without it dpkg
# refuses with "trying to overwrite '/usr/bin/rugnux', which is also in package jfjoch-viewer".
set(CPACK_DEBIAN_RUGNUX_PACKAGE_BREAKS "jfjoch-viewer (<< ${JFJOCH_VERSION})")
set(CPACK_DEBIAN_RUGNUX_PACKAGE_REPLACES "jfjoch-viewer (<< ${JFJOCH_VERSION})")
# Enable automatic shlib dependency discovery
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
# Optional DEB metadata (tune as needed)
set(CPACK_DEBIAN_PACKAGE_SECTION "science")
# set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "https://example.org/jfjoch")
# DKMS component (maps from RPM settings)
if (JFJOCH_INSTALL_DRIVER_SOURCE)
# Dependencies and arch for the DKMS component
# Note: component name "driver-dkms" maps to variable suffix "DRIVER_DKMS"
set(CPACK_DEBIAN_DRIVER_DKMS_PACKAGE_DEPENDS "dkms, gcc, bash, sed")
set(CPACK_DEBIAN_DRIVER_DKMS_PACKAGE_ARCHITECTURE "all")
# Provide Debian control scripts (postinst/prerm).
# We rename existing scripts to Debian control names at configure time.
set(_dkms_ctrl_dir "${CMAKE_CURRENT_BINARY_DIR}/cpack-debian-driver-dkms")
file(MAKE_DIRECTORY "${_dkms_ctrl_dir}")
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/fpga/pcie_driver/postinstall.sh
${_dkms_ctrl_dir}/postinst COPYONLY)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/fpga/pcie_driver/preuninstall.sh
${_dkms_ctrl_dir}/prerm COPYONLY)
set(CPACK_DEBIAN_DRIVER_DKMS_PACKAGE_CONTROL_EXTRA
"${_dkms_ctrl_dir}/postinst;${_dkms_ctrl_dir}/prerm")
endif()
elseif (EXISTS "/etc/redhat-release")
# RHEL/Rocky: .rpm with components (original behavior)
set(CPACK_GENERATOR "RPM")
set(CPACK_RPM_COMPONENT_INSTALL ON)
set(CPACK_RPM_MAIN_COMPONENT jfjoch)
set(CPACK_RPM_PACKAGE_RELEASE_DIST ON)
set(CPACK_RPM_FILE_NAME "RPM-DEFAULT")
set(CPACK_RPM_PACKAGE_VERSION ${JFJOCH_VERSION})
set(CPACK_RPM_PACKAGE_RELEASE 1)
set(CPACK_RPM_PACKAGE_SUMMARY "Jungfraujoch data acquisition system")
set(CPACK_RPM_PACKAGE_DESCRIPTION "Jungfraujoch")
set(CPACK_RPM_PACKAGE_LICENSE "GPLv3")
set(CPACK_RPM_EXCLUDE_FROM_AUTO_FILELIST_ADDITION
/usr/src /usr/share /usr/share/dbus-1 /usr/share/dbus-1/services /usr/share/pixmaps /usr/share/applications)
# rugnux ships as plain "rugnux", not "jfjoch-rugnux" -- see the DEB branch above for why.
set(CPACK_RPM_RUGNUX_PACKAGE_NAME "rugnux")
set(CPACK_RPM_RUGNUX_PACKAGE_SUMMARY "Offline crystallographic data analysis for Jungfraujoch")
set(CPACK_RPM_RUGNUX_PACKAGE_DESCRIPTION
"rugnux processes a stored Jungfraujoch HDF5 dataset: spot finding, indexing, geometry
refinement, Bragg integration, scaling and merging. It needs none of the acquisition stack -- no
broker, no detector, no FPGA card -- so it can be installed on a machine that only processes data.")
# /usr/bin/rugnux moved out of the jfjoch-viewer package into this one. Conflicts against the
# OLD viewer only, and deliberately NOT Obsoletes: obsoleting jfjoch-viewer would uninstall the
# viewer, which still exists and is still wanted. rpm turns "-" into "_" in a package version
# (an installed release reads 1.0.0_rc.160), so the bound has to be spelled the same way or the
# comparison silently never matches.
string(REPLACE "-" "_" _jfjoch_rpm_version "${JFJOCH_VERSION}")
set(CPACK_RPM_RUGNUX_PACKAGE_CONFLICTS "jfjoch-viewer < ${_jfjoch_rpm_version}")
# DKMS component scripts and requirements for RPM
if (JFJOCH_INSTALL_DRIVER_SOURCE)
set(CPACK_RPM_DRIVER-DKMS_PACKAGE_REQUIRES "dkms, gcc, bash, sed")
set(CPACK_RPM_DRIVER-DKMS_PACKAGE_ARCHITECTURE "noarch")
set(CPACK_RPM_DRIVER-DKMS_POST_INSTALL_SCRIPT_FILE
${CMAKE_CURRENT_SOURCE_DIR}/fpga/pcie_driver/postinstall.sh)
set(CPACK_RPM_DRIVER-DKMS_PRE_UNINSTALL_SCRIPT_FILE
${CMAKE_CURRENT_SOURCE_DIR}/fpga/pcie_driver/preuninstall.sh)
endif()
else()
# Fallback if OS is unknown
set(CPACK_GENERATOR "TGZ")
endif()
# Ship the notices (top-level LICENSE, the third-party manifest, the verbatim license texts, and
# the acknowledgements the programs refer a user to) with every package variant. Installed per
# component so the files land in whichever component package(s) the current build mode produces.
#
# Each component gets a directory of its own, named after the package's principal binary:
# share/doc/jfjoch_broker, jfjoch_writer, jfjoch_viewer, jfjoch_driver_dkms. No two components
# may write the same path, and no component writes the plain share/doc/jfjoch these all used to
# share. Deployment updates the packages one at a time (jfjoch first, then jfjoch-writer), so a
# path claimed by two packages blocks the first step as soon as a release changes its content -
# and leaving the main package on the old shared path would keep blocking it for anyone
# upgrading from a release that still had the collision. These notices were the only files any
# two components had in common.
FOREACH(_component IN LISTS CPACK_COMPONENTS_ALL)
IF (APPLE AND _component STREQUAL "viewer")
CONTINUE() # inside the .app bundle instead, see viewer/CMakeLists.txt
ENDIF()
IF (_component STREQUAL "xds_plugin")
CONTINUE() # its own, shorter set, see xds-plugin/CMakeLists.txt
ENDIF()
IF (_component STREQUAL "jfjoch")
SET(_suffix broker)
ELSE()
STRING(REPLACE "-" "_" _suffix ${_component})
ENDIF()
INSTALL(FILES ${JFJOCH_NOTICE_FILES}
DESTINATION share/doc/jfjoch_${_suffix} COMPONENT ${_component})
INSTALL(DIRECTORY ${CMAKE_SOURCE_DIR}/licenses/
DESTINATION share/doc/jfjoch_${_suffix}/licenses COMPONENT ${_component}
PATTERN "COLLECT.sh" EXCLUDE)
ENDFOREACH()
INCLUDE(CPack)