Cristian Adam

Modifying the default CMake build types

CMake has for single configuration configurators the following build types (configuration):

  • Empty (Qt Creator wrongly refers to this as “Default”)
  • Debug
  • Release
  • RelWithDebInfo – Release with debug information, needed for profiling / post mortem debugging
  • MinSizeRel – Release optimized for size, and not for speed.

If we have a look at CMake’s Modules/Compiler/GNU.cmake we can see:

  # Initial configuration flags.
  string(APPEND CMAKE_${lang}_FLAGS_INIT " ")
  string(APPEND CMAKE_${lang}_FLAGS_DEBUG_INIT " -g")
  string(APPEND CMAKE_${lang}_FLAGS_MINSIZEREL_INIT " -Os -DNDEBUG")
  string(APPEND CMAKE_${lang}_FLAGS_RELEASE_INIT " -O3 -DNDEBUG")
  string(APPEND CMAKE_${lang}_FLAGS_RELWITHDEBINFO_INIT " -O2 -g -DNDEBUG")

The empty build type usually contains the common build flags for all build types. It is generated from the CMAKE_C_FLAGS_INIT / CMAKE_CXX_FLAGS_INIT variables, and the CFLAGS / CXXFLAGS system environment variables.

But in the case of an IDE like Qt Creator makes no sense to have, you will end up for GCC with a -O0 (Debug) build. I’ve opened QTCREATORBUG-22013 in this regard.

CMake uses the CMAKE_<LANG>_FLAGS_<CONFIG>_INIT variables which will be used to populate the CMAKE_CMAKE_<LANG>_FLAGS_<CONFIG> variables.

There are cases when you might want to change the default build types:

  • Want to have -g1 for RelWithDebInfo, because your binaries are becoming too big
  • Want to improve build times in Debug mode with -gsplit-dwarf
  • Want to link to a different version of the CRT
  • Want to enable all possible warnings from the compiler

Lastly, we want to do all this without putting if clauses in the code, and manually changing the CMAKE_<LANG>_FLAGS variables. The rule of thumb is: if you have to change compiler flags, you should do it in a toolchain file!

Writing a CMake toolchain file

If we read the CMake documentation about writing a toolchain, we can see how easy is to write such a toolchain file. You pass the path to the compiler, while CMake will do autodetection for you. This works fine for GNU GCC / Clang / Visual C++ compilers.

Here is what you have to set for using clang as a cross compiler for Arm platform:

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

set(triple arm-linux-gnueabihf)

set(CMAKE_C_COMPILER clang)
set(CMAKE_C_COMPILER_TARGET ${triple})
set(CMAKE_CXX_COMPILER clang++)
set(CMAKE_CXX_COMPILER_TARGET ${triple})

There is nothing about CMAKE_<LANG>_FLAGS_<CONFIG>, because it is assumed we are using the defaults. If one needs to add something special to CMAKE_<LANG>_FLAGS_<CONFIG> variable, you are supposed to use the CMAKE_<LANG>_FLAGS_<CONFIG>_INIT variables.

Android NDK Toolchain

The Android NDK CMake toolchain wants to have for Release build type debugging information enabled, and the -O2 compilation flag, while the default CMake Release build type is using -O3. Basically having the default CMake RelWithDebInfo build type.

In the NDK19 we can see in the android.toolchain.cmake the following:

# Debug and release flags.
list(APPEND ANDROID_COMPILER_FLAGS_DEBUG -O0)
if(ANDROID_ABI MATCHES "^armeabi" AND ANDROID_ARM_MODE STREQUAL thumb)
  list(APPEND ANDROID_COMPILER_FLAGS_RELEASE -Oz)
else()
  list(APPEND ANDROID_COMPILER_FLAGS_RELEASE -O2)
endif()
list(APPEND ANDROID_COMPILER_FLAGS_RELEASE -DNDEBUG)
if(ANDROID_TOOLCHAIN STREQUAL clang)
  list(APPEND ANDROID_COMPILER_FLAGS_DEBUG -fno-limit-debug-info)
endif()

Which is then followed by (edited a bit for brevity):

# Set or retrieve the cached flags.
# This is necessary in case the user sets/changes flags in subsequent
# configures. If we included the Android flags in here, they would get
# overwritten.
set(CMAKE_C_FLAGS ""
  CACHE STRING "Flags used by the compiler during all build types.")
set(CMAKE_CXX_FLAGS ""
  CACHE STRING "Flags used by the compiler during all build types.")
set(CMAKE_C_FLAGS_DEBUG ""
  CACHE STRING "Flags used by the compiler during debug builds.")
set(CMAKE_CXX_FLAGS_DEBUG ""
  CACHE STRING "Flags used by the compiler during debug builds.")
set(CMAKE_C_FLAGS_RELEASE ""
  CACHE STRING "Flags used by the compiler during release builds.")
set(CMAKE_CXX_FLAGS_RELEASE ""
  CACHE STRING "Flags used by the compiler during release builds.")

set(CMAKE_C_FLAGS             "${ANDROID_COMPILER_FLAGS} ${CMAKE_C_FLAGS}")
set(CMAKE_CXX_FLAGS           "${ANDROID_COMPILER_FLAGS} ${ANDROID_COMPILER_FLAGS_CXX} ${CMAKE_CXX_FLAGS}")
set(CMAKE_C_FLAGS_DEBUG       "${ANDROID_COMPILER_FLAGS_DEBUG} ${CMAKE_C_FLAGS_DEBUG}")
set(CMAKE_CXX_FLAGS_DEBUG     "${ANDROID_COMPILER_FLAGS_DEBUG} ${CMAKE_CXX_FLAGS_DEBUG}")
set(CMAKE_C_FLAGS_RELEASE     "${ANDROID_COMPILER_FLAGS_RELEASE} ${CMAKE_C_FLAGS_RELEASE}")
set(CMAKE_CXX_FLAGS_RELEASE   "${ANDROID_COMPILER_FLAGS_RELEASE} ${CMAKE_CXX_FLAGS_RELEASE}")

The comment in the above code shows some problems one might have while editing CMAKE_<LANG>_FLAGS_<CONFIG> variables.

Static linking to CRT with Visual C++

On Windows CMake has selected dynamic linking to the CRT for its build types, namely the /MD compiler flag.

But what if we want to link statically to the CRT with the /MT compiler flag, thus avoiding the need of deploying the CRT runtime on older Windows versions?

Here is what Google Test is doing in its googletest/cmake/internal_utils.cmake:

# Tweaks CMake's default compiler/linker settings to suit Google Test's needs.
#
# This must be a macro(), as inside a function string() can only
# update variables in the function scope.
macro(fix_default_compiler_settings_)
  if (MSVC)
    # For MSVC, CMake sets certain flags to defaults we want to override.
    # This replacement code is taken from sample in the CMake Wiki at
    # http://www.cmake.org/Wiki/CMake_FAQ#Dynamic_Replace.
    foreach (flag_var
             CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_RELEASE
             CMAKE_CXX_FLAGS_MINSIZEREL CMAKE_CXX_FLAGS_RELWITHDEBINFO)
      if (NOT BUILD_SHARED_LIBS AND NOT gtest_force_shared_crt)
        # When Google Test is built as a shared library, it should also use
        # shared runtime libraries.  Otherwise, it may end up with multiple
        # copies of runtime library data in different modules, resulting in
        # hard-to-find crashes. When it is built as a static library, it is
        # preferable to use CRT as static libraries, as we don't have to rely
        # on CRT DLLs being available. CMake always defaults to using shared
        # CRT libraries, so we override that default here.
        string(REPLACE "/MD" "-MT" ${flag_var} "${${flag_var}}")
      endif()

      # We prefer more strict warning checking for building Google Test.
      # Replaces /W3 with /W4 in defaults.
      string(REPLACE "/W3" "/W4" ${flag_var} "${${flag_var}}")
    endforeach()
  endif()
endmacro()

This means that you need to call this macro in your CMake code, and that it will affect the compilation of all subsequent targets.

We can avoid this by having a toolchain file:

include_guard(GLOBAL)

include(CMakeInitializeConfigs)

function(cmake_initialize_per_config_variable _PREFIX _DOCSTRING)
  if (_PREFIX MATCHES "CMAKE_(C|CXX)_FLAGS")
    string(REPLACE "/W3" "/W4" ${_PREFIX}_INIT "${${_PREFIX}_INIT}")
    
    foreach (config
      ${_PREFIX}_DEBUG_INIT
      ${_PREFIX}_RELEASE_INIT
      ${_PREFIX}_RELWITHDEBINFO_INIT
      ${_PREFIX}_MINSIZEREL_INIT)
      
      string(REPLACE "/MD" "/MT" ${config} "${${config}}")
    endforeach()
    endif()
    
  _cmake_initialize_per_config_variable(${ARGV})
endfunction()

This unfortunately only works starting with CMake version 3.11, released in March 2018!

CMake 3.11 has gathered the generation of all config variable generation in one function. This is an internal function, and it’s functionality has not been documented in the 3.11 release notes. We have the variable CMAKE_NOT_USING_CONFIG_FLAGS documented, variable which is used in the cmake_initialize_per_config_variable function.

cmake_initialize_per_config_variable will be called at the point of generating the CMAKE_<LANG>_FLAGS_<CONFIG>, which is done after the toolchain code has been processed.

CMake versions lower than 3.11

The CMAKE_<LANG>_FLAGS_<CONFIG>_INIT variables are defined in different places, for Clang / GCC you have them in Modules/Compiler/GNU.cxx, for Visual C++ they are in Modules/Platform/Windows-MSVC.cmake. They are also defined with string(APPEND, which means that they will overpower your toolchain versions.

I am mentioning this because you might get something like this working for GNU like compilers for CMake versions lower than 3.11:

include(Compiler/GNU)

foreach(lang C CXX ASM)
  # Make sure that the CMAKE_<LANG>_FLAGS_RELEASE_INIT has been generated by CMake
  __compiler_gnu(${lang})
  
  string(REPLACE "-O3" "-O2 -g" CMAKE_${lang}_FLAGS_RELEASE_INIT "${CMAKE_${lang}_FLAGS_RELEASE_INIT}")
endforeach()

# Ignore CMake's own calls later after toolchain has been processed
macro(__compiler_gnu lang)
endmacro()

But this will partially work for Visual C++. Compiler feature detection won’t be working, etc. pensive

With cmake_initialize_per_config_variable you can replace / modify the CMAKE_<LANG>_FLAGS_<CONFIG>_INIT values at will.

Android NDK toolchain patch

Armed with this information, I decided to hack the Android NDK toolchain. Below you have the patch:


diff -Naur cmake/android.toolchain.cmake cmake-3.11/android.toolchain.cmake
--- cmake/android.toolchain.cmake    2019-02-21 21:12:32.303346658 +0100
+++ cmake-3.11/android.toolchain.cmake    2019-02-21 21:41:46.985539190 +0100
@@ -35,7 +35,9 @@
 # ANDROID_DISABLE_FORMAT_STRING_CHECKS
 # ANDROID_CCACHE
 
-cmake_minimum_required(VERSION 3.6.0)
+cmake_minimum_required(VERSION 3.11)
+
+include_guard(GLOBAL)
 
 # Inhibit all of CMake's own NDK handling code.
 set(CMAKE_SYSTEM_VERSION 1)
@@ -578,48 +580,6 @@
 endif()
 
 
-# Set or retrieve the cached flags.
-# This is necessary in case the user sets/changes flags in subsequent
-# configures. If we included the Android flags in here, they would get
-# overwritten.
-set(CMAKE_C_FLAGS ""
-  CACHE STRING "Flags used by the compiler during all build types.")
-set(CMAKE_CXX_FLAGS ""
-  CACHE STRING "Flags used by the compiler during all build types.")
-set(CMAKE_ASM_FLAGS ""
-  CACHE STRING "Flags used by the compiler during all build types.")
-set(CMAKE_C_FLAGS_DEBUG ""
-  CACHE STRING "Flags used by the compiler during debug builds.")
-set(CMAKE_CXX_FLAGS_DEBUG ""
-  CACHE STRING "Flags used by the compiler during debug builds.")
-set(CMAKE_ASM_FLAGS_DEBUG ""
-  CACHE STRING "Flags used by the compiler during debug builds.")
-set(CMAKE_C_FLAGS_RELEASE ""
-  CACHE STRING "Flags used by the compiler during release builds.")
-set(CMAKE_CXX_FLAGS_RELEASE ""
-  CACHE STRING "Flags used by the compiler during release builds.")
-set(CMAKE_ASM_FLAGS_RELEASE ""
-  CACHE STRING "Flags used by the compiler during release builds.")
-set(CMAKE_MODULE_LINKER_FLAGS ""
-  CACHE STRING "Flags used by the linker during the creation of modules.")
-set(CMAKE_SHARED_LINKER_FLAGS ""
-  CACHE STRING "Flags used by the linker during the creation of dll's.")
-set(CMAKE_EXE_LINKER_FLAGS ""
-  CACHE STRING "Flags used by the linker.")
-
-set(CMAKE_C_FLAGS             "${ANDROID_COMPILER_FLAGS} ${CMAKE_C_FLAGS}")
-set(CMAKE_CXX_FLAGS           "${ANDROID_COMPILER_FLAGS} ${ANDROID_COMPILER_FLAGS_CXX} ${CMAKE_CXX_FLAGS}")
-set(CMAKE_ASM_FLAGS           "${ANDROID_COMPILER_FLAGS} ${CMAKE_ASM_FLAGS}")
-set(CMAKE_C_FLAGS_DEBUG       "${ANDROID_COMPILER_FLAGS_DEBUG} ${CMAKE_C_FLAGS_DEBUG}")
-set(CMAKE_CXX_FLAGS_DEBUG     "${ANDROID_COMPILER_FLAGS_DEBUG} ${CMAKE_CXX_FLAGS_DEBUG}")
-set(CMAKE_ASM_FLAGS_DEBUG     "${ANDROID_COMPILER_FLAGS_DEBUG} ${CMAKE_ASM_FLAGS_DEBUG}")
-set(CMAKE_C_FLAGS_RELEASE     "${ANDROID_COMPILER_FLAGS_RELEASE} ${CMAKE_C_FLAGS_RELEASE}")
-set(CMAKE_CXX_FLAGS_RELEASE   "${ANDROID_COMPILER_FLAGS_RELEASE} ${CMAKE_CXX_FLAGS_RELEASE}")
-set(CMAKE_ASM_FLAGS_RELEASE   "${ANDROID_COMPILER_FLAGS_RELEASE} ${CMAKE_ASM_FLAGS_RELEASE}")
-set(CMAKE_SHARED_LINKER_FLAGS "${ANDROID_LINKER_FLAGS} ${CMAKE_SHARED_LINKER_FLAGS}")
-set(CMAKE_MODULE_LINKER_FLAGS "${ANDROID_LINKER_FLAGS} ${CMAKE_MODULE_LINKER_FLAGS}")
-set(CMAKE_EXE_LINKER_FLAGS    "${ANDROID_LINKER_FLAGS} ${ANDROID_LINKER_FLAGS_EXE} ${CMAKE_EXE_LINKER_FLAGS}")
-
 # Compatibility for read-only variables.
 # Read-only variables for compatibility with the other toolchain file.
 # We'll keep these around for the existing projects that still use them.
@@ -686,3 +646,34 @@
     set(CMAKE_ANDROID_ARM_MODE ${ANDROID_ARM_MODE})
   endif()
 endif()
+
+include(CMakeInitializeConfigs)
+
+function(cmake_initialize_per_config_variable _PREFIX _DOCSTRING)
+
+  if (_PREFIX MATCHES "CMAKE_(C|CXX|ASM)_FLAGS")
+    set(CMAKE_${CMAKE_MATCH_1}_FLAGS_INIT "${ANDROID_COMPILER_FLAGS}")
+
+    foreach (config DEBUG RELEASE)
+      set(CMAKE_${CMAKE_MATCH_1}_FLAGS_${config}_INIT "${ANDROID_COMPILER_FLAGS_${config}}")
+    endforeach()
+
+    # Append the ANDROID_COMPILER_FLAGS_CXX flags
+    if (DEFINED ANDROID_COMPILER_FLAGS_${CMAKE_MATCH_1})
+      string(APPEND CMAKE_${CMAKE_MATCH_1}_FLAGS_INIT " ${ANDROID_COMPILER_FLAGS_${CMAKE_MATCH_1}}")
+    endif()
+  endif()
+
+  if (_PREFIX MATCHES "CMAKE_(SHARED|MODULE|EXE)_LINKER_FLAGS")
+    foreach (config SHARED MODULE EXE)
+      set(CMAKE_${config}_LINKER_FLAGS_INIT "${ANDROID_LINKER_FLAGS}")
+
+      # Append the ANDROID_LINKER_FLAGS_EXE flags
+      if (DEFINED ANDROID_LINKER_FLAGS_${config})
+        string(APPEND CMAKE_${config}_LINKER_FLAGS_INIT " ${ANDROID_LINKER_FLAGS_${config}}")
+      endif()
+    endforeach()
+  endif()
+
+  _cmake_initialize_per_config_variable(${ARGV})
+endfunction()

The new code involves a bit more time to figure out what it does, but you have the benefit of having in the CMakeCache.txt the CMAKE_<LANG>_FLAGS_<CONFIG> values, as opposed to having empty values as you get with the default toolchain.

Roundup

As a conclusion to this article is that you should never touch CMAKE_<LANG>_FLAGS_<CONFIG> variables directly. All the compiler build flags should be set in a toolchain, even if you don’t do cross compiling.

This way you can have a consistent build, with the same compiler flags used for all targets / subprojects!

Comments