CMake: How to Inspect and Configure the Compiler
A tutorial on using CMake to configure compilers and their flags for development projects
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:
- 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. - 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. - Inspecting the Default Build Types
The default compiler flags CMake uses for its build configurations are identified and discussed. - 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
andinterpolate.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 calledinterp
.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 theinterp
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:
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.
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 themain
target will look in the binary directory for include files. Any target that links tomain
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:
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:
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:
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: