Project Architecture
A professional CMake project separates concerns into distinct directories, each with a focused responsibility. This structure scales from small libraries to large multi-component applications without requiring reorganization as the project grows. The top-level CMakeLists.txt acts as an orchestrator — it defines global settings, finds dependencies, and delegates to subdirectories via add_subdirectory().
flowchart TD
ROOT["myproject/"] --> CMAKE["cmake/"]
ROOT --> SRC["src/"]
ROOT --> INCLUDE["include/myproject/"]
ROOT --> TESTS["tests/"]
ROOT --> DOCS["docs/"]
ROOT --> EXAMPLES["examples/"]
ROOT --> PACKAGING["packaging/"]
ROOT --> PRESETS["CMakePresets.json"]
ROOT --> TOPLEVEL["CMakeLists.txt"]
CMAKE --> CM1["FindCustomLib.cmake"]
CMAKE --> CM2["CompilerWarnings.cmake"]
CMAKE --> CM3["Sanitizers.cmake"]
CMAKE --> CM4["Coverage.cmake"]
SRC --> SRC1["CMakeLists.txt"]
SRC --> SRC2["core/"]
SRC --> SRC3["utils/"]
SRC --> SRC4["app/"]
INCLUDE --> INC1["core.hpp"]
INCLUDE --> INC2["utils.hpp"]
INCLUDE --> INC3["export.hpp"]
INCLUDE --> INC4["version.hpp.in"]
TESTS --> T1["CMakeLists.txt"]
TESTS --> T2["unit/"]
TESTS --> T3["integration/"]
DOCS --> D1["CMakeLists.txt"]
DOCS --> D2["Doxyfile.in"]
DOCS --> D3["conf.py.in"]
style ROOT fill:#132440,color:#fff
style TOPLEVEL fill:#3B9797,color:#fff
style PRESETS fill:#3B9797,color:#fff
include/myproject/ (installed for consumers), private sources live in src/ (never installed), CMake helper modules live in cmake/ (reusable across projects), and tests are fully isolated in tests/ with their own CMakeLists.txt.
Top-Level CMakeLists.txt Design
The root CMakeLists.txt establishes project-wide settings, determines build options, and delegates to subdirectories. It should never contain target definitions directly — those belong in the subdirectory CMakeLists.txt files.
# CMakeLists.txt — Top-level orchestrator
cmake_minimum_required(VERSION 3.25)
project(myproject
VERSION 2.1.0
DESCRIPTION "A professional-grade C++ library"
HOMEPAGE_URL "https://github.com/myorg/myproject"
LANGUAGES CXX
)
# Prevent in-source builds
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR)
message(FATAL_ERROR "In-source builds are not allowed. Use: cmake -B build")
endif()
# Standard project-wide settings
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Add our cmake/ directory to the module path
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
# Project options
option(MYPROJECT_BUILD_TESTS "Build unit and integration tests" ON)
option(MYPROJECT_BUILD_DOCS "Build documentation" OFF)
option(MYPROJECT_BUILD_EXAMPLES "Build example programs" ON)
option(MYPROJECT_ENABLE_COVERAGE "Enable code coverage" OFF)
option(MYPROJECT_ENABLE_SANITIZERS "Enable sanitizers (ASan+UBSan)" OFF)
option(MYPROJECT_INSTALL "Generate install target" ON)
# Include helper modules
include(CompilerWarnings)
include(Sanitizers)
# Dependencies
find_package(fmt REQUIRED)
find_package(spdlog REQUIRED)
find_package(nlohmann_json 3.11 REQUIRED)
include(FetchContent)
FetchContent_Declare(
expected
GIT_REPOSITORY https://github.com/TartanLlama/expected.git
GIT_TAG v1.1.0
)
FetchContent_MakeAvailable(expected)
# Source tree
add_subdirectory(src)
# Tests
if(MYPROJECT_BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
# Documentation
if(MYPROJECT_BUILD_DOCS)
add_subdirectory(docs)
endif()
# Examples
if(MYPROJECT_BUILD_EXAMPLES)
add_subdirectory(examples)
endif()
# Installation
if(MYPROJECT_INSTALL)
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
include(cmake/Install.cmake)
endif()
MYPROJECT_) to avoid collisions when your project is consumed as a subdirectory via FetchContent or add_subdirectory(). This namespacing ensures options don't conflict with parent or sibling projects.
Version Management
Professional projects need a single source of truth for their version number. CMake's project(VERSION) provides the canonical version, but production builds should also encode Git metadata for traceability. The standard approach uses configure_file() to generate a header that embeds version information at compile time.
Git-Derived Version
# cmake/GitVersion.cmake — Extract version info from Git
find_package(Git QUIET)
set(MYPROJECT_GIT_HASH "unknown")
set(MYPROJECT_GIT_DESCRIBE "unknown")
set(MYPROJECT_GIT_DIRTY FALSE)
if(GIT_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git")
execute_process(
COMMAND ${GIT_EXECUTABLE} rev-parse --short=8 HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE MYPROJECT_GIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
execute_process(
COMMAND ${GIT_EXECUTABLE} describe --tags --always --dirty
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE MYPROJECT_GIT_DESCRIBE
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
execute_process(
COMMAND ${GIT_EXECUTABLE} diff --quiet HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
RESULT_VARIABLE GIT_DIFF_RESULT
)
if(NOT GIT_DIFF_RESULT EQUAL 0)
set(MYPROJECT_GIT_DIRTY TRUE)
endif()
endif()
message(STATUS "Git version: ${MYPROJECT_GIT_DESCRIBE} (${MYPROJECT_GIT_HASH})")
message(STATUS "Git dirty: ${MYPROJECT_GIT_DIRTY}")
// include/myproject/version.hpp.in — Template processed by configure_file()
#pragma once
// Semantic version from project(VERSION)
#define MYPROJECT_VERSION_MAJOR @myproject_VERSION_MAJOR@
#define MYPROJECT_VERSION_MINOR @myproject_VERSION_MINOR@
#define MYPROJECT_VERSION_PATCH @myproject_VERSION_PATCH@
#define MYPROJECT_VERSION_STRING "@myproject_VERSION@"
// Git metadata for build traceability
#define MYPROJECT_GIT_HASH "@MYPROJECT_GIT_HASH@"
#define MYPROJECT_GIT_DESCRIBE "@MYPROJECT_GIT_DESCRIBE@"
#define MYPROJECT_GIT_DIRTY @MYPROJECT_GIT_DIRTY@
// Compile-time build info
#define MYPROJECT_BUILD_TYPE "@CMAKE_BUILD_TYPE@"
#define MYPROJECT_COMPILER_ID "@CMAKE_CXX_COMPILER_ID@"
#define MYPROJECT_COMPILER_VERSION "@CMAKE_CXX_COMPILER_VERSION@"
namespace myproject {
struct VersionInfo {
static constexpr int major = MYPROJECT_VERSION_MAJOR;
static constexpr int minor = MYPROJECT_VERSION_MINOR;
static constexpr int patch = MYPROJECT_VERSION_PATCH;
static constexpr const char* string = MYPROJECT_VERSION_STRING;
static constexpr const char* git_hash = MYPROJECT_GIT_HASH;
static constexpr const char* git_describe = MYPROJECT_GIT_DESCRIBE;
static constexpr bool git_dirty = MYPROJECT_GIT_DIRTY;
};
} // namespace myproject
# src/CMakeLists.txt — Generate version header
include(GitVersion)
configure_file(
"${CMAKE_SOURCE_DIR}/include/myproject/version.hpp.in"
"${CMAKE_BINARY_DIR}/generated/myproject/version.hpp"
@ONLY
)
# Make the generated header available
target_include_directories(myproject_core PUBLIC
$<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/generated>
)
Dependency Strategy
Professional projects rarely exist in isolation — they depend on external libraries for functionality that would be unreasonable to reimplement. The key decision is how to acquire each dependency. The strategy depends on dependency size, update frequency, and whether your consumers will also need it.
flowchart TD
A[Need External Library] --> B{Large/Complex?}
B -->|Yes| C{System-installable?}
B -->|No| D{Header-only?}
C -->|Yes| E[find_package]
C -->|No| F{Team uses package manager?}
D -->|Yes| G[FetchContent]
D -->|No| H{Build takes <30s?}
F -->|vcpkg| I[vcpkg.json manifest]
F -->|Conan| J[conanfile.txt]
F -->|Neither| K[FetchContent + cache]
H -->|Yes| G
H -->|No| L[find_package + instructions]
E --> M[CMake Config Package]
G --> M
I --> M
J --> M
K --> M
L --> M
style A fill:#132440,color:#fff
style M fill:#3B9797,color:#fff
style E fill:#16476A,color:#fff
style G fill:#16476A,color:#fff
vcpkg and Conan Manifest Mode
{
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
"name": "myproject",
"version-semver": "2.1.0",
"dependencies": [
"fmt",
"spdlog",
"nlohmann-json",
{
"name": "catch2",
"version>=": "3.5.0"
},
{
"name": "boost-asio",
"platform": "!emscripten"
}
],
"builtin-baseline": "c9fa965c2a1b1f3f028469f0f103d23c1e40e261",
"overrides": [
{ "name": "fmt", "version": "11.0.2" }
]
}
# Using vcpkg in toolchain mode — CMakePresets.json integration
# No changes needed in CMakeLists.txt! vcpkg integrates transparently.
# The preset sets CMAKE_TOOLCHAIN_FILE to vcpkg's toolchain.
# For Conan 2.x, dependencies appear as normal find_package() calls
# after running: conan install . --output-folder=build --build=missing
find_package(fmt REQUIRED)
find_package(spdlog REQUIRED)
# Same CMakeLists.txt works with vcpkg, Conan, or system packages
set(FMT_DIR "/usr/local/lib/cmake/fmt") or similar absolute paths. This breaks portability and CI. Always rely on find_package() with proper CMAKE_PREFIX_PATH configuration, or use a package manager manifest that handles paths automatically.
Library Design
Designing a library with CMake means thinking carefully about what you expose to consumers versus what stays internal. The PUBLIC, PRIVATE, and INTERFACE keywords on target_* commands control this boundary precisely. Combined with GenerateExportHeader, your library can present a clean ABI on all platforms.
Header-Only + Compiled Split
# src/CMakeLists.txt — Library with header-only and compiled components
# Compiled library with exported symbols
add_library(myproject_core
core/engine.cpp
core/config.cpp
utils/logging.cpp
utils/string_utils.cpp
)
add_library(myproject::core ALIAS myproject_core)
# Generate platform-specific export macros
include(GenerateExportHeader)
generate_export_header(myproject_core
BASE_NAME MYPROJECT
EXPORT_FILE_NAME "${CMAKE_BINARY_DIR}/generated/myproject/export.hpp"
)
target_include_directories(myproject_core
PUBLIC
$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
$<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/generated>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(myproject_core
PUBLIC
nlohmann_json::nlohmann_json # Appears in our public headers
PRIVATE
fmt::fmt # Used only in .cpp files
spdlog::spdlog # Used only in .cpp files
tl::expected # Used only in .cpp files
)
target_compile_features(myproject_core PUBLIC cxx_std_20)
# Header-only utilities — INTERFACE library
add_library(myproject_headers INTERFACE)
add_library(myproject::headers ALIAS myproject_headers)
target_include_directories(myproject_headers INTERFACE
$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
target_compile_features(myproject_headers INTERFACE cxx_std_20)
# Application executable
add_executable(myproject_app app/main.cpp)
add_executable(myproject::app ALIAS myproject_app)
target_link_libraries(myproject_app PRIVATE myproject::core)
// include/myproject/core.hpp — Public API with export macros
#pragma once
#include "myproject/export.hpp"
#include <nlohmann/json.hpp>
#include <string>
#include <string_view>
namespace myproject {
/// Configuration loaded from JSON files
class MYPROJECT_EXPORT Config {
public:
explicit Config(std::string_view path);
~Config();
[[nodiscard]] std::string get(std::string_view key) const;
[[nodiscard]] int get_int(std::string_view key, int default_val = 0) const;
[[nodiscard]] bool has(std::string_view key) const;
void reload();
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
/// Core processing engine
class MYPROJECT_EXPORT Engine {
public:
explicit Engine(const Config& config);
~Engine();
void start();
void stop();
[[nodiscard]] bool is_running() const noexcept;
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace myproject
GenerateExportHeader, your library compiles with proper __declspec(dllexport) on Windows and visibility attributes on Unix — no platform-specific #ifdef needed.
Target Properties Best Practices
Compile Features and ALIAS Targets
Modern CMake expresses requirements through target properties rather than global variables. Every target declares what it needs (compile features, definitions, include paths) and what it provides to consumers. ALIAS targets with namespace prefixes ensure uniform syntax whether a library is consumed via find_package() or add_subdirectory().
# cmake/CompilerWarnings.cmake — Reusable warning configuration
add_library(myproject_warnings INTERFACE)
add_library(myproject::warnings ALIAS myproject_warnings)
target_compile_options(myproject_warnings INTERFACE
$<$<CXX_COMPILER_ID:MSVC>:
/W4 /WX /permissive- /utf-8
/wd4251 # DLL interface warning (expected with PIMPL)
>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:
-Wall -Wextra -Wpedantic -Werror
-Wconversion -Wsign-conversion
-Wnon-virtual-dtor -Wold-style-cast
-Wcast-align -Woverloaded-virtual
-Wshadow -Wformat=2
>
)
# Apply warnings to our targets (not to dependencies)
target_link_libraries(myproject_core PRIVATE myproject::warnings)
target_link_libraries(myproject_app PRIVATE myproject::warnings)
add_library(myproject::core ALIAS myproject_core). This means consumers use the same target_link_libraries(... myproject::core) syntax regardless of how they obtain your library. CMake will error if a namespaced target doesn't exist, catching typos early.
Testing Infrastructure
A professional project has tests at multiple levels — unit tests for individual components, integration tests for subsystem interactions, and possibly end-to-end tests for the complete application. CTest provides the orchestration layer, while frameworks like Catch2 or GoogleTest handle assertion logic and test discovery.
CTest Integration with Coverage
# tests/CMakeLists.txt — Test infrastructure
include(CTest)
# Fetch Catch2 for unit testing
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.7.1
)
FetchContent_MakeAvailable(Catch2)
list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras)
include(Catch)
# Unit test executable
add_executable(unit_tests
unit/test_config.cpp
unit/test_engine.cpp
unit/test_string_utils.cpp
)
target_link_libraries(unit_tests PRIVATE
myproject::core
Catch2::Catch2WithMain
)
# Auto-discover tests from Catch2 TEST_CASE macros
catch_discover_tests(unit_tests
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
PROPERTIES
LABELS "unit"
TIMEOUT 30
)
# Integration tests
add_executable(integration_tests
integration/test_engine_lifecycle.cpp
integration/test_config_reload.cpp
)
target_link_libraries(integration_tests PRIVATE
myproject::core
Catch2::Catch2WithMain
)
catch_discover_tests(integration_tests
PROPERTIES
LABELS "integration"
TIMEOUT 120
)
# cmake/Coverage.cmake — Code coverage with gcov/lcov
option(MYPROJECT_ENABLE_COVERAGE "Enable code coverage" OFF)
if(MYPROJECT_ENABLE_COVERAGE)
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
message(WARNING "Coverage requires GCC or Clang")
return()
endif()
message(STATUS "Code coverage enabled")
# Add coverage flags to the library (not tests)
target_compile_options(myproject_core PRIVATE --coverage -fprofile-arcs -ftest-coverage)
target_link_options(myproject_core PRIVATE --coverage)
# Custom target to generate coverage report
find_program(LCOV lcov REQUIRED)
find_program(GENHTML genhtml REQUIRED)
add_custom_target(coverage
COMMAND ${LCOV} --capture --directory . --output-file coverage.info
--rc branch_coverage=1
COMMAND ${LCOV} --remove coverage.info
'/usr/*' '*/tests/*' '*/build/*' '*/_deps/*'
--output-file coverage_filtered.info
--rc branch_coverage=1
COMMAND ${GENHTML} coverage_filtered.info
--output-directory coverage_report
--branch-coverage --title "myproject Coverage"
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Generating code coverage report..."
VERBATIM
)
endif()
# Run tests with coverage
cmake -B build -DMYPROJECT_ENABLE_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
cmake --build build
cd build && ctest --output-on-failure
# Generate HTML coverage report
cmake --build build --target coverage
# Open build/coverage_report/index.html in browser
Documentation Pipeline
Professional C++ projects combine Doxygen (API extraction from source comments) with Sphinx (narrative documentation in reStructuredText) via the Breathe bridge. This produces beautiful, searchable documentation that includes both hand-written guides and auto-generated API reference — all built as part of the CMake workflow.
Build-Time Documentation Generation
# docs/CMakeLists.txt — Documentation build
find_package(Doxygen REQUIRED)
find_program(SPHINX_EXECUTABLE sphinx-build REQUIRED)
# Configure Doxyfile with project version and paths
set(DOXYGEN_INPUT_DIR "${CMAKE_SOURCE_DIR}/include/myproject")
set(DOXYGEN_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/doxygen")
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in"
"${CMAKE_CURRENT_BINARY_DIR}/Doxyfile"
@ONLY
)
# Doxygen target — generates XML for Breathe
add_custom_target(doxygen
COMMAND ${DOXYGEN_EXECUTABLE} "${CMAKE_CURRENT_BINARY_DIR}/Doxyfile"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Generating Doxygen XML..."
VERBATIM
)
# Configure Sphinx conf.py with version and Breathe path
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/conf.py.in"
"${CMAKE_CURRENT_BINARY_DIR}/conf.py"
@ONLY
)
# Sphinx target — generates HTML from RST + Doxygen XML
add_custom_target(docs
COMMAND ${SPHINX_EXECUTABLE}
-b html
-c "${CMAKE_CURRENT_BINARY_DIR}"
"${CMAKE_CURRENT_SOURCE_DIR}/source"
"${CMAKE_CURRENT_BINARY_DIR}/html"
DEPENDS doxygen
COMMENT "Building Sphinx documentation..."
VERBATIM
)
# Optional: install documentation
install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/html/"
DESTINATION ${CMAKE_INSTALL_DOCDIR}
COMPONENT documentation
OPTIONAL
)
# Build documentation
cmake -B build -DMYPROJECT_BUILD_DOCS=ON
cmake --build build --target docs
# Documentation available at build/docs/html/index.html
Static Analysis Integration
CMake integrates static analysis tools directly into the build process via the CMAKE_<LANG>_CLANG_TIDY, CMAKE_<LANG>_CPPCHECK, and CMAKE_<LANG>_INCLUDE_WHAT_YOU_USE variables. When set, CMake runs the analysis tool on every source file during compilation, catching issues as part of the normal development workflow rather than requiring a separate analysis pass.
clang-tidy, cppcheck, and Include-What-You-Use
# cmake/StaticAnalysis.cmake — Optional static analysis integration
option(MYPROJECT_ENABLE_CLANG_TIDY "Run clang-tidy during build" OFF)
option(MYPROJECT_ENABLE_CPPCHECK "Run cppcheck during build" OFF)
option(MYPROJECT_ENABLE_IWYU "Run include-what-you-use during build" OFF)
if(MYPROJECT_ENABLE_CLANG_TIDY)
find_program(CLANG_TIDY_EXE NAMES clang-tidy clang-tidy-18 REQUIRED)
set(CMAKE_CXX_CLANG_TIDY
${CLANG_TIDY_EXE}
--config-file=${CMAKE_SOURCE_DIR}/.clang-tidy
--header-filter=${CMAKE_SOURCE_DIR}/include/.*
--warnings-as-errors=*
)
message(STATUS "clang-tidy enabled: ${CLANG_TIDY_EXE}")
endif()
if(MYPROJECT_ENABLE_CPPCHECK)
find_program(CPPCHECK_EXE cppcheck REQUIRED)
set(CMAKE_CXX_CPPCHECK
${CPPCHECK_EXE}
--enable=warning,performance,portability
--suppress=missingIncludeSystem
--inline-suppr
--error-exitcode=1
--std=c++20
)
message(STATUS "cppcheck enabled: ${CPPCHECK_EXE}")
endif()
if(MYPROJECT_ENABLE_IWYU)
find_program(IWYU_EXE include-what-you-use REQUIRED)
set(CMAKE_CXX_INCLUDE_WHAT_YOU_USE
${IWYU_EXE}
-Xiwyu --mapping_file=${CMAKE_SOURCE_DIR}/.iwyu.imp
-Xiwyu --no_fwd_decls
)
message(STATUS "IWYU enabled: ${IWYU_EXE}")
endif()
# .clang-tidy — Project-wide configuration
---
Checks: >
-*,
bugprone-*,
cert-*,
cppcoreguidelines-*,
misc-*,
modernize-*,
performance-*,
readability-*,
-modernize-use-trailing-return-type,
-readability-identifier-length,
-cppcoreguidelines-avoid-magic-numbers,
-readability-magic-numbers
WarningsAsErrors: '*'
HeaderFilterRegex: 'include/myproject/.*'
CheckOptions:
- key: readability-identifier-naming.ClassCase
value: CamelCase
- key: readability-identifier-naming.FunctionCase
value: lower_case
- key: readability-identifier-naming.VariableCase
value: lower_case
- key: readability-identifier-naming.NamespaceCase
value: lower_case
- key: readability-identifier-naming.ConstantCase
value: UPPER_CASE
- key: modernize-use-auto.MinTypeNameLength
value: 5
...
CMAKE_CXX_CLANG_TIDY during local development for instant feedback, and run cppcheck + IWYU in CI where build time is less critical. The preset system (covered later) makes toggling these trivial.
Sanitizer Configurations
Runtime sanitizers detect memory errors, data races, and undefined behavior that static analysis cannot catch. Professional projects configure sanitizers as CMake presets so developers can switch to a sanitized build with a single command. Each sanitizer requires a dedicated build because they're generally incompatible with each other.
ASan, TSan, and UBSan
# cmake/Sanitizers.cmake — Sanitizer configuration
option(MYPROJECT_SANITIZER "Enable sanitizer (asan, tsan, ubsan, msan)" "")
if(MYPROJECT_SANITIZER)
if(MYPROJECT_SANITIZER STREQUAL "asan")
set(SANITIZER_FLAGS "-fsanitize=address -fno-omit-frame-pointer")
set(SANITIZER_LINK_FLAGS "-fsanitize=address")
elseif(MYPROJECT_SANITIZER STREQUAL "tsan")
set(SANITIZER_FLAGS "-fsanitize=thread")
set(SANITIZER_LINK_FLAGS "-fsanitize=thread")
elseif(MYPROJECT_SANITIZER STREQUAL "ubsan")
set(SANITIZER_FLAGS "-fsanitize=undefined -fno-omit-frame-pointer")
set(SANITIZER_LINK_FLAGS "-fsanitize=undefined")
elseif(MYPROJECT_SANITIZER STREQUAL "msan")
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")
message(FATAL_ERROR "MSan requires Clang")
endif()
set(SANITIZER_FLAGS "-fsanitize=memory -fno-omit-frame-pointer -fsanitize-memory-track-origins=2")
set(SANITIZER_LINK_FLAGS "-fsanitize=memory")
else()
message(FATAL_ERROR "Unknown sanitizer: ${MYPROJECT_SANITIZER}")
endif()
# Apply to all targets in this directory and below
add_compile_options(${SANITIZER_FLAGS})
add_link_options(${SANITIZER_LINK_FLAGS})
message(STATUS "Sanitizer enabled: ${MYPROJECT_SANITIZER}")
endif()
# Build with AddressSanitizer
cmake --preset asan
cmake --build --preset asan
ctest --preset asan
# Build with ThreadSanitizer (for concurrent code)
cmake --preset tsan
cmake --build --preset tsan
ctest --preset tsan
# Build with UndefinedBehaviorSanitizer
cmake --preset ubsan
cmake --build --preset ubsan
ctest --preset ubsan
Installation and Packaging
Making your library consumable by other CMake projects requires proper installation rules. The install(EXPORT) command generates CMake config files that allow find_package(myproject) to work seamlessly. CPack then wraps the installed tree into distributable packages (DEB, RPM, NSIS, ZIP).
install(EXPORT) and CPack
# cmake/Install.cmake — Installation and export rules
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
# Install the library
install(TARGETS myproject_core myproject_headers myproject_warnings
EXPORT myprojectTargets
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
# Install public headers
install(DIRECTORY "${CMAKE_SOURCE_DIR}/include/myproject"
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h"
)
# Install generated headers (export.hpp, version.hpp)
install(DIRECTORY "${CMAKE_BINARY_DIR}/generated/myproject"
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
FILES_MATCHING PATTERN "*.hpp"
)
# Install the executable
install(TARGETS myproject_app
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
# Generate and install CMake package config
install(EXPORT myprojectTargets
FILE myprojectTargets.cmake
NAMESPACE myproject::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/myproject
)
# Generate version compatibility file
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/myprojectConfigVersion.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
# Generate config file from template
configure_package_config_file(
"${CMAKE_SOURCE_DIR}/cmake/myprojectConfig.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/myprojectConfig.cmake"
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/myproject
)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/myprojectConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/myprojectConfigVersion.cmake"
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/myproject
)
# cmake/myprojectConfig.cmake.in — Package configuration template
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
# Re-find our public dependencies so consumers get them automatically
find_dependency(nlohmann_json 3.11)
include("${CMAKE_CURRENT_LIST_DIR}/myprojectTargets.cmake")
check_required_components(myproject)
# packaging/CPack.cmake — Binary package generation
set(CPACK_PACKAGE_NAME "myproject")
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
set(CPACK_PACKAGE_VENDOR "MyOrg")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A professional-grade C++ library")
set(CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/myorg/myproject")
set(CPACK_PACKAGE_CONTACT "team@myorg.dev")
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE")
set(CPACK_RESOURCE_FILE_README "${CMAKE_SOURCE_DIR}/README.md")
# Component-based installation
set(CPACK_COMPONENTS_ALL runtime development documentation)
set(CPACK_COMPONENT_RUNTIME_DISPLAY_NAME "Runtime Libraries")
set(CPACK_COMPONENT_DEVELOPMENT_DISPLAY_NAME "Development Files")
set(CPACK_COMPONENT_DOCUMENTATION_DISPLAY_NAME "Documentation")
# DEB-specific
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.31)")
set(CPACK_DEBIAN_PACKAGE_SECTION "libs")
set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT)
# RPM-specific
set(CPACK_RPM_PACKAGE_LICENSE "MIT")
set(CPACK_RPM_PACKAGE_GROUP "Development/Libraries")
set(CPACK_RPM_FILE_NAME RPM-DEFAULT)
# NSIS (Windows installer)
set(CPACK_NSIS_DISPLAY_NAME "MyProject ${PROJECT_VERSION}")
set(CPACK_NSIS_PACKAGE_NAME "MyProject")
set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON)
include(CPack)
# Generate packages after building
cmake -B build -DCMAKE_BUILD_TYPE=Release -DMYPROJECT_INSTALL=ON
cmake --build build
cd build
# Generate all configured package types
cpack -G "DEB;RPM;TGZ"
# Generate Windows installer
cpack -G NSIS
# Generate specific component packages
cpack -G DEB -D CPACK_COMPONENTS_ALL=runtime
CI/CD Integration
A professional CMake project runs its full validation suite on every pull request — compilation across multiple platforms, all test levels, static analysis, sanitizer builds, and optionally package generation. CMake Presets make CI configuration dramatically simpler because the CI script only needs to reference preset names rather than repeating all the flags.
flowchart LR
A[Push/PR] --> B[Configure]
B --> C[Build]
C --> D[Test]
D --> E[Analysis]
E --> F[Package]
F --> G[Deploy]
B --> B1[Linux GCC]
B --> B2[Linux Clang]
B --> B3[macOS Clang]
B --> B4[Windows MSVC]
D --> D1[Unit Tests]
D --> D2[Integration Tests]
D --> D3[Sanitizer Tests]
E --> E1[clang-tidy]
E --> E2[Coverage Report]
F --> F1[DEB/RPM]
F --> F2[Windows NSIS]
F --> F3[GitHub Release]
style A fill:#132440,color:#fff
style G fill:#BF092F,color:#fff
style D fill:#3B9797,color:#fff
# .github/workflows/ci.yml — GitHub Actions CI
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build-and-test:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
preset: ci-linux-gcc
compiler: gcc-14
- os: ubuntu-24.04
preset: ci-linux-clang
compiler: clang-18
- os: macos-14
preset: ci-macos
compiler: AppleClang
- os: windows-latest
preset: ci-windows
compiler: MSVC
runs-on: ${{ matrix.os }}
name: ${{ matrix.compiler }}
steps:
- uses: actions/checkout@v4
- name: Install dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y ninja-build lcov
# vcpkg bootstrap
git clone https://github.com/microsoft/vcpkg.git /opt/vcpkg
/opt/vcpkg/bootstrap-vcpkg.sh
- name: Cache vcpkg packages
uses: actions/cache@v4
with:
path: build/vcpkg_installed
key: vcpkg-${{ matrix.os }}-${{ hashFiles('vcpkg.json') }}
restore-keys: vcpkg-${{ matrix.os }}-
- name: Configure
run: cmake --preset ${{ matrix.preset }}
- name: Build
run: cmake --build --preset ${{ matrix.preset }}
- name: Test
run: ctest --preset ${{ matrix.preset }}
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.compiler }}
path: build/*/Testing/
sanitizers:
runs-on: ubuntu-24.04
strategy:
matrix:
sanitizer: [asan, tsan, ubsan]
name: ${{ matrix.sanitizer }}
steps:
- uses: actions/checkout@v4
- name: Configure
run: cmake --preset ${{ matrix.sanitizer }}
- name: Build
run: cmake --build --preset ${{ matrix.sanitizer }}
- name: Test
run: ctest --preset ${{ matrix.sanitizer }}
coverage:
runs-on: ubuntu-24.04
needs: build-and-test
steps:
- uses: actions/checkout@v4
- name: Install tools
run: sudo apt-get install -y ninja-build lcov
- name: Configure and build
run: |
cmake --preset coverage
cmake --build --preset coverage
- name: Run tests and generate coverage
run: |
ctest --preset coverage
cmake --build --preset coverage --target coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: build/coverage/coverage_filtered.info
fail_ci_if_error: true
package:
runs-on: ubuntu-24.04
needs: [build-and-test, sanitizers]
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Build release
run: |
cmake --preset release
cmake --build --preset release
cd build/release && cpack -G "DEB;TGZ"
- name: Upload packages
uses: actions/upload-artifact@v4
with:
name: packages
path: build/release/*.deb build/release/*.tar.gz
GitLab CI Configuration
# .gitlab-ci.yml — GitLab CI equivalent
stages:
- build
- test
- analyze
- package
variables:
GIT_SUBMODULE_STRATEGY: recursive
.cmake-base:
image: gcc:14
before_script:
- apt-get update && apt-get install -y cmake ninja-build
- git clone https://github.com/microsoft/vcpkg.git /opt/vcpkg
- /opt/vcpkg/bootstrap-vcpkg.sh
build:linux:
extends: .cmake-base
stage: build
script:
- cmake --preset ci-linux-gcc
- cmake --build --preset ci-linux-gcc
artifacts:
paths: [build/]
expire_in: 1 hour
test:linux:
extends: .cmake-base
stage: test
needs: [build:linux]
script:
- ctest --preset ci-linux-gcc --output-on-failure
analyze:clang-tidy:
stage: analyze
image: silkeh/clang:18
script:
- cmake --preset ci-clang-tidy
- cmake --build --preset ci-clang-tidy 2>&1 | tee clang-tidy.log
artifacts:
paths: [clang-tidy.log]
when: always
package:release:
extends: .cmake-base
stage: package
only: [tags]
script:
- cmake --preset release
- cmake --build --preset release
- cd build/release && cpack -G "DEB;RPM;TGZ"
artifacts:
paths: [build/release/*.deb, build/release/*.rpm, build/release/*.tar.gz]
Preset Workflows
CMake Presets (CMakePresets.json) encode all build configurations declaratively — eliminating the need to remember or document long command-line invocations. Workflow presets go further by chaining configure → build → test into a single command. This is the definitive way to ensure everyone (developers, CI, new contributors) uses identical settings.
CMakePresets.json
{
"version": 6,
"cmakeMinimumRequired": { "major": 3, "minor": 25, "patch": 0 },
"configurePresets": [
{
"name": "base",
"hidden": true,
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": {
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
"MYPROJECT_BUILD_TESTS": "ON",
"MYPROJECT_BUILD_EXAMPLES": "ON"
},
"environment": {
"VCPKG_ROOT": "/opt/vcpkg"
},
"toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
},
{
"name": "dev",
"displayName": "Development",
"inherits": "base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"MYPROJECT_ENABLE_CLANG_TIDY": "ON"
}
},
{
"name": "release",
"displayName": "Release",
"inherits": "base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"MYPROJECT_BUILD_TESTS": "OFF",
"MYPROJECT_INSTALL": "ON"
}
},
{
"name": "asan",
"displayName": "AddressSanitizer",
"inherits": "base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"MYPROJECT_SANITIZER": "asan"
}
},
{
"name": "tsan",
"displayName": "ThreadSanitizer",
"inherits": "base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"MYPROJECT_SANITIZER": "tsan"
}
},
{
"name": "ubsan",
"displayName": "UndefinedBehaviorSanitizer",
"inherits": "base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"MYPROJECT_SANITIZER": "ubsan"
}
},
{
"name": "coverage",
"displayName": "Code Coverage",
"inherits": "base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"MYPROJECT_ENABLE_COVERAGE": "ON"
}
},
{
"name": "ci-linux-gcc",
"displayName": "CI Linux GCC",
"inherits": "base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo",
"CMAKE_CXX_COMPILER": "g++-14"
}
},
{
"name": "ci-linux-clang",
"displayName": "CI Linux Clang",
"inherits": "base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo",
"CMAKE_CXX_COMPILER": "clang++-18"
}
},
{
"name": "ci-clang-tidy",
"displayName": "CI Static Analysis",
"inherits": "base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_COMPILER": "clang++-18",
"MYPROJECT_ENABLE_CLANG_TIDY": "ON"
}
},
{
"name": "ci-macos",
"displayName": "CI macOS",
"inherits": "base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
}
},
{
"name": "ci-windows",
"displayName": "CI Windows",
"inherits": "base",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
}
],
"buildPresets": [
{ "name": "dev", "configurePreset": "dev" },
{ "name": "release", "configurePreset": "release" },
{ "name": "asan", "configurePreset": "asan" },
{ "name": "tsan", "configurePreset": "tsan" },
{ "name": "ubsan", "configurePreset": "ubsan" },
{ "name": "coverage", "configurePreset": "coverage" },
{ "name": "ci-linux-gcc", "configurePreset": "ci-linux-gcc" },
{ "name": "ci-linux-clang", "configurePreset": "ci-linux-clang" },
{ "name": "ci-clang-tidy", "configurePreset": "ci-clang-tidy" },
{ "name": "ci-macos", "configurePreset": "ci-macos" },
{ "name": "ci-windows", "configurePreset": "ci-windows" }
],
"testPresets": [
{
"name": "dev",
"configurePreset": "dev",
"output": { "outputOnFailure": true },
"execution": { "jobs": 0 }
},
{ "name": "asan", "configurePreset": "asan", "output": { "outputOnFailure": true } },
{ "name": "tsan", "configurePreset": "tsan", "output": { "outputOnFailure": true } },
{ "name": "ubsan", "configurePreset": "ubsan", "output": { "outputOnFailure": true } },
{ "name": "coverage", "configurePreset": "coverage", "output": { "outputOnFailure": true } },
{ "name": "ci-linux-gcc", "configurePreset": "ci-linux-gcc", "output": { "outputOnFailure": true } },
{ "name": "ci-linux-clang", "configurePreset": "ci-linux-clang", "output": { "outputOnFailure": true } },
{ "name": "ci-macos", "configurePreset": "ci-macos", "output": { "outputOnFailure": true } },
{ "name": "ci-windows", "configurePreset": "ci-windows", "output": { "outputOnFailure": true } }
],
"workflowPresets": [
{
"name": "dev",
"displayName": "Development Workflow",
"steps": [
{ "type": "configure", "name": "dev" },
{ "type": "build", "name": "dev" },
{ "type": "test", "name": "dev" }
]
},
{
"name": "ci-full",
"displayName": "Full CI Workflow",
"steps": [
{ "type": "configure", "name": "ci-linux-gcc" },
{ "type": "build", "name": "ci-linux-gcc" },
{ "type": "test", "name": "ci-linux-gcc" }
]
}
]
}
# Developer daily workflow — single command does everything
cmake --workflow --preset dev
# CI workflow
cmake --workflow --preset ci-full
# List all available presets
cmake --list-presets
cmake --build --list-presets
ctest --list-presets
CMakePresets.json to version control (shared configuration). Create CMakeUserPresets.json (gitignored) for personal overrides like custom compiler paths, local vcpkg roots, or IDE-specific settings. User presets inherit from the shared ones.
Putting It All Together
Let's walk through building a complete project from scratch that incorporates every technique from this series. This is a real-world C++ library called nexus — a high-performance event processing engine with a clean public API, comprehensive testing, documentation, CI/CD, and cross-platform packaging.
nexus — Professional Event Processing Library
This walkthrough demonstrates building a production-ready CMake project from the ground up, applying all 32 prior parts' techniques in a cohesive workflow.
# Step 1: Create the project skeleton
mkdir -p nexus/{src/{core,io,utils},include/nexus,tests/{unit,integration}}
mkdir -p nexus/{cmake,docs/source,examples,packaging}
cd nexus
git init
# Step 2: Create vcpkg manifest
cat > vcpkg.json <<'EOF'
{
"name": "nexus",
"version-semver": "1.0.0",
"dependencies": ["fmt", "spdlog", "nlohmann-json", "catch2"]
}
EOF
# Step 3: Initialize CMakePresets.json
# (Use the full preset file from the previous section)
# Step 4: Create .clang-tidy, .clang-format, .gitignore
echo "build/" > .gitignore
echo "CMakeUserPresets.json" >> .gitignore
# nexus/CMakeLists.txt — Complete top-level file
cmake_minimum_required(VERSION 3.25)
project(nexus
VERSION 1.0.0
DESCRIPTION "High-performance event processing engine"
HOMEPAGE_URL "https://github.com/myorg/nexus"
LANGUAGES CXX
)
# Prevent in-source builds
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR)
message(FATAL_ERROR "In-source builds not allowed. Use: cmake -B build")
endif()
# Global settings
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Module path
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
# Options
option(NEXUS_BUILD_TESTS "Build tests" ON)
option(NEXUS_BUILD_DOCS "Build documentation" OFF)
option(NEXUS_BUILD_EXAMPLES "Build examples" ON)
option(NEXUS_ENABLE_COVERAGE "Enable coverage" OFF)
option(NEXUS_INSTALL "Generate install target" ON)
set(NEXUS_SANITIZER "" CACHE STRING "Sanitizer: asan, tsan, ubsan, msan")
# Include modules
include(CompilerWarnings)
include(Sanitizers)
if(NEXUS_ENABLE_COVERAGE)
include(Coverage)
endif()
# Dependencies
find_package(fmt REQUIRED)
find_package(spdlog REQUIRED)
find_package(nlohmann_json 3.11 REQUIRED)
# Source
add_subdirectory(src)
# Tests
if(NEXUS_BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
# Docs
if(NEXUS_BUILD_DOCS)
add_subdirectory(docs)
endif()
# Examples
if(NEXUS_BUILD_EXAMPLES)
add_subdirectory(examples)
endif()
# Install
if(NEXUS_INSTALL)
include(Install)
endif()
# nexus/src/CMakeLists.txt — Library and application targets
include(GenerateExportHeader)
include(GitVersion)
# Core library
add_library(nexus_core
core/engine.cpp
core/event_loop.cpp
core/scheduler.cpp
io/tcp_listener.cpp
io/buffer_pool.cpp
utils/logging.cpp
utils/thread_pool.cpp
)
add_library(nexus::core ALIAS nexus_core)
generate_export_header(nexus_core
BASE_NAME NEXUS
EXPORT_FILE_NAME "${CMAKE_BINARY_DIR}/generated/nexus/export.hpp"
)
# Version header
configure_file(
"${CMAKE_SOURCE_DIR}/include/nexus/version.hpp.in"
"${CMAKE_BINARY_DIR}/generated/nexus/version.hpp"
@ONLY
)
target_include_directories(nexus_core
PUBLIC
$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
$<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/generated>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(nexus_core
PUBLIC nlohmann_json::nlohmann_json
PRIVATE fmt::fmt spdlog::spdlog nexus::warnings
)
target_compile_features(nexus_core PUBLIC cxx_std_20)
set_target_properties(nexus_core PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION_MAJOR}
OUTPUT_NAME nexus
)
# Application
add_executable(nexus_app app/main.cpp)
add_executable(nexus::app ALIAS nexus_app)
target_link_libraries(nexus_app PRIVATE nexus::core nexus::warnings)
// nexus/include/nexus/core.hpp — Clean public API
#pragma once
#include "nexus/export.hpp"
#include <functional>
#include <memory>
#include <string_view>
#include <chrono>
namespace nexus {
/// Event identifier type
using EventId = std::uint64_t;
/// Event handler callback
using EventHandler = std::function<void(EventId, std::string_view payload)>;
/// Configuration for the event engine
struct NEXUS_EXPORT EngineConfig {
std::size_t thread_count = std::thread::hardware_concurrency();
std::size_t max_queue_size = 65536;
std::chrono::milliseconds idle_timeout{100};
bool enable_metrics = true;
};
/// High-performance event processing engine
class NEXUS_EXPORT Engine {
public:
explicit Engine(EngineConfig config = {});
~Engine();
// Non-copyable, movable
Engine(const Engine&) = delete;
Engine& operator=(const Engine&) = delete;
Engine(Engine&&) noexcept;
Engine& operator=(Engine&&) noexcept;
/// Register a handler for events matching the given pattern
void subscribe(std::string_view pattern, EventHandler handler);
/// Publish an event to all matching subscribers
void publish(std::string_view topic, std::string_view payload);
/// Start the event processing loop
void start();
/// Stop processing and drain the queue
void stop();
/// Check if the engine is currently running
[[nodiscard]] bool is_running() const noexcept;
/// Get the number of events processed since start
[[nodiscard]] std::uint64_t events_processed() const noexcept;
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace nexus
// nexus/tests/unit/test_engine.cpp — Catch2 unit tests
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <nexus/core.hpp>
#include <thread>
#include <atomic>
#include <latch>
using namespace nexus;
using namespace std::chrono_literals;
TEST_CASE("Engine lifecycle", "[engine]") {
Engine engine({.thread_count = 2, .max_queue_size = 1024});
SECTION("starts and stops cleanly") {
engine.start();
REQUIRE(engine.is_running());
engine.stop();
REQUIRE_FALSE(engine.is_running());
}
SECTION("double start is safe") {
engine.start();
engine.start(); // Should not throw
REQUIRE(engine.is_running());
engine.stop();
}
SECTION("stop without start is safe") {
engine.stop(); // Should not throw
REQUIRE_FALSE(engine.is_running());
}
}
TEST_CASE("Event pub/sub", "[engine][events]") {
Engine engine({.thread_count = 2});
std::atomic<int> count{0};
std::latch done{3};
engine.subscribe("test.*", [&](EventId, std::string_view payload) {
count.fetch_add(1);
done.count_down();
});
engine.start();
engine.publish("test.alpha", "payload1");
engine.publish("test.beta", "payload2");
engine.publish("test.gamma", "payload3");
REQUIRE(done.try_wait_for(5s));
REQUIRE(count.load() == 3);
REQUIRE(engine.events_processed() >= 3);
engine.stop();
}
TEST_CASE("Engine config validation", "[engine][config]") {
SECTION("zero threads uses hardware concurrency") {
EngineConfig config{.thread_count = 0};
Engine engine(config);
// Should not crash — falls back to hardware_concurrency()
}
}
# Complete development workflow
cd nexus
# Configure + build + test in one command
cmake --workflow --preset dev
# Run sanitizer check
cmake --preset asan
cmake --build --preset asan
ctest --preset asan
# Generate coverage report
cmake --preset coverage
cmake --build --preset coverage
ctest --preset coverage
cmake --build --preset coverage --target coverage
# → Open build/coverage/coverage_report/index.html
# Build release package
cmake --preset release
cmake --build --preset release
cd build/release && cpack -G TGZ
# Verify the installed package works
cmake --install build/release --prefix /tmp/nexus-install
ls /tmp/nexus-install/lib/cmake/nexus/ # Config files present
# Test consumption from another project
mkdir /tmp/consumer && cd /tmp/consumer
cat > CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.25)
project(consumer LANGUAGES CXX)
find_package(nexus 1.0 REQUIRED)
add_executable(demo main.cpp)
target_link_libraries(demo PRIVATE nexus::core)
EOF
cmake -B build -DCMAKE_PREFIX_PATH=/tmp/nexus-install
cmake --build build
./build/demo # Works!
- ✅ Namespaced ALIAS targets (
nexus::core) - ✅ Generator expressions for build/install interface split
- ✅
GenerateExportHeaderfor cross-platform shared library symbols - ✅
configure_file()for version embedding from Git - ✅ Proper PUBLIC/PRIVATE/INTERFACE separation
- ✅ CMakePresets.json for reproducible configurations
- ✅ Workflow presets for one-command development
- ✅ CTest integration with test discovery
- ✅ Coverage with lcov/gcov
- ✅ Sanitizer presets (ASan, TSan, UBSan)
- ✅ Static analysis via
CMAKE_CXX_CLANG_TIDY - ✅ CPack for binary distribution
- ✅ Proper
install(EXPORT)for downstream consumption - ✅ Multi-platform CI with matrix builds
- ✅ vcpkg manifest mode for dependency management
Series Complete — Congratulations!
You've Completed the Entire CMake Mastery Series
Over 33 articles, you've journeyed from writing your first cmake_minimum_required() to building production-grade projects with cross-platform CI/CD, sanitizer testing, static analysis, and professional packaging. Here's what you've mastered:
- Parts 1–6: CMake fundamentals — language, targets, libraries, compiler configuration
- Parts 7–11: Advanced features — generator expressions, environment detection, dependencies, external projects
- Parts 12–15: Testing and code generation — CTest, framework integration, configure-time operations
- Parts 16–20: Project organization — structuring, IDEs, C++20 modules, analysis tools, documentation
- Parts 21–23: Distribution — installation, CPack packaging, CMake Presets
- Parts 24–29: Specialized topics — mixed languages, cross-compilation, generators, CDash, porting, reproducibility
- Parts 30–32: Platform mastery — build optimization, Apple platforms, Python extensions
- Part 33: The capstone — combining everything into a professional project
You now have the knowledge to build, test, analyze, package, and distribute C++ software at a professional level using CMake. The techniques in this series apply equally to personal projects and enterprise-scale codebases with thousands of source files.
Keep building. Keep shipping. Keep mastering your craft.