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.

Greg Filak
Published

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.

  1. Debug: For debugging. Enables debug symbols and disables optimizations..
  2. Release: For distribution. Enables optimizations and disables debug symbols.
  3. RelWithDebInfo: Release with Debug Info. A hybrid that enables optimizations but also generates debug symbols. Good for profiling or debugging optimized code.
  4. 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 to 1 if the active configuration is Debug, and 0 otherwise.
  • $<1:...> evaluates to whatever content comes after the :, which is ENABLE_LOGGING in this example. $<0:...> evaluates to an empty string.
  • So, if we build in Debug, the whole expression becomes ENABLE_LOGGING, and the target_compile_definitions() command adds DENABLE_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, and MinSizeRel, 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() or target_compile_options() in conjunction with a generator expression.
Next Lesson
Lesson 25 of 25

Cross-Platform Configurations

Learn the tools and patterns needed to write portable CMakeLists.txt files that work seamlessly across platforms and compilers

Have a question about this lesson?
Answers are generated by AI models and may not be accurate