Files
libbravais/CMakeLists.txt
2026-10-20 16:49:53 +02:00

776 lines
34 KiB
CMake

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()