CMake: How to Inspect and Configure the Compiler
By: Dane Bulat
Tags: Linux Tutorial, Linux, Cmake, Programming, Compilers

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.

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