DEV Community

DevOps Man
DevOps Man

Posted on • Edited on

Mastering CMake: A Practical Guide for DevOps Engineers and Developers

CMake is the backbone of modern C++ build systems. Whether you're compiling a simple executable or orchestrating large-scale multi-module projects with external libraries, mastering CMake can make your builds cleaner, faster, and more maintainable.

This guide captures everything you need to know to write robust CMakeLists.txt files with real-world examples and explanations.

Table of Contents

  1. Basic CMake Commands

  2. Project Structure

  3. Commonly Used Variables

  4. Best Practices

  5. Compiler Warnings

  6. Configuring files

  7. Unit Testing with Catch2

  8. Compile Features and Definitions

  9. Sanitizers

  10. IPO & LTO

  11. Generator Expressions

  12. External Libraries (Git Submodules & FetchContent)

  13. Useful CMake CLI Flags


Basic CMake Commands

cmake_minimum_required(VERSION 3.10)
This command specifies the minimum CMake version required for your project. It should be the first line in your top-level CMakeLists.txt.

project(MyApp VERSION 1.0 LANGUAGES CXX)
Defines the project name, version, and language.

add_executable(MyApp main.cpp helper.cpp)
This tells CMake to compile main.cpp and helper.cpp into an executable named MyApp. You can list as many source files as needed. The target name (MyApp) is used in other commands, like target_link_libraries(MyApp PRIVATE SomeLib)

add_library(MyLib STATIC mylib.cpp)
Defines a library named MyLib.

STATIC: Creates a static library (.lib or .a). Linked at compile time.
SHARED: Creates a shared (dynamic) library (.dll or .so). Loaded at runtime.
MODULE: Creates a library that is not linked but loaded at runtime (e.g., for plugins).
INTERFACE: No output file is built. Used for header-only libraries.

target_include_directories(MyLib PUBLIC include)
Specifies include directories. Types:

PUBLIC: Used by both the library and users
PRIVATE: Used only by the library
INTERFACE: Used only by users

add_subdirectory(utils)
Includes utils/CMakeLists.txt in the build.

target_link_libraries(MyApp PRIVATE MyLib)
Links MyLib with MyApp.

set(MY_VAR "value")
Defines a variable.

option(BUILD_TESTS "Build test binaries" ON)
Defines a boolean option with description.

if(USE_MY_FEATURE)
    message(STATUS "Building with my feature")
else()
    message(STATUS "Building without my feature")
endif()
Enter fullscreen mode Exit fullscreen mode

Conditional logic in CMake.


Project Structure

project-root/
├── CMakeLists.txt
├── main.cpp
├── include/
│   └── mylib.hpp
├── src/
│   └── mylib.cpp
├── tests/
│   ├── CMakeLists.txt
│   └── test_main.cpp
Enter fullscreen mode Exit fullscreen mode

Commonly Used Variables

CMAKE_SOURCE_DIR: Top-level source directory
CMAKE_PROJECT_NAME: Name set in project()
CMAKE_BINARY_DIR: Top-level build directory
CMAKE_CURRENT_SOURCE_DIR: Source dir of the current CMakeLists.txt
CMAKE_CURRENT_LIST_DIR: Directory containing the currently processed CMake file
CMAKE_MODULE_PATH: List of additional module directories


Best Practices

✅ Always list files manually instead of using file(GLOB ...)
✅ Modularize using add_subdirectory()
✅ Use target_* commands (not global ones)
✅ Separate headers and sources by folder
❌ Don’t pollute global scope with variables


Compiler Warnings

target_compile_options() adds compiler flags/options at compile time to a specific target (executable or library).
Use portable warnings with:

target_compile_options(MyApp PRIVATE
  $<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra -Wpedantic>
  $<$<CXX_COMPILER_ID:MSVC>:/W4>
)
Enter fullscreen mode Exit fullscreen mode

🚀 Common Compiler Flags (for GCC/Clang)

Flag Meaning
-Wall Enable most common warnings
-Wextra Enable additional warnings
-Werror Treat warnings as errors
-O2, -O3 Optimization levels (O2 = good speed, O3 = aggressive)
-g Generate debug symbols for gdb or lldb
-std=c++17 Use C++17 standard (replace with c++11, c++20, etc.)
-DDEBUG Define macro DEBUG, useful for conditional code blocks
-fPIC Position Independent Code (for shared libraries)

Configuring Files with configure_file

CMake’s configure_file() command lets you copy and process a template file at configure time, replacing variable placeholders. For example, if you have a header template file.h.in:

// file.h.in
#define VERSION @PROJECT_VERSION@
Enter fullscreen mode Exit fullscreen mode

and in your CMakeLists.txt you do:

project(MyApp VERSION 1.2.3)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/file.h.in
               ${CMAKE_CURRENT_BINARY_DIR}/file.h
               @ONLY)
Enter fullscreen mode Exit fullscreen mode

CMake will produce file.h in the build directory with @PROJECT_VERSION@ replaced by the actual version string (e.g. #define VERSION 1.2.3). In general, configure_file() copies a “.in” file to the build tree and substitutes any ${VAR} with the current CMake variable values
cliutils.gitlab.io
. This is very useful for generating headers (or even .cmake files) that convey build-time settings like version, feature toggles, or compile options. Remember to add the build directory to your include path (e.g. via target_include_directories) so that code can #include "file.h" from the generated location.


Unit Testing with Catch2

Setup
git submodule add https://github.com/catchorg/Catch2.git external/Catch2

CMakeLists.txt

add_subdirectory(external/Catch2)
add_executable(tests test_main.cpp)
target_link_libraries(tests PRIVATE Catch2::Catch2WithMain)
include(CTest)
include(Catch)
catch_discover_tests(tests)
Enter fullscreen mode Exit fullscreen mode

Compile Features and Definitions

Enforce Features
target_compile_features(MyApp PRIVATE cxx_std_17)

Add Macros
target_compile_definitions(MyApp PRIVATE VERSION=1.0)


Sanitizers

target_compile_options(MyApp PRIVATE -fsanitize=address)
target_link_options(MyApp PRIVATE -fsanitize=address)
Enter fullscreen mode Exit fullscreen mode

Apply similar for undefined sanitizer.


IPO & LTO

Enable link-time optimization:
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)

Or per target:
set_property(TARGET MyApp PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)


Generator Expressions

Inline conditional logic:
$<$<CONFIG:Debug>:-DDEBUG>


External Libraries

Git Submodules
Add submodule: git submodule add <repo> external/LibName
In root CMake:

add_subdirectory(external/LibName)
target_link_libraries(MyApp PRIVATE LibName)
Enter fullscreen mode Exit fullscreen mode

FetchContent

include(FetchContent)
FetchContent_Declare(
  fmt
  GIT_REPOSITORY https://github.com/fmtlib/fmt.git
  GIT_TAG master
)
FetchContent_MakeAvailable(fmt)
target_link_libraries(MyApp PRIVATE fmt::fmt)
Enter fullscreen mode Exit fullscreen mode

Useful CMake CLI Flags

cmake -S . -B build/ -G "Ninja"
-S: Source directory
-B: Build directory
-G: Generator

cmake --build build/ --target MyApp
Builds only MyApp

cmake --build . --target extLib
Build external target


Dependency Graph

Use Graphviz to visualize target dependencies:

cmake --graphviz=graph.dot .
dot -Tpng graph.dot -o graph.png
Enter fullscreen mode Exit fullscreen mode

Conclusion

Mastering CMake is about understanding its modular philosophy. Start with small clean builds, add subdirectories and targets gradually, and leverage the power of target_ commands for scalability and control.


Useful links:

Top comments (0)