cmake_minimum_required(VERSION 3.20)
project(orderbook-dbengine VERSION 0.1.0 LANGUAGES CXX)

# ── Version ───────────────────────────────────────────────────────────────────
#
# `project(... VERSION)` above is the source, and it reaches the binary as a compile definition
# rather than being retyped. It was retyped: a literal `v0.1.0` in `tools/ob_tcp_server.cpp` was the
# startup line's version, and nothing checked it against this line or against `pyproject.toml`
# (#90). #90 took that one out and missed two more, the welcome banner every client reads and the
# interactive CLI's greeting, because the test it wrote could not see a version inside a longer
# literal (#148).
set(OB_VERSION "${PROJECT_VERSION}")

# ── Language standard ──────────────────────────────────────────────────────────
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

# ── Compiler ───────────────────────────────────────────────────────────────────
# C++20 as this code uses it needs GCC 12 or clang 15. An older compiler used to be taken - without
# the warnings below - and to fail deep in the build: GCC 11, the default on Amazon Linux 2023, on a
# static_assert in src/tcp_server.cpp that needs C++20's constexpr std::string (#213). It is refused
# here, saying what to use instead.
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS "12")
    message(FATAL_ERROR
        "GCC ${CMAKE_CXX_COMPILER_VERSION} is older than this engine needs: GCC 12 or newer, or clang 15. "
        "On Amazon Linux 2023 install gcc14-c++ and configure with CC=gcc14-gcc CXX=gcc14-g++ in a new build directory.")
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS "15")
    message(FATAL_ERROR
        "clang ${CMAKE_CXX_COMPILER_VERSION} is older than this engine needs: clang 15 or newer, or GCC 12.")
endif()

# ── Compiler warnings ──────────────────────────────────────────────────────────
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "12")
    add_compile_options(-Wall -Wextra -Werror)
    # GCC 12 - the floor above - misreads libstdc++'s own inline code at -O2: 12.3, Ubuntu 22.04's,
    # reports -Wrestrict on assigning a string literal to a std::string, and 12.3 and 12.4 report
    # -Wmaybe-uninitialized on copying a std::optional's value after has_value(). A Release build
    # with GCC 12 - what the README's Build section and the packages use - failed on both (#221).
    # Under GCC 12 the two stay warnings without failing the build; under GCC 13 and later, which
    # report neither on the same code, and under clang, every warning still fails it.
    if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "13")
        add_compile_options(-Wno-error=restrict -Wno-error=maybe-uninitialized)
    endif()
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "15")
    add_compile_options(-Wall -Wextra -Werror)
endif()

# ── SIMD options ───────────────────────────────────────────────────────────────
option(OB_ENABLE_AVX2   "Enable AVX2 SIMD optimizations"   OFF)
option(OB_ENABLE_AVX512 "Enable AVX-512 SIMD optimizations" OFF)

if(OB_ENABLE_AVX512)
    add_compile_options(-mavx512f)
    add_compile_definitions(OB_ENABLE_AVX512)
elseif(OB_ENABLE_AVX2)
    add_compile_options(-mavx2)
    add_compile_definitions(OB_ENABLE_AVX2)
endif()

# ── Dependencies via FetchContent ──────────────────────────────────────────────
# Every dependency is pinned to a commit SHA, with the corresponding tag in a
# trailing comment. Tags are mutable upstream; a SHA is not. When bumping a
# dependency, resolve the new SHA with:
#   git ls-remote https://github.com/<owner>/<repo>.git 'refs/tags/<tag>^{}'
include(FetchContent)

# Test/benchmark deps are only needed when building tests
option(OB_BUILD_TESTS "Build tests and benchmarks (requires gtest, benchmark, rapidcheck)" ON)
option(OB_BUILD_FUZZERS "Build libFuzzer harnesses (requires Clang and OB_ENABLE_ASAN)" OFF)

if(OB_BUILD_FUZZERS AND NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    message(FATAL_ERROR "OB_BUILD_FUZZERS requires Clang. Configure a separate build with -DCMAKE_CXX_COMPILER=clang++.")
endif()

if(OB_BUILD_TESTS)
    # Google Test
    FetchContent_Declare(
        googletest
        GIT_REPOSITORY https://github.com/google/googletest.git
        GIT_TAG        f8d7d77c06936315286eb55f8de22cd23c188571  # v1.14.0
    )
    set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
    set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)
    FetchContent_MakeAvailable(googletest)

    # Google Benchmark
    FetchContent_Declare(
        benchmark
        GIT_REPOSITORY https://github.com/google/benchmark.git
        GIT_TAG        344117638c8ff7e239044fd0fa7085839fc03021  # v1.8.3
    )
    set(BENCHMARK_ENABLE_TESTING OFF CACHE BOOL "" FORCE)
    set(BENCHMARK_ENABLE_INSTALL OFF CACHE BOOL "" FORCE)
    set(BENCHMARK_DOWNLOAD_DEPENDENCIES ON CACHE BOOL "" FORCE)
    FetchContent_MakeAvailable(benchmark)

    # RapidCheck
    FetchContent_Declare(
        rapidcheck
        GIT_REPOSITORY https://github.com/emil-e/rapidcheck.git
        # Pinned to a commit: rapidcheck publishes no releases, and tracking
        # master meant an upstream change could break or alter our build without
        # a change on our side.
        GIT_TAG        6e8dadfdafa3a74eabb52ead87f8787f72eccd0b  # master as of 2026-08-14
    )
    set(RC_ENABLE_GTEST ON CACHE BOOL "" FORCE)
    FetchContent_MakeAvailable(rapidcheck)
endif()

# nlohmann/json (header-only, used by shard_map serialization)
FetchContent_Declare(
    nlohmann_json
    GIT_REPOSITORY https://github.com/nlohmann/json.git
    GIT_TAG        9cca280a4d0ccf0c08f47a99aa71d1b0e52f8d03  # v3.11.3
)
set(JSON_BuildTests OFF CACHE BOOL "" FORCE)
set(JSON_Install OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(nlohmann_json)

# ── Instrumented builds: sanitizers and coverage ──────────────────────────────
#
# Placed here, and the placement is the point. `add_compile_options()` only affects targets created
# *after* the call, and every one of this project's libraries is created below. These blocks used to
# sit further down the file, past all of them — so `OB_ENABLE_ASAN`, `OB_ENABLE_TSAN` and
# `OB_ENABLE_COVERAGE` instrumented `ob_tcp_server` and the test executables and **none of the
# twenty-eight static libraries where the engine actually lives**.
#
# The evidence was one grep: in the ASan tree, `orderbook_engine.dir/flags.make` carried no
# `-fsanitize` and `ob_tcp_server.dir/flags.make` did. Coverage said 59% of 2387 lines, when the
# tree has an order of magnitude more than that — 6 of 34 source files were being measured. A
# sanitizer job and a coverage number that both look like they cover the tree and cover a sixth of
# it are the same failure mode as a required check that gates nothing.
#
# After FetchContent on purpose, so googletest, benchmark, rapidcheck and nlohmann/json stay
# uninstrumented: they are not what these builds are asking about, and UBSan findings inside a
# dependency would fail our build with -fno-sanitize-recover.

# ── Sanitizers ────────────────────────────────────────────────────────────────
#
# Two separate options because ASan and TSan cannot be linked into the same binary. ASan is paired
# with UBSan, which composes with it and costs almost nothing extra.
#
# Both keep -O1 and frame pointers: -O0 hides optimisation-dependent bugs and makes the suite slow
# enough that nobody runs it, while -O2 loses the stack detail that makes a report actionable.
option(OB_ENABLE_ASAN "Build with AddressSanitizer and UndefinedBehaviorSanitizer" OFF)
option(OB_ENABLE_TSAN "Build with ThreadSanitizer" OFF)

if(OB_ENABLE_ASAN AND OB_ENABLE_TSAN)
    message(FATAL_ERROR "OB_ENABLE_ASAN and OB_ENABLE_TSAN cannot be enabled together: the two "
                        "runtimes cannot coexist in one binary. Build them as separate trees.")
endif()

if((OB_ENABLE_ASAN OR OB_ENABLE_TSAN) AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    # GCC 13 reports -Wmaybe-uninitialized inside <bits/std_function.h> when instrumentation
    # changes what it can see through std::function, on code that is fine — a diagnostic about a
    # system header, not about ours. -Werror turns it into a build failure, so it is disabled for
    # sanitizer trees only. The ordinary build keeps every warning fatal.
    #
    # GCC only: clang has no -Wmaybe-uninitialized, and an unknown -Wno-* is itself a diagnostic
    # there, which -Werror turns into the failure this line exists to prevent.
    add_compile_options(-Wno-maybe-uninitialized)
endif()

if(OB_ENABLE_ASAN)
    add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer
                        -fno-sanitize-recover=undefined -g -O1)
    add_link_options(-fsanitize=address,undefined)
    message(STATUS "AddressSanitizer + UndefinedBehaviorSanitizer enabled")
endif()

if(OB_ENABLE_TSAN)
    add_compile_options(-fsanitize=thread -fno-omit-frame-pointer -g -O1)
    add_link_options(-fsanitize=thread)
    message(STATUS "ThreadSanitizer enabled")
endif()

option(OB_ENABLE_COVERAGE "Enable gcov/llvm-cov code coverage instrumentation" OFF)

if(OB_ENABLE_COVERAGE)
    if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
        add_compile_options(--coverage -O0 -g)
        add_link_options(--coverage)
    elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
        add_compile_options(-fprofile-instr-generate -fcoverage-mapping -O0 -g)
        add_link_options(-fprofile-instr-generate)
    else()
        message(WARNING "Coverage not supported for compiler: ${CMAKE_CXX_COMPILER_ID}")
    endif()
    message(STATUS "Coverage instrumentation enabled")
endif()

# libFuzzer instrumentation must reach the libraries, just like ASan/UBSan (#83, #38).
# Only the harness executables link the fuzzer main; ordinary targets retain their own main.
if(OB_BUILD_FUZZERS)
    if(NOT OB_ENABLE_ASAN)
        message(FATAL_ERROR "OB_BUILD_FUZZERS requires -DOB_ENABLE_ASAN=ON for ASan and UBSan coverage.")
    endif()
    add_compile_options(-fsanitize=fuzzer-no-link)
    set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
    message(STATUS "libFuzzer instrumentation enabled for engine libraries and harnesses")
endif()

# ── Core library ───────────────────────────────────────────────────────────────
add_library(orderbook_core INTERFACE)
target_include_directories(orderbook_core INTERFACE
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)
target_compile_features(orderbook_core INTERFACE cxx_std_20)

# The version reaches every translation unit that links the engine, so `ob::version()` has one
# definition and nobody retypes the number.
target_compile_definitions(orderbook_core INTERFACE OB_VERSION="${OB_VERSION}")

# ── SoA buffer compiled library ────────────────────────────────────────────────
add_library(orderbook_soa STATIC src/soa_buffer.cpp)

# The one translation unit ThreadSanitizer cannot compile.
#
# The SoA buffer's seqlock is built on `std::atomic_thread_fence`, and GCC refuses outright:
# "'atomic_thread_fence' is not supported with '-fsanitize=thread' [-Werror=tsan]". TSan models
# happens-before through atomic operations, not through standalone fences, so it cannot reason about
# a seqlock even if it compiled one.
#
# So this file is excluded rather than the option abandoned. What that costs is real and worth stating:
# **races inside the seqlock itself are outside TSan's reach**, and reports about the data it guards,
# raised from instrumented callers, need reading against the seqlock's design rather than taken at
# face value.
#
# Discovered when #83 moved the instrumentation above the libraries. Until then this file was not
# instrumented either — along with the other twenty-seven — so the build succeeded by accident and
# nobody learned that TSan and this data structure are incompatible.
if(OB_ENABLE_TSAN)
    target_compile_options(orderbook_soa PRIVATE -fno-sanitize=thread -Wno-tsan)
endif()
target_link_libraries(orderbook_soa PUBLIC orderbook_core)

# ── Sequence tracker compiled library ─────────────────────────────────────────
add_library(orderbook_sequence STATIC src/sequence_tracker.cpp src/version_vector.cpp)
target_link_libraries(orderbook_sequence PUBLIC orderbook_core orderbook_logger)

# ── WAL compiled library ───────────────────────────────────────────────────────
add_library(orderbook_wal STATIC src/wal.cpp)
target_link_libraries(orderbook_wal PUBLIC orderbook_core orderbook_epoch orderbook_hlc)

# ── Epoch compiled library ─────────────────────────────────────────────────────
add_library(orderbook_epoch STATIC src/epoch.cpp)
target_link_libraries(orderbook_epoch PUBLIC orderbook_core)

# ── Coordinator compiled library (etcd v3 REST client) ─────────────────────────
find_package(CURL REQUIRED)
add_library(orderbook_coordinator STATIC src/coordinator.cpp)
target_link_libraries(orderbook_coordinator PUBLIC orderbook_epoch orderbook_logger CURL::libcurl)

# ── Auth compiled library (wire authentication, #30) ───────────────────────────
#
# libcrypto only, not libssl. HMAC, the CSPRNG and the constant-time comparison all live in
# libcrypto; libssl arrives with TLS in part two of #30. Declared explicitly rather than leaned on
# through CURL, which links OpenSSL transitively - a dependency you inherit is one that disappears
# when the library you inherited it from changes its build.
find_package(OpenSSL REQUIRED)
add_library(orderbook_auth STATIC src/auth.cpp)
target_link_libraries(orderbook_auth PUBLIC orderbook_logger OpenSSL::Crypto)

# ── TLS compiled library (#30 part three) ──────────────────────────────────────
#
# libssl as well as libcrypto here, which orderbook_auth deliberately did not need: the record
# layer is what this is for. Still separate from orderbook_auth, and the dependency points one way:
# a translation unit that hashes a secret has no business pulling in the TLS state machine, while
# this one needs `sanitise_for_log()` from there - a peer's certificate common name is a string the
# peer chose, and every such string reaching a log goes through one funnel (pitfall 117). Copying
# the sanitiser here to keep the libraries independent would make it two funnels.
add_library(orderbook_tls STATIC src/tls.cpp)
target_link_libraries(orderbook_tls PUBLIC orderbook_logger orderbook_auth OpenSSL::SSL OpenSSL::Crypto)

# ── Failover compiled library (role transitions and monitoring) ────────────────
add_library(orderbook_failover STATIC src/failover.cpp)
# `orderbook_metrics` since #112: the monitor loop's per-iteration boundary counts the ticks it
# survives, and that counter is the only outward sign a tick failed - PING, ROLE and the log's
# silence all look the same either way.
target_link_libraries(orderbook_failover PUBLIC orderbook_coordinator orderbook_epoch
                                                orderbook_logger orderbook_metrics)

# ── Codec compiled library ─────────────────────────────────────────────────────
add_library(orderbook_codec STATIC src/codec.cpp)
target_link_libraries(orderbook_codec PUBLIC orderbook_core)

# ── MMAP store compiled library ────────────────────────────────────────────────

# ── Columnar store compiled library ────────────────────────────────────────────
# ── Query column names and sets ───────────────────────────────────────────────
# A leaf on purpose: the store needs it to know which files to open, the formatter needs it to
# write a header, and the client needs it to recognise a header it cannot read. One definition of
# the seven names rather than three.
add_library(orderbook_query_columns STATIC src/query_columns.cpp)
target_link_libraries(orderbook_query_columns PUBLIC orderbook_logger)

add_library(orderbook_columnar STATIC src/columnar_store.cpp src/decoded_columns.cpp)
target_link_libraries(orderbook_columnar PUBLIC orderbook_codec orderbook_logger orderbook_query_columns)

# ── LZ4 compression library ────────────────────────────────────────────────────
find_package(PkgConfig)
if(PkgConfig_FOUND)
    pkg_check_modules(LZ4 IMPORTED_TARGET liblz4)
endif()
if(NOT LZ4_FOUND)
    find_library(LZ4_LIBRARY NAMES lz4)
    find_path(LZ4_INCLUDE_DIR NAMES lz4frame.h)
    if(LZ4_LIBRARY AND LZ4_INCLUDE_DIR)
        set(LZ4_FOUND TRUE)
    else()
        message(FATAL_ERROR "LZ4 library not found. Install liblz4-dev or equivalent.")
    endif()
endif()

add_library(orderbook_compression STATIC src/compression.cpp)
target_link_libraries(orderbook_compression PUBLIC orderbook_core)
if(TARGET PkgConfig::LZ4)
    target_link_libraries(orderbook_compression PUBLIC PkgConfig::LZ4)
else()
    target_link_libraries(orderbook_compression PUBLIC ${LZ4_LIBRARY})
    target_include_directories(orderbook_compression PUBLIC ${LZ4_INCLUDE_DIR})
endif()

# ── ZSTD, for the compressed column blocks of segment format 3 ─────────────────
if(PkgConfig_FOUND)
    pkg_check_modules(ZSTD IMPORTED_TARGET libzstd)
endif()
if(NOT ZSTD_FOUND)
    find_library(ZSTD_LIBRARY NAMES zstd)
    find_path(ZSTD_INCLUDE_DIR NAMES zstd.h)
    if(ZSTD_LIBRARY AND ZSTD_INCLUDE_DIR)
        set(ZSTD_FOUND TRUE)
    else()
        message(FATAL_ERROR "ZSTD library not found. Install libzstd-dev (Debian, Ubuntu) or "
                            "libzstd-devel (Amazon Linux, Fedora, RHEL).")
    endif()
endif()

# ── Column blocks of segment format 3 ──────────────────────────────────────────
add_library(orderbook_column_codec STATIC src/column_codec.cpp)
target_link_libraries(orderbook_column_codec PUBLIC orderbook_codec orderbook_logger)
if(TARGET PkgConfig::LZ4)
    target_link_libraries(orderbook_column_codec PRIVATE PkgConfig::LZ4)
else()
    target_link_libraries(orderbook_column_codec PRIVATE ${LZ4_LIBRARY})
    target_include_directories(orderbook_column_codec PRIVATE ${LZ4_INCLUDE_DIR})
endif()
if(TARGET PkgConfig::ZSTD)
    target_link_libraries(orderbook_column_codec PRIVATE PkgConfig::ZSTD)
else()
    target_link_libraries(orderbook_column_codec PRIVATE ${ZSTD_LIBRARY})
    target_include_directories(orderbook_column_codec PRIVATE ${ZSTD_INCLUDE_DIR})
endif()
target_link_libraries(orderbook_columnar PUBLIC orderbook_column_codec)

# ── Metrics registry compiled library ──────────────────────────────────────────
# ── Structured logger compiled library ─────────────────────────────────────────
add_library(orderbook_logger STATIC src/logger.cpp)
target_link_libraries(orderbook_logger PUBLIC orderbook_core)

add_library(orderbook_metrics STATIC src/metrics.cpp)
# Needs the logger: a write to an unregistered metric name updates nothing, so it
# has to be reported rather than swallowed.
target_link_libraries(orderbook_metrics PUBLIC orderbook_core orderbook_logger)

# ── Machine resources: how many CPUs this process can actually use ────────────
# The affinity mask and the tightest cgroup CPU limit, as a pure function of what the process can
# see, so the cases a container, a cpuset and a fractional quota produce are unit tests.
add_library(orderbook_machine STATIC src/machine.cpp)
target_link_libraries(orderbook_machine PUBLIC orderbook_core)

# ── HLC (Hybrid Logical Clock) compiled library ───────────────────────────────
add_library(orderbook_hlc STATIC src/hlc.cpp)
target_link_libraries(orderbook_hlc PUBLIC orderbook_core orderbook_logger)

# ── Conflict resolver compiled library (multi-master LWW) ─────────────────────
add_library(orderbook_conflict_resolver STATIC src/conflict_resolver.cpp)
target_link_libraries(orderbook_conflict_resolver PUBLIC orderbook_hlc orderbook_core orderbook_logger)

# ── Peer registry compiled library (multi-master peer discovery) ───────────────
add_library(orderbook_peer_registry STATIC src/peer_registry.cpp)
# `orderbook_metrics` since #112: the lease loop's boundary counts what it caught, and the
# linker is what said so.
target_link_libraries(orderbook_peer_registry PUBLIC orderbook_hlc orderbook_coordinator orderbook_logger orderbook_metrics nlohmann_json::nlohmann_json)

# ── Anti-entropy compiled library (multi-master gap detection and repair) ──────
add_library(orderbook_anti_entropy STATIC src/anti_entropy.cpp)
target_link_libraries(orderbook_anti_entropy PUBLIC orderbook_peer_registry orderbook_hlc orderbook_logger)

# ── Async snapshot builder (snapshot creation off the io thread, #79) ─────────
add_library(orderbook_async_snapshot STATIC src/async_snapshot.cpp)
target_link_libraries(orderbook_async_snapshot PUBLIC orderbook_core orderbook_sequence orderbook_logger)

# ── Multi-master wire framing ─────────────────────────────────────────────────
# Kept separate from the manager so byte parsing can be fuzzed without pulling in Engine and the
# networking state machine. The manager and the fuzzer link this same production implementation.
add_library(orderbook_mm_framing STATIC src/mm_framing.cpp)
target_link_libraries(orderbook_mm_framing PUBLIC orderbook_core)

# ── Multi-master manager compiled library (peer networking, WAL broadcast) ─────
add_library(orderbook_multi_master STATIC src/multi_master.cpp src/mm_snapshot.cpp)
# The engine and the WAL too, which it calls into and which name it: a cycle of static libraries,
# which CMake repeats on the link line. Undeclared, it linked only while some earlier library on the
# line happened to pull the engine's objects in first - and #196's coordinator, linking the client,
# moved that earlier library, and every test linking the coordinator lost `Engine::install_snapshot`.
target_link_libraries(orderbook_multi_master PUBLIC orderbook_mm_framing orderbook_async_snapshot orderbook_peer_registry orderbook_conflict_resolver orderbook_anti_entropy orderbook_hlc orderbook_compression orderbook_logger orderbook_engine orderbook_wal)

# ── Shard map compiled library ─────────────────────────────────────────────────
add_library(orderbook_shard_map STATIC src/shard_map.cpp)
target_link_libraries(orderbook_shard_map PUBLIC orderbook_core orderbook_logger nlohmann_json::nlohmann_json)

# ── Shard router compiled library (client-side routing) ────────────────────────
add_library(orderbook_shard_router STATIC src/shard_router.cpp)
# `orderbook_metrics` since #131, and it is the stated cost of one mechanism instead of two.
# `LoopGuard` is header-only, so `caught()` is inlined here and names
# `MetricsRegistry::increment_counter` even though this loop always passes a null registry -
# it runs in the caller's process and the one that owns a registry is the server. The
# alternative was a second hand-written boundary for the two client-side loops, which is the
# drift `log_episode.hpp` exists to prevent. `orderbook_metrics` depends only on
# `orderbook_core` and `orderbook_logger`, both of which this library already links, so the
# dependency graph does not grow.
target_link_libraries(orderbook_shard_router PUBLIC orderbook_shard_map orderbook_coordinator orderbook_logger orderbook_metrics)

# ── Shard coordinator compiled library ──────────────────────────────────────────
# The migration's connection to the shard a symbol moves to (#196) is a client's, in a translation
# unit of its own: `orderbook/client.hpp` and the engine's headers define different `ob::Level`s.
add_library(orderbook_shard_coordinator STATIC src/shard_coordinator.cpp src/symbol_mover.cpp)
target_link_libraries(orderbook_shard_coordinator PUBLIC orderbook_shard_map orderbook_coordinator orderbook_engine orderbook_logger orderbook_client)

# ── Metrics HTTP server compiled library ───────────────────────────────────────
add_library(orderbook_metrics_server STATIC src/metrics_server.cpp)
target_link_libraries(orderbook_metrics_server PUBLIC orderbook_metrics orderbook_logger)

# ── Aggregation engine compiled library ────────────────────────────────────────
add_library(orderbook_agg STATIC src/aggregation.cpp)
target_link_libraries(orderbook_agg PUBLIC orderbook_soa)

# ── Query engine compiled library ─────────────────────────────────────────────
add_library(orderbook_query STATIC src/query_engine.cpp)
# Needs the logger: rejected queries must say so, not fail in silence.
target_link_libraries(orderbook_query PUBLIC orderbook_columnar orderbook_agg orderbook_logger)

# ── Which segments merge (#165 part 2b): a pure policy, tested on its own ──────
add_library(orderbook_compaction STATIC src/compaction.cpp)
target_link_libraries(orderbook_compaction PUBLIC orderbook_core)

# ── Writes admitted at the rate the device takes (#190 step 5): a policy, tested on its own ──────
add_library(orderbook_admission STATIC src/admission.cpp)
target_link_libraries(orderbook_admission PUBLIC orderbook_core)

# ── Engine facade compiled library ────────────────────────────────────────────
add_library(orderbook_engine STATIC src/engine.cpp src/engine_compaction.cpp src/replication.cpp)
target_link_libraries(orderbook_engine PUBLIC orderbook_query orderbook_wal orderbook_failover orderbook_compression orderbook_metrics orderbook_logger orderbook_multi_master orderbook_sequence orderbook_auth orderbook_tls orderbook_compaction orderbook_admission)

# ── Backup and restore (#34) ──────────────────────────────────────────────────
# The server's BACKUP and the offline ob_restore: engine-side, and nothing of the client, whose
# headers define an `ob::Level` and an `ob::NodeRole` of their own.
add_library(orderbook_backup STATIC src/backup.cpp)
target_link_libraries(orderbook_backup PUBLIC orderbook_engine nlohmann_json::nlohmann_json)

# ── C API compiled library ────────────────────────────────────────────────────
add_library(orderbook_capi STATIC src/c_api.cpp)
target_link_libraries(orderbook_capi PUBLIC orderbook_query orderbook_wal)

# ── Coverage ──────────────────────────────────────────────────────────────────
if(OB_ENABLE_COVERAGE)
    # Find lcov/genhtml (gcov path) or llvm-profdata/llvm-cov (clang path)
    find_program(LCOV_EXECUTABLE    lcov)
    find_program(GENHTML_EXECUTABLE genhtml)
    find_program(LLVM_PROFDATA      llvm-profdata)
    find_program(LLVM_COV           llvm-cov)

    if(CMAKE_CXX_COMPILER_ID MATCHES "GNU" AND LCOV_EXECUTABLE AND GENHTML_EXECUTABLE)
        add_custom_target(coverage
            COMMENT "Generating gcov HTML coverage report"
            COMMAND ${LCOV_EXECUTABLE} --directory . --zerocounters
            COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
            COMMAND ${LCOV_EXECUTABLE}
                    --directory .
                    --capture
                    --output-file coverage.info
                    --ignore-errors mismatch
            COMMAND ${LCOV_EXECUTABLE}
                    --remove coverage.info
                    '*/tests/*' '*/benchmarks/*' '*/build/*'
                    '*/_deps/*' '/usr/*'
                    --output-file coverage_filtered.info
            COMMAND ${GENHTML_EXECUTABLE}
                    coverage_filtered.info
                    --output-directory coverage_html
            COMMAND ${CMAKE_COMMAND} -E echo
                    "Coverage report: ${CMAKE_BINARY_DIR}/coverage_html/index.html"
            WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
        )
    elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND LLVM_PROFDATA AND LLVM_COV)
        # Collect all test executables for llvm-cov
        set(OB_TEST_BINARIES
            tests/test_soa_buffer
            tests/test_wal
            tests/test_codec
            tests/test_columnar_store
            tests/test_aggregation
            tests/test_query_engine
            tests/test_engine
            tests/test_c_api
            tests/test_types_and_data_model
        )
        add_custom_target(coverage
            COMMENT "Generating llvm-cov HTML coverage report"
            COMMAND ${CMAKE_COMMAND} -E env LLVM_PROFILE_FILE=coverage_%p.profraw
                    ${CMAKE_CTEST_COMMAND} --output-on-failure
            COMMAND ${LLVM_PROFDATA} merge -sparse coverage_*.profraw
                    -o coverage.profdata
            COMMAND ${LLVM_COV} show
                    ${CMAKE_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/../tests/test_engine
                    -instr-profile=coverage.profdata
                    -format=html
                    -output-dir=coverage_html
                    # Double quotes, and neither of the two obvious alternatives. Single quotes
                    # are not string delimiters in CMake, so `'...'` passed the apostrophes
                    # through to llvm-cov and the filter ignored nothing; bare, the `(` opens a
                    # CMake token of its own. Both forms make CMake print "Syntax Warning in cmake
                    # code" on every configure, which is how this was noticed.
                    "-ignore-filename-regex=(_deps|tests|benchmarks)/"
            COMMAND ${CMAKE_COMMAND} -E echo
                    "Coverage report: ${CMAKE_BINARY_DIR}/coverage_html/index.html"
            WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
        )
    else()
        add_custom_target(coverage
            COMMAND ${CMAKE_COMMAND} -E echo
                    "Coverage tools not found. Install lcov+genhtml (GCC) or llvm-profdata+llvm-cov (Clang)."
        )
    endif()
endif()

# ── TCP Server library (testable components) ──────────────────────────────────
add_library(orderbook_tcp_server_lib STATIC
    src/tcp_server.cpp
    src/command_parser.cpp
    src/response_formatter.cpp
    src/session.cpp
    src/subscription_hub.cpp
)
target_link_libraries(orderbook_tcp_server_lib PUBLIC orderbook_engine orderbook_backup orderbook_logger orderbook_metrics_server orderbook_shard_coordinator orderbook_auth orderbook_tls orderbook_machine)

# ── Native C++ client library ──────────────────────────────────────────────────
add_library(orderbook_client STATIC src/client.cpp)
# `orderbook_metrics` for the reason given above `orderbook_shard_router`: the pool's
# health-check loop is guarded by the same header the server's loops use, and it passes a
# null registry.
target_link_libraries(orderbook_client PUBLIC orderbook_core orderbook_shard_router orderbook_auth orderbook_tls orderbook_metrics orderbook_query_columns)

option(OB_CLIENT_LZ4 "Enable optional LZ4 compression in orderbook_client" OFF)
if(OB_CLIENT_LZ4)
    target_compile_definitions(orderbook_client PUBLIC OB_CLIENT_LZ4)
    target_link_libraries(orderbook_client PUBLIC orderbook_compression)
endif()

# ── TCP Server executable ─────────────────────────────────────────────────────
add_executable(ob_tcp_server tools/ob_tcp_server.cpp)
target_link_libraries(ob_tcp_server PRIVATE orderbook_tcp_server_lib)

# ── Backup tools (#34) ────────────────────────────────────────────────────────
# ob_restore checks a backup and restores it offline; ob_backup asks a running server for one.
add_executable(ob_restore tools/ob_restore.cpp)
target_link_libraries(ob_restore PRIVATE orderbook_backup)
add_executable(ob_backup tools/ob_backup.cpp)
target_link_libraries(ob_backup PRIVATE orderbook_client orderbook_auth)

# ── The io_uring transport was removed (#147) ────────────────────────────────
#
# Refused rather than ignored: a build script that still passes -DOB_USE_IO_URING=ON would otherwise
# configure cleanly, build the epoll server, and look for an `ob_tcp_server_iouring` that no longer
# exists - or worse, be read as having built it. Measured before it went: it sent a pipelining client
# bytes of the server's heap, ran `--fsync-policy every` with 2 fsyncs for 200 acknowledged writes,
# and answered PING more slowly than the epoll server's `--profile boost`.
if(OB_USE_IO_URING)
    message(FATAL_ERROR "OB_USE_IO_URING: the io_uring transport was removed, see roadmap #147. "
                        "The epoll server with --profile boost is the low-latency configuration.")
endif()

# ── CLI tool ───────────────────────────────────────────────────────────────────
if(OB_BUILD_TESTS)
    add_executable(ob_cli tools/ob_cli.cpp)
    target_link_libraries(ob_cli PRIVATE orderbook_engine)
endif()

# ── Shared library for Python/FFI bindings ─────────────────────────────────────
add_library(orderbook_shared SHARED src/c_api.cpp)
target_link_libraries(orderbook_shared PUBLIC orderbook_engine)
set_target_properties(orderbook_shared PROPERTIES
    OUTPUT_NAME "orderbook_shared"
    POSITION_INDEPENDENT_CODE ON
)

# ── Packages that run beyond the machine that built them (#212) ──────────────────
#
# A binary asks for the libstdc++ symbol versions of the compiler that built it: GCC 12's for
# GLIBCXX_3.4.30, which RHEL 9 and its rebuilds, on GCC 11's libstdc++, do not have. The packaging
# build links libstdc++ statically into the four things the packages install, so the C++ runtime they
# need is the one inside them; the C library, libgcc_s and the libraries the packages name as
# dependencies stay shared. The C API library keeps the libstdc++ inside it to itself
# (--exclude-libs), so a process that loads it - a Python interpreter, perhaps beside another C++
# library - sees the C API and nothing else.
option(OB_STATIC_LIBSTDCXX "Link libstdc++ statically into the packaged binaries and library (#212)" OFF)
if(OB_STATIC_LIBSTDCXX)
    if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
        message(FATAL_ERROR "OB_STATIC_LIBSTDCXX links GCC's libstdc++, and this is ${CMAKE_CXX_COMPILER_ID}")
    endif()
    foreach(packaged ob_tcp_server ob_restore ob_backup orderbook_shared)
        target_link_options(${packaged} PRIVATE -static-libstdc++)
    endforeach()
    target_link_options(orderbook_shared PRIVATE -Wl,--exclude-libs,libstdc++.a)
    message(STATUS "OB_STATIC_LIBSTDCXX: libstdc++ linked into ob_tcp_server, ob_restore, ob_backup "
                   "and liborderbook_shared")
endif()

# ── Install rules ─────────────────────────────────────────────────────────────
#
# Destinations are **explicitly relative** and the packaging prefix is `/`, so the .deb and the
# tarball contain byte-identical paths. Three attempts got here and both failures are worth keeping.
#
# `${CMAKE_INSTALL_SYSCONFDIR}` is relative, so with the prefix at /usr the config landed in
# `/usr/etc/orderbook/ob.conf` while `packaging/debian/conffiles` declared `/etc/orderbook/ob.conf`.
# The declaration would then have named a path the package does not contain, so the conffile mark
# would have done nothing and the first upgrade would have silently reverted every local edit.
#
# Making it absolute `/etc/orderbook` fixed the .deb and broke the tarball in a worse way: the
# archive generator honours an absolute destination literally and **tried to create /etc/orderbook
# on the build host**. It failed here only for want of privileges — a build running as root, or in a
# container, would have written into the host's /etc while producing a package.
#
# So: relative paths, with CPACK_PACKAGING_INSTALL_PREFIX putting them under `/`. A plain
# `cmake --install` then places files under `<prefix>/usr/...`, which is odd and acceptable: these
# rules exist for packaging. (They were guarded by `if(NOT SKBUILD)` while the Python wheel ran this
# build for `orderbook_shared`; the wheel builds no native code since #42, so nothing else runs them.)
#
# The directories these rules create implicitly take this mode rather than the builder's umask: a
# .deb built under umask 0002 carried fourteen group-writable directories, /etc/orderbook among them
# (#210). CPack takes CPACK_INSTALL_DEFAULT_DIRECTORY_PERMISSIONS from it.
set(CMAKE_INSTALL_DEFAULT_DIRECTORY_PERMISSIONS
    OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
install(TARGETS ob_tcp_server ob_restore ob_backup
    RUNTIME DESTINATION usr/bin
    COMPONENT runtime
)
# The C API, for the Python client's local mode (#42), in a directory of the package's own rather
# than beside the system's libraries: it has no SONAME, it is loaded by path, and nothing links it by
# name - so it needs no ldconfig and claims no name in /usr/lib. The client looks for it here.
install(TARGETS orderbook_shared
    LIBRARY DESTINATION usr/lib/orderbook-dbengine
    COMPONENT runtime
)
install(DIRECTORY include/orderbook
    DESTINATION usr/include
    COMPONENT runtime
    FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h"
)
install(FILES packaging/ob.conf
    DESTINATION etc/orderbook
    COMPONENT runtime
)
install(FILES packaging/ob_tcp_server.service
    DESTINATION usr/lib/systemd/system
    COMPONENT runtime
)
install(FILES packaging/ob_tcp_server.1
    DESTINATION usr/share/man/man1
    COMPONENT runtime
)
install(FILES README.md SECURITY.md LICENSE docs/cli.md
    DESTINATION usr/share/doc/orderbook-dbengine
    COMPONENT runtime
)

# ── CPack ─────────────────────────────────────────────────────────────────────
set(CPACK_PACKAGE_NAME "orderbook-dbengine")
set(CPACK_PACKAGE_VENDOR "Smart Data Engines")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY
    "Level 2 orderbook database engine for HFT environments")
set(CPACK_PACKAGE_HOMEPAGE_URL
    "https://github.com/Smart-Data-Engines/low-cost-and-low-latency-orderbook-dbengine")
set(CPACK_PACKAGE_CONTACT "Krzysztof Macewicz <krzysztof@smartdataengines.com>")
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE")
set(CPACK_PACKAGE_INSTALL_DIRECTORY "orderbook-dbengine")
# DEB and a plain tarball always. RPM **only when rpmbuild exists**, which is a decision rather
# than a convenience: `rpmbuild` is not on the development machine, so an unconditional RPM
# generator would fail every local `cpack` run, and a generator configuration nobody can execute is
# a mechanism that looks present and is not. The packaging CI job installs `rpm`, so the RPM is
# produced and inspected there — and that job runs on pull requests touching packaging as well as on
# tags, because a job first exercised on a tag is a job first exercised when it matters most.
set(CPACK_GENERATOR "DEB;TGZ")
find_program(RPMBUILD_EXECUTABLE rpmbuild)
if(RPMBUILD_EXECUTABLE)
    list(APPEND CPACK_GENERATOR "RPM")
    set(CPACK_RPM_PACKAGE_GROUP "Applications/Databases")
    # The project's licence: this said MIT, which the repository never was (#42).
    set(CPACK_RPM_PACKAGE_LICENSE "Apache-2.0")
    set(CPACK_RPM_FILE_NAME RPM-DEFAULT)   # orderbook-dbengine-X.Y.Z-1.<arch>.rpm
    # /etc/orderbook/ob.conf must survive an upgrade, the same requirement the .deb meets through
    # its conffiles declaration.
    set(CPACK_RPM_USER_FILELIST "%config(noreplace) /etc/orderbook/ob.conf")
    # rpm claims directories it did not create otherwise, and /etc, /usr/bin and the rest belong to
    # the distribution's filesystem package.
    set(CPACK_RPM_EXCLUDE_FROM_AUTO_FILELIST_ADDITION
        "/etc;/usr;/usr/bin;/usr/include;/usr/lib;/usr/lib/systemd;/usr/lib/systemd/system;/usr/share;/usr/share/doc;/usr/share/man;/usr/share/man/man1")
    # The unit runs as `orderbook`, which only the .deb's postinst created: after an RPM install the
    # service had no user to start as (#211). The scripts do what packaging/debian/ does.
    set(CPACK_RPM_PRE_INSTALL_SCRIPT_FILE "${CMAKE_CURRENT_SOURCE_DIR}/packaging/rpm/preinst")
    set(CPACK_RPM_POST_INSTALL_SCRIPT_FILE "${CMAKE_CURRENT_SOURCE_DIR}/packaging/rpm/postinst")
    set(CPACK_RPM_PRE_UNINSTALL_SCRIPT_FILE "${CMAKE_CURRENT_SOURCE_DIR}/packaging/rpm/preun")
    set(CPACK_RPM_PACKAGE_REQUIRES_PRE "/usr/sbin/useradd, /usr/bin/getent")
    # rpmbuild's dependency generator records each symbol version the link took, and libcurl's are
    # Debian's - CURL_OPENSSL_4 - which no RPM distribution's libcurl declares: dnf refused the
    # package on Amazon Linux 2023 for it (#212). The loader does not ask for them of a libcurl that
    # defines no versions, which Amazon Linux's does not; the soname dependency,
    # libcurl.so.4()(64bit), stays.
    # No backslash in it: between this file, the configuration CMake writes for cpack and rpm's macro
    # expansion, backslashes were taken away - written two and four deep, the filter still matched
    # nothing. A bracket matches its character literally in any regex dialect.
    set(CPACK_RPM_SPEC_MORE_DEFINE "%global __requires_exclude ^libcurl[.]so[.]4[(]CURL_")
endif()
# The archive generators record the staging files' owner - the builder's uid, `runner` (1001) on a
# CI runner - and no CMake up to 4.2 lets a project choose it, so this script writes each .tar.gz
# again with every entry root's (#210).
set(CPACK_POST_BUILD_SCRIPTS "${CMAKE_CURRENT_SOURCE_DIR}/packaging/archive_owned_by_root.cmake")
# Both generators produce the same relative layout; see the note above the install rules.
set(CPACK_PACKAGING_INSTALL_PREFIX "/")
# Every file name carries the architecture (#42): a release has x86_64 and aarch64 builds, and the
# defaults gave both the same name - `orderbook-dbengine-X.Y.Z-Linux.deb` and `.tar.gz`.
set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT)   # orderbook-dbengine_X.Y.Z_<arch>.deb
set(CPACK_SYSTEM_NAME "Linux-${CMAKE_SYSTEM_PROCESSOR}")   # orderbook-dbengine-X.Y.Z-Linux-<arch>.tar.gz

# One package rather than one per component: the headers are a few kilobytes, and splitting them
# into -dev would create two packages that must be installed together for the common case.
#
# CPACK_COMPONENTS_ALL is deliberately **not** set. It only takes effect with component install
# enabled, and setting it while component install is off reads as a filter that is doing something.
set(CPACK_DEB_COMPONENT_INSTALL OFF)
set(CPACK_ARCHIVE_COMPONENT_INSTALL OFF)

set(CPACK_DEBIAN_PACKAGE_SECTION "database")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
# Shared library dependencies resolved by dpkg-shlibdeps rather than listed by hand, because a
# hand-written list goes stale at the first new link and the symptom is an install that succeeds and
# a binary that will not start.
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
# The configuration file is marked conffile, so a package upgrade does not overwrite an operator's
# edits. Without this the first upgrade silently reverts every local change.
set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA
    "${CMAKE_CURRENT_SOURCE_DIR}/packaging/debian/postinst;${CMAKE_CURRENT_SOURCE_DIR}/packaging/debian/prerm;${CMAKE_CURRENT_SOURCE_DIR}/packaging/debian/conffiles")

include(CPack)

# ── Tests ──────────────────────────────────────────────────────────────────────
if(OB_BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)

    # ── Benchmarks ─────────────────────────────────────────────────────────────
    add_subdirectory(benchmarks)
endif()

# ── Parser fuzzing (optional, separate from CTest) ──────────────────────────────
if(OB_BUILD_FUZZERS)
    add_subdirectory(fuzz)
endif()
