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 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 "$,$,$>") # Debug and Strict are the runtime-instrumented pair; Analyzer must stay clean # of sanitizers or -fanalyzer goes quiet (see above). set(BRAVAIS_IS_SANITIZED "$,$>") # 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 $, 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}>" "$<$:${BRAVAIS_C_FLAGS_OPTIMIZED}>" # The static analyzer runs only in its own configuration, and never # alongside the sanitizers. "$<$:${BRAVAIS_ANALYZER_FLAGS}>" # Strict is Debug with the warning set turned into a build gate. "$<$:${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 $ 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 $ 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 $ $ $ 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()