CMake: How to Inspect and Configure the Compiler

A tutorial on using CMake to configure compilers and their flags for development projects

Dane Bulat
16 min readDec 24, 2020

Introduction

The CMake program makes it possible to write simple configuration files to control a project’s compilation process and generate native build files across platforms. It is also possible to configure the compilation process based on the host system’s environment in a granular way.

This tutorial details how to inspect, configure and output a project’s compiler settings using CMake. We will progress in incremental steps as described below:

  1. Inspecting the Default Compiler
    This section details how to find out which compiler and compiler flags CMake uses on your system by default. These settings are then used to compile the sample project included with this tutorial.
  2. Selecting a Compiler and Inspecting its Properties
    We then look at how to select another C++ compiler on the system to build our project, as well as outputting properties such as its path, ID and version.
  3. Inspecting the Default Build Types
    The default compiler flags CMake uses for its build configurations are identified and discussed.
  4. Conditionally Adding Compiler Flags
    Our configuration is modified to conditionally set compiler flags depending on the selected compiler and build type.

Upon completing this tutorial, you will have built up enough knowledge to be able to configure the compiler to your exact specifications for building a project with CMake.

Source Files

A small C++ sample project has been setup on GitHub to accompany this tutorial. Run the following git command to download it to your system:

$ git clone https://github.com/danebulat/cmake-compiler-flags.git

The project contains an initial CMake configuration file along with some source files in a single directory:

> root
---> CMakeLists.txt # Initial CMake configuration
---> interpolate.cpp # Source file for 'interp' library
---> interpolate.h # Header file for 'interp' library
---> main.cpp # Source file for the executable
---> steps/
  • interpolate.cpp and interpolate.h
    These files implement some standard easing functions that are commonly used to produce tween animations. It's not necessary to understand how the easing functions work to complete this tutorial. CMake simply compiles these files into a shared library called interp.
  • main.cpp
    A simple program that calls some of the easing functions and outputs the results. CMake compiles this file into an executable and links it to the interp shared library.
  • CMakeLists.txt
    The CMake configuration file which describes how the project is built.
  • steps/
    A directory containing snapshots of project files that correspond to each stage of the tutorial.

Our initial configuration file sets some project properties and compiles the source files into an executable and shared library. Let’s go through some of the configuration to get acquainted with our initial setup.

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

Features of the C++ 14 standard are requested to compile the project’s targets. We also opt to not request compiler specific extensions.

set(BUILD_SHARED_LIBS ON)

The BUILD_SHARED_LIBS variable is turned on, which means a shared library is always produced if we do not pass a library type to the add_library command.

add_library(interp interpolate.h interpolate.cpp)

The interpolate.h and interpolate.cpp source files are compiled into a shared library called interp. CMake will use the host operating system's preferred naming scheme for shared libraries. For example, a shared library called libinterp.so will be generated on a Linux operating system.

add_executable(main main.cpp)
target_link_libraries(main interp)

The main.cpp file is compiled into an executable file called main, which subsequently links to the interp shared library

Take some time to read the comments in each source file to get a better understanding of our initial setup. We will take a look at how to build and run the program next.

1. Inspecting the Default Compiler

When CMake is invoked to read a configuration file, temporary files are generated, including a cache and a CMakeFiles directory containing information specific to the environment. CMake uses this information to select the most appropriate compiler for the project.

Let’s produce these files by creating a build directory within our sample project and invoking CMake to read the configuration file:

$ cd cmake-compiler-flags
$ mkdir build && cd build
$ cmake ..
-| The CXX compiler identification is GNU 10.2.0
... Check for working CXX compiler: /usr/bin/c++ - skipped

Information about the selected compiler is output when the configuration file is read. The compiler ID and version number complete the first line of output. The compiler’s location on the file system is also output a few lines down. In my case, GCC version 10.2.0 is selected, and is located at /usr/bin/c++.

These values may be different on your own system. For example, the Clang compiler will probably be selected on Mac OS with a corresponding binary named clang++. We will take a look at ways to switch the compiler in a following section.

It’s worth mentioning that your file system may have multiple copies of the same compiler mapped to different names. For example, common binary names for the GCC compiler include c++, gcc and g++. This may happen when you download a Linux distribution's development group which contains various packages to facilitate development.

If you are unsure about which compilers you have on your system, I recommend passing the --version and --help flags to the binaries in question:

# A path is returned if the compiler is installed on your system
$ which c++ g++ gcc
# Check compiler name and version
$ c++ --version
$ gcc --version
$ g++ --version
# Review the help listing
$ g++ --help | less
# Compare file size
$ ls -l $(which c++ g++ gcc)

Moving forward, it is also important to know which flags are passed to the compiler when targets are built. Compiler flags will also vary depending on the selected build type.

For example, a debug build will likely generate artifacts containing debugging symbols and less-optimized assembly code. Conversely, a release build usually strips out all debugging information and generates optimized assembly code to improve a program’s performance. The compiler must receive appropriate flags in order to satisfy a particular build type.

For now, we need to know that all build types use compiler flags defined in the CMAKE_CXX_FLAGS variable. Projects that haven't selected a build type, such as our own, will pass the flags defined in this variable to the compiler. It is defined as an empty string by default, and flags are appended either through the cache interface or by modifying CMakeLists.txt. Another important variable to be aware of is CMAKE_CXX_COMPILER, which stores the path to the selected compiler.

Before we compile our sample project, let’s inspect its cache file to confirm the presence of CMAKE_CXX_FLAGS in addition to which compiler is selected:

# Open cache in build directory
$ ccmake .

The ccmake program presents a CLI which enables us to interact with our project's cache file. The very top variable called CMAKE_BUILD_TYPE is an empty string, meaning our project has not selected a build type yet. The variables we are interested in are revealed after pressing the t key to toggle advance mode — CMAKE_CXX_COMPILER and CMAKE_CXX_FLAGS will now be displayed. When you are finished inspecting the cache, press q to exit:

Inspecting variables in the project’s cache.

Now we know which compiler is selected and the (lack of) flags it will receive, let’s build the project and run our program:

# Build project
$ cmake --build .
# Run program
$ ./main
-| Begin...
Result 0: 1 ...

2. Selecting a Compiler and Inspecting its Properties

This section will cover a few useful techniques for finding information about the compiler and outputting its properties. We will firstly take a look at the CMake --system-information flag, which dumps detailed information about your system either to the screen or a file. Let's try it out in the sample project's root directory:

$ cmake --system-information information.txt

From here we can open information.txt in a text editor such as Vim and search for specific information about our environment. Go ahead and search for the following variables to check which values are assigned by default:

  • CMAKE_CXX_COMPILER_LOADED: True if C++ was enabled for the project.
  • CMAKE_CXX_COMPILER_ID: The unique compiler identification string.
  • CMAKE_COMPILER_IS_GNUCXX: True if the C++ compiler is part of GCC.
  • CMAKE_CXX_COMPILER_VERSION: A string of the C++ compiler version.
  • CMAKE_CXX_COMPILER: Path to the selected C++ compiler.
  • CMAKE_C_COMPILER: Path to the selected C compiler.

Values given to each variable in information.txt will vary depending on the host operating system. Also note that identical variables are defined for other CMake supported languages. For example, C language variable names specify C instead of CXX.

All variables included in this file can be referenced inside CMakeLists.txt and even sent to your application to facilitate conditional compiling.

Inspecting system information generated by CMake.

Selecting Another Compiler

Let’s discuss how to select another C++ compiler to build a project. Perhaps you are a fan of the Clang/LLVM compiler and would prefer to use it instead of GCC. As you may have discovered, CMake stores the path of the selected compiler inside a variable called CMAKE_CXX_COMPILER. This variable can be set in two ways:

  • Using a switch on the command line:
    cmake -DCMAKE_CXX_COMPILER=<compiler name> ..
  • Using an environment variable:
    export CXX=<compiler name>

If you would like the --system-information flag to detect another compiler, you will need to export an environment variable called CXX:

# Create CXX environment variable
$ export CXX=clang++
$ echo $CXX
-| clang++
# Overwrite information.txt with updated system information
$ cmake --system-information information.txt

Clang will now be selected if CMake is re-invoked to read CMakeLists.txt. Alternatively, a compiler can be specified on the command line by explicitly setting CMAKE_CXX_COMPILER. Doing so would take precedence over any value stored in the CXX environment variable:

# Enter build directory and remove all files (clean)
$ cd build && rm -fr *
# Read configuration and set g++ compiler
$ cmake -DCMAKE_CXX_COMPILER=clang++ ..
-| The CXX compiler identification is Clang 11.0.0
... Check for working CXX compiler: /usr/bin/clang++ - skipped

At this point, the sample project can be built and run in the same way as before. However, the compilation and linking phases will be handled by Clang instead of GCC:

$ cmake --build .
$ ./main
-| Begin...
Result 0: 1 ...

Outputting Compiler Information at Configuration Time

Additional information can be sent to standard output when CMake is invoked to read the configuration file. For example, an application may have a configuration option to control whether it will link to shared libraries or static libraries at compile time. It would be nice to see a visual confirmation of this setting at configuration time before we commit to the build process.

With this in mind, we will update our configuration file to output information about the selected compiler. As a matter of fact, we will reference the same variables that we searched for in the information.txt file earlier on. Add the following code to CMakeLists.txt to see this in action:

   set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)>> if(CMAKE_CXX_COMPILER_LOADED)
message(STATUS "Compiler path: ${CMAKE_CXX_COMPILER}")
message(STATUS "Compiler ID: ${CMAKE_CXX_COMPILER_ID}")
message(STATUS "Compiler version:
${CMAKE_CXX_COMPILER_VERSION}")
message(STATUS "Compiler is part of GCC:
${CMAKE_COMPILER_IS_GNUCXX}")
endif()

Our configuration firstly checks if a C++ compiler is enabled via the CMAKE_CXX_COMPILER_LOADED variable. If a compiler is enabled, the message command is called for each variable we wish to log. Notice that CMake variables are referenced inside a string by specifying a dollar sign followed by curly braces around the variable name.

Now our messages are output whenever CMake is invoked to read CMakeLists.txt:

$ cmake ..
-| Compiler path: /usr/bin/clang++
Compiler ID: Clang
Compiler version: 11.0.0
Compiler is part of GCC: ...

Outputting Compiler Information at Runtime

It is also possible to access CMake variables in an application’s source code. This is achieved by providing CMake a configuration file that contains preprocessor definitions corresponding to variables we’d like to expose to the application. CMake will process this file and output a regular C++ header file that our application can include.

Having an executable output compiler information is a good feature for a debug build. However, we won’t want to do this in a release build for obvious reasons. To solve this issue, we will define an option in CMakeLists.txt that will instruct the application to either compile the output code or not:

   set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)

>> option(OUTPUT_COMPILER_INFO
"Output compiler information when launching the main
executable." ON)

Another issue we face is the behavior of the CMAKE_COMPILER_IS_GNUCXX variable. Because CMAKE_COMPILER_IS_GNUCXX either contains the value 1 or no value at all, mapping it to a preprocessor definition is error prone. Instead, we will set up a new variable called CXX_COMPILER_IS_GNU and assign it either 1 or 0 depending on whether CMAKE_COMPILER_IS_GNUCXX contains a value:

   option(OUTPUT_COMPILER_INFO "..." ON)>> if(CMAKE_COMPILER_IS_GNUCXX MATCHES 1)
set(CXX_COMPILER_IS_GNU 1)
else()
set(CXX_COMPILER_IS_GNU 0)
endif()

Let’s now turn our focus to generating a header file that contains definitions to corresponding variables in our configuration. This is done by calling the configure_file command using the following syntax:

configure_file(<input> <output>)

Similar to a template, the input file contains placeholders for variables defined in CMakeLists.txt. The syntax for declaring a variable placeholder is @VARIABLE_NAME@. An output file will copy the input file and set the placeholders to actual values defined in our build configuration. Let's firstly create an input file in the sample project's root directory called config.h.in and add some definitions within it:

#pragma once
#define COMPILER_LOADED @CMAKE_CXX_COMPILER_LOADED@
#define COMPILER_IS_GNU @CXX_COMPILER_IS_GNU@
#define COMPILER_NAME "@CMAKE_CXX_COMPILER@"
#define COMPILER_ID "@CMAKE_CXX_COMPILER_ID@"
#define COMPILER_VERSION "@CMAKE_CXX_COMPILER_VERSION@"
#cmakedefine OUTPUT_COMPILER_INFO

We proceed by making a call to configure_file in our configuration file. Immediately after, a call to target_include_directories specifies the location of where config.h will be generated such that main can find it on the file system:

   target_link_libraries(main interp)

>> # generate configuration file
configure_file(config.h.in config.h @ONLY)
# include binary directory to find config.h
target_include_directories(main PRIVATE ${PROJECT_BINARY_DIR})
  • @ONLY specifies that placeholders can be declared only with the @ symbol.
  • The PRIVATE keyword means that only the main target will look in the binary directory for include files. Any target that links to main will not search directories listed in this call.

Lastly, update main.cpp to include config.h. The preprocessor definitions defined in config.h can now be referenced within the application to do things like conditional compiling and debugging. In our case, we define a function that outputs properties of the selected compiler, and subsequently call it in the main function. Additionally, OUTPUT_COMPILER_INFO is used to perform conditional compiling - if it contains a false value, our new code is not compiled:

Updated program that outputs compiler information.

With our new build configuration complete, let’s see what happens when we select another compiler:

$ cd build && rm -fr *
$ cmake -DCMAKE_CXX_COMPILER=g++ ..
$ cmake --build .
$ ./main
-| Compiler name: /usr/bin/g++
Compiler ID: GNU
Compiler version: 10.2.0
GCC compiler: 1

Furthermore, if you set OUTPUT_COMPILER_INFO to OFF in the cache and rebuild, our output function will not compile.

3. Inspecting the Default Build Types

This section will focus on inspecting build types and their corresponding compiler flags. The CMake BUILD_TYPE variable specifies which build type configuration is selected at build time, and is empty by default. When a build type is not selected for a project, the compiler will only receive flags defined in the CMAKE_CXX_FLAGS variable.

It is therefore a good idea to select a build type before compiling a project. CMake includes four build types out-of-the-box which are:

  • Debug: Fast compilation times, no optimization, debugging symbols are maintained.
  • Release: Compiles with a high level of optimization. Prioritizes runtime performance over compilation time.
  • RelWithDebugInfo: Compiles with a good level of optimization, debugging symbols are maintained.
  • MinSizeRel: Compiles with optimization that prioritizes executable file size and speed over compilation time.

For each build type mentioned above, CMake creates a corresponding variable to store its compiler flags. The variable names along with their flags are detailed below:

  • CMAKE_CXX_FLAGS_DEBUG: -g
  • CMAKE_CXX_FLAGS_MINSIZEREL: -Os -DNDEBUG
  • CMAKE_CXX_FLAGS_RELEASE: -O3 -DNDEBUG
  • CMAKE_CXX_FLAGS_RELWITHDEBUGINFO: -O2 -g -DNDEBUG

Optimization flags increases both compilation time and performance of the generated code. The following table describes each compiler flag mentioned above:

Compiler flags used by CMakes build types.

You should now have a good idea of which build type to adopt at certain stages of development. A debug build would be selected If you are implementing new features or analyzing runtime behavior for bottlenecks. However, a release build would be adopted if your product is on the verge of being feature-complete and you need to measure the application’s performance in a real-world environment.

Build type variables and their corresponding compiler flags can be accessed from within the cache. Toggle advanced mode after launching either ccmake or cmake-gui to check them out.

Selecting a Build Type

Selecting a build type can be done in a number of ways. One method is to explicitly set the CMAKE_BUILD_TYPE variable in your configuration:

   set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)>> if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()

This code sets the build type to Release providing a value for CMAKE_BUILD_TYPE is not passed on the command line. This means the project will always inherit a built type when it is configured. Keep in mind that a new value can always be set via the cache interface before starting the build process.

Another method usually adopted to set the CMAKE_BUILD_TYPE variable is via the command line at configuration time:

$ cmake -D CMAKE_BUILD_TYPE=Debug ..

In this case, the Debug configuration is selected instead of a Release.

To demonstrate build types in action, some code can be added to main.cpp to conditionally compile some code. We will output a message that will change depending on whether the debug build type is selected or not. Remember the -NDEBUG symbol that the release build types define? We will use that to determine which build type is selected.

Let’s proceed by adding some code to main.cpp:

>> #ifdef NDEBUG
std::cout << "NDEBUG Defined (Release)"
<< std::endl << std::endl;
#else
std::cout << "NDEBUG Not Defined (Debug)"
<< std::endl << std::endl;
#endif
std::cout << "Begin..." << std::endl;

Test out this new functionality by doing a few more builds of the sample project, making sure to select a different build type and compiler combination each time:

# Clean generated files
$ cmake --build . --target clean
# Configure a debug build
$ cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=clang++ ..
$ cmake --build .
# Run exectuable
$ ./main
-| Compiler name: /usr/bin/clang++
...
NDEBUG Defined (Release)

4. Conditionally Adding Compiler Flags

Even though many compiler flags work with both GCC and Clang, there could be times were you want to use a flag that is only available for a single compiler. For example, a developer who is compiling with GCC on a Linux system may want to use additional GCC specific flags to optimize the program even more. The same scenario may happen when the Clang compiler is adopted for a project.

It would also be nice to accumulate a set of flags for each build type our project supports. To facilitate this functionality, our solution firstly creates separate variables to store global (all build types), debug, and release flags:

     message(STATUS "Compiler is part of GCC: ${ ... }")
endif()
>> set(CXX_FLAGS)
set(CXX_FLAGS_DEBUG)
set(CXX_FLAGS_RELEASE)

A couple of flags are immediately appended to CXX_FLAGS in order to compile all targets with position independent code, and to enable the recommended compiler warnings:

   set(CXX_FLAGS_RELEASE)>> list(APPEND CXX_FLAGS "-fPIC" "-Wall")

From here we construct a couple of if statements that check which compiler is selected at build time. Our configuration checks for a GNU compiler as well as the Clang compiler. Depending on the selected compiler, appropriate flags are appended to our variables:

   list(APPEND CXX_FLAGS "-fPIC" "-Wall")

>> if(CMAKE_CXX_COMPILER_ID MATCHES GNU)
list(APPEND CXX_FLAGS "-fno-rtti" "-fno-exceptions")
list(APPEND CXX_FLAGS_DEBUG "-Wsuggest-final-types"
"-Wsuggest-final-methods" "-Wsuggest-override")
list(APPEND CXX_FLAGS_RELEASE "-O3" "-Wno-unused")
endif()
if(CMAKE_CXX_COMPILER_ID MATCHES Clang)
list(APPEND CXX_FLAGS "-fno-rtti" "-fno-exceptions"
"-Qunused-arguments" "-fcolor-diagnostics")
list(APPEND CXX_FLAGS_DEBUG "-Wdocumentation")
list(APPEND CXX_FLAGS_RELEASE "-O3" "-Wno-unused")
endif()

Note that additional if statements can be added to check for other compilers your project wishes to support.

Moving forward, a call to target_compile_options will let us assign compiler flags to a given target. We firstly instruct CMake to compile the interp shared library using compiler flags defined in CXX_FLAGS:

>> target_compile_options(interp PRIVATE ${CXX_FLAGS})   add_executable(main main.cpp)

On top of the global compiler flags, generator expressions are used to assign either the debug or release compiler flags to the main target:

>> target_compile_options(main PRIVATE ${CXX_FLAGS}
"$<$<CONFIG:Debug>:${CXX_FLAGS_DEBUG}>"
"$<$<CONFIG:Release>:${CXX_FLAGS_RELEASE}>")
target_link_libraries(main interp)

The CONFIG generator expression returns the selected build type. We use it here to assign correct compiler flags based on the selected build type. For example, assuming CONFIG returns Debug, $<CONFIG:Debug> will expand to $<Debug:Debug>, resulting in a true value. In this case, compiler flags defined in CXX_FLAGS_DEBUG are used. On the other hand, CMake will select the flags defined in CXX_FLAGS_RELEASED if CONFIG returns Release.

It is also important to know that compile options can be added with three levels of visibility:

  • PRIVATE: Options will only be applied to the given target and not to other targets consuming it.
  • INTERFACE: Options on a given target will only be applied to targets consuming it.
  • PUBLIC: Options will be applied to the given target and all other targets consuming it.

Our configuration specifies the PRIVATE attribute, which means consuming targets will not inherit the consumed target's compiler flags.

Build your sample project a few times again, but this time append -- VERBOSE=1 at the end of the build command. This option will enable us to confirm which compiler flags were received by the compiler:

$ cd build && rm -fr *
$ cmake -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Debug ..
$ cmake --build . -- VERBOSE=1

You will notice that CMake compiles the shared library and executable using different compiler flags. The selected build type also influences which flags are passed to the compiler.

Outputting Selected Compiler Flags

Confirming which compiler flags a build receives via the verbose option on the command line is not very efficient. A better option would be to output the flags at configuration time. With this in mind, we will use the CMakePrintHelpers module to output our variables.

Update your configuration file to output compiler flags just before the shared library is built:

>> include(CMakePrintHelpers)
cmake_print_variables(CXX_FLAGS)
cmake_print_variables(CXX_FLAGS_DEBUG)
cmake_print_variables(CXX_FLAGS_RELEASE)

add_library(interp interpolate.h interpolate.cpp)

We include the CMakePrintHelpers module and immediately call cmake_print_variables to output the contents of each variable on a single line. It is also possible to specify multiple variables in a single call.

The manual page for any CMake module can be accessed on the command line via the --help-module option. For example, the following command opens a manual page for the CMakePrintHelpers module:

$ cmake --help-module CMakePrintHelpers

Selected flags are now output for a particular build at configuration time:

Building a release configuration of our project.

In Conclusion

I hope this tutorial has provided enough information for you to configure your own development projects with confidence when it comes to the compiler. I encourage you to experiment further with the sample project and perform some more conditional compilation based on the build type and compiler selected for a particular build.

Related Articles

Other articles I have written that focus on developing with CMake:

--

--