Chapter 7: Build Systems and Project Structure

Real C++ projects require more than g++ main.cpp. This chapter covers professional build workflows.

Why Build Systems?

┌─────────────────────────────────────────────────────────────┐
│              Without Build System                            │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  g++ -std=c++20 -Wall -Wextra -I./include -I./third_party  │
│      src/main.cpp src/utils.cpp src/network.cpp            │
│      -lpthread -lssl -lcrypto -o myapp                     │
│                                                              │
│  Problems:                                                   │
│  - Recompiles everything every time                         │
│  - Hard to maintain as project grows                        │
│  - Different commands for different platforms               │
│  - No dependency tracking                                    │
│                                                              │
├─────────────────────────────────────────────────────────────┤
│              With Build System                               │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  cmake -B build && cmake --build build                      │
│                                                              │
│  Benefits:                                                   │
│  - Incremental builds (only recompile changed files)        │
│  - Cross-platform support                                    │
│  - Automatic dependency tracking                             │
│  - Easy library integration                                  │
│                                                              │
└─────────────────────────────────────────────────────────────┘

CMake: The Standard Build System

CMake is the de facto standard for C++ projects. Learn it well.

Basic CMakeLists.txt

# Minimum CMake version required
cmake_minimum_required(VERSION 3.20)

# Project name and metadata
project(MyApp
    VERSION 1.0.0
    LANGUAGES CXX
    DESCRIPTION "My C++ Application"
)

# Set C++ standard
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)  # Don't use GNU extensions

# Create executable from sources
add_executable(myapp
    src/main.cpp
    src/utils.cpp
)

# Include directories
target_include_directories(myapp PRIVATE
    ${CMAKE_SOURCE_DIR}/include
)

Building with CMake

# Create build directory and generate build files
cmake -B build

# Build the project
cmake --build build

# Or with make directly
cd build && make

# Build with specific configuration
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build

# Clean build
rm -rf build && cmake -B build && cmake --build build

# Verbose build (see commands)
cmake --build build --verbose

Project Structure

myproject/
├── CMakeLists.txt           # Root CMake file
├── cmake/                   # CMake modules and helpers
│   ├── FindSomeLib.cmake
│   └── CompilerWarnings.cmake
├── src/                     # Source files
│   ├── main.cpp
│   ├── module1/
│   │   ├── CMakeLists.txt
│   │   ├── module1.cpp
│   │   └── module1.hpp
│   └── module2/
│       ├── CMakeLists.txt
│       └── ...
├── include/                 # Public headers
│   └── myproject/
│       └── public_api.hpp
├── tests/                   # Test files
│   ├── CMakeLists.txt
│   └── test_module1.cpp
├── third_party/            # External dependencies
│   └── somelib/
├── docs/                   # Documentation
├── build/                  # Build output (gitignored)
└── .gitignore

Variables and Options

# Set variables
set(MY_VAR "value")
set(SOURCE_FILES
    src/main.cpp
    src/utils.cpp
    src/network.cpp
)

# Options (configurable by user)
option(BUILD_TESTS "Build unit tests" ON)
option(BUILD_SHARED_LIBS "Build shared libraries" OFF)

# Use variables
add_executable(myapp ${SOURCE_FILES})

# Conditional logic
if(BUILD_TESTS)
    add_subdirectory(tests)
endif()

# Platform detection
if(WIN32)
    target_compile_definitions(myapp PRIVATE WINDOWS)
elseif(APPLE)
    target_compile_definitions(myapp PRIVATE MACOS)
elseif(UNIX)
    target_compile_definitions(myapp PRIVATE LINUX)
endif()

Targets: The Modern CMake Way

# Create library target
add_library(mylib
    src/mylib.cpp
)

# INTERFACE: Affects users of target
# PUBLIC: Affects target and its users
# PRIVATE: Affects only the target itself

# Include directories
target_include_directories(mylib
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:include>
    PRIVATE
        ${CMAKE_SOURCE_DIR}/src
)

# Compile definitions (preprocessor macros)
target_compile_definitions(mylib
    PRIVATE
        MYLIB_BUILDING
    PUBLIC
        MYLIB_VERSION="${PROJECT_VERSION}"
)

# Compile options (flags)
target_compile_options(mylib
    PRIVATE
        -Wall -Wextra -Wpedantic
)

# Link libraries
target_link_libraries(mylib
    PUBLIC
        some_dependency
    PRIVATE
        internal_helper
)

# Link executable to library
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE mylib)

Finding and Using Libraries

# Find installed library
find_package(OpenSSL REQUIRED)
target_link_libraries(myapp PRIVATE OpenSSL::SSL OpenSSL::Crypto)

# Find with components
find_package(Boost 1.70 REQUIRED COMPONENTS filesystem system)
target_link_libraries(myapp PRIVATE Boost::filesystem Boost::system)

# Find optional library
find_package(Doxygen)
if(Doxygen_FOUND)
    # Add documentation target
endif()

# pkg-config (for libraries without CMake support)
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBFOO REQUIRED IMPORTED_TARGET libfoo)
target_link_libraries(myapp PRIVATE PkgConfig::LIBFOO)

FetchContent: Download Dependencies

include(FetchContent)

# Declare dependency
FetchContent_Declare(
    fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG 10.1.1
)

FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.14.0
)

# Make available
FetchContent_MakeAvailable(fmt googletest)

# Use in your target
target_link_libraries(myapp PRIVATE fmt::fmt)

Subdirectories

# Root CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(MyProject LANGUAGES CXX)

add_subdirectory(src/core)
add_subdirectory(src/app)
add_subdirectory(tests)

# src/core/CMakeLists.txt
add_library(core
    core.cpp
)
target_include_directories(core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

# src/app/CMakeLists.txt
add_executable(app main.cpp)
target_link_libraries(app PRIVATE core)

Testing with CTest

# Enable testing
enable_testing()

# Add test executable
add_executable(tests
    test_main.cpp
    test_utils.cpp
)

# Link with test framework
FetchContent_MakeAvailable(googletest)
target_link_libraries(tests PRIVATE GTest::gtest_main)

# Register tests
include(GoogleTest)
gtest_discover_tests(tests)

# Or simple CTest
add_test(NAME MyTest COMMAND tests)

Running tests:

cd build
ctest --output-on-failure
ctest -R "TestName"  # Run specific test
ctest -j4            # Parallel tests

Install Targets

# Install executable
install(TARGETS myapp
    RUNTIME DESTINATION bin
)

# Install library
install(TARGETS mylib
    EXPORT MyLibTargets
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
    RUNTIME DESTINATION bin
)

# Install headers
install(DIRECTORY include/
    DESTINATION include
)

# Create package config
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
    "${CMAKE_CURRENT_BINARY_DIR}/MyLibConfigVersion.cmake"
    VERSION ${PROJECT_VERSION}
    COMPATIBILITY AnyNewerVersion
)

install(EXPORT MyLibTargets
    FILE MyLibTargets.cmake
    NAMESPACE MyLib::
    DESTINATION lib/cmake/MyLib
)

CMake Presets (Modern Approach)

Create CMakePresets.json:

{
    "version": 6,
    "cmakeMinimumRequired": {
        "major": 3,
        "minor": 21,
        "patch": 0
    },
    "configurePresets": [
        {
            "name": "default",
            "hidden": true,
            "generator": "Ninja",
            "binaryDir": "${sourceDir}/build/${presetName}",
            "cacheVariables": {
                "CMAKE_CXX_STANDARD": "20"
            }
        },
        {
            "name": "debug",
            "inherits": "default",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Debug"
            }
        },
        {
            "name": "release",
            "inherits": "default",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Release"
            }
        }
    ],
    "buildPresets": [
        {
            "name": "debug",
            "configurePreset": "debug"
        },
        {
            "name": "release",
            "configurePreset": "release"
        }
    ]
}

Usage:

cmake --preset debug
cmake --build --preset debug

Package Managers

vcpkg

Microsoft's cross-platform package manager.

# Install vcpkg
git clone https://github.com/Microsoft/vcpkg.git
./vcpkg/bootstrap-vcpkg.sh

# Install packages
./vcpkg/vcpkg install fmt nlohmann-json boost-asio

# Create vcpkg.json manifest

vcpkg.json:

{
    "name": "my-project",
    "version": "1.0.0",
    "dependencies": [
        "fmt",
        "nlohmann-json",
        {
            "name": "boost-asio",
            "version>=": "1.83.0"
        }
    ]
}

CMake integration:

# In CMakeLists.txt or via command line
# cmake -B build -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake

find_package(fmt CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
target_link_libraries(myapp PRIVATE fmt::fmt nlohmann_json::nlohmann_json)

Conan

# Install Conan
pip install conan

# Create conanfile.txt

conanfile.txt:

[requires]
fmt/10.1.1
nlohmann_json/3.11.2
boost/1.83.0

[generators]
CMakeDeps
CMakeToolchain

[options]
boost/*:without_python=True

Usage:

conan install . --output-folder=build --build=missing
cmake -B build -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake
cmake --build build

Compiler Warnings

Create cmake/CompilerWarnings.cmake:

function(set_project_warnings project_name)
    set(MSVC_WARNINGS
        /W4
        /WX  # Treat warnings as errors
        /permissive-
    )

    set(CLANG_WARNINGS
        -Wall
        -Wextra
        -Wpedantic
        -Werror  # Treat warnings as errors
        -Wshadow
        -Wcast-align
        -Wunused
        -Wconversion
        -Wsign-conversion
        -Wnull-dereference
        -Wdouble-promotion
        -Wformat=2
        -Wimplicit-fallthrough
    )

    set(GCC_WARNINGS
        ${CLANG_WARNINGS}
        -Wmisleading-indentation
        -Wduplicated-cond
        -Wduplicated-branches
        -Wlogical-op
        -Wuseless-cast
    )

    if(MSVC)
        set(PROJECT_WARNINGS ${MSVC_WARNINGS})
    elseif(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang")
        set(PROJECT_WARNINGS ${CLANG_WARNINGS})
    elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
        set(PROJECT_WARNINGS ${GCC_WARNINGS})
    endif()

    target_compile_options(${project_name} PRIVATE ${PROJECT_WARNINGS})
endfunction()

Use in CMakeLists.txt:

include(cmake/CompilerWarnings.cmake)
set_project_warnings(myapp)

Static Analysis in Build

# clang-tidy
find_program(CLANG_TIDY_EXE NAMES clang-tidy)
if(CLANG_TIDY_EXE)
    set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_EXE};-checks=*,-fuchsia-*")
endif()

# cppcheck
find_program(CPPCHECK_EXE NAMES cppcheck)
if(CPPCHECK_EXE)
    set(CMAKE_CXX_CPPCHECK
        "${CPPCHECK_EXE}"
        "--enable=warning,style,performance,portability"
        "--suppress=missingIncludeSystem"
    )
endif()

Sanitizers

# Create option for sanitizers
option(ENABLE_SANITIZERS "Enable AddressSanitizer and UBSan" OFF)

if(ENABLE_SANITIZERS)
    add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer)
    add_link_options(-fsanitize=address,undefined)
endif()

Build with:

cmake -B build -DENABLE_SANITIZERS=ON
cmake --build build
./build/myapp  # Will report memory errors

Complete Example CMakeLists.txt

cmake_minimum_required(VERSION 3.20)

project(ProductionApp
    VERSION 1.0.0
    LANGUAGES CXX
    DESCRIPTION "Production-ready C++ application"
)

# Global settings
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)  # For clangd

# Options
option(BUILD_TESTS "Build unit tests" ON)
option(BUILD_DOCS "Build documentation" OFF)
option(ENABLE_SANITIZERS "Enable sanitizers" OFF)

# Include helpers
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
include(CompilerWarnings)

# Dependencies
include(FetchContent)
FetchContent_Declare(fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG 10.1.1
)
FetchContent_MakeAvailable(fmt)

# Main library
add_library(core
    src/core/utils.cpp
    src/core/network.cpp
)
target_include_directories(core
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:include>
    PRIVATE
        ${CMAKE_SOURCE_DIR}/src
)
target_link_libraries(core PUBLIC fmt::fmt)
set_project_warnings(core)

# Main executable
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE core)
set_project_warnings(app)

# Sanitizers
if(ENABLE_SANITIZERS)
    target_compile_options(app PRIVATE -fsanitize=address,undefined)
    target_link_options(app PRIVATE -fsanitize=address,undefined)
endif()

# Tests
if(BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

# Installation
install(TARGETS app RUNTIME DESTINATION bin)
install(TARGETS core LIBRARY DESTINATION lib ARCHIVE DESTINATION lib)
install(DIRECTORY include/ DESTINATION include)

Exercises

  1. CMake Project Setup

    • Create a project with multiple source files
    • Add a library and link it to executable
    • Verify incremental builds work
  2. External Dependencies

    • Use FetchContent to add fmt library
    • Add a test framework (GoogleTest or Catch2)
    • Create a simple test
  3. Build Configurations

    • Set up Debug and Release configurations
    • Add sanitizer support
    • Configure compiler warnings
  4. Package Manager

    • Set up vcpkg or Conan
    • Add multiple dependencies
    • Build and verify

Previous: 06 - STL Deep Dive Next: 08 - Best Practices