cmake_minimum_required(VERSION 3.21)

project(bravais
    VERSION 0.1.0
    DESCRIPTION "Sans-IO C library for the Bravais VPN protocol (post-quantum handshake, DPLPMTUD, transport shaping)"
    HOMEPAGE_URL "https://bravais.org"
    LANGUAGES C)

# ---------------------------------------------------------------------------
# Platform guard: POSIX-only for now.
#
# libbravais itself is sans-IO and makes no syscalls, so the *sources* should
# port to Windows unchanged. Two things in this file do not:
#
#   - the whole flag machinery below is GCC/Clang; MSVC needs its own branch
#     (/W4, /analyze, /fsanitize=address, /GS, /guard:cf)
#   - static and shared cannot share OUTPUT_NAME on Windows, because the DLL's
#     import library is also bravais.lib and would collide with the static one
#
# Port bravaisd's platform backends first; this can wait.
# ---------------------------------------------------------------------------
if(WIN32)
    message(FATAL_ERROR "libbravais has no MSVC/Windows build yet (the sources are portable; this file is not).")
endif()

# ---------------------------------------------------------------------------
# Options
# ---------------------------------------------------------------------------
option(BRAVAIS_BUILD_STATIC "Build libbravais.a" ON)
option(BRAVAIS_BUILD_SHARED "Build libbravais.so" ON)
option(BRAVAIS_BUILD_TESTS "Build libbravais tests" OFF)
option(BRAVAIS_BUILD_FUZZERS "Build libbravais fuzz targets (needs Clang)" OFF)
option(BRAVAIS_BUILD_EXAMPLES "Build libbravais example programs" OFF)

if(NOT BRAVAIS_BUILD_STATIC AND NOT BRAVAIS_BUILD_SHARED)
    message(FATAL_ERROR "Both BRAVAIS_BUILD_STATIC and BRAVAIS_BUILD_SHARED are OFF; nothing to build.")
endif()

# Crypto is a hard dependency of proto/ and crypto/, but not of util/ or wire/.
# Turn this off to bootstrap the cursor and the frame parser on a machine
# without liboqs installed; turn it back on before writing a line of crypto/.
option(BRAVAIS_ENABLE_CRYPTO "Link libsodium and liboqs (required for crypto/ and proto/)" ON)

# The warning set, the sanitizers and the custom configurations below are for
# working *on* bravais, not for working *with* it. A project that vendors us via
# add_subdirectory() wants a library, not our -Werror policy or an ASan runtime
# it never asked for, so the whole developer flag machinery defaults to
# top-level builds. Force it on with -DBRAVAIS_DEVELOPER_BUILD=ON if you are
# vendoring bravais and want it built the way we build it.
option(BRAVAIS_DEVELOPER_BUILD
    "Build bravais with its own warning set, configurations and instrumentation"
    ${PROJECT_IS_TOP_LEVEL})

# ---------------------------------------------------------------------------
# Feature macros so POSIX/BSD calls resolve under -std=c23. On glibc,
# _POSIX_C_SOURCE + _DEFAULT_SOURCE expose them; on macOS, _POSIX_C_SOURCE
# alone *hides* BSD extensions, so _DARWIN_C_SOURCE is added to restore the
# full set.
#
# libbravais is sans-IO and should need none of this: no sockets, no clocks
# (the caller passes nowMs), no file descriptors. It is defined anyway so that
# a stray <endian.h> or a libsodium header expecting a POSIX environment does
# not fail oddly, and so tests/ and bravaisd can reuse the same list. If you
# find yourself *needing* one of these in src/, that is a sans-IO violation --
# fix the code, not this list.
# ---------------------------------------------------------------------------
set(BRAVAIS_FEATURE_DEFS _POSIX_C_SOURCE=200809L _DEFAULT_SOURCE)
if(APPLE)
    list(APPEND BRAVAIS_FEATURE_DEFS _DARWIN_C_SOURCE)
endif()

# ---------------------------------------------------------------------------
# Build configuration
#
#   cmake -S . -B build          -DCMAKE_BUILD_TYPE=Debug     # -Og -g3, ASan + UBSan
#   cmake -S . -B build-strict   -DCMAKE_BUILD_TYPE=Strict    # Debug plus -Werror
#   cmake -S . -B build-analyzer -DCMAKE_BUILD_TYPE=Analyzer  # -Og -g3, GCC -fanalyzer
#   cmake -S . -B build-release  -DCMAKE_BUILD_TYPE=Release   # -O3, _FORTIFY_SOURCE
#
# Debug finds memory bugs at run time, Analyzer finds them at compile time,
# Release survives them. Strict is Debug with warnings promoted to errors -- the
# configuration CI should gate on, kept separate so a new warning never blocks
# someone mid-debugging. The warning set is identical in all four; only the
# instrumentation and the error policy differ.
#
# Debug and Analyzer are separate configurations rather than one Debug build
# with everything switched on, because the two tools actively interfere: ASan's
# instrumentation inflates the CFG enough that -fanalyzer exhausts its
# exploration budget and silently stops reporting.
#
# ThreadSanitizer is deliberately absent: it cannot be combined with ASan, so it
# would need a third configuration of its own. libbravais is single-threaded by
# design -- a bravais_conn_t is owned by one thread and the library spawns
# none -- so TSan belongs in bravaisd's build, not here.
#
# MemorySanitizer is also absent, and is more tempting than TSan: a parser fed
# attacker-controlled bytes is exactly what MSan is for. It needs every
# dependency rebuilt instrumented (libsodium and liboqs included) or it drowns
# in false positives, so it is a separate build tree with its own toolchain
# file, not a configuration here.
# ---------------------------------------------------------------------------
set(BRAVAIS_CUSTOM_CONFIGS Strict Analyzer)
set(BRAVAIS_BUILD_TYPES Debug Strict Analyzer Release RelWithDebInfo MinSizeRel)

if(BRAVAIS_DEVELOPER_BUILD)
    get_property(BRAVAIS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
    if(BRAVAIS_MULTI_CONFIG)
        foreach(_cfg IN LISTS BRAVAIS_CUSTOM_CONFIGS)
            if(NOT "${_cfg}" IN_LIST CMAKE_CONFIGURATION_TYPES)
                list(APPEND CMAKE_CONFIGURATION_TYPES ${_cfg})
            endif()
        endforeach()
    elseif(NOT CMAKE_BUILD_TYPE)
        set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type" FORCE)
        message(STATUS "No CMAKE_BUILD_TYPE specified; defaulting to Debug")
    endif()
    set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS ${BRAVAIS_BUILD_TYPES})

    # Strict and Analyzer are custom configurations, so CMake has no built-in
    # flags for them. Both inherit Debug's, and imported targets need a mapping
    # because they only ship Debug/Release/NOCONFIG. liboqs and libsodium both
    # arrive as imported targets, so this mapping is load-bearing here in a way
    # it was not when the only dependency was Threads::Threads.
    foreach(_cfg IN LISTS BRAVAIS_CUSTOM_CONFIGS)
        string(TOUPPER ${_cfg} _cfg_upper)
        set(CMAKE_C_FLAGS_${_cfg_upper} "${CMAKE_C_FLAGS_DEBUG}"
            CACHE STRING "Flags used by the C compiler during ${_cfg} builds.")
        mark_as_advanced(CMAKE_C_FLAGS_${_cfg_upper})
        foreach(_linker EXE SHARED MODULE STATIC)
            set(CMAKE_${_linker}_LINKER_FLAGS_${_cfg_upper} "${CMAKE_${_linker}_LINKER_FLAGS_DEBUG}"
                CACHE STRING "Flags used by the linker during ${_cfg} builds.")
            mark_as_advanced(CMAKE_${_linker}_LINKER_FLAGS_${_cfg_upper})
        endforeach()
        set(CMAKE_MAP_IMPORTED_CONFIG_${_cfg_upper} Debug "" Release RelWithDebInfo)
    endforeach()
endif()

# Debug, Strict and Analyzer share the developer flag set (-Og -g3, warnings).
set(BRAVAIS_IS_DEBUGLIKE "$<OR:$<CONFIG:Debug>,$<CONFIG:Strict>,$<CONFIG:Analyzer>>")
# Debug and Strict are the runtime-instrumented pair; Analyzer must stay clean
# of sanitizers or -fanalyzer goes quiet (see above).
set(BRAVAIS_IS_SANITIZED "$<OR:$<CONFIG:Debug>,$<CONFIG:Strict>>")

# The Strict configuration always errors on warnings; this option additionally
# promotes them in Debug and Analyzer. OFF by default even though the tree is
# clean today, so a new compiler version's new warning never blocks someone
# mid-debugging -- build Strict when you want the gate.
option(BRAVAIS_WERROR "Debug/Analyzer: treat warnings as errors (always on in Strict)" OFF)
option(BRAVAIS_ENABLE_SANITIZERS "Debug/Strict configs: AddressSanitizer + UndefinedBehaviorSanitizer" ON)
option(BRAVAIS_ENABLE_ANALYZER "Analyzer config: the compiler's static analyzer (GCC -fanalyzer)" ON)
option(BRAVAIS_ENABLE_HARDENING "All configs: stack protector, _FORTIFY_SOURCE, RELRO/NOW, CFI" ON)
option(BRAVAIS_ENABLE_LTO "Optimized configs: link-time optimization" OFF)

# How much of the control-flow path -fanalyzer prints per report. 1 lists only
# the state transitions (opened here / first close here / leaks here), which is
# what you want while triaging; raise it when a report needs the branch-by-branch
# path that explains how it got there. GCC silently accepts out-of-range values,
# so validate here instead.
set(BRAVAIS_ANALYZER_VERBOSITY 1 CACHE STRING "Analyzer config: -fanalyzer path detail, 0 (terse) to 5 (full)")
set_property(CACHE BRAVAIS_ANALYZER_VERBOSITY PROPERTY STRINGS 0 1 2 3 4 5)
if(NOT BRAVAIS_ANALYZER_VERBOSITY MATCHES "^[0-5]$")
    message(FATAL_ERROR
        "BRAVAIS_ANALYZER_VERBOSITY must be an integer from 0 to 5, got '${BRAVAIS_ANALYZER_VERBOSITY}'")
endif()

# ---------------------------------------------------------------------------
# Flag probing
#
# Most of the interesting memory diagnostics below only exist from a certain
# GCC/Clang version onwards, and several are architecture specific. Probe every
# flag instead of gating on compiler version, so an older or foreign toolchain
# silently gets the subset it understands rather than failing to configure.
#
# A flag the driver merely tolerates is not a flag that does anything (Clang
# accepts -fstack-clash-protection on arm64 and then ignores it), so the probe
# promotes "argument unused" to an error where the compiler supports that.
# ---------------------------------------------------------------------------
include(CheckCCompilerFlag)
include(CheckLinkerFlag)

check_c_compiler_flag(-Werror=unused-command-line-argument BRAVAIS_HAS_WERROR_UNUSED_ARG)
if(BRAVAIS_HAS_WERROR_UNUSED_ARG)
    set(BRAVAIS_FLAG_PROBE_STRICT "-Werror=unused-command-line-argument")
else()
    set(BRAVAIS_FLAG_PROBE_STRICT "")
endif()

function(bravais_append_supported_c_flags out_var)
    set(_accepted ${${out_var}})
    set(CMAKE_REQUIRED_FLAGS "${BRAVAIS_FLAG_PROBE_STRICT}")
    foreach(_flag IN LISTS ARGN)
        string(MAKE_C_IDENTIFIER "BRAVAIS_HAS_CFLAG_${_flag}" _cache_var)
        check_c_compiler_flag("${_flag}" ${_cache_var})
        if(${_cache_var})
            list(APPEND _accepted "${_flag}")
        endif()
    endforeach()
    set(${out_var} "${_accepted}" PARENT_SCOPE)
endfunction()

# Instrumentation such as -fsanitize=address has to reach the linker as well,
# and is only usable if the matching runtime library actually exists, so the
# probe hands the flag to both halves of the try-compile.
#
# Flags are also probed cumulatively, in the order given: some are only legal in
# the presence of an earlier one (-fsanitize=pointer-compare is rejected unless
# -fsanitize=address is already on the command line), and probing it alone would
# quietly drop it.
function(bravais_append_supported_instrument_flags out_var)
    set(_accepted ${${out_var}})
    set(_context "")
    foreach(_flag IN LISTS ARGN)
        string(MAKE_C_IDENTIFIER "BRAVAIS_HAS_INSTRUMENT_${_flag}" _cache_var)
        string(JOIN " " CMAKE_REQUIRED_FLAGS ${BRAVAIS_FLAG_PROBE_STRICT} ${_context})
        set(CMAKE_REQUIRED_LINK_OPTIONS ${_context} "${_flag}")
        check_c_compiler_flag("${_flag}" ${_cache_var})
        if(${_cache_var})
            list(APPEND _accepted "${_flag}")
            list(APPEND _context "${_flag}")
        endif()
    endforeach()
    set(${out_var} "${_accepted}" PARENT_SCOPE)
endfunction()

function(bravais_append_supported_link_flags out_var)
    set(_accepted ${${out_var}})
    foreach(_flag IN LISTS ARGN)
        string(MAKE_C_IDENTIFIER "BRAVAIS_HAS_LDFLAG_${_flag}" _cache_var)
        check_linker_flag(C "${_flag}" ${_cache_var})
        if(${_cache_var})
            list(APPEND _accepted "${_flag}")
        endif()
    endforeach()
    set(${out_var} "${_accepted}" PARENT_SCOPE)
endfunction()

# ---------------------------------------------------------------------------
# Flag sets
#
# The per-config sets are applied through generator expressions so multi-config
# generators (Xcode, VS) work too.
# ---------------------------------------------------------------------------
set(BRAVAIS_C_WARNINGS "")
set(BRAVAIS_C_FLAGS_DEBUGLIKE "")
set(BRAVAIS_C_FLAGS_OPTIMIZED "")
# The two instrumentation sets, each owned by one configuration and never both.
set(BRAVAIS_SANITIZER_FLAGS "")
set(BRAVAIS_ANALYZER_FLAGS "")
# Instrumentation that must be handed to the linker as well.
set(BRAVAIS_INSTRUMENT_COMPILE "")
set(BRAVAIS_INSTRUMENT_LINK "")
set(BRAVAIS_WERROR_FLAG -Werror)

# No MSVC branch: the WIN32 guard above makes this file unreachable there.
if(BRAVAIS_DEVELOPER_BUILD)
    # Portable warning set, memory-safety first.
    #
    # -Wconversion and -Wsign-conversion matter more here than in most C
    # projects: this library spends its life converting between size_t lengths,
    # uint64_t varints and the narrow fields of a wire format, and a silent
    # truncation in that path is a parser bug by another name.
    bravais_append_supported_c_flags(BRAVAIS_C_WARNINGS
        -Wall
        -Wextra
        -Wpedantic
        -Wshadow
        -Wconversion
        -Wsign-conversion
        -Wcast-qual
        -Wcast-align
        -Wstrict-prototypes
        -Wmissing-prototypes
        -Wold-style-definition
        -Wbad-function-cast
        -Wwrite-strings
        -Wformat=2
        -Wnull-dereference
        -Wdouble-promotion
        -Wfloat-equal
        -Wvla
        -Wstack-protector
        -Wpointer-arith
        -Wundef
        -Winit-self
        -Wmissing-include-dirs
        -Wswitch-enum
    )

    if(CMAKE_C_COMPILER_ID MATCHES "GNU")
        # GCC-only diagnostics. The -W*=N forms ask for the strictest level:
        # more false positives, but they catch out-of-bounds writes, dangling
        # pointers and double frees that -Wall/-Wextra walk straight past.
        bravais_append_supported_c_flags(BRAVAIS_C_WARNINGS
            -Warray-bounds=2
            -Wstringop-overflow=4
            -Wstringop-truncation
            -Wformat-overflow=2
            -Wformat-truncation=2
            -Wuse-after-free=3
            -Wdangling-pointer=2
            -Wfree-nonheap-object
            -Walloc-zero
            -Walloca
            -Wduplicated-cond
            -Wduplicated-branches
            -Wlogical-op
            -Wjump-misses-init
            -Wtrampolines
            -Wflex-array-member-not-at-end
        )
        # -fanalyzer is a whole-path symbolic execution pass (leaks, double
        # free, use-after-free, NULL derefs across function boundaries). It is
        # the only reason the Analyzer configuration exists.
        if(BRAVAIS_ENABLE_ANALYZER)
            bravais_append_supported_c_flags(BRAVAIS_ANALYZER_FLAGS
                -fanalyzer-verbosity=${BRAVAIS_ANALYZER_VERBOSITY}
                -fanalyzer
            )
        endif()
    elseif(CMAKE_C_COMPILER_ID MATCHES "Clang")
        # Clang-only diagnostics. Clang has no in-compiler equivalent of
        # -fanalyzer, so the Analyzer config is simply an uninstrumented Debug
        # build here -- which is exactly the base `scan-build cmake --build
        # build-analyzer` wants.
        bravais_append_supported_c_flags(BRAVAIS_C_WARNINGS
            -Warray-bounds-pointer-arithmetic
            -Wconditional-uninitialized
            -Wshift-sign-overflow
            -Wassign-enum
            -Wcomma
            -Wloop-analysis
            -Wthread-safety
            -Wover-aligned
        )
    endif()

    # -Og keeps the code steppable while still running the optimizer passes
    # that -Wmaybe-uninitialized / -Wstringop-* rely on; at -O0 those warnings
    # go quiet. -fno-omit-frame-pointer buys readable sanitizer backtraces.
    bravais_append_supported_c_flags(BRAVAIS_C_FLAGS_DEBUGLIKE
        -Og
        -g3
        -fno-omit-frame-pointer
    )
    if(BRAVAIS_WERROR)
        list(APPEND BRAVAIS_C_FLAGS_DEBUGLIKE ${BRAVAIS_WERROR_FLAG})
    endif()

    if(BRAVAIS_ENABLE_HARDENING)
        bravais_append_supported_c_flags(BRAVAIS_C_FLAGS_DEBUGLIKE
            -fstack-protector-strong
        )
        # _FORTIFY_SOURCE needs an optimized build to see through the buffer
        # sizes, and it fights ASan's interceptors, so it is Release-only.
        bravais_append_supported_c_flags(BRAVAIS_C_FLAGS_OPTIMIZED
            -U_FORTIFY_SOURCE
            -D_FORTIFY_SOURCE=3
            -fstack-protector-strong
            -fstack-clash-protection
        )
        bravais_append_supported_instrument_flags(BRAVAIS_INSTRUMENT_COMPILE
            -fcf-protection=full         # x86_64 CET
            -mbranch-protection=standard # aarch64 BTI/PAC
        )
        bravais_append_supported_link_flags(BRAVAIS_INSTRUMENT_LINK
            "LINKER:-z,relro"
            "LINKER:-z,now"
            "LINKER:-z,noexecstack"
        )
    endif()

    if(BRAVAIS_ENABLE_SANITIZERS)
        # Everything that composes with ASan. LeakSanitizer is not listed
        # because ASan already includes it where it is supported, and
        # -fsanitize=leak cannot be combined with -fsanitize=address.
        # -fno-sanitize-recover makes UB abort instead of printing and
        # continuing, so a bad shift or overflow cannot be ignored in CI.
        #
        # pointer-compare/pointer-subtract are inert unless
        # ASAN_OPTIONS=detect_invalid_pointer_pairs=2 is set at run time. Do not
        # set it on macOS/arm64: that runtime check misfires there and reports
        # invalid-pointer-pair against 0xfffffffffffffff0 for code containing no
        # pointer comparison at all. The compile flags stay because they cost
        # nothing and the check is sound on Linux.
        bravais_append_supported_instrument_flags(BRAVAIS_SANITIZER_FLAGS
            -fsanitize=address
            -fsanitize-address-use-after-scope
            -fsanitize=pointer-compare
            -fsanitize=pointer-subtract
            -fsanitize=undefined
            -fsanitize=bounds-strict
            -fno-sanitize-recover=undefined
            -fno-omit-frame-pointer
        )
    endif()

    # Each instrumentation set belongs to exactly one configuration. Keeping
    # them in separate configs is the point of the Analyzer build, so never
    # emit both.
    if(BRAVAIS_SANITIZER_FLAGS)
        list(APPEND BRAVAIS_INSTRUMENT_COMPILE "$<${BRAVAIS_IS_SANITIZED}:${BRAVAIS_SANITIZER_FLAGS}>")
        list(APPEND BRAVAIS_INSTRUMENT_LINK "$<${BRAVAIS_IS_SANITIZED}:${BRAVAIS_SANITIZER_FLAGS}>")
    endif()

    if(BRAVAIS_ENABLE_LTO)
        include(CheckIPOSupported)
        check_ipo_supported(RESULT BRAVAIS_IPO_SUPPORTED OUTPUT BRAVAIS_IPO_ERROR)
        if(NOT BRAVAIS_IPO_SUPPORTED)
            message(WARNING "LTO requested but unsupported by this toolchain: ${BRAVAIS_IPO_ERROR}")
        endif()
    endif()
endif()

# Applies the developer flag set to one of our targets. Deliberately a function
# rather than an INTERFACE library: the library targets go through
# install(EXPORT), and even a PRIVATE link to an INTERFACE target lands in
# INTERFACE_LINK_LIBRARIES as $<LINK_ONLY:...>, at which point CMake refuses to
# generate the export set unless that target is exported too. A function has no
# such footprint. Visible in tests/ and fuzz/ because add_subdirectory()
# inherits functions defined before the call.
function(bravais_apply_build_flags target)
    # Before the gate: strict C23 is what bravais targets, not a developer
    # nicety.
    #
    # target_compile_features(c_std_23) alone is not enough to pin the dialect.
    # It only states a *lower bound*, so when the compiler's own default already
    # meets it CMake emits no -std flag at all - and C_EXTENSIONS is only
    # consulted when a flag is emitted. GCC 16 defaults to gnu23, so it would
    # silently build with extensions on while AppleClang (default C17) got
    # -std=c2x. Setting C_STANDARD explicitly makes the dialect identical on
    # both. The compile feature stays as well: it is what propagates the C23
    # requirement to consumers, which these properties do not.
    set_target_properties(${target} PROPERTIES
        C_STANDARD 23
        C_STANDARD_REQUIRED ON
        C_EXTENSIONS OFF
    )
    if(NOT BRAVAIS_DEVELOPER_BUILD)
        return()
    endif()
    # Our warning policy and our dialect are nobody else's business, so these
    # stay PRIVATE and never reach INTERFACE_COMPILE_OPTIONS.
    target_compile_options(${target} PRIVATE
        "${BRAVAIS_C_WARNINGS}"
        "$<${BRAVAIS_IS_DEBUGLIKE}:${BRAVAIS_C_FLAGS_DEBUGLIKE}>"
        "$<$<NOT:${BRAVAIS_IS_DEBUGLIKE}>:${BRAVAIS_C_FLAGS_OPTIMIZED}>"
        # The static analyzer runs only in its own configuration, and never
        # alongside the sanitizers.
        "$<$<CONFIG:Analyzer>:${BRAVAIS_ANALYZER_FLAGS}>"
        # Strict is Debug with the warning set turned into a build gate.
        "$<$<CONFIG:Strict>:${BRAVAIS_WERROR_FLAG}>"
    )

    # Instrumentation is different: it is not a policy, it is an ABI. An
    # ASan-instrumented libbravais.a emits calls to __asan_* that only resolve
    # if the *final* link line also carries -fsanitize=address, so a consumer of
    # a Debug/Strict build must inherit the flags or it fails to link with a
    # wall of undefined ___asan_* symbols.
    #
    # libbravais.so does resolve its own __asan_* at link time, so it would
    # build either way -- but an ASan DSO loaded by an uninstrumented executable
    # needs the runtime initialised first, which means LD_PRELOAD and a bad
    # afternoon. Propagating the flags to consumers of the shared library too
    # makes the sane case automatic and the insane case someone else's explicit
    # choice. Hence PUBLIC on both library kinds, PRIVATE on executables, which
    # are link leaves.
    get_target_property(_target_type ${target} TYPE)
    if(_target_type STREQUAL "STATIC_LIBRARY" OR _target_type STREQUAL "SHARED_LIBRARY")
        set(_instrument_scope PUBLIC)
    else()
        set(_instrument_scope PRIVATE)
    endif()
    target_compile_options(${target} ${_instrument_scope} "${BRAVAIS_INSTRUMENT_COMPILE}")
    target_link_options(${target} ${_instrument_scope} "${BRAVAIS_INSTRUMENT_LINK}")
    if(BRAVAIS_ENABLE_LTO AND BRAVAIS_IPO_SUPPORTED)
        set_target_properties(${target} PROPERTIES
            INTERPROCEDURAL_OPTIMIZATION_RELEASE ON
            INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO ON
            INTERPROCEDURAL_OPTIMIZATION_MINSIZEREL ON
        )
    endif()
endfunction()

# ---------------------------------------------------------------------------
# Dependencies
#
# libsodium: X25519, ChaCha20-Poly1305, BLAKE2b, HKDF, constant-time compare,
#            sodium_memzero. Found via pkg-config; it ships no CMake package.
# liboqs:    ML-KEM-768. Ships a CMake config package.
#
# Both are PRIVATE link dependencies. The public header exposes neither, so a
# consumer of libbravais never needs their include directories -- and the
# static-library case still links them transitively because CMake records
# PRIVATE deps of a static library as $<LINK_ONLY:...> in the export.
# ---------------------------------------------------------------------------
if(BRAVAIS_ENABLE_CRYPTO)
    find_package(PkgConfig REQUIRED)
    pkg_check_modules(SODIUM IMPORTED_TARGET libsodium>=1.0.18)
    find_package(liboqs CONFIG)

    if(NOT SODIUM_FOUND OR NOT liboqs_FOUND)
        message(FATAL_ERROR
            "libbravais needs libsodium (found: ${SODIUM_FOUND}) and liboqs (found: ${liboqs_FOUND}).\n"
            "  Debian/Ubuntu: apt install libsodium-dev, then build liboqs from source\n"
            "  macOS:         brew install libsodium liboqs\n"
            "To bootstrap util/ and wire/ without them, reconfigure with "
            "-DBRAVAIS_ENABLE_CRYPTO=OFF.")
    endif()
endif()

# ---------------------------------------------------------------------------
# Sources
#
# One list, two targets. Uncomment each group as it lands; CMake errors at
# configure time on a source file that does not exist yet, which is a feature
# (it keeps this list honest) and an annoyance (it means editing here as you
# go). Order mirrors the build order in docs/ROADMAP.md: wire before crypto,
# crypto before proto.
# ---------------------------------------------------------------------------
set(GLOB_RECURSE BRAVAIS_SOURCES
    src/util/*.c
    src/wire/*.c
)

if(BRAVAIS_ENABLE_CRYPTO)
    set (GLOB_RECURSE BRAVAIS_SOURCES
        ${BRAVAIS_SOURCES}
        src/crypto/*.c
        src/proto/*.c
    )
endif()

# ---------------------------------------------------------------------------
# Library targets
#
# Static and shared are built as two targets from one source list, which
# compiles every translation unit twice. The obvious alternative -- one OBJECT
# library consumed by both -- saves that, but $<TARGET_OBJECTS:> does not carry
# usage requirements, so the include directories, compile features and link
# dependencies would have to be restated on all three targets, and the object
# library has to join the export set for install(EXPORT) to generate at all.
# For a library this size, compiling twice is cheaper than that bookkeeping.
#
# Both get POSITION_INDEPENDENT_CODE: the shared library needs it, and the
# static one gets linked into bravaisd, which is built PIE.
# ---------------------------------------------------------------------------
set(BRAVAIS_LIB_TARGETS "")

if(BRAVAIS_BUILD_STATIC)
    add_library(bravais_static STATIC ${BRAVAIS_SOURCES})
    add_library(bravais::static ALIAS bravais_static)
    list(APPEND BRAVAIS_LIB_TARGETS bravais_static)
    set_target_properties(bravais_static PROPERTIES OUTPUT_NAME bravais)
endif()

if(BRAVAIS_BUILD_SHARED)
    add_library(bravais_shared SHARED ${BRAVAIS_SOURCES})
    add_library(bravais::shared ALIAS bravais_shared)
    list(APPEND BRAVAIS_LIB_TARGETS bravais_shared)
    # SOVERSION tracks the major version, which is 0 -- and 0.x explicitly
    # promises nothing about ABI. Bump the minor on every ABI break while the
    # major is 0, and start taking SOVERSION seriously at 1.0.
    set_target_properties(bravais_shared PROPERTIES
        OUTPUT_NAME bravais
        VERSION ${PROJECT_VERSION}
        SOVERSION ${PROJECT_VERSION_MAJOR}
    )
endif()

# `bravais::bravais` is what consumers should link. It follows the usual CMake
# convention and resolves to the shared library when both are built.
if(BRAVAIS_BUILD_SHARED)
    add_library(bravais::bravais ALIAS bravais_shared)
else()
    add_library(bravais::bravais ALIAS bravais_static)
endif()

# ---------------------------------------------------------------------------
# Symbol visibility
#
# Everything is hidden by default and the public API is opted in one symbol at
# a time with BRAVAIS_API, generated below. This is the one place where the
# static and shared builds genuinely differ, and getting it wrong is how a
# library ends up exporting its own bravais_internal_* helpers forever.
#
#   bravais.h:          BRAVAIS_API bravais_result_t bravais_conn_new(...);
#   static consumers:   BRAVAIS_STATIC_DEFINE makes BRAVAIS_API expand to nothing
#
# The generated header lands in the build tree and is installed alongside the
# hand-written ones.
# ---------------------------------------------------------------------------
include(GenerateExportHeader)

if(BRAVAIS_BUILD_SHARED)
    set(BRAVAIS_EXPORT_HEADER_TARGET bravais_shared)
else()
    set(BRAVAIS_EXPORT_HEADER_TARGET bravais_static)
endif()

generate_export_header(${BRAVAIS_EXPORT_HEADER_TARGET}
    BASE_NAME BRAVAIS
    EXPORT_MACRO_NAME BRAVAIS_API
    NO_EXPORT_MACRO_NAME BRAVAIS_LOCAL
    DEPRECATED_MACRO_NAME BRAVAIS_DEPRECATED
    STATIC_DEFINE BRAVAIS_STATIC_DEFINE
    EXPORT_FILE_NAME ${CMAKE_CURRENT_BINARY_DIR}/export.h.raw
)

# generate_export_header() cannot put anything above its include guard
# (CUSTOM_CONTENT_FROM_VARIABLE lands at the bottom), so it writes to a scratch
# file and the SPDX line is stamped on here. configure_file() only rewrites the
# real header when its content changes, so reconfiguring does not force a
# rebuild of everything that includes it.
file(READ ${CMAKE_CURRENT_BINARY_DIR}/export.h.raw _export_h)
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/export.h.stamped
    "// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR MIT\n${_export_h}")
configure_file(${CMAKE_CURRENT_BINARY_DIR}/export.h.stamped
    ${CMAKE_CURRENT_BINARY_DIR}/include/bravais/export.h COPYONLY)

foreach(_target IN LISTS BRAVAIS_LIB_TARGETS)
    set_target_properties(${_target} PROPERTIES
        C_VISIBILITY_PRESET hidden
        VISIBILITY_INLINES_HIDDEN ON
        POSITION_INDEPENDENT_CODE ON
    )

    target_include_directories(${_target}
        PUBLIC
            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
            $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>
            $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
        PRIVATE
            ${CMAKE_CURRENT_SOURCE_DIR}/src
    )

    # PUBLIC, so consumers of bravais::bravais are compiled as C23 too. This is
    # exported as INTERFACE_COMPILE_FEATURES, which means consumers need CMake
    # 3.21 or newer to understand it. bravais_apply_build_flags() turns
    # extensions off, asking for -std=c2x rather than -std=gnu2x; the feature
    # macros above put back everything strict mode hides.
    target_compile_features(${_target} PUBLIC c_std_23)

    # PRIVATE: only the library translation units need these.
    target_compile_definitions(${_target} PRIVATE ${BRAVAIS_FEATURE_DEFS})

    if(BRAVAIS_ENABLE_CRYPTO)
        target_link_libraries(${_target} PRIVATE PkgConfig::SODIUM OQS::oqs)
    endif()

    bravais_apply_build_flags(${_target})
endforeach()

# A consumer linking the static library must see BRAVAIS_API as empty, or the
# declarations carry a dllimport/visibility attribute that does not match what
# was compiled. PUBLIC so it reaches them through the export.
if(BRAVAIS_BUILD_STATIC)
    target_compile_definitions(bravais_static PUBLIC BRAVAIS_STATIC_DEFINE)
endif()

# ---------------------------------------------------------------------------
# Optional subdirectories
# ---------------------------------------------------------------------------
if(BRAVAIS_BUILD_EXAMPLES)
    add_subdirectory(examples)
endif()

if(BRAVAIS_BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

# Fuzzers link the static library so libFuzzer's -fsanitize=fuzzer sees every
# translation unit's coverage instrumentation. They need Clang; GCC has no
# libFuzzer.
if(BRAVAIS_BUILD_FUZZERS)
    if(NOT CMAKE_C_COMPILER_ID MATCHES "Clang")
        message(FATAL_ERROR
            "BRAVAIS_BUILD_FUZZERS needs Clang (libFuzzer); this is ${CMAKE_C_COMPILER_ID}. "
            "Reconfigure with -DCMAKE_C_COMPILER=clang.")
    endif()
    if(NOT BRAVAIS_BUILD_STATIC)
        message(FATAL_ERROR "BRAVAIS_BUILD_FUZZERS needs BRAVAIS_BUILD_STATIC=ON.")
    endif()
    add_subdirectory(fuzz)
endif()

# ---------------------------------------------------------------------------
# Install / export (skipped when consumed via add_subdirectory()).
# ---------------------------------------------------------------------------
if(PROJECT_IS_TOP_LEVEL)
    include(GNUInstallDirs)
    include(CMakePackageConfigHelpers)

    install(TARGETS ${BRAVAIS_LIB_TARGETS}
        EXPORT bravaisTargets
        ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
        LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
        RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
        INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
    )

    # Hand-written public headers.
    install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/
        DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
        FILES_MATCHING PATTERN "*.h"
    )

    # ...and the generated one.
    install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/bravais/export.h
        DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/bravais
    )

    install(EXPORT bravaisTargets
        FILE bravaisTargets.cmake
        NAMESPACE bravais::
        DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/bravais
    )

    configure_package_config_file(
        ${CMAKE_CURRENT_SOURCE_DIR}/cmake/bravaisConfig.cmake.in
        ${CMAKE_CURRENT_BINARY_DIR}/bravaisConfig.cmake
        INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/bravais
    )

    write_basic_package_version_file(
        ${CMAKE_CURRENT_BINARY_DIR}/bravaisConfigVersion.cmake
        VERSION ${PROJECT_VERSION}
        COMPATIBILITY SameMajorVersion
    )

    install(FILES
        ${CMAKE_CURRENT_BINARY_DIR}/bravaisConfig.cmake
        ${CMAKE_CURRENT_BINARY_DIR}/bravaisConfigVersion.cmake
        DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/bravais
    )

    # pkg-config, for the consumers who are not using CMake. Cheap to ship and
    # the thing a distro packager will ask for first.
    configure_file(
        ${CMAKE_CURRENT_SOURCE_DIR}/cmake/bravais.pc.in
        ${CMAKE_CURRENT_BINARY_DIR}/bravais.pc
        @ONLY
    )
    install(FILES ${CMAKE_CURRENT_BINARY_DIR}/bravais.pc
        DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig
    )
endif()

# ---------------------------------------------------------------------------
# Configuration summary
# ---------------------------------------------------------------------------
if(BRAVAIS_DEVELOPER_BUILD)
    message(STATUS "bravais: version         ${PROJECT_VERSION}")
    message(STATUS "bravais: build type      ${CMAKE_BUILD_TYPE}")
    message(STATUS "bravais: compiler        ${CMAKE_C_COMPILER_ID} ${CMAKE_C_COMPILER_VERSION}")
    message(STATUS "bravais: C standard      C23 (extensions off)")
    message(STATUS "bravais: libraries       static=${BRAVAIS_BUILD_STATIC} shared=${BRAVAIS_BUILD_SHARED}")
    message(STATUS "bravais: crypto          ${BRAVAIS_ENABLE_CRYPTO}")
    message(STATUS "bravais: warnings-as-err ${BRAVAIS_WERROR} (always on in Strict)")
    message(STATUS "bravais: sanitizers      [Debug/Strict] ${BRAVAIS_SANITIZER_FLAGS}")
    message(STATUS "bravais: static analyzer [Analyzer]     ${BRAVAIS_ANALYZER_FLAGS}")
    message(STATUS "bravais: hardening       ${BRAVAIS_ENABLE_HARDENING}")
    message(STATUS "bravais: LTO             ${BRAVAIS_ENABLE_LTO}")
    if(NOT BRAVAIS_ENABLE_CRYPTO)
        message(STATUS "bravais: NOTE - crypto is OFF; crypto/ and proto/ are not built. "
                       "This is a bootstrap configuration, not a usable library.")
    endif()
    if(CMAKE_BUILD_TYPE STREQUAL "Analyzer" AND NOT BRAVAIS_ANALYZER_FLAGS)
        message(STATUS "bravais: NOTE - Analyzer config has no static analyzer on "
                       "${CMAKE_C_COMPILER_ID}; reconfigure with -DCMAKE_C_COMPILER=gcc "
                       "or run scan-build over this build tree")
    endif()
else()
    message(STATUS "bravais: consumed as a subproject; developer flags off "
                   "(-DBRAVAIS_DEVELOPER_BUILD=ON to enable)")
endif()