cmake_minimum_required(VERSION 3.22)

project(CTSim VERSION 7.0.0 LANGUAGES C CXX)

# CMake 4 normalizes a path before matching it against the exclude patterns
# in file(GET_RUNTIME_DEPENDENCIES), which the Windows install rules below
# rely on.  Without the policy set it warns once for every library it
# examines, and it examines every one Windows supplies.  Guarded, because
# CMake 3 does not know the policy and rejects a name it has never heard of.
if(POLICY CMP0207)
  cmake_policy(SET CMP0207 NEW)
endif()

# ---------------------------------------------------------------------------
# How to install a missing package, in the words of this platform.  Naming
# apt and brew on Windows told a reader to run commands that do not exist
# there; ctsim_install_hint(VAR apt-name brew-name pacman-name) sets VAR to
# whichever applies.
# ---------------------------------------------------------------------------
function(ctsim_install_hint outvar apt brew pacman)
  if(WIN32)
    if(pacman STREQUAL "")
      set(${outvar} "" PARENT_SCOPE)
    else()
      set(${outvar} "pacman -S mingw-w64-ucrt-x86_64-${pacman}" PARENT_SCOPE)
    endif()
  elseif(APPLE)
    set(${outvar} "brew install ${brew}" PARENT_SCOPE)
  else()
    set(${outvar} "apt install ${apt}" PARENT_SCOPE)
  endif()
endfunction()

# Pre-release suffix in Debian's tilde form: "7.0.0~m0" sorts before "7.0.0"
# and after every 6.x, which is what we want for the milestone builds of the
# Qt port.  CMake's project(VERSION) accepts only numbers, so the suffix is
# carried separately and joined into VERSION for config.h and the packages.
# Empty it for the 7.0.0 release.
set(CTSIM_VERSION_SUFFIX "")
set(CTSIM_VERSION_FULL "${PROJECT_VERSION}${CTSIM_VERSION_SUFFIX}")
string(TOLOWER ${PROJECT_NAME} PROJECT_NAME_LOWER)

# ---------------------------------------------------------------------------
# Options
# ---------------------------------------------------------------------------
set(CTSIM_GUI "qt" CACHE STRING "Graphical interface to build: qt, none")
set_property(CACHE CTSIM_GUI PROPERTY STRINGS qt none)
option(CTSIM_USE_OPENMP   "Use OpenMP parallelism in libctsim"              ON)
option(CTSIM_USE_READLINE "Line editing in the ctsim shell"                  ON)
# Which line-editing library: libedit is preferred because it is BSD licensed
# and is what Debian and macOS ship, but GNU readline works equally well now
# that CTSim is GPL-3-or-later (see the note in the ChangeLog).
set(CTSIM_READLINE "auto" CACHE STRING "Line editing library: auto, editline, readline")
set_property(CACHE CTSIM_READLINE PROPERTY STRINGS auto editline readline)
option(CTSIM_WITH_HDF5    "HDF5 file format (default for newly written files)" ON)
option(CTSIM_BUILD_TESTS  "Build regression tests (ctest)"                  ON)
option(CTSIM_WARNINGS     "Enable -Wall style warnings"                     ON)
option(CTSIM_NATIVE_ARCH  "Tune for the build machine CPU (non-portable)"  OFF)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
  set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()

list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
include(GNUInstallDirs)
# GNUInstallDirs derives DOCDIR from the project name, which is "CTSim";
# Debian, the Help menu and every convention want share/doc/ctsim.
set(CMAKE_INSTALL_DOCDIR "${CMAKE_INSTALL_DATAROOTDIR}/doc/ctsim")
include(CheckIncludeFile)
include(CheckFunctionExists)
include(TestBigEndian)

# ---------------------------------------------------------------------------
# Mandatory dependencies: zlib, libpng, FFTW3 (double precision)
# ---------------------------------------------------------------------------
find_package(ZLIB REQUIRED)
find_package(PNG  REQUIRED)
find_package(PkgConfig)
if(PkgConfig_FOUND)
  pkg_check_modules(FFTW3 IMPORTED_TARGET fftw3)
endif()
if(NOT FFTW3_FOUND)   # e.g. Windows/vcpkg or macOS without pkg-config
  find_library(FFTW3_LIBRARY NAMES fftw3 libfftw3 fftw3-3)
  find_path(FFTW3_INCLUDE_DIR fftw3.h)
  if(FFTW3_LIBRARY AND FFTW3_INCLUDE_DIR)
    add_library(PkgConfig::FFTW3 UNKNOWN IMPORTED)
    set_target_properties(PkgConfig::FFTW3 PROPERTIES
      IMPORTED_LOCATION "${FFTW3_LIBRARY}" INTERFACE_INCLUDE_DIRECTORIES "${FFTW3_INCLUDE_DIR}")
    set(FFTW3_FOUND TRUE)
  endif()
endif()
if(NOT FFTW3_FOUND)
  ctsim_install_hint(_fftw_hint libfftw3-dev fftw fftw)
  message(FATAL_ERROR "FFTW3 (double precision) is required: ${_fftw_hint}")
endif()

# ---------------------------------------------------------------------------
# Optional dependencies
# ---------------------------------------------------------------------------
if(CTSIM_USE_OPENMP)
  # Apple's clang supports OpenMP but ships neither the runtime nor the
  # headers, so a plain find_package fails on macOS however the build is
  # configured.  Homebrew's "libomp" keg supplies both; point CMake at it
  # before asking, which is what the Homebrew instructions in INSTALL assume.
  if(APPLE AND NOT DEFINED OpenMP_CXX_FLAGS)
    find_program(_brew_executable brew)
    if(_brew_executable)
      execute_process(COMMAND ${_brew_executable} --prefix libomp
                      OUTPUT_VARIABLE _libomp_prefix
                      OUTPUT_STRIP_TRAILING_WHITESPACE
                      ERROR_QUIET RESULT_VARIABLE _libomp_result)
      if(_libomp_result EQUAL 0 AND EXISTS "${_libomp_prefix}/include/omp.h")
        set(OpenMP_CXX_FLAGS "-Xpreprocessor -fopenmp -I${_libomp_prefix}/include")
        set(OpenMP_CXX_LIB_NAMES omp)
        set(OpenMP_omp_LIBRARY "${_libomp_prefix}/lib/libomp.dylib")
        message(STATUS "OpenMP: using Homebrew libomp at ${_libomp_prefix}")
      endif()
    endif()
  endif()
  find_package(OpenMP COMPONENTS CXX)
  set(HAVE_OPENMP ${OpenMP_CXX_FOUND})
  if(APPLE AND NOT HAVE_OPENMP)
    message(STATUS "OpenMP not found: reconstruction and projection run on one "
                   "core.  On macOS, \"brew install libomp\" provides it.")
  endif()
endif()

if(CTSIM_USE_READLINE)
  # libedit first.  It provides the same readline() / add_history() entry points
  # through <editline/readline.h>, is BSD licensed, ships with macOS, and is what
  # Debian already links CTSim against -- so preferring it removes the need for a
  # distribution patch.
  if(CTSIM_READLINE STREQUAL "auto" OR CTSIM_READLINE STREQUAL "editline")
    find_path(EDITLINE_INCLUDE_DIR editline/readline.h)
    find_library(EDITLINE_LIBRARY NAMES edit)
    if(EDITLINE_INCLUDE_DIR AND EDITLINE_LIBRARY)
      set(HAVE_READLINE TRUE)
      set(HAVE_EDITLINE_READLINE_H TRUE)
      set(CTSIM_LINEEDIT_INCLUDE_DIR ${EDITLINE_INCLUDE_DIR})
      set(CTSIM_LINEEDIT_LIBRARY ${EDITLINE_LIBRARY})
      set(CTSIM_LINEEDIT_NAME "libedit")
    endif()
  endif()

  if(NOT HAVE_READLINE AND (CTSIM_READLINE STREQUAL "auto" OR CTSIM_READLINE STREQUAL "readline"))
    find_path(READLINE_INCLUDE_DIR readline/readline.h)
    find_library(READLINE_LIBRARY NAMES readline)
    if(READLINE_INCLUDE_DIR AND READLINE_LIBRARY)
      set(HAVE_READLINE TRUE)
      set(HAVE_READLINE_READLINE_H TRUE)
      set(CTSIM_LINEEDIT_INCLUDE_DIR ${READLINE_INCLUDE_DIR})
      set(CTSIM_LINEEDIT_LIBRARY ${READLINE_LIBRARY})
      set(CTSIM_LINEEDIT_NAME "GNU readline")
    endif()
  endif()

  if(HAVE_READLINE)
    # stifle_history caps the saved history.  GNU readline has it; the
    # libedit MSYS2 ships does not, and there the history is simply not
    # capped.
    include(CMakePushCheckState)
    cmake_push_check_state()
    set(CMAKE_REQUIRED_INCLUDES ${CTSIM_LINEEDIT_INCLUDE_DIR})
    set(CMAKE_REQUIRED_LIBRARIES ${CTSIM_LINEEDIT_LIBRARY})
    check_function_exists(stifle_history HAVE_STIFLE_HISTORY)
    cmake_pop_check_state()

    message(STATUS "shell line editing: ${CTSIM_LINEEDIT_NAME}")
  else()
    message(STATUS "shell line editing: none found")
  endif()
endif()

if(CTSIM_WITH_HDF5)
  # 6.5.0: HDF5 is the default format for newly written image and projection
  # files.  Reading detects the format from the file, so legacy files keep
  # working; a build without HDF5 simply keeps writing the legacy format.
  find_package(HDF5 COMPONENTS C)
  if(HDF5_FOUND)
    set(HAVE_HDF5 TRUE)
    message(STATUS "HDF5 ${HDF5_VERSION}: default format for new files")
  else()
    message(WARNING "HDF5 not found; CTSim will read and write only its legacy format")
  endif()
endif()

# DICOM is not optional.  It was, when the alternative was a library nobody
# packaged; DCMTK is now in every distribution CTSim is built on, and reading
# a scanner's own files is too much of the point of the program to be a
# build-time choice that half the builds turn off.
#
# DCMTK >= 3.6.5 ships DCMTKConfig.cmake (Debian/Ubuntu: libdcmtk-dev).
ctsim_install_hint(_dcmtk_hint libdcmtk-dev dcmtk dcmtk)
find_package(DCMTK QUIET)
if(NOT DCMTK_FOUND)
  message(FATAL_ERROR "DCMTK is required for DICOM support: ${_dcmtk_hint}")
endif()
set(HAVE_DCMTK TRUE)

# Debian/Ubuntu's DCMTKTargets.cmake lists "nsl" in DCMTK::ofstd's
# INTERFACE_LINK_LIBRARIES, so every consumer is asked to link -lnsl even
# though libnsl is only needed by DCMTK's network module (dcmnet), which CTSim
# does not use.  None of dcmdata, ofstd or oflog reference it and the linked
# binary ends up with no libnsl dependency, so drop it when the library is
# absent instead of making people install libnsl-dev for nothing.
find_library(CTSIM_NSL_LIBRARY nsl)
mark_as_advanced(CTSIM_NSL_LIBRARY)
if(NOT CTSIM_NSL_LIBRARY)
  foreach(_dcmtk_target DCMTK::ofstd DCMTK::oflog DCMTK::dcmdata)
    if(TARGET ${_dcmtk_target})
      get_target_property(_dcmtk_libs ${_dcmtk_target} INTERFACE_LINK_LIBRARIES)
      if(_dcmtk_libs)
        list(REMOVE_ITEM _dcmtk_libs nsl)
        set_target_properties(${_dcmtk_target} PROPERTIES
                              INTERFACE_LINK_LIBRARIES "${_dcmtk_libs}")
      endif()
    endif()
  endforeach()
  message(STATUS "DCMTK: libnsl not installed; removed from the link interface (unused here)")
endif()

message(STATUS "DICOM support via DCMTK ${DCMTK_VERSION}")

if(NOT CTSIM_GUI STREQUAL "qt" AND NOT CTSIM_GUI STREQUAL "none")
  message(FATAL_ERROR "CTSIM_GUI=${CTSIM_GUI}: the choices are qt and none.  "
          "\"wx\" and \"both\" went with the wxWidgets interface in 7.0.0; a build "
          "directory configured before then still has the old value in its cache -- "
          "reconfigure with -DCTSIM_GUI=qt.")
endif()
if(CTSIM_GUI STREQUAL "qt")
  # 6.4 is what Ubuntu 24.04 ships and is enough for the skeleton; later
  # milestones (QOpenGLWidget) may raise this to 6.5.
  # Svg ships separately from the base modules on Debian/Ubuntu (qt6-svg-dev),
  # which is the component most often missing; say so instead of leaving the
  # generic "Qt6_FOUND is FALSE" message to explain itself.
  # The 3-D view calls the fixed-function OpenGL API directly, so the GL
  # library has to be linked by name.  Qt supplies it as a side effect on
  # Linux and macOS, which is why this was never needed until Windows, where
  # the same calls came back as undefined references to __imp_glLoadIdentity
  # and its neighbours.  OpenGL::GL is opengl32 there, libGL on Linux and the
  # framework on macOS.
  set(OpenGL_GL_PREFERENCE GLVND)
  find_package(OpenGL REQUIRED)

  find_package(Qt6 6.4 QUIET COMPONENTS Core Gui Widgets OpenGLWidgets PrintSupport Svg Test)
  if(NOT Qt6_FOUND)
    set(_qt_missing "")
    foreach(_mod Core Gui Widgets OpenGLWidgets PrintSupport Svg)
      if(NOT TARGET Qt6::${_mod})
        list(APPEND _qt_missing ${_mod})
      endif()
    endforeach()
    message(FATAL_ERROR
      "Qt 6 (>= 6.4) with these components is required for CTSIM_GUI=${CTSIM_GUI}: "
      "Core Gui Widgets OpenGLWidgets PrintSupport Svg.  Missing: ${_qt_missing}\n"
      "  Debian/Ubuntu:  apt install qt6-base-dev qt6-base-dev-tools qt6-svg-dev libgl-dev\n"
      "  macOS:          brew install qt\n"
      "  Windows:        vcpkg install qtbase qtsvg\n"
      "  or point CMAKE_PREFIX_PATH at a Qt installation.")
  endif()
  qt_standard_project_setup()
  set(HAVE_QT TRUE)
  set(HAVE_SGP TRUE)
  message(STATUS "Qt ${Qt6_VERSION}: building ctsim")
endif()

# ---------------------------------------------------------------------------
# Platform probes (replace the autoconf checks)
# ---------------------------------------------------------------------------
# Only getopt.h is still consulted: every other header this loop probed for
# guarded an include the tree no longer needs, and each probe was a chance
# for a platform to be told something that then went unused.
check_include_file(getopt.h HAVE_GETOPT_H)
check_function_exists(getopt        HAVE_GETOPT)
check_function_exists(getopt_long   CTSIM_SYSTEM_GETOPT_LONG)
test_big_endian(WORDS_BIGENDIAN)
# Upstream quirk: HAVE_GETOPT_LONG really means "use the bundled getopt/ directory" (phase 1.5 renames it)
if(NOT CTSIM_SYSTEM_GETOPT_LONG)
  set(HAVE_GETOPT_LONG TRUE)
endif()

# The commit the program was built from, for the welcome message.  Empty
# when this is not a git checkout, which is the case for a source package.
set(CTSIM_GIT_COMMIT "")
find_package(Git QUIET)
if(GIT_FOUND AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git")
  # The commit, not "git describe": describe names the nearest reachable
  # tag, which before a release is whatever milestone was tagged last, so
  # a 7.0.0 build reported itself as so many commits after 7.0.0~m1.  The
  # message says "built from", and a commit is what that means.
  execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short=12 HEAD
                  WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
                  OUTPUT_VARIABLE CTSIM_GIT_COMMIT
                  OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
  # Uncommitted changes: the commit alone would not describe the build.
  execute_process(COMMAND ${GIT_EXECUTABLE} diff --quiet HEAD
                  WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
                  RESULT_VARIABLE _git_dirty ERROR_QUIET)
  if(NOT _git_dirty EQUAL 0 AND NOT CTSIM_GIT_COMMIT STREQUAL "")
    string(APPEND CTSIM_GIT_COMMIT "-dirty")
  endif()
endif()

configure_file(cmake/config.h.in "${CMAKE_BINARY_DIR}/config.h" @ONLY)

# ---------------------------------------------------------------------------
# Common interface target: flags + config.h + include/
# ---------------------------------------------------------------------------
add_library(ctsim_common INTERFACE)
target_include_directories(ctsim_common INTERFACE "${CMAKE_BINARY_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_compile_definitions(ctsim_common INTERFACE HAVE_CONFIG_H=1)

# ct.h -- which every CTSim source includes -- includes png.h, zlib.h and
# fftw3.h, so every target needs their include directories, not just the one
# that calls into them.  They are here rather than on ctsim_lib alone because
# on Linux these headers are in /usr/include and the omission is invisible,
# while on macOS with Homebrew (/opt/homebrew/include) and on Windows with
# vcpkg the build fails with "png.h file not found" in libctsupport.
target_link_libraries(ctsim_common INTERFACE PNG::PNG ZLIB::ZLIB PkgConfig::FFTW3)
if(CTSIM_WARNINGS AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
  # -Wvla and -Wunused-const-variable are what Apple clang reports by default
  # and GCC does not; asking GCC for them keeps the Linux build honest about
  # warnings that would otherwise only appear on macOS.  The "=1" level form
  # is GCC's own -- clang has the flag without a level and rejects it with
  # one -- so each compiler gets its spelling.
  target_compile_options(ctsim_common INTERFACE -Wall -Wextra -Wno-unused-parameter -Wvla)
  if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    target_compile_options(ctsim_common INTERFACE -Wunused-const-variable=1)
  else()
    target_compile_options(ctsim_common INTERFACE -Wunused-const-variable)
  endif()
elseif(CTSIM_WARNINGS AND MSVC)
  target_compile_options(ctsim_common INTERFACE /W3)
endif()
option(CTSIM_WERROR "Treat warnings as errors (CI)" OFF)
# Address and undefined-behaviour sanitizers, for a checking build.  Not a
# default: they cost several times the run time and the regression suite
# is timed.  6.2.0 reported the tree clean under both, but the rebinning
# and PPM paths had no test then and were not exercised.
option(CTSIM_SANITIZE "Build with AddressSanitizer and UBSan"           OFF)
if(CTSIM_WERROR AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
  target_compile_options(ctsim_common INTERFACE -Werror)
endif()

if(CTSIM_SANITIZE)
  if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
    # -fno-omit-frame-pointer so a report names the frames that matter.
    add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer -g)
    add_link_options(-fsanitize=address,undefined)
    # AddressSanitizer does not see an index past a vector's size but
    # within its capacity: the memory is the vector's own.  libstdc++
    # checks the index itself when asked, and several distributions build
    # every package that way, where such an index aborts the program.
    add_compile_definitions(_GLIBCXX_ASSERTIONS)
    message(STATUS "Sanitizers: address, undefined, libstdc++ assertions")
  else()
    message(WARNING "CTSIM_SANITIZE ignored: not GCC or Clang")
  endif()
endif()
if(CTSIM_NATIVE_ARCH AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
  target_compile_options(ctsim_common INTERFACE -march=native)
endif()
if(HAVE_OPENMP)
  target_link_libraries(ctsim_common INTERFACE OpenMP::OpenMP_CXX)
endif()
if(MSVC)
  target_compile_definitions(ctsim_common INTERFACE MSVC=1 _CRT_SECURE_NO_WARNINGS NOMINMAX)
endif()
if(WIN32)
  target_link_libraries(ctsim_common INTERFACE ws2_32)   # htonl/ntohl in fnetorderstream
endif()

# ---------------------------------------------------------------------------
# Libraries
# ---------------------------------------------------------------------------
if(HAVE_GETOPT_LONG)
  add_library(ctgetopt STATIC getopt/getopt.c getopt/getopt1.c)
  # config.h lives in the build directory; getopt.h includes it by name.
  target_include_directories(ctgetopt PRIVATE ${CMAKE_BINARY_DIR})
  target_include_directories(ctgetopt PUBLIC getopt)
  target_link_libraries(ctgetopt PRIVATE ctsim_common)
endif()

add_library(ctsupport STATIC
  libctsupport/strfuncs.cpp libctsupport/syserror.cpp libctsupport/fnetorderstream.cpp
  libctsupport/consoleio.cpp libctsupport/mathfuncs.cpp libctsupport/xform.cpp
  libctsupport/clip.cpp libctsupport/hashtable.cpp libctsupport/timeformat.cpp
  libctsupport/interpolator.cpp libctsupport/globalvars.cpp libctsupport/hooks.cpp)
target_link_libraries(ctsupport PUBLIC ctsim_common)
# 6.4.0: libctsupport is toolkit-neutral; it no longer includes src/ctsim.h or
# links the GUI toolkit.  Applications install handlers (see ctsupport_hooks.h).

# Library target is ctsim_lib (output libctsim.a); the GUI executable owns the plain name "ctsim".
add_library(ctsim_lib STATIC
  libctsim/filter.cpp libctsim/scanner.cpp libctsim/projections.cpp libctsim/phantom.cpp
  libctsim/imagefile.cpp libctsim/backprojectors.cpp libctsim/array2dfile.cpp libctsim/trace.cpp
  libctsim/procsignal.cpp libctsim/reconstruct.cpp libctsim/fourier.cpp libctsim/dicom.cpp
  libctsim/hdf5file.cpp
  libctsim/plotfile.cpp
  libctsim/pngmeta.cpp
)
set_target_properties(ctsim_lib PROPERTIES OUTPUT_NAME ctsim)
target_link_libraries(ctsim_lib PUBLIC ctsupport)   # PNG, zlib and FFTW3 come from ctsim_common
if(HAVE_GETOPT_LONG)
  target_link_libraries(ctsim_lib PUBLIC ctgetopt)
endif()
if(HAVE_DCMTK)
  target_link_libraries(ctsim_lib PUBLIC DCMTK::dcmdata DCMTK::ofstd DCMTK::oflog)
endif()
if(HAVE_HDF5)
  target_link_libraries(ctsim_lib PUBLIC ${HDF5_C_LIBRARIES})
  target_include_directories(ctsim_lib PUBLIC ${HDF5_C_INCLUDE_DIRS})
endif()

# libctgraphics: SGP (2-D canvas abstraction) + EZPlot. Needs a toolkit backend, so GUI builds only.
if(HAVE_SGP)
  add_library(ctgraphics STATIC libctgraphics/ezplot.cpp libctgraphics/ezset.cpp libctgraphics/pol.cpp
              libctgraphics/sgp.cpp libctgraphics/transformmatrix.cpp)
  target_link_libraries(ctgraphics PUBLIC ctsupport)
  if(HAVE_QT)
    # The QPainter backend (M1).  Only sgp_qt.cpp includes Qt headers.
    target_sources(ctgraphics PRIVATE libctgraphics/sgp_qt.cpp)
    target_link_libraries(ctgraphics PUBLIC Qt6::Gui)
  endif()
  target_link_libraries(ctsim_lib PUBLIC ctgraphics)   # trace hooks in scanner/reconstruct/phantom
endif()

# ---------------------------------------------------------------------------
# Command-line tools: one binary, busybox-style dispatch on argv[0]
# ---------------------------------------------------------------------------
# The functions and the shell that dispatches them, in a library of their
# own so that ctsim and ctsim-nogui share one copy.  Which one a person runs
# should not change what a function does.
add_library(ctsim_shell STATIC
  tools/shell.cpp tools/if1.cpp tools/if2.cpp tools/ifinfo.cpp tools/ifexport.cpp
  tools/phm2if.cpp tools/phm2pj.cpp tools/png2phm.cpp tools/pj2if.cpp tools/pjinfo.cpp tools/pjrec.cpp
  tools/phm2helix.cpp tools/pjHinterp.cpp tools/linogram.cpp
  tools/dicomimport.cpp tools/pjfourier.cpp tools/ctconvert.cpp tools/ctdump.cpp)
target_link_libraries(ctsim_shell PUBLIC ctsim_lib)
# So that a binary run from the build tree finds scripts/benchmarks.
target_compile_definitions(ctsim_shell PRIVATE CTSIM_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")

add_executable(ctsim-nogui tools/ctsim-nogui.cpp)
target_link_libraries(ctsim-nogui PRIVATE ctsim_shell)
if(HAVE_READLINE)
  target_include_directories(ctsim_shell PUBLIC ${CTSIM_LINEEDIT_INCLUDE_DIR})
  target_link_libraries(ctsim_shell PUBLIC ${CTSIM_LINEEDIT_LIBRARY})
endif()

set(CTSIM_TOOL_ALIASES if1 if2 ifexport ifinfo phm2if png2phm phm2pj phm2helix pj2if pjinfo pjrec pjHinterp linogram dicomimport pjfourier ctconvert ctdump)
# On Windows the executables name the libraries they need and CMake resolves
# the closure; see the block at the end of this file.  Everywhere else a
# shared library is found through the system's own mechanism and nothing is
# copied.
if(WIN32)
  set(_ctsim_dep_set RUNTIME_DEPENDENCY_SET ctsim_windows_runtime)
else()
  set(_ctsim_dep_set "")
endif()
install(TARGETS ctsim-nogui ${_ctsim_dep_set}
        RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT Tools)
if(WIN32)
  foreach(alias ${CTSIM_TOOL_ALIASES})
    file(WRITE "${CMAKE_BINARY_DIR}/shims/${alias}.cmd" "@\"%~dp0ctsim-nogui.exe\" ${alias} %*\n")
    install(PROGRAMS "${CMAKE_BINARY_DIR}/shims/${alias}.cmd" DESTINATION ${CMAKE_INSTALL_BINDIR})
  endforeach()
else()
  # CMAKE_INSTALL_PREFIX is escaped so that it is read when the install
  # runs, where "cmake --install --prefix" has set it.  The FULL_ form of
  # the directory is fixed at configure time, and sent the links to the
  # configured prefix whatever prefix the install was given.
  foreach(alias ${CTSIM_TOOL_ALIASES})
    install(CODE "execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink ctsim-nogui \
      \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/${alias}\")")
  endforeach()
endif()
# Sample scripts.  These were @prefix@ templates substituted by autoconf; with
# autoconf gone they are configured here and installed alongside the
# documentation.
foreach(_sample tools/sample-ctsim)
  get_filename_component(_sample_name ${_sample} NAME)
  configure_file(${CMAKE_CURRENT_SOURCE_DIR}/${_sample}.sh.in
                 ${CMAKE_CURRENT_BINARY_DIR}/${_sample_name}.sh @ONLY)
  install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/${_sample_name}.sh
          DESTINATION ${CMAKE_INSTALL_DATADIR}/ctsim/examples COMPONENT Tools)
endforeach()
# The phantom generator phm2helix calls, one phantom per view.  Source
# rather than a binary: it is a page of C, and tests/helical.sh compiles it.
install(FILES scripts/helical/dynphm.c DESTINATION ${CMAKE_INSTALL_DATADIR}/ctsim/examples COMPONENT Tools)
# The demonstration script, which Help > Run Demonstration looks for.
install(DIRECTORY scripts/benchmarks/ DESTINATION ${CMAKE_INSTALL_DATADIR}/ctsim/benchmarks
        COMPONENT Tools FILES_MATCHING PATTERN "*.bench")
install(DIRECTORY scripts/demos/ DESTINATION ${CMAKE_INSTALL_DATADIR}/ctsim/demos
        COMPONENT Tools FILES_MATCHING PATTERN "*.ctsim")

file(GLOB CTSIM_MAN_PAGES "${CMAKE_CURRENT_SOURCE_DIR}/man/*.1")
install(FILES ${CTSIM_MAN_PAGES} DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 COMPONENT Tools)

# ---------------------------------------------------------------------------

# ---------------------------------------------------------------------------
# The graphical application (src/qt/)
# ---------------------------------------------------------------------------
if(HAVE_QT)
  # The application is "ctsim".  It was "ctsim-qt" through the port, while
  # the wxWidgets application still existed and owned the name.
  #
  # On Windows the executable carries an icon and version information, which
  # Explorer shows in a file's Properties; on macOS the bundle carries the
  # same icon as .icns.  Both come from packaging/.
  set(_ctsim_platform_sources)
  if(WIN32)
    configure_file(${CMAKE_CURRENT_SOURCE_DIR}/packaging/ctsim.rc.in
                   ${CMAKE_BINARY_DIR}/ctsim.rc @ONLY)
    list(APPEND _ctsim_platform_sources ${CMAKE_BINARY_DIR}/ctsim.rc)
  elseif(APPLE)
    # The bundle's icon.  Without it macOS shows a blank document icon, and
    # this was missing until 7.0.0~m8 because the file did not exist and the
    # rule below was silently skipped -- hence the check rather than a quiet
    # "if it is there".
    set(_ctsim_icns ${CMAKE_CURRENT_SOURCE_DIR}/packaging/ctsim.icns)
    if(NOT EXISTS ${_ctsim_icns})
      message(FATAL_ERROR "packaging/ctsim.icns is missing; the application "
              "bundle would have no icon.  Generate it from packaging/ctsim.svg: "
              "rsvg-convert -w N -h N packaging/ctsim.svg -o N.png for "
              "16 32 48 128 256 512, then png2icns (Linux) or iconutil (macOS).")
    endif()
    list(APPEND _ctsim_platform_sources ${_ctsim_icns})
    set_source_files_properties(${_ctsim_icns} PROPERTIES
                                MACOSX_PACKAGE_LOCATION Resources)
    set(MACOSX_BUNDLE_ICON_FILE ctsim.icns)
    set(MACOSX_BUNDLE_BUNDLE_NAME CTSim)
    set(MACOSX_BUNDLE_GUI_IDENTIFIER org.ctsim.CTSim)
    set(MACOSX_BUNDLE_BUNDLE_VERSION "${CTSIM_VERSION_FULL}")
    set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}")
    set(MACOSX_BUNDLE_COPYRIGHT "Copyright (C) 1983-2026 Kevin M. Rosenberg")
  endif()

  qt_add_executable(ctsim WIN32 MACOSX_BUNDLE
    ${_ctsim_platform_sources}
    src/qt/main.cpp src/qt/app.cpp src/qt/mainwindow.cpp src/qt/shellwindow.cpp
    src/qt/document.cpp src/qt/documentwindow.cpp src/qt/scriptcaption.cpp src/qt/format.cpp src/qt/manual.cpp src/qt/printing.cpp src/qt/plotdocument.cpp src/qt/imagedocument.cpp
    src/qt/projectiondocument.cpp src/qt/phantomdocument.cpp
    src/qt/docmanager.cpp src/qt/scriptrunner.cpp
    src/qt/views/sgpcanvas.cpp src/qt/views/plotview.cpp src/qt/views/imageview.cpp
    src/qt/views/textview.cpp
    src/qt/views/projectionview.cpp src/qt/views/phantomview.cpp
    src/qt/views/graph3dview.cpp src/qt/dialogs/tracedialog.cpp
    src/qt/jobs/job.cpp src/qt/jobs/ctjobs.cpp src/qt/jobs/jobmanager.cpp src/qt/jobs/jobsdock.cpp
    src/qt/dialogs/scaledialogs.cpp src/qt/dialogs/imagedialogs.cpp
    src/qt/dialogs/propertiesdialog.cpp src/qt/dialogs/settingsdialog.cpp
    src/qt/dialogs/dialogmemory.cpp src/qt/dialogs/tipdialog.cpp
    src/qt/app.h src/qt/mainwindow.h src/qt/document.h src/qt/documentwindow.h src/qt/format.h
    src/qt/manual.h src/qt/printing.h src/qt/plotdocument.h
    src/qt/imagedocument.h src/qt/docmanager.h
    src/qt/projectiondocument.h src/qt/phantomdocument.h src/qt/scriptrunner.h
    src/qt/views/sgpcanvas.h src/qt/views/plotview.h src/qt/views/imageview.h
    src/qt/views/textview.h
    src/qt/views/projectionview.h src/qt/views/phantomview.h
    src/qt/views/graph3dview.h src/qt/dialogs/tracedialog.h
    src/qt/jobs/job.h src/qt/jobs/ctjobs.h src/qt/jobs/jobmanager.h src/qt/jobs/jobsdock.h
    src/qt/dialogs/scaledialogs.h src/qt/dialogs/imagedialogs.h
    src/qt/dialogs/propertiesdialog.h src/qt/dialogs/settingsdialog.h
    src/qt/dialogs/dialogmemory.h src/qt/dialogs/tipdialog.h
    src/shared/tipstrings.cpp)
  # Icons come straight from packaging/ so there is one copy in the tree.
  qt_add_resources(ctsim "ctsim_resources"
    PREFIX "/"
    BASE packaging
    FILES packaging/ctsim.svg packaging/icons/ctsim-256.png)
  target_include_directories(ctsim PRIVATE src/qt src/shared ${CMAKE_BINARY_DIR})
  target_compile_definitions(ctsim PRIVATE
    CTSIM_INSTALL_DOCDIR="${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DOCDIR}")
  if(APPLE)
    # The 3-D view uses the fixed-function OpenGL API, which macOS has marked
    # deprecated since 10.14 but still ships.  Apple's headers warn on every
    # call -- 29 of them -- and the only way to stop that short of rewriting
    # the view for shaders is this definition, which Qt's own examples use.
    target_compile_definitions(ctsim PRIVATE GL_SILENCE_DEPRECATION)
  endif()
  target_link_libraries(ctsim PRIVATE ctsim_shell ctsim_lib
    Qt6::Core Qt6::Gui Qt6::Widgets Qt6::OpenGLWidgets Qt6::PrintSupport Qt6::Svg
    OpenGL::GL)
  target_link_libraries(ctsim PRIVATE ctgraphics)
  # Install components: Runtime is the application, Tools the command-line
  # programs with their man pages and examples, Documentation the manual.
  #
  # On macOS the application is a bundle and its home is /Applications, not
  # /usr/local; "BUNDLE DESTINATION ." puts ctsim.app at the top of whatever
  # prefix is given, so
  #     cmake --install build --component Runtime --prefix /Applications
  # is the manual install, and cpack's disk image has ctsim.app at its root.
  install(TARGETS ctsim ${_ctsim_dep_set}
          RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT Runtime
          BUNDLE DESTINATION . COMPONENT Runtime)
endif()

if(HAVE_QT)
  # Desktop entry and icons, so the graphical program appears in menus.
  if(UNIX AND NOT APPLE)
    install(FILES packaging/ctsim.desktop COMPONENT Runtime
            DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
    install(FILES packaging/ctsim.svg COMPONENT Runtime
            DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps)
    foreach(_size 16 22 24 32 48 64 128 256)
      install(FILES packaging/icons/ctsim-${_size}.png COMPONENT Runtime
              DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/${_size}x${_size}/apps
              RENAME ctsim.png)
    endforeach()
  endif()
endif()

# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
if(CTSIM_BUILD_TESTS)
  enable_testing()
  # Unit tests: pieces with analytically known behaviour (Siddon integrals,
  # file round trips, non-square images), as opposed to whole-pipeline goldens.
  add_executable(ctsim_unit_tests tests/unit_tests.cpp)
  target_link_libraries(ctsim_unit_tests PRIVATE ctsim_shell ctsim_lib)
  # Qt6::Test drives the shell window's keys; it exists only in a Qt build.
  if(CTSIM_GUI STREQUAL "qt")
    target_link_libraries(ctsim_unit_tests PRIVATE Qt6::Test)
  endif()
  # The plot-command reader lives in ctgraphics, which exists only when the
  # build has a canvas to draw on; its tests are compiled out otherwise.
  if(HAVE_SGP)
    target_link_libraries(ctsim_unit_tests PRIVATE ctgraphics)
  endif()
  if(HAVE_QT)
    # The SGP backend test renders offscreen; it needs a QGuiApplication.
    # The M2 tests construct real widgets, so Qt6::Widgets as well, and they
    # compile the Qt sources they exercise.
    target_sources(ctsim_unit_tests PRIVATE
      src/qt/document.cpp src/qt/documentwindow.cpp src/qt/scriptcaption.cpp src/qt/format.cpp src/qt/manual.cpp src/qt/printing.cpp src/qt/manual.cpp src/qt/format.cpp src/qt/imagedocument.cpp src/qt/plotdocument.cpp
      src/qt/projectiondocument.cpp src/qt/phantomdocument.cpp
      src/qt/docmanager.cpp src/qt/scriptrunner.cpp src/qt/app.cpp
      src/qt/views/imageview.cpp src/qt/views/sgpcanvas.cpp src/qt/views/plotview.cpp
      src/qt/views/textview.cpp
    src/qt/views/projectionview.cpp src/qt/views/phantomview.cpp src/qt/mainwindow.cpp src/qt/shellwindow.cpp
      src/qt/jobs/job.cpp src/qt/jobs/ctjobs.cpp src/qt/jobs/jobmanager.cpp
      src/qt/jobs/jobsdock.cpp src/qt/views/graph3dview.cpp src/qt/dialogs/tracedialog.cpp
      src/qt/dialogs/scaledialogs.cpp src/qt/dialogs/imagedialogs.cpp
    src/qt/dialogs/propertiesdialog.cpp src/qt/dialogs/settingsdialog.cpp
    src/qt/dialogs/dialogmemory.cpp src/qt/dialogs/tipdialog.cpp
      src/shared/tipstrings.cpp)
    target_compile_definitions(ctsim_unit_tests PRIVATE
  CTSIM_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
    if(APPLE)
      target_compile_definitions(ctsim_unit_tests PRIVATE GL_SILENCE_DEPRECATION)
    endif()
target_include_directories(ctsim_unit_tests PRIVATE src/qt src/shared)
    # The tests construct the main window, which uses the application icons;
    # without the resources linked Qt logs a missing-file warning on every run.
    qt_add_resources(ctsim_unit_tests "ctsim_test_resources"
      PREFIX "/" BASE packaging
      FILES packaging/ctsim.svg packaging/icons/ctsim-256.png)
    # ctgraphics is linked by the HAVE_SGP block above, which is true
    # whenever HAVE_QT is; naming it here as well made the linker report a
    # duplicate library.
    target_link_libraries(ctsim_unit_tests PRIVATE Qt6::Gui Qt6::Widgets
      Qt6::OpenGLWidgets Qt6::PrintSupport OpenGL::GL)
  endif()
  # One ctest entry per group, so ctest names what ran and a group can be run
  # alone.  The ids are the ones ctsim_unit_tests --list prints.
  set(CTSIM_UNIT_GROUPS siddon phantom image nonsquare netorder plotfile plothdf5 pngphm hdf5 history)
  if(HAVE_QT)
    list(APPEND CTSIM_UNIT_GROUPS sgpqt imageview projphm jobs graph3d trace dialogmem helpclip)
  endif()
  foreach(_group ${CTSIM_UNIT_GROUPS})
    add_test(NAME unit.${_group} COMMAND ctsim_unit_tests --group ${_group}
             WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
  endforeach()
  if(HAVE_QT)
    # The Qt group renders offscreen and needs a platform plugin.
    # Only these two build widgets; the others never touch Qt at run time.
    set_tests_properties(unit.sgpqt unit.imageview unit.projphm unit.jobs unit.graph3d
                         unit.trace unit.dialogmem unit.helpclip PROPERTIES
                         ENVIRONMENT "QT_QPA_PLATFORM=offscreen;QT_QPA_OFFSCREEN_NO_GLX=1"
                         ENVIRONMENT_MODIFICATION "DISPLAY=unset:")
  endif()

  # The tool man pages are generated from the tools' own --help (see
  # man/sync-options.py); this fails when a tool's options change without the
  # pages being regenerated.
  find_package(Python3 COMPONENTS Interpreter QUIET)
  if(Python3_Interpreter_FOUND)
    add_test(NAME man_pages
             COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/man/sync-options.py --check
                     $<TARGET_FILE:ctsim-nogui>)
  endif()
  # Every function the shell offers must also be installed under its own
  # name.  png2phm was missing from CTSIM_TOOL_ALIASES and so had no symlink
  # in the package, which nothing caught: the function worked through the
  # shell, and only an installed tree showed the gap.
  add_test(NAME tool_aliases
           COMMAND sh ${CMAKE_CURRENT_SOURCE_DIR}/tests/check-aliases.sh
                      $<TARGET_FILE:ctsim-nogui> ${CTSIM_TOOL_ALIASES})

  # The helical chain -- phm2helix, pjHinterp, pjrec -- which nothing else
  # exercises.  It builds scripts/helical/dynphm.c, and where there is no
  # compiler to build it with it exits 77, so ctest reports a skip and not
  # a pass.
  add_test(NAME helical
           COMMAND sh ${CMAKE_CURRENT_SOURCE_DIR}/tests/helical.sh
                      $<TARGET_FILE:ctsim-nogui> ${CMAKE_C_COMPILER})
  set_tests_properties(helical PROPERTIES SKIP_RETURN_CODE 77)

  # The install rules name files by path, and nothing in the build reads
  # those paths: a file that moves leaves the build and every other test
  # green and "cmake --install" broken, which is how a package build finds
  # out.  So install into a scratch prefix.  Linux only: on Windows and
  # macOS the install step deploys Qt and signs, which is packaging work
  # and not a test.
  if(UNIX AND NOT APPLE)
    add_test(NAME install_tree
             COMMAND ${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR}
                     --prefix ${CMAKE_BINARY_DIR}/install-check)
  endif()

  if(HAVE_QT)
    # GUI parity: the same operations run through the Qt application and
    # through ctsim-nogui must produce identical files (tests/gui-parity.sh).
    add_test(NAME gui_parity
             COMMAND sh ${CMAKE_CURRENT_SOURCE_DIR}/tests/gui-parity.sh
                        $<TARGET_FILE:ctsim-nogui> $<TARGET_FILE:ctsim>
             WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
    set_tests_properties(gui_parity PROPERTIES
                         ENVIRONMENT "QT_QPA_PLATFORM=offscreen;QT_QPA_OFFSCREEN_NO_GLX=1"
                         ENVIRONMENT_MODIFICATION "DISPLAY=unset:")
  
    # The same operations in sequence, driven by a script, against the same
    # pipeline run with ctsim-nogui.
    add_test(NAME gui_sequence
             COMMAND sh ${CMAKE_CURRENT_SOURCE_DIR}/tests/gui-sequence.sh
                        $<TARGET_FILE:ctsim-nogui> $<TARGET_FILE:ctsim>
             WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
    set_tests_properties(gui_sequence PROPERTIES
                         ENVIRONMENT "QT_QPA_PLATFORM=offscreen;QT_QPA_OFFSCREEN_NO_GLX=1"
                         ENVIRONMENT_MODIFICATION "DISPLAY=unset:")

endif()

  # Named for what it is.  It was "herman_suite" when it did nothing but
  # compare herman reconstructions against golden files; it now covers the
  # raster phantoms, DICOM, the Fourier reconstructions and the unit tests
  # as well, and a failure in any of those was reported under the name of a
  # phantom that had nothing to do with it.
  add_test(NAME regression_suite
           COMMAND sh ${CMAKE_CURRENT_SOURCE_DIR}/tests/run-tests.sh run $<TARGET_FILE:ctsim-nogui>)
  set_tests_properties(regression_suite PROPERTIES ENVIRONMENT
                       "CTSIM_REQUIRE_RASTER=1;CTSIM_SKIP_PYTHON_TESTS=1")

  # The built binary must agree with the newest ChangeLog entry.  Its own
  # test, because it asks a question about the release rather than about a
  # reconstruction, and a failure should say so in its name.
  add_test(NAME version_changelog
           COMMAND sh ${CMAKE_CURRENT_SOURCE_DIR}/tests/run-tests.sh version
                      $<TARGET_FILE:ctsim-nogui>)

  # Physics validation: CTSim's reconstruction of the raw DICOM scan against the
  # scanner's own reconstruction.  Registered separately so it can be run alone,
  # and so a missing python3/numpy shows up as "skipped" rather than a failure.
  find_package(Python3 COMPONENTS Interpreter QUIET)
  if(Python3_Interpreter_FOUND)
    add_test(NAME dicom_physics_validation
             COMMAND ${Python3_EXECUTABLE}
                     ${CMAKE_CURRENT_SOURCE_DIR}/tests/validate_dicom_recon.py
                     $<TARGET_FILE:ctsim-nogui>)
    set_tests_properties(dicom_physics_validation PROPERTIES SKIP_RETURN_CODE 77)
  endif()
endif()

# ---------------------------------------------------------------------------
# Manual (6.4.0: Markdown + pandoc, replacing the unmaintained tex2rtf)
# ---------------------------------------------------------------------------
# The manual is built from doc/manual/*.md by pandoc; the PDF additionally
# needs a LaTeX engine and rsvg-convert for the figures.  Each is looked for
# and reported, because without pandoc the "manual" targets do not exist at
# all and "cmake --build . --target manual" then fails with nothing more
# helpful than "unknown target".
# A manual built elsewhere and committed to the tree.  Used when pandoc is
# not installed, which is every Windows machine and any minimal build host.
set(CTSIM_MANUAL_ARTIFACTS "${CMAKE_CURRENT_SOURCE_DIR}/artifacts/manual"
    CACHE PATH "A prebuilt manual to use when pandoc is not available")

find_program(PANDOC_EXECUTABLE pandoc)
find_program(PDFLATEX_EXECUTABLE pdflatex
             HINTS /Library/TeX/texbin /usr/local/texlive/bin)
find_program(RSVG_CONVERT_EXECUTABLE rsvg-convert)
if(PANDOC_EXECUTABLE)
  # How to ask for MathML.  pandoc spells this --mathml up to 3.1 and
  # --math-method=mathml after, deprecating the old name; neither accepts
  # the other.  Asked here rather than assumed, because the answer differs
  # between the pandoc on a current Ubuntu and the one on a current macOS.
  # The probe converts a real document: --version returns before the
  # option is parsed, so it would accept anything.
  file(WRITE "${CMAKE_BINARY_DIR}/pandoc-probe.md" "probe\n")
  execute_process(
    COMMAND ${PANDOC_EXECUTABLE} --math-method=mathml -f markdown -t html
            -o ${CMAKE_BINARY_DIR}/pandoc-probe.html
            ${CMAKE_BINARY_DIR}/pandoc-probe.md
    RESULT_VARIABLE _pandoc_math_new OUTPUT_QUIET ERROR_QUIET)
  if(_pandoc_math_new EQUAL 0)
    set(CTSIM_PANDOC_MATH "--math-method=mathml")
  else()
    set(CTSIM_PANDOC_MATH "--mathml")
  endif()
  message(STATUS "Manual: MathML via ${CTSIM_PANDOC_MATH}")

  # The LaTeX fragment needs the icon's absolute path, so it is configured.
  set(CTSIM_LOGO_PDF "${CMAKE_BINARY_DIR}/manual/ctsim-logo.pdf")
  configure_file(doc/manual/logo.tex.in "${CMAKE_BINARY_DIR}/manual-logo.tex" @ONLY)

  set(CTSIM_MANUAL_CHAPTERS
      concepts install gui textui algorithms sgp)
  set(_manual_sources)
  foreach(_chapter ${CTSIM_MANUAL_CHAPTERS})
    list(APPEND _manual_sources "${CMAKE_CURRENT_SOURCE_DIR}/doc/manual/${_chapter}.md")
  endforeach()

  add_custom_target(manual
    COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/manual
    # The HTML refers to figures/ relative to itself, so they travel with it.
    COMMAND ${CMAKE_COMMAND} -E copy_directory
            ${CMAKE_CURRENT_SOURCE_DIR}/doc/manual/figures
            ${CMAKE_BINARY_DIR}/manual/figures
    # The logo the fragment refers to, beside the HTML that refers to it.
    COMMAND ${CMAKE_COMMAND} -E copy
            ${CMAKE_CURRENT_SOURCE_DIR}/packaging/ctsim.svg
            ${CMAKE_BINARY_DIR}/manual/ctsim-logo.svg
    COMMAND ${PANDOC_EXECUTABLE} --standalone --toc --metadata=title:CTSim-${CTSIM_VERSION_FULL}-User-Manual
            --resource-path=${CMAKE_CURRENT_SOURCE_DIR}/doc/manual
            ${CTSIM_PANDOC_MATH}
            --include-before-body=${CMAKE_CURRENT_SOURCE_DIR}/doc/manual/logo.html
            -o ${CMAKE_BINARY_DIR}/manual/ctsim.html ${_manual_sources}
    DEPENDS ${_manual_sources}
    COMMENT "Building the manual (HTML) into ${CMAKE_BINARY_DIR}/manual")

  add_custom_target(manual-pdf
    COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/manual
    COMMAND ${CMAKE_COMMAND} -E copy_directory
            ${CMAKE_CURRENT_SOURCE_DIR}/doc/manual/figures
            ${CMAKE_BINARY_DIR}/manual/figures
    # --resource-path lets pandoc find figures/ from the source tree whatever
    # the working directory; the SVG figures are converted by rsvg-convert on
    # the way into LaTeX.  See doc/manual/README.md for the packages needed.
    # pdflatex cannot read SVG, so the icon is converted once here; the
    # LaTeX fragment includes the result by name.
    COMMAND rsvg-convert --format=pdf --width=256
            --output=${CMAKE_BINARY_DIR}/manual/ctsim-logo.pdf
            ${CMAKE_CURRENT_SOURCE_DIR}/packaging/ctsim.svg
    COMMAND ${PANDOC_EXECUTABLE} --standalone --toc --number-sections
            --pdf-engine=pdflatex
            --resource-path=${CMAKE_CURRENT_SOURCE_DIR}/doc/manual
            --resource-path=${CMAKE_BINARY_DIR}/manual
            --metadata=title:CTSim-${CTSIM_VERSION_FULL}-User-Manual
            -V geometry:margin=1in -V colorlinks=true
            --include-in-header=${CMAKE_BINARY_DIR}/manual-logo.tex
            -o ${CMAKE_BINARY_DIR}/manual/ctsim.pdf ${_manual_sources}
    DEPENDS ${_manual_sources}
    COMMENT "Building the manual (PDF); needs pdflatex and rsvg-convert, see doc/manual/README.md")

  ctsim_install_hint(_rsvg_hint librsvg2-bin librsvg librsvg)
  if(PDFLATEX_EXECUTABLE AND RSVG_CONVERT_EXECUTABLE)
    message(STATUS "Manual: targets \"manual\" (HTML) and \"manual-pdf\" (PDF)")
  elseif(PDFLATEX_EXECUTABLE)
    message(STATUS "Manual: targets \"manual\" and \"manual-pdf\"; rsvg-convert not "
                   "found, so the PDF will have no figures "
                   "(${_rsvg_hint})")
  else()
    message(STATUS "Manual: target \"manual\" (HTML).  No LaTeX engine, so "
                   "\"manual-pdf\" will fail "
                   "(brew install basictex, then add /Library/TeX/texbin to PATH | "
                   "apt install texlive-latex-recommended lmodern)")
  endif()

elseif(EXISTS "${CTSIM_MANUAL_ARTIFACTS}/ctsim.html")
  # No pandoc, but a manual built earlier is in the tree.  It is the same on
  # every platform, so a copy is as good as a build -- and this is the only
  # way to package a manual on Windows, where pandoc is not available at all.
  #
  # Part of the default build rather than a target to be asked for: the
  # point of it is that a package built with no thought about the manual
  # still has one.  It is a file copy, so it costs nothing.
  add_custom_target(manual ALL
    COMMAND ${CMAKE_COMMAND} -E copy_directory
            "${CTSIM_MANUAL_ARTIFACTS}" "${CMAKE_BINARY_DIR}/manual"
    COMMENT "Copying the prebuilt manual from artifacts/manual (pandoc not found)")
  # manual-pdf is the same copy, so that whatever asks for either gets the
  # manual rather than an error about a target that does not exist.
  add_custom_target(manual-pdf DEPENDS manual)

  set(_manual_have "")
  foreach(_f ctsim.html ctsim.pdf)
    if(EXISTS "${CTSIM_MANUAL_ARTIFACTS}/${_f}")
      string(APPEND _manual_have " ${_f}")
    endif()
  endforeach()
  message(STATUS "Manual: pandoc not found; using the prebuilt"
                 "${_manual_have} from artifacts/manual")
else()
  ctsim_install_hint(_pandoc_hint pandoc pandoc "")
  if(_pandoc_hint STREQUAL "")
    set(_pandoc_hint "MSYS2 has no pandoc package; install it from https://pandoc.org")
  endif()
  message(STATUS "Manual: pandoc not found and artifacts/manual is not there, "
                 "so there is no manual to install (${_pandoc_hint}); "
                 "see doc/manual/README.md")
endif()

# Installed when the files are there, whether this build made them or not.
# The manual is the same on every platform, so it can be built once -- on a
# machine that has pandoc -- and its output copied into ${CMAKE_BINARY_DIR}
# to be packaged somewhere that has not.  OPTIONAL: a build without it
# installs the rest and the Help menu reports that it found nothing.
# Which manual gets installed is decided when install runs, not when cmake
# configures, because the build directory's manual may be made after this
# file is read -- "cmake --build . --target manual" is a separate step, and
# with pandoc present the manual target is not part of the default build.
# The rule: whatever is in the build directory wins; failing that, the
# prebuilt manual in artifacts/manual.  Configuring on the pandoc test
# instead meant that a machine with pandoc, where the manual had not been
# asked for, packaged no manual at all even though artifacts/manual held
# one.
install(CODE "
  set(_src \"${CMAKE_BINARY_DIR}/manual\")
  set(_from \"the build directory\")
  if(NOT EXISTS \"\${_src}/ctsim.html\" AND NOT EXISTS \"\${_src}/ctsim.pdf\")
    set(_src \"${CTSIM_MANUAL_ARTIFACTS}\")
    set(_from \"artifacts/manual\")
  endif()
  # Built from CMAKE_INSTALL_PREFIX as the install script sees it, not from
  # the absolute path this file computed: cpack redirects the prefix to a
  # staging directory, and a hardcoded path writes outside the package.
  set(_doc \"\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DOCDIR}\")
  set(_any OFF)
  foreach(_f ctsim.html ctsim.pdf ctsim-logo.svg)   # the logo the HTML refers to
    if(EXISTS \"\${_src}/\${_f}\")
      file(INSTALL \"\${_src}/\${_f}\" DESTINATION \"\${_doc}\")
      set(_any ON)
    endif()
  endforeach()
  if(EXISTS \"\${_src}/figures\")
    file(INSTALL \"\${_src}/figures\" DESTINATION \"\${_doc}\")
  endif()
  if(_any)
    message(STATUS \"Manual installed from \${_from}\")
  else()
    message(STATUS \"No manual to install: neither the build directory nor \"
                   \"artifacts/manual has one\")
  endif()
" COMPONENT Documentation)

# ---------------------------------------------------------------------------
# Packaging
# ---------------------------------------------------------------------------
set(CPACK_PACKAGE_NAME ctsim)
set(CPACK_PACKAGE_VERSION "${CTSIM_VERSION_FULL}")   # 7.0.0~m0: Debian orders the tilde correctly
set(CPACK_PACKAGE_VENDOR "Kevin M. Rosenberg")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Computed tomography simulator")
set(CPACK_PACKAGE_CONTACT "Kevin M. Rosenberg <kevin@rosenberg.net>")
# The agreement page both installers show.  As plain COPYING it began in
# the middle of a sentence about the Herman phantom and never said what was
# being installed or by whom; NSIS and the macOS disk image both render
# RTF, so it is written out with the logo and a heading above the terms.
set(_ctsim_license_rtf "${CMAKE_BINARY_DIR}/ctsim-license.rtf")
set(_ctsim_license_args
    -DCOPYING_IN=${CMAKE_CURRENT_SOURCE_DIR}/COPYING
    -DLOGO_IN=${CMAKE_CURRENT_SOURCE_DIR}/packaging/license-logo.png
    -DRTF_OUT=${_ctsim_license_rtf}
    -DCTSIM_VERSION=${CTSIM_VERSION_FULL}
    -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/make-license-rtf.cmake)
# Written now, because CPack checks at configure time that the licence it
# has been given exists.  Written again at build time by the rule below, so
# that editing COPYING does not need cmake to be run again.
execute_process(COMMAND ${CMAKE_COMMAND} ${_ctsim_license_args}
                RESULT_VARIABLE _license_result)
if(NOT _license_result EQUAL 0)
  message(FATAL_ERROR "could not write ${_ctsim_license_rtf}")
endif()
add_custom_command(OUTPUT "${_ctsim_license_rtf}"
  COMMAND ${CMAKE_COMMAND} ${_ctsim_license_args}
  DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/COPYING"
          "${CMAKE_CURRENT_SOURCE_DIR}/packaging/license-logo.png"
          "${CMAKE_CURRENT_SOURCE_DIR}/cmake/make-license-rtf.cmake"
  COMMENT "Writing the installer licence")
add_custom_target(ctsim_license_rtf ALL DEPENDS "${_ctsim_license_rtf}")
set(CPACK_RESOURCE_FILE_LICENSE "${_ctsim_license_rtf}")
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
set(CPACK_DEBIAN_PACKAGE_SECTION science)
set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "http://www.ctsim.org/")
set(CPACK_GENERATOR "TGZ")
# Packages, and cpack's _CPack_Packages staging directory, go to the build
# directory.  cpack's own default is the directory it is run from, which for
# "cpack --config build/CPackConfig.cmake" at the top of the tree means the
# source tree -- a disk image and a staging tree dropped among the sources.
set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}")

# CPack also builds source tarballs: "cmake --build build --target package_source"
# produces one from the working tree, excluding build directories and editor
# leftovers.  This is separate from the binary packages above.
# ZIP as well: it is what a Windows machine opens without being given a
# tool, and what a release page is usually asked for alongside the tarball.
set(CPACK_SOURCE_GENERATOR "TGZ;TXZ;ZIP")
set(CPACK_SOURCE_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CTSIM_VERSION_FULL}")
set(CPACK_SOURCE_IGNORE_FILES
    "/build.*/"
    "/\\\\.git/"
    "/\\\\.gitlab-ci\\\\.yml$"
    "~$"
    "\\\\.o$"
    "\\\\.a$"
    "/CMakeCache\\\\.txt$"
    "/CMakeFiles/"
    "/__pycache__/"
    "\\\\.pyc$"
    "/_CPack_Packages/"       # cpack's own staging, when run inside the tree
    "/\\\\.vscode/"
    "/\\\\.idea/"
    "/\\\\.DS_Store$"
    "/b/"                     # the usual build directory
    "/\\\\.cache/")
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
  list(APPEND CPACK_GENERATOR DEB)
endif()

# Relocatable packages for macOS and Windows.  A Qt application cannot simply
# be copied to another machine: the Qt libraries and the platform plugin have
# to travel with it.  Qt's own deployment support does that at install time,
# and it deploys only the Qt modules and plugins the application actually
# links.
#
# macdeployqt still reports that it cannot resolve QtPdf and
# QtVirtualKeyboard on a Homebrew machine.  Those are named by plugins Qt
# ships, not by anything CTSim uses, and Homebrew keeps them in formulae
# separate from qtbase; the messages say ERROR but deployment carries on and
# the bundle runs.  Installing the full "qt" formula rather than qtbase and
# qtsvg silences them.
if(HAVE_QT AND (APPLE OR WIN32))
  qt_generate_deploy_app_script(TARGET ctsim
                                OUTPUT_SCRIPT _ctsim_deploy_script
                                NO_UNSUPPORTED_PLATFORM_ERROR
                                NO_TRANSLATIONS)
  install(SCRIPT ${_ctsim_deploy_script} COMPONENT Runtime)
endif()

if(HAVE_QT AND APPLE)
  # A Mac is given one thing to drag, so the bundle carries everything: the
  # application, the command-line tools, their manual pages, and the manual
  # the Help menu opens (which it looks for in Contents/Resources/manual).
  # Without this a drag-installed CTSim has no manual and no ctsim-nogui.
  install(TARGETS ctsim-nogui RUNTIME DESTINATION ctsim.app/Contents/MacOS
          COMPONENT Runtime)
  install(FILES ${CTSIM_MAN_PAGES}
          DESTINATION ctsim.app/Contents/Resources/man/man1 COMPONENT Runtime)
  install(FILES ${CMAKE_BINARY_DIR}/manual/ctsim.pdf ${CMAKE_BINARY_DIR}/manual/ctsim.html
          DESTINATION ctsim.app/Contents/Resources/manual OPTIONAL COMPONENT Runtime)
  install(DIRECTORY ${CMAKE_BINARY_DIR}/manual/figures
          DESTINATION ctsim.app/Contents/Resources/manual OPTIONAL COMPONENT Runtime)
  install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/packaging/macos-command-line-tools.txt
          DESTINATION . COMPONENT Runtime RENAME "Command-line tools.txt")
  install(DIRECTORY scripts/benchmarks/ DESTINATION ctsim.app/Contents/Resources/benchmarks
          FILES_MATCHING PATTERN "*.bench")
  install(DIRECTORY scripts/demos/ DESTINATION ctsim.app/Contents/Resources/demos
          COMPONENT Runtime FILES_MATCHING PATTERN "*.ctsim")

  # "cpack -G DragNDrop" then makes a disk image with ctsim.app at its root.
  list(APPEND CPACK_GENERATOR DragNDrop)
  set(CPACK_DMG_VOLUME_NAME "CTSim ${CTSIM_VERSION_FULL}")

  # The window the mounted image opens in.  macOS does not draw the picture
  # in an RTF licence -- the agreement it shows is text, and the logo in the
  # licence appears on Windows but not here -- so this is where a Mac user
  # sees the logo.
  set(CPACK_DMG_BACKGROUND_IMAGE
      "${CMAKE_CURRENT_SOURCE_DIR}/packaging/dmg-background.png")

  # The licence set above as CPACK_RESOURCE_FILE_LICENSE becomes the
  # agreement the disk image shows when it is opened; CPack embeds it for
  # every language and supplies the buttons itself.
  #
  # Not CPACK_DMG_SLA_DIR: that asks for a directory holding both a licence
  # and a menu file naming the buttons for each language, and without the
  # menu file the generator refuses to start.  Given both, CPack says it is
  # ignoring the directory and then still demands the menu file from it.
  set(CPACK_PACKAGING_INSTALL_PREFIX "/")
  set(CPACK_COMPONENTS_ALL Runtime)
endif()

if(WIN32)
  # The libraries the executables were linked against, copied beside them.
  # windeployqt (above) brings Qt's own and its plugins; it knows nothing
  # about the compiler runtime, or FFTW, or DCMTK, or HDF5 -- which is why
  # an installed CTSim reported libgcc_s_seh-1.dll, then libstdc++-6.dll,
  # then libfftw3-3.dll, then libdcmdata.dll, one at a time.  CMake reads
  # the executables and follows what they ask for.
  #
  # DIRECTORIES is where to look: the compiler's own bin directory, which
  # under MSYS2 is also where every package puts its DLLs.
  get_filename_component(_ctsim_compiler_bin "${CMAKE_CXX_COMPILER}" DIRECTORY)
  # Windows' own libraries are found so that they can be recognised and
  # left behind.  Without the system directory to look in, every one of
  # them -- kernel32, user32, dwrite, dxgi and a long tail besides -- is
  # searched for among the MinGW libraries, not found, and reported as
  # unresolved, which is a great many warnings for a question nobody asked.
  # POST_EXCLUDE then drops whatever was found there, so none is copied:
  # a program must use the system's own, not a copy in its own directory.
  file(TO_CMAKE_PATH "$ENV{SystemRoot}" _ctsim_system_root)
  if(NOT _ctsim_system_root)
    set(_ctsim_system_root "C:/Windows")
  endif()
  install(RUNTIME_DEPENDENCY_SET ctsim_windows_runtime
          PRE_EXCLUDE_REGEXES  "api-ms-win-.*" "ext-ms-.*"
          # Spelled out letter by letter because CMake's regexes have no
          # case-insensitive flag -- "(?i)" does not compile -- and Windows
          # reports its own directory as WINDOWS, Windows or windows
          # depending on who is asking.
          POST_EXCLUDE_REGEXES ".*[/\\][Ww][Ii][Nn][Dd][Oo][Ww][Ss][/\\].*"
          DIRECTORIES "${_ctsim_compiler_bin}"
                      "${_ctsim_system_root}/System32"
                      "${_ctsim_system_root}/SysWOW64"
          DESTINATION ${CMAKE_INSTALL_BINDIR}
          COMPONENT Runtime)
endif()

if(HAVE_QT AND WIN32)
  list(APPEND CPACK_GENERATOR NSIS)
  set(CPACK_NSIS_PACKAGE_NAME "CTSim")
  set(CPACK_NSIS_DISPLAY_NAME "CTSim ${CTSIM_VERSION_FULL}")
  set(CPACK_NSIS_INSTALLED_ICON_NAME "bin\\\\ctsim.exe")
  set(CPACK_NSIS_MUI_ICON "${CMAKE_CURRENT_SOURCE_DIR}/packaging/ctsim.ico")
  set(CPACK_NSIS_MUI_UNIICON "${CMAKE_CURRENT_SOURCE_DIR}/packaging/ctsim.ico")


  # The logo on every page, and beside the welcome and finish pages.
  set(CPACK_NSIS_MUI_HEADERIMAGE
      "${CMAKE_CURRENT_SOURCE_DIR}/packaging/nsis-header.bmp")
  set(CPACK_NSIS_MUI_WELCOMEFINISHPAGE_BITMAP
      "${CMAKE_CURRENT_SOURCE_DIR}/packaging/nsis-welcome.bmp")
  set(CPACK_NSIS_MUI_UNWELCOMEFINISHPAGE_BITMAP
      "${CMAKE_CURRENT_SOURCE_DIR}/packaging/nsis-welcome.bmp")
  set(CPACK_NSIS_HELP_LINK "http://www.ctsim.org/")
  set(CPACK_NSIS_URL_INFO_ABOUT "http://www.ctsim.org/")
  set(CPACK_NSIS_MODIFY_PATH ON)
  set(CPACK_NSIS_CREATE_ICONS_EXTRA
      "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\CTSim.lnk' '$INSTDIR\\\\bin\\\\ctsim.exe'")
  set(CPACK_NSIS_DELETE_ICONS_EXTRA
      "Delete '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\CTSim.lnk'")
endif()

include(CPack)

message(STATUS "CTSim ${PROJECT_VERSION}: GUI=${CTSIM_GUI} OpenMP=${HAVE_OPENMP} "
               "readline=${HAVE_READLINE} DICOM=${HAVE_DCMTK} "
               "bundled-getopt=${HAVE_GETOPT_LONG}")
if(HAVE_QT AND APPLE)
  # A Qt application on macOS is built as a bundle, so there is no plain
  # "ctsim" file in the build directory to look for.
  message(STATUS "  the application is built as ${CMAKE_BINARY_DIR}/ctsim.app; "
                 "run it with \"open ctsim.app\" or "
                 "ctsim.app/Contents/MacOS/ctsim --version")
endif()
