Build Configurations (Debug, Release, etc.)
Learn how CMake manages different build configurations like Debug and Release, the difference between generator types, and how to apply settings conditionally using modern techniques.
So far, every time we've built our project, we've created the same kind of output. But in real-world projects, we rarely build our code just one way. We need different "flavors" of your build for different purposes.
How many flavors and what they are used for varies from product to product, and the processes the team uses to create that product. However, almost all projects use at least a Debug
and Release
variation:
- A
Debug
build, compiled with no optimizations and full debugging symbols, so you can step through it with a debugger to find bugs. - A
Release
build, compiled with maximum optimizations and no debug info, making it fast and small for shipping to users.
These different sets of build settings are called build configurations or build types.
This lesson will teach you how CMake handles this task. We'll discover that there are two fundamentally different kinds of build systems, learn how to control the build type, and learn the modern, portable way to apply configuration-specific settings.
Specifying the Generator with -G
CMake can generate a wide range of project types as part of the configure step. The generator being used is available as the CMAKE_GENERATOR
variable:
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
message(STATUS "Using ${CMAKE_GENERATOR}")
project(Greeter VERSION "1.0")
add_subdirectory(greeter)
add_subdirectory(app)
cmake ..
-- Using Ninja
To see a list of all generators available on your system, you can run cmake --help
. The output will include a section listing the generators that are available in your environment:
cmake --help
If we want to use a different generator, deleting everything in the existing /build
directory first is recommended.
Then, to tell CMake which generator to use, we provide its exact name as listed in the --help
command using the -G
option.
For example, if we wanted to create a Visual Studio 2022 solution, and that was listed as an option in our cmake --help
output, we would use the following command:
cmake .. -G "Visual Studio 17 2022"
In our build directory, we should now see a Greeter.sln
file which we can open in Visual Studio. This file, in conjunction with the .vcxproj
files, includes all the configuration options we specified in our CMakeLists.txt
files, but in a format that is natively understood by Visual Studio.
Single-Config vs. Multi-Config Generators
The most important concept to understand is that CMake generates two different kinds of build systems. The way you handle configurations depends entirely on which type you're using.
Single-Configuration Generators
Tools like Unix Makefiles and Ninja are single-configuration generators. This means the entire build directory is dedicated to building just one configuration at a time.
A common way of supporting multiple configurations in this context is by having a different directory dedicated to each configuration.
These directories mighht have names like build-debug
and build-release
, or debug
and release
subfolders of our main /build
directory. For example:
(project root)/
└─ build/
├─ debug/
└─ release/
└─ ... (other directories)
We then choose the build type (e.g., Debug
or Release
) when you run the configure step. We first ensure we're running the command in the correct directory. For example, to create a Debug
build, we'd navigate to the /build/debug
directory.
Then, to specify our build type, we set the CMAKE_BUILD_TYPE
cache variable using the -D
option, for example, -DCMAKE_BUILD_TYPE=Debug
In our case, we'll also switch from using ..
to ../..
. This is because we now need to navigate up two levels (eg from /build/debug
to the project root) to locate our CMakeLists.txt
file, where we previously only needed to navigate up one level (from /build
)
cmake ../.. -DCMAKE_BUILD_TYPE=Debug
-- Configuring done (0.1s)
-- Generating done (0.0s)
-- Build files have been written to: D:/Projects/cmake/build/debug
The -B
and -S
Options
If we want to run the cmake
command from a different directory, we can specify where the build output should be using the -B
option.
For example, if we were in the /build
directory, and wanted our output to go into the /build/debug
directory, our command would look like this:
cmake .. -B debug/ -DCMAKE_BUILD_TYPE=Debug
If we want to be explicit about what the ..
represents (the location of our CMakeLists.txt
file) we can use the -S
option:
cmake -S .. -B debug/ -DCMAKE_BUILD_TYPE=Debug
Our /build/debug
directory is now dedicated to the Debug
version of our software and, within that directory, we can build that version in the usual way:
cmake --build .
The key point to remember is that, with single-config generators, the decision of what build type we create is made at configure time. The generated files support only that build type.
However, we can run the configure step multiple times, in different directories and with different CMAKE_BUILD_TYPE
settings, thereby creating projects with different build types.
Multi-Configuration Generators
IDEs like Visual Studio and Xcode, as well as the Ninja Multi-Config generator, work differently. They are designed to handle multiple configurations within a single generated project.
The generated Visual Studio Solution (.sln
) file, for example, contains all the rules needed to build Debug
, Release
, and any other configurations. This is why, if you open that .sln
file in Visual Studio, there will be a dropdown menu to let you switch between configurations on the fly:

With these multi-configuration generators, you do not set CMAKE_BUILD_TYPE
at configure time. Instead, you choose the configuration at build time. This means we build our project in much the same way we've done in the past. The only difference is that, at the build step, we set the --config
option:
cmake ..
cmake --build . --config Debug
This distinction is the source of much confusion for CMake beginners. If we check the CMAKE_BUILD_TYPE
in our CMakeLists.txt
or use it to drive logic within an if()
command, that will work for Makefiles but will fail for Visual Studio, because CMAKE_BUILD_TYPE
is not set at configure time for multi-config generators.
We'll learn how to write this configuration logic later in the lesson.
The CMAKE_CONFIGURATION_TYPES
Variable
The CMAKE_CONFIGURATION_TYPES
variable stores the list of build types that our multi-config generator supports. If we're not using a multi-config generator, this variable will not be set:
if(CMAKE_CONFIGURATION_TYPES)
message(STATUS "Generator is multi-config: ${CMAKE_GENERATOR}")
message(STATUS "Available configs: ${CMAKE_CONFIGURATION_TYPES}")
else()
message(STATUS "Generator is single-config: ${CMAKE_GENERATOR}")
message(STATUS "Current build type: ${CMAKE_BUILD_TYPE}")
endif()
-- Generator is multi-config: Visual Studio 17 2022
-- Available configurations: Debug;Release;RelWithDebInfo;MinSizeRel
Standard Build Types
CMake comes with four standard, built-in build types. For each one, it knows a default set of compiler flags.
Debug
: For debugging. Enables debug symbols and disables optimizations..Release
: For distribution. Enables optimizations and disables debug symbols.RelWithDebInfo
: Release with Debug Info. A hybrid that enables optimizations but also generates debug symbols. Good for profiling or debugging optimized code.MinSizeRel
: Minimum Size Release. Optimizes for the smallest binary size instead of speed.
We can check what compiler flags are associated with a configuration by examining the CMAKE_CXX_FLAGS_<CONFIG>
variables. For example:
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(Greeter VERSION "1.0")
message("Debug: ${CMAKE_CXX_FLAGS_DEBUG}")
message("Release: ${CMAKE_CXX_FLAGS_RELEASE}")
message("RelWithDebInfo: ${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")
message("MinSizeRel: ${CMAKE_CXX_FLAGS_MINSIZEREL}")
add_subdirectory(greeter)
add_subdirectory(app)
Debug: -g
Release: -O3 -DNDEBUG
RelWithDebInfo: -O2 -g -DNDEBUG
MinSizeRel: -Os -DNDEBUG
Generator Expressions
We've seen that the CMAKE_BUILD_TYPE
is only available at configure time if we're using a single-config generator, but we want our CMakeLists.txt
file to be portable. It should support both single and multi-config generators,
To this end, we typically avoid using the CMAKE_BUILD_TYPE
expression in our configuration logic. We instead use generator expressions.
A generator expression is a special piece of syntax, written as $<...>
, that is not evaluated at configure time. Instead, it's written into the native build files and evaluated at build time. This allows the expression to react to the configuration chosen by the user in their IDE or with the --config
flag.
The most common generator expression is $<CONFIG:...>
. Let's see a common example of using such an expression, and then explain what's going on.
Imagine we we want to define a preprocessor macro ENABLE_LOGGING
only in Debug
builds of our GreeterApp
. Rather than being tempted to check the CMAKE_BUILD_TYPE
in an if()
statement, we should instead use a generator expression:
app/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
add_executable(GreeterApp src/main.cpp)
target_link_libraries(GreeterApp GreeterLib)
target_compile_definitions(GreeterApp PRIVATE
$<$<CONFIG:Debug>:ENABLE_LOGGING>
)
Let's break this down:
$<CONFIG:Debug>
: This part of the expression evaluates to1
if the active configuration isDebug
, and0
otherwise.$<1:...>
evaluates to whatever content comes after the:
, which isENABLE_LOGGING
in this example.$<0:...>
evaluates to an empty string.- So, if we build in
Debug
, the whole expression becomesENABLE_LOGGING
, and thetarget_compile_definitions()
command addsDENABLE_LOGGING
to the compiler flags. - If we build using any other type, the expression becomes an empty string, and the command does nothing.
This logic is evaluated by the native build system, so it works perfectly whether Debug
was chosen at configure time (eg Makefiles) or build time (eg Visual Studio).
app/main.cpp
#include <iostream>
#include <greeter/Greeter.h>
int main() {
#ifdef ENABLE_LOGGING
std::cout << "I'll be logging extra stuff in this build\n";
#endif
std::cout << get_greeting();
return 0;
}
cmake .. -DCMAKE_BUILD_TYPE=Debug
cmake --build .
./app/GreeterApp
I'll be logging extra stuff in this build
Hello from the modular greeter library!
More Examples
Other common uses for generator expressions include conditionally adding files to a build:
# Add a file only in Debug configurations
target_sources(GreeterApp PRIVATE
src/main.cpp
$<$<CONFIG:Debug>:src/debug_helpers.cpp>
)
We can also use them for conditionally linking to different versions of a library. Many precompiled third-party libraries provide a debug version (e.g., liblogger_d.a
) and a release version (liblogger.a
).
You can handle this cleanly with generator expressions:
target_link_libraries(GreeterApp PRIVATE
# Link to the debug version of a library in Debug builds
$<$<CONFIG:Debug>:logger_d>
# Link to the optimized version in Release builds
$<$<CONFIG:Release>:logger>
)
This tells CMake: "When building in Debug mode, link to logger_d
. When building in Release mode, link to logger
."
Custom Build Types and Customizing Flags
While the four standard types are usually enough, you can define your own. For example, you might want a Profile
build with specific instrumentation flags.
We'll learn the best way to define a completely new build type later in the course, but for now, let's see how we can modify the flags of an existing build type. For instance, let's imagine we wanted to treat warnings as errors in our Debug
build.
To do this, we'd use the add_compile_options()
command with a generator expression. To treat warnings as errors, you'd set -Werror
if you're using Clang/GCC, or /WX
for MSVC. We've written this for Clang/GCC in this example, but we'll learn how to make it more portable in the next lesson:
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(Greeter VERSION "1.0")
add_compile_options(
$<$<CONFIG:Debug>:-Werror>
)
add_subdirectory(greeter)
add_subdirectory(app)
Customizing for a Single Target
Note that the add_compile_options()
command in this example is different from the target_compile_options()
we used previously. The add_compile_options()
applies the options to all targets in this CMakeLists.txt
file, including any that are added through add_subdirectory()
.
If we wanted to apply the options to only one target, we'd use target_compile_options()
. We provide our target's name as the first argument, and the propagation behavior (PRIVATE
, INTERFACE
, or PUBLIC
) as the second:
target_compile_options(SomeTarget PRIVATE
$<$<CONFIG:Debug>:-Werror>
)
With either approach, configuring or building in Debug
mode and introducing a warning in our code will cause compilation to fail as intended:
app/main.cpp
#include <iostream>
#include <greeter/Greeter.h>
[[deprecated("Don't use this")]]
void NastyFunction() {};
int main() {
NastyFunction();
std::cout << get_greeting();
return 0;
}
cmake -DCMAKE_BUILD_TYPE=Debug ..
cmake --build .
error: 'void NastyFunction()' is deprecated: Don't use this [-Werror=deprecated-declarations]
8 | NastyFunction();
| ~~~~~~~~~~~~~^~
cc1plus.exe: all warnings being treated as errors
ninja: build stopped: subcommand failed.
Summary
Build configurations are an important part of any C++ project, and CMake provides a portable framework for managing them.
- Standard Build Types: CMake knows about
Debug
,Release
,RelWithDebInfo
, andMinSizeRel
, and provides default flags for each. - Two Generator Types: Single-configuration (Makefiles, Ninja) set the build type at configure time (
-DCMAKE_BUILD_TYPE=Debug
). Multi-configuration (Visual Studio, Xcode) set it at build time (-config Debug
). - Generator Expressions: Use
$<...>
expressions, especially$<CONFIG:...>
, to write configuration-specific logic that works portably across all generator types. - Customize Flags: To modify the default flags for a build type, use the
add_compile_options()
ortarget_compile_options()
in conjunction with a generator expression.
Cross-Platform Configurations
Learn the tools and patterns needed to write portable CMakeLists.txt files that work seamlessly across platforms and compilers