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
CMake Project Setup
- Create a project with multiple source files
- Add a library and link it to executable
- Verify incremental builds work
External Dependencies
- Use FetchContent to add fmt library
- Add a test framework (GoogleTest or Catch2)
- Create a simple test
Build Configurations
- Set up Debug and Release configurations
- Add sanitizer support
- Configure compiler warnings
Package Manager
- Set up vcpkg or Conan
- Add multiple dependencies
- Build and verify
Previous: 06 - STL Deep Dive Next: 08 - Best Practices