Change default value of CMAKE_CXX_FLAGS_DEBUG and friends in CMake

Florian's answer using toolchain files is a good one for earlier versions of CMake. But CMake 3.19 added a feature called presets that helps manage common sets of cache variables for your project. Basically, you create at least one of two files, CMakePresets.json and CMakeUserPresets.json (usually added to .gitignore or similar), that contain specifications of how to configure the project.

For example, you might write:

{
  "version": 1,
  "cmakeMinimumRequired": {
    "major": 3,
    "minor": 19,
    "patch": 0
  },
  "configurePresets": [
    {
      "name": "default",
      "displayName": "Default",
      "description": "Build using Ninja and a GCC-like compiler",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build",
      "cacheVariables": {
        "CMAKE_CXX_FLAGS_DEBUG": "-ggdb3 -O0"
      }
    },
    {
      "name": "default-vcpkg",
      "displayName": "Default (vcpkg)",
      "description": "Default build with vcpkg (from VCPKG_ROOT)",
      "inherits": "default",
      "cacheVariables": {
        "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
      }
    }
  ]
}

Then, from the source directory, your CMake command line would just become:

$ cmake --preset=default

This approach has a few advantages:

  1. It makes the command line a lot simpler
  2. It's compatible with other toolchain files (like vcpkg's in the second preset)
  3. It can override flags that are usually unconditionally added to the *_INIT flags.
  4. You don't have to write awkward logic in your CMakeLists.txt.
  5. Presets are opt-in for the user, which is important if you're distributing a library.

Expanding on points 4 and 5: it is a bad idea to add flags unless they absolutely must be there to compile correctly and there isn't a built-in feature for reaching those flags (eg. CMAKE_CXX_STANDARD). If someone tries to compile your library with a different compiler (or even a different version of the same compiler) they could run into issues if, for example, you add a warning flag that's too new or not supported. You can work around this with generator expressions and/or complex logic (like the _UNDEF trick above), but it's generally just easier and more convenient to use a toolchain or these new presets.

For instance, to correctly add -Wsuggest-override, you would need to write:

target_compile_options(lib PRIVATE $<$<AND:$<VERSION_GREATER_EQUAL:$<CXX_COMPILER_VERSION>,5.1>,$<COMPILE_LANG_AND_ID:CXX,GNU>>:-Wsuggest-override>)

# ... or ...

# Note: only correct if using "PRIVATE". Must use a genex for INTERFACE/PUBLIC because the whole genex gets exported, whereas this flag will get exported verbatim.
if (CMAKE_CXX_COMPILER_ID MATCHES "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 5.1)
  target_compile_options(lib PRIVATE -Wsuggest-override)
endif ()

Or you could just put the flag in a toolchain/preset where you already know what compiler you're using.


I just wanted to add the four possibilities I see:

  1. Having your own toolchain files containing the presets for each compiler you support like:

    GNUToolchain.cmake

     set(CMAKE_CXX_FLAGS_DEBUG "-ggdb3 -O0" CACHE STRING "")
    

    And then use it with

     cmake -DCMAKE_TOOLCHAIN_FILE:string=GNUToolchain.cmake ...
    
  2. You can try to determine the compiler by checking CMAKE_GENERATOR (which is valid before the project() command):

    CMakeLists.txt

     if("${CMAKE_GENERATOR}" MATCHES "Makefiles" OR 
        ("${CMAKE_GENERATOR}" MATCHES "Ninja" AND NOT WIN32))
         set(CMAKE_CXX_FLAGS_DEBUG "-ggdb3 -O0" CACHE STRING "")
     endif()
    
     project(your_project C CXX)
    
  3. You can use CMAKE_USER_MAKE_RULES_OVERRIDE to give a script with your own ..._INIT values:

    It is loaded after CMake’s builtin compiler and platform information modules have been loaded but before the information is used. The file may set platform information variables to override CMake’s defaults.

    MyInitFlags.cmake

     # Overwrite the init values choosen by CMake
     if (CMAKE_CXX_COMPILER_ID MATCHES "GNU")
         set(CMAKE_CXX_FLAGS_DEBUG_INIT "-ggdb3 -O0")
     endif()
    

    CMakeLists.txt

     set(CMAKE_USER_MAKE_RULES_OVERRIDE "MyInitFlags.cmake")
    
     project(your_project C CXX)
    
  4. You can simplify your solution from March 1st by checking against the ..._INIT variants of the compiler flag variables:

    CMakeLists.txt

     project(your_project C CXX)
    
     if (DEFINED CMAKE_CXX_FLAGS_DEBUG_INIT AND  
         "${CMAKE_CXX_FLAGS_DEBUG_INIT}" STREQUAL "${CMAKE_CXX_FLAGS_DEBUG}")
         # Overwrite the init values choosen by CMake
         if (CMAKE_CXX_COMPILER_ID MATCHES "GNU")
             set(CMAKE_CXX_FLAGS_DEBUG "-ggdb3 -O0" CACHE STRING "" FORCE)
         endif()
     endif()
    

Comments:

I prefer and use the toolchain variant. But I admit it has the disadvantage of having to give the toolchain file manually (if you are not calling cmake via a script/batch file).

References:

  • CMake: In which order are files parsed (cache, toolchain, etc.)?
  • cmake - Global linker flag setting (for all targets in directory)
  • Switching between GCC and Clang/LLVM using CMake