Learning Cmake
Table of Contents
project
The project() command is used to define a new CMake project. In this example, it creates a project named mini_trader and specifies that the project uses C++ via the CXX language.
By declaring LANGUAGES CXX, CMake knows that it needs to locate and configure a suitable C++ compiler (such as g++ or clang++). This step also initializes important variables and toolchain settings required for building C++ targets later in the project.
While project() can support multiple languages, explicitly specifying only the languages you need helps keep configuration faster and more predictable.
1project(mini_trader LANGUAGES CXX)option
The option() command in CMake defines a boolean configuration flag (ON/OFF) that users can toggle when configuring a project. It creates a cache variable, meaning the value is stored and can be modified through the command line or CMake GUI tools.
1option(<variable> "<help_text>" [value])The optional [value] sets the default state of the option. If not provided, it defaults to OFF.
1option(ENABLE_TSAN "Enable ThreadSanitizer" OFF)
2option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
3option(ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF)Users can override these defaults at configuration time using the -D flag:
1mkdir build
2cd build
3cmake -DENABLE_TSAN=ON ..
4makeOptions are commonly used to control optional features such as enabling tests, documentation, or debug tooling. Since they are stored in CMakeCache.txt, their values persist across multiple CMake runs until explicitly changed.
You can use these variables in conditional logic to modify build behavior:
1set(SANITIZER_FLAGS)
2
3if (ENABLE_TSAN)
4 message(STATUS "ThreadSanitizer enabled")
5 list(APPEND SANITIZER_FLAGS
6 -fsanitize=thread
7 -g
8 -O1
9 )
10endif()In CMake, a list is a semicolon-separated sequence of values. The set() command can be used to create or initialize lists. For example:
1set(var a b c) # a;b;c
2set(var "a b c") # single string elementIn this example,SANITIZER_FLAGS is a list used to collect compiler flags. When ENABLE_TSAN is enabled, the following flags are appended:-fsanitize=thread enables ThreadSanitizer to detect data races and unsafe concurrent access,-g includes debug symbols for readable stack traces, and -O1 applies light optimization (higher levels such as O3 may interfere with sanitizer accuracy).
add_subdirectory
add_subdirectory tells CMake to enter another folder, read its CMakeLists.txt, and treat everything defined there as part of the same build.
1add_subdirectory(tests)When CMake processes this line, it immediately steps into the tests/ directory, loads tests/CMakeLists.txt, and executes it just as if its contents were written in the main file.
Any targets created inside that subdirectory, such as executables or libraries defined with add_executable or add_library, become part of the same overall project. These targets can be built with make, linked against other targets, and discovered and run by ctest.
The big picture in modern CMake is that everything revolves around targets. A target represents a concrete buildable unit, such as an executable or a library, and CMake manages dependencies and relationships between these targets automatically.
add_executable
The add_executable() command defines an executable target from a set of source files. It is one of the core building blocks in CMake, responsible for telling the build system how to produce a runnable binary.
Only source files (such as .cppor .c) need to be listed. Header files do not need to be explicitly included, as modern compilers automatically track dependencies through #include directives.
1add_library(db_core
2 src/buffer_pool.cpp
3 src/disk_manager.cpp
4 src/page.cpp
5 src/lru_replacer.cpp
6)
7
8add_executable(databasecpp
9 src/main.cpp
10)
11
12target_link_libraries(databasecpp PRIVATE db_core)In this example,add_library() is used to group related source files into a reusable module called db_core. This allows the core database logic to be developed and maintained independently of the final executable.
The add_executable() command then defines the final application databasecpp, using main.cpp as its entry point.
Finally,target_link_libraries() links the executable with the db_core library. The PRIVATE keyword means that this dependency is only required for building the executable itself and is not propagated to other targets.
Structuring projects this way promotes modular design, making code easier to reuse, test, and maintain instead of placing all implementation directly inside a single executable target.
target_include_directories
target_include_directories controls where the compiler looks for header files when it encounters an #include directive.
For example, when the compiler sees an include like the following, it needs to know which directories to search in order to find the corresponding header files.
1#include "order_book.hpp"
2#include <boost/asio.hpp>The compiler first checks the directory of the current source file. If the header is not found there, it then searches through the include directories provided by target_include_directories.
The general syntax of this command is shown below. The visibility keyword determines how the include directories are applied to the target and any targets that depend on it.
1target_include_directories(<target>
2 PRIVATE | PUBLIC | INTERFACE
3 <dir1> <dir2> ...
4)Using PRIVATE means the include directories are only used when compiling this target. PUBLIC applies the directories to this target and anything that links against it, while INTERFACE applies them only to dependent targets.
In the example below, the Boost include directory is added so the compiler can find Boost headers such as boost/asio.hpp .
1set(BOOST_INCLUDEDIR "/usr/local/opt/boost/include")
2
3add_executable(
4 mini_trader
5 src/main.cpp
6 src/order_book.cpp
7 src/tcp_server.cpp
8)
9
10# Important: all source files must be listed so they are compiled and linked
11target_include_directories(
12 mini_trader
13 PRIVATE ${Boost_INCLUDE_DIRS}
14)target_link_libraries
target_link_libraries tells the linker which external libraries your program depends on. While header files are needed during compilation, libraries are required at link time to resolve symbols for functions and objects your code uses.
For example, if your code uses Boost, POSIX threads, or Google Test, the compiler may successfully build your source files, but the final executable will fail to link unless those libraries are explicitly linked.
The general syntax is shown below. Similar to include directories, the visibility keyword controls whether the linked libraries are used only by this target or also propagated to targets that depend on it.
1target_link_libraries(<target>
2 PRIVATE | PUBLIC | INTERFACE
3 lib1 lib2 ...
4)Using PRIVATE means the libraries are linked only when building this target. PUBLIC links them for this target and also exposes them to dependent targets, while INTERFACE applies them only to dependents.
In the example below, the executable is linked against Boost libraries and the system threading library. This ensures that all Boost and threading symbols used in the code are correctly resolved when producing the final binary.
1set(BOOST_LIBRARYDIR "/usr/local/opt/boost/lib")
2
3add_executable(
4 mini_trader
5 src/main.cpp
6 src/order_book.cpp
7 src/tcp_server.cpp
8)
9
10# All required libraries must be linked to produce the final executable
11target_link_libraries(
12 mini_trader
13 PRIVATE ${Boost_LIBRARIES} Threads::Threads
14)target_compile_options vs target_link_options
The target_compile_options() and target_link_options() commands are used to apply flags to a specific target in CMake. They control different stages of the build process: compilation and linking.
target_compile_options() applies flags during the compilation step, when source files are compiled into object files. This is where you typically add flags such as warnings, optimizations, or sanitizer instrumentation.
target_link_options() applies flags during the linking step, when object files are combined into the final executable or library. Some features, such as sanitizers, require flags to be present at both compile time and link time to work correctly.
1if (SANITIZER_FLAGS)
2 target_compile_options(databasecpp PUBLIC ${SANITIZER_FLAGS})
3 target_link_options(databasecpp PUBLIC ${SANITIZER_FLAGS})
4endif()In this example, the same set of sanitizer flags is applied to both stages. This ensures that instrumentation is added during compilation and that the required runtime libraries are correctly linked.
The PUBLIC keyword means that these options will also propagate to targets that depend on databasecpp. In practice, compile and link options are often marked as PRIVATE unless you explicitly want dependent targets to inherit them.
Using target-specific commands like these is preferred over global variables (such as CMAKE_CXX_FLAGS) because it keeps configuration modular, predictable, and easier to maintain as your project grows.