Stop Using the Default Compiler? CMake Compiler Configuration Enables Code Compilation Anywhere!

Click the blue textStop Using the Default Compiler? CMake Compiler Configuration Enables Code Compilation Anywhere!Follow the authorStop Using the Default Compiler? CMake Compiler Configuration Enables Code Compilation Anywhere!

1. Background Introduction

CMake compiler configuration not only relates to performance but also directly affects the maintainability and portability of the code. By explicitly specifying the compiler and its options, you can ensure consistent behavior of the code across different platforms and reduce maintenance costs.

Limitations of using the default compiler:

  1. The default compiler in CMake typically relies on the pre-defined compiler path in the system environment variables, which often points to the system’s built-in compiler version. These versions may be relatively outdated, failing to fully utilize the optimization features and language standard support provided by the latest compilers. For example, lacking support for new features in C++20 or C++23, or having efficiency bottlenecks in code generation.
  2. In complex project environments, relying on the system’s default compiler may lead to compatibility issues with other libraries or toolchains. Different compiler versions may have ABI incompatibilities, leading to linking errors or runtime crashes.

Limitations of default compiler configuration:

  1. The default compiler configuration does not automatically enable certain critical compilation options, such as LTO or PGO.
  2. For GPU-accelerated applications, the default compiler does not automatically select CUDA or OpenCL compilers and may lack optimizations for specific GPU architectures, resulting in inefficient computation.
  3. The default compiler configuration cannot handle certain special compilation requirements, such as cross-compilation or embedded development.
  4. The default configuration does not enable sufficient debug information generation options, making the debugging process difficult. Detailed debug information is crucial for locating complex issues in large projects.

The importance of custom compiler configuration:

  1. By customizing the CMake compiler configuration, you can select the latest version of the compiler and enable various optimization options (such as <span>-O3</span>, <span>-flto</span>, <span>-march=native</span>), significantly improving compilation speed and program execution efficiency. Precise optimization configurations can maximize hardware resource utilization and shorten program runtime.
  2. Custom compiler configurations can enable stricter warning levels (<span>-Wall</span>, <span>-Wextra</span>, <span>-Werror</span>), allowing early detection of potential code defects and programming errors, avoiding runtime crashes and logical errors, thus improving code quality and reliability.
  3. By customizing the CMake compiler configuration, you can easily achieve cross-platform compilation, supporting different operating systems (Windows, Linux, macOS) and architectures (x86, ARM, RISC-V). This ensures consistent behavior of the code across different platforms, avoiding platform-related errors.

This article will comprehensively introduce the best practices for CMake compiler configuration, building high-quality, portable applications.

2. CMake Compiler Variables

CMake has several key variables used to control the selection and configuration of the compiler.

<span>CMAKE_C_COMPILER</span>: C language compiler. Specifies the full path to the executable file of the compiler used to compile C language source code. If this variable is not explicitly set, CMake will automatically search for a C compiler in the system.

  • You can use the <span>find_program()</span> command to find the C compiler and assign the result to <span>CMAKE_C_COMPILER</span>.
  • When cross-compiling, this variable must be explicitly set to the C compiler for the target platform.

<span>CMAKE_CXX_COMPILER</span>: C++ language compiler. Specifies the full path to the executable file of the compiler used to compile C++ language source code. Similar to <span>CMAKE_C_COMPILER</span>, if not explicitly set, CMake will automatically search for a C++ compiler.

  • It is recommended to use the <span>find_program()</span> command to find the C++ compiler and assign it to this variable.
  • In C++ projects, ensure this variable points to the correct C++ compiler.

<span>CMAKE_Fortran_COMPILER</span>: Fortran language compiler. Specifies the full path to the executable file of the compiler used to compile Fortran language source code. This variable only needs to be set if the project includes Fortran code.

<span>CMAKE_C_FLAGS</span>: C language compiler options. Specifies the command-line options passed to the C compiler. Any valid C compiler options, such as warning levels, optimization levels, debug options, etc., can be added to this variable.

<span>CMAKE_CXX_FLAGS</span>: C++ language compiler options. Specifies the command-line options passed to the C++ compiler. Similar to <span>CMAKE_C_FLAGS</span>, but for the C++ compiler. Options such as <span>-std=c++11</span>, <span>-std=c++14</span>, <span>-std=c++17</span>, <span>-std=c++20</span> can be used to specify the C++ language standard.

<span>CMAKE_BUILD_TYPE</span>: Build type. Specifies the build type, affecting the optimization level of the compiler and the generation of debug information. Users can override this variable’s value by specifying <span>-DCMAKE_BUILD_TYPE=<build_type></span> on the command line.Common values:

  • <span>Debug</span>: Includes debug information, no optimization.
  • <span>Release</span>: Optimized, no debug information.
  • <span>RelWithDebInfo</span>: Optimized, includes debug information.
  • <span>MinSizeRel</span>: Optimized for code size.

Advanced usage:

  • Use <span>CMAKE_<LANG>_FLAGS_<CONFIG></span> for finer control: You can set different compiler options for different build types (e.g., Debug, Release).

  • Use <span>target_compile_options()</span> command: Set compiler options for a specific target (e.g., executable or library) instead of globally. This method is more flexible and can avoid affecting the builds of other targets.

  • CMake variable-defined compiler options affect globally, while options defined by the <span>target_compile_options()</span><code><span> command override global settings. Command-line specified options have the highest priority.</span>

  • Be cautious with <span>-march=native</span>: This option optimizes the compiler for the current machine’s CPU, which may cause the built program to be unable to run or perform poorly on other CPUs. Use with caution.

  • For large projects, precompiled headers can be used to speed up compilation. CMake provides some built-in macros to support PCH.

Variable lookup order: When CMake needs to determine the value of a variable (e.g., <span>CMAKE_CXX_COMPILER</span> or <span>CMAKE_CXX_FLAGS</span>), it will look for different sources in a specific order:

  • Command line parameters (<span>-D</span> option): Variables passed on the command line using <span>-D<variable>=<value></span>.
  • Environment variables: Environment variables set in the operating system. CMake reads some specific environment variables, such as <span>CC</span> (C compiler), <span>CXX</span> (C++ compiler), etc.
  • CMake cache file (CMakeCache.txt): If the variable already exists in the cache, CMake will prioritize using the value from the cache.
  • CMakeLists.txt file: Variables set using the <span>set()</span> command in the CMakeLists.txt file.
  • CMake default values: If none of the above sources find the variable, CMake will use the built-in default value.

How to use the variables:

  • CMake uses these variables to control the build process.
  • You can reference the values of these variables in the CMakeLists.txt file using <span>${<variable>}</span> syntax.
  • <span>add_compile_options()</span> and <span>target_compile_options()</span> commands will use these variables to set compilation options.

Priority rules: The higher the priority, the sooner its value is used by CMake.

Priority Variable Source Description
1 Command line parameters (<span>-D</span>) Variables explicitly specified on the CMake command line using the <span>-D</span> option have the highest priority. These parameters will override values set in CMake cache files, environment variables, and variables with the same name set in CMakeLists.txt files.
2 CMake cache file If the variable already exists in the <span>CMakeCache.txt</span> file, CMake will prioritize using the value from the cache. Unless explicitly overridden using the <span>-D</span> option, the value in the cache will remain unchanged.
3 Environment variables CMake reads some specific environment variables (e.g., <span>CC</span>, <span>CXX</span>) to determine the compiler. Note: Not all CMake variables can be set via environment variables.
4 CMakeLists.txt file Variables set using the <span>set()</span> command in the CMakeLists.txt file have a lower priority.
5 CMake default values If none of the above sources find the variable, CMake will use the built-in default value.

3. Specifying the Compiler

CMake has the ability to automatically select the appropriate compiler based on the target platform and the selected generator, and can set reasonable default flags for these compilers.

By default, CMake’s automatic selection mechanism can handle simple scenarios, but for projects that require specific compiler versions, customized compilation environments, or cross-compilation, simply relying on CMake’s automatic selection is insufficient. It is necessary to explicitly specify the compiler to ensure controllability and reproducibility of the build process.

By explicitly specifying the compiler, you can also take advantage of the features and optimization options of different compilers, such as using a specific version of GCC for better code optimization or using Clang for static code analysis. Additionally, in multi-platform development, selecting different compilers for different platforms is key to ensuring code correctness and performance.

3.1. How to Operate?

How to choose a specific compiler? For example, if you want to use the Intel compiler, Portland Group compiler, or other compilers not in the default compiler list, what should you do?

As mentioned earlier, CMake stores compiler information for each language in the <span>CMAKE_<LANG>_COMPILER</span> variable. Here, <span><LANG></span> represents the programming languages supported by CMake, such as <span>CXX</span> (C++), <span>C</span> (C), or <span>Fortran</span>. Therefore, you can set this variable to specify the compiler you want to use in two main ways:

(1) Through the CMake command line interface (CLI) using the <span>-D</span> option: This method allows you to directly specify the compiler when running the CMake command. For example:

$ cmake -D CMAKE_CXX_COMPILER=clang++ ..

Where <span>..</span> refers to the parent directory where the CMakeLists.txt file is located.

(2) By exporting environment variables: CMake checks certain environment variables, such as <span>CXX</span> (C++ compiler), <span>CC</span> (C compiler), and <span>FC</span> (Fortran compiler). You can specify the compiler by setting these environment variables. For example:

$ env CXX=clang++ cmake ..

Both methods allow passing appropriate options to configure the suitable compiler. It is important to understand the priority relationship between these methods (as mentioned earlier).

[Important Tip]: CMake can sense the runtime environment and allows setting various options through the CLI’s <span>-D</span> switch or environment variables. When both methods are used simultaneously, the CLI’s <span>-D</span> option will override the settings of the environment variables. Nevertheless, it is still strongly recommended to explicitly set the compiler using the <span>-D</span> option because explicit is better than implicit. Environment variables may be set to values unsuitable for the current project, leading to unexpected build behavior. Explicit specification ensures consistency and predictability in the build process.

[Important Note]: If the compiler is not in the standard path or requires a specific compiler wrapper, you need to pass the full path of the compiler executable or wrapper to CMake. For example:

$ cmake -D CMAKE_CXX_COMPILER=/opt/intel/bin/icpc ..

[Best Practice]: It is strongly recommended to set the compiler using the <span>-D CMAKE_<LANG>_COMPILER</span> option instead of exporting <span>CXX</span>, <span>CC</span>, and <span>FC</span> environment variables. This is the only way to ensure cross-platform compatibility and better collaboration with non-POSIX compliant systems. Additionally, using the <span>-D</span> option can prevent environment variable pollution in the build environment, thus avoiding impacts on the build behavior of external libraries built with the project. Environment variables may interfere with the build process of external libraries, while using the <span>-D</span> option can isolate the choice of compiler, thus avoiding these issues.

In addition to these two methods, you can also specify the compiler using the <span>set()</span> command in the CMakeLists.txt file: <span>set(CMAKE_CXX_COMPILER /usr/bin/clang++)</span>. The downside is that it requires modifying the CMakeLists.txt file.

3.2. Default Compiler Selection Strategy

During the configuration phase, CMake performs a series of platform tests aimed at identifying the available compilers on the system and assessing their suitability for the current project build requirements. The so-called “suitable compiler” depends not only on the target platform of the project (e.g., Windows, Linux, or macOS) but also closely relates to the selected generator (e.g., Unix Makefiles, Ninja, or Visual Studio).

The first key test performed by CMake is based on the compiler name for the programming language used in the project. For example, if the <span>cc</span> command successfully compiles C code, CMake will consider it a usable C compiler and set it as the default compiler for C projects. In fact, CMake searches a predefined list of compiler names and attempts to execute simple compilation tests to verify their availability.

Default compiler selection strategies for different platforms and generators are as follows:

  • GNU/Linux: When using Unix Makefiles or Ninja as the generator, the GCC compiler family typically becomes the default choice for C++, C, and Fortran projects. The specific choice of <span>g++</span>, <span>gcc</span>, and <span>gfortran</span> depends on the actual versions and configurations installed on the system.

  • Microsoft Windows: If Visual Studio is chosen as the generator, CMake typically selects the C++ and C compilers (cl.exe) that come with Visual Studio. Since Visual Studio is the mainstream development environment on the Windows platform, its compiler integrates well with Visual Studio’s IDE and debugger.

  • MinGW/MSYS: If MinGW or MSYS Makefiles are chosen as the generator, the MinGW compiler is used by default. MinGW (Minimalist GNU for Windows) provides a set of GNU toolchains for building C/C++ applications on the Windows platform. MSYS (Minimal System) provides a POSIX-compatible shell environment, facilitating the use of GNU tools on Windows.

Note that CMake’s compiler selection strategy is customizable. By explicitly setting the <span>CMAKE_<LANG>_COMPILER</span> variable, you can force CMake to use a specified compiler, ignoring its default selection strategy. This is very suitable for situations that require using a specific version of the compiler, cross-compilation, or using non-standard compilers.

3.3. More Content

CMake provides the <span>--system-information</span> flag, which can output all CMake-related information about the system to the screen or a file. This is useful for diagnosing build issues, understanding CMake’s configuration, and viewing default compiler settings.

$ cmake --system-information information.txt

This command will output detailed system information to a file named <span>information.txt</span>. In this file, you can find the default values of variables such as <span>CMAKE_CXX_COMPILER</span>, <span>CMAKE_C_COMPILER</span>, and <span>CMAKE_Fortran_COMPILER</span>, which indicate the CMake-selected C++, C, and Fortran compilers, respectively. Additionally, the file contains default compiler flags and many other useful information (which will be seen in subsequent chapters regarding the usage of these flags).

CMake also provides a series of additional variables for interacting with the compiler and obtaining information about the compiler:

  • <span>CMAKE_<LANG>_COMPILER_LOADED</span>: If the <span><LANG></span> language (e.g., <span>C</span>, <span>CXX</span>, or <span>Fortran</span>) is enabled for the project, this variable will be set to <span>TRUE</span>. This can be used to check whether the compiler for a specific language has been successfully loaded.

  • <span>CMAKE_<LANG>_COMPILER_ID</span>: This is a compiler identification string used to indicate the vendor of the compiler. Some common compiler IDs include:

    Note that not all compilers or languages guarantee the definition of this variable. Therefore, it is best to check for its existence before using this variable.

    • <span>GNU</span>: For the GNU Compiler Collection (GCC).
    • <span>AppleClang</span>: For the Clang compiler on macOS.
    • <span>MSVC</span>: For the Microsoft Visual Studio compiler.
  • <span>CMAKE_COMPILER_IS_GNU<LANG></span>: If the compiler for the <span><LANG></span> language is part of the GNU Compiler Collection, this logical variable is set to <span>TRUE</span>. The <span><LANG></span> part of the variable name follows GNU conventions:

    • <span>CC</span>: For the C language.
    • <span>CXX</span>: For the C++ language.
    • <span>G77</span>: For the Fortran language. (Note: Although the variable name is <span>G77</span>, it actually applies to newer Fortran compilers as well.)
  • <span>CMAKE_<LANG>_COMPILER_VERSION</span>: This variable contains a string indicating the version of the compiler for the given language. The version information format is usually <span>major[.minor[.patch[.tweak]]]</span>. For example, <span>8.1.0</span> or <span>12.2.1</span>. Similar to <span>CMAKE_<LANG>_COMPILER_ID</span>, not all compilers or languages guarantee the definition of this variable.

To demonstrate how to use these variables, you can try using different compiler configurations in the following example <span>CMakeLists.txt</span> file.

cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-08-compiler LANGUAGES C CXX)

message(STATUS "Is the C++ compiler loaded? ${CMAKE_CXX_COMPILER_LOADED}")
if(CMAKE_CXX_COMPILER_LOADED)
    message(STATUS "The C++ compiler ID is: ${CMAKE_CXX_COMPILER_ID}")
    message(STATUS "Is the C++ from GNU? ${CMAKE_COMPILER_IS_GNUCXX}")
    message(STATUS "The C++ compiler version is: ${CMAKE_CXX_COMPILER_VERSION}")
endif()

message(STATUS "Is the C compiler loaded? ${CMAKE_C_COMPILER_LOADED}")
if(CMAKE_C_COMPILER_LOADED)
    message(STATUS "The C compiler ID is: ${CMAKE_C_COMPILER_ID}")
    message(STATUS "Is the C from GNU? ${CMAKE_COMPILER_IS_GNUCC}")
    message(STATUS "The C compiler version is: ${CMAKE_C_COMPILER_VERSION}")
endif()

This example does not include any targets and will not build anything. The focus is entirely on the configuration step, i.e., how CMake recognizes and configures the compiler.

Run this example:

$ mkdir -p build
$ cd build
$ cmake ..

Output:

-- The C compiler identification is GNU 11.4.0
-- The CXX compiler identification is GNU 11.4.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Is the C++ compiler loaded? 1
-- The C++ compiler ID is: GNU
-- Is the C++ from GNU? 1
-- The C++ compiler version is: 11.4.0
-- Is the C compiler loaded? 1
-- The C compiler ID is: GNU
-- Is the C from GNU? 1
-- The C compiler version is: 11.4.0
-- Configuring done
-- Generating done

By modifying the <span>CMAKE_CXX_COMPILER</span> and <span>CMAKE_C_COMPILER</span> variables, you can try using different compilers and observe the changes in the output.

Additionally, you can select compilers based on the platform: Use the <span>if()</span> statement and the <span>CMAKE_SYSTEM_NAME</span> variable. For example:

if (CMAKE_SYSTEM_NAME MATCHES "Windows")
    set(CMAKE_CXX_COMPILER "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.34.31931/bin/Hostx64/x64/cl.exe")
elseif(CMAKE_SYSTEM_NAME MATCHES "Linux")
    set(CMAKE_CXX_COMPILER /usr/bin/g++)
endif()

4. Setting Compiler Options

We have previously learned how to probe CMake for compiler information and how to switch compilers in a project. Now, we will explore how to control the compiler flags for the project, which is a crucial aspect of the build process that directly affects the performance, size, and compatibility of the final executable or library.

CMake provides great flexibility to adjust or extend compiler flags through two main methods:

  • Treat compiler options as target properties. This means you can set different compiler options for each target (e.g., executable or library) without changing CMake’s global default settings. The advantage is that it allows different optimization levels, warning levels, or language standards for different targets, and does not affect the compilation options of other targets in the project, making the codebase more modular and maintainable.
  • **Use the <span>-DCLI</span> flag to modify <span>CMAKE_<LANG>_FLAGS_<CONFIG></span>**. These variables control global compiler flags that will affect all targets in the project; use with caution.

Which method to choose?

  • It is recommended to prioritize the target property-based method. This method is more flexible, maintainable, and can avoid conflicts that may arise from global variables.
  • Only use the <span>-DCLI</span> flag to modify <span>CMAKE_<LANG>_FLAGS_<CONFIG></span> variables when you need to quickly set global compilation options or override CMake’s default behavior.

4.1. Specific Steps

Create a geometry library (to calculate the area of different geometric shapes) to demonstrate how to set different compiler options in CMake for different targets.

The area calculation logic for different geometric shapes is spread across multiple independent files. The project includes functions for calculating the area of multiple geometric shapes, each with its corresponding header file (<span>.hpp</span>) and source file (<span>.cpp</span>). The project’s directory structure is as follows:

recipe_06_foreach/
├── CMakeLists.txt
├── helloWorld.cpp
├── include
│   ├── geometryCircle.hpp
│   ├── geometryPolygon.hpp
│   ├── geometryRhombus.hpp
│   ├── geometrySquare.hpp
│   └── message.hpp
└── src
    ├── geometryCircle.cpp
    ├── geometryPolygon.cpp
    ├── geometryRhombus.cpp
    ├── geometrySquare.cpp
    └── message.cpp

Files to be compiled:

  • 4 header files (<span>*.hpp</span>): These files define the interfaces for the geometric shape classes.
  • 5 source files (<span>*.cpp</span>): These files implement the specific logic of the geometric shape classes, as well as the main program logic in <span>helloWorld.cpp</span>.

To keep the article concise, the complete code will not be listed here. The full code example can be found in the following Git repository:https://gitee.com/long-xu/cmake-learning-code/recipe_08_compiler_option

<span>helloWorld.cpp</span> content:

#include &lt;iostream&gt;
#include &lt;vector&gt;
#include "include/geometryCircle.hpp"
#include "include/geometryPolygon.hpp"
#include "include/geometryRhombus.hpp"
#include "include/geometrySquare.hpp"

int main() 
{
    try {
        geometry::Circle circle(5.0);
        std::cout &lt;&lt; "Circle area: " &lt;&lt; circle.getArea() &lt;&lt; std::endl;

        std::vector&lt;std::pair&lt;double, double&gt;&gt; polygon_vertices = {
            {0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.0, 1.0}
        };
        geometry::Polygon polygon(polygon_vertices);
        std::cout &lt;&lt; "Polygon area: " &lt;&lt; polygon.getArea() &lt;&lt; std::endl;

        geometry::Rhombus rhombus(6.0, 8.0);
        std::cout &lt;&lt; "Rhombus area: " &lt;&lt; rhombus.getArea() &lt;&lt; std::endl;

        geometry::Square square(4.0);
        std::cout &lt;&lt; "Square area: " &lt;&lt; square.getArea() &lt;&lt; std::endl;
    } catch (const std::exception&amp; e) {
        std::cerr &lt;&lt; "Error: " &lt;&lt; e.what() &lt;&lt; std::endl;
        return 1;
    }

    return 0;
}

Now that we have the source code, the next step is to configure the CMake project and start experimenting with compiler flags. The goal is to gain a deeper understanding of how to leverage CMake’s capabilities to control the compilation process and customize compiler flags as needed.

(1) Specify the minimum required CMake version for the project and declare the project name and programming language used.

cmake_minimum_required(VERSION 3.10 FATAL_ERROR)
project(recipe-08 LANGUAGES CXX)

(2) Print the current compiler flags. First, print the default flags currently used by CMake to understand CMake’s default behavior.

message("C++ compiler flags: ${CMAKE_CXX_FLAGS}")

(3) Prepare a list of flags to set the compilation options. The <span>-fPIC</span> flag generates position-independent code, typically used for creating shared libraries. The <span>-Wall</span> flag enables all warnings. The <span>-Wextra</span> flag enables additional warnings. The <span>-Wpedantic</span> flag enables strict ANSI/ISO C/C++ standard compliance checks.

(4) Add a static library target named <span>geometry</span> and specify its source files.

add_library(geometry STATIC 
        include/geometryCircle.hpp 
        include/geometrySquare.hpp 
        include/geometryPolygon.hpp
        include/geometryRhombus.hpp
        src/geometryCircle.cpp 
        src/geometrySquare.cpp
        src/geometryPolygon.cpp
        src/geometryRhombus.cpp)

(5) Set the compilation options for the geometry library target.

target_compile_options(geometry
    PRIVATE
    ${flags}
)

(6) Add an executable target named <span>compute-areas</span> and specify its source files.

add_executable(compute-areas compute-areas.cpp)

(7) Set compilation options for the <span>compute-areas</span> executable target.

target_compile_options(compute-areas
    PRIVATE
    "-fPIC"
)

(8) Link the <span>compute-areas</span> executable to the <span>geometry</span> library.

target_link_libraries(compute-areas geometry)

Complete content of CMakeLists.txt:

cmake_minimum_required(VERSION 3.5 FATAL_ERROR) # CMake version check
project(recipe_08 VERSION 0.1 LANGUAGES CXX)

message(STATUS "C++ compiler flag: ${CMAKE_CXX_COMPILER_FLAGS}")

list(APPEND flags "-fPIC" "-Wall")
if (NOT WIN32)
    list(APPEND flags "-Wextra" "-Wpedantic")
endif()

add_library(geometry STATIC 
            include/geometryCircle.hpp 
            include/geometrySquare.hpp 
            include/geometryPolygon.hpp
            include/geometryRhombus.hpp
            src/geometryCircle.cpp 
            src/geometrySquare.cpp
            src/geometryPolygon.cpp
            src/geometryRhombus.cpp)

target_compile_options(geometry PRIVATE ${flags})

add_executable(compute-areas helloWorld.cpp src/message.cpp) # Add message.cpp
target_compile_options(compute-areas PRIVATE "-fPIC")
target_link_libraries(compute-areas geometry)

4.2. Working Process

This example demonstrates how to finely control compiler options in CMake and achieve the passing and isolation of compilation options through target property visibility. The focus is on the <span>target_compile_options()</span> command and its <span>INTERFACE</span>, <span>PUBLIC</span>, and <span>PRIVATE</span> visibility options.

In the example, the <span>geometry</span> target is added with the <span>-Wall</span>, <span>-Wextra</span>, and <span>-Wpedantic</span> warning flags, ensuring that both the <span>geometry</span> and <span>compute-areas</span> targets use the <span>-fPIC</span> flag.

<span>target_compile_options()</span> specifies compilation options and defines the scope of these options’ impact on the target and its dependencies. The three visibility options are defined as follows:

  • <span>PRIVATE</span>: The compilation options apply only to the current target and are not passed to any other targets that depend on this target. In the example, even though <span>compute-areas</span> links to the <span>geometry</span> library, <span>compute-areas</span> will not inherit the compilation options added to the <span>geometry</span> target using <span>PRIVATE</span>. This achieves isolation of compilation options.

  • <span>INTERFACE</span>: The compilation options apply only to other targets that depend on the current target, and do not apply to the current target itself. In other words, the <span>geometry</span> target itself will not be affected by the options added with <span>INTERFACE</span>, but all targets linked to <span>geometry</span>, such as <span>compute-areas</span>, will inherit these options. This is typically used to specify interface requirements, such as certain libraries requiring their dependent targets to use specific predefined macros.

  • <span>PUBLIC</span>: The compilation options apply to both the current target and all other targets that depend on the current target. This is the most commonly used option for setting public compilation requirements for targets and their users.

The visibility of target properties in CMake is one of its core concepts. By controlling compilation options through <span>target_compile_options()</span>, not only can you achieve fine-grained management of compilation options, but you can also better leverage CMake’s advanced features, such as configuring generator expressions. Importantly, this method avoids directly modifying global CMake variables <span>CMAKE_<LANG>_FLAGS_<CONFIG></span>, thus reducing potential conflicts and unpredictability.

How to confirm that these flags are applied correctly to each target as expected? There are two main methods.

One of the most direct methods: through the detailed output of the build system (VERBOSE=1). This can be done by passing additional parameters to the underlying build tool to enable detailed output. The specific steps are as follows:

mkdir -p build
cd build
cmake ..
cmake --build . -- VERBOSE=1

After executing the <span>cmake --build . -- VERBOSE=1</span> command, the build system will output detailed compilation commands, including the full parameters of the compiler calls. By analyzing this output, you can clearly see which compilation options were applied to which source files.

......
[ 12%] Building CXX object CMakeFiles/geometry.dir/src/geometryCircle.cpp.o
/usr/bin/c++   -fPIC -Wall -Wextra -Wpedantic -MD -MT CMakeFiles/geometry.dir/src/geometryCircle.cpp.o -MF CMakeFiles/geometry.dir/src/geometryCircle.cpp.o.d -o CMakeFiles/geometry.dir/src/geometryCircle.cpp.o -c /home/fly/workspace/cmake-learning-code/recipe_08_compiler_option/src/geometryCircle.cpp
[ 25%] Building CXX object CMakeFiles/geometry.dir/src/geometrySquare.cpp.o
/usr/bin/c++   -fPIC -Wall -Wextra -Wpedantic -MD -MT CMakeFiles/geometry.dir/src/geometrySquare.cpp.o -MF CMakeFiles/geometry.dir/src/geometrySquare.cpp.o.d -o CMakeFiles/geometry.dir/src/geometrySquare.cpp.o -c /home/fly/workspace/cmake-learning-code/recipe_08_compiler_option/src/geometrySquare.cpp
[ 37%] Building CXX object CMakeFiles/geometry.dir/src/geometryPolygon.cpp.o
/usr/bin/c++   -fPIC -Wall -Wextra -Wpedantic -MD -MT CMakeFiles/geometry.dir/src/geometryPolygon.cpp.o -MF CMakeFiles/geometry.dir/src/geometryPolygon.cpp.o.d -o CMakeFiles/geometry.dir/src/geometryPolygon.cpp.o -c /home/fly/workspace/cmake-learning-code/recipe_08_compiler_option/src/geometryPolygon.cpp
[ 50%] Building CXX object CMakeFiles/geometry.dir/src/geometryRhombus.cpp.o
/usr/bin/c++   -fPIC -Wall -Wextra -Wpedantic -MD -MT CMakeFiles/geometry.dir/src/geometryRhombus.cpp.o -MF CMakeFiles/geometry.dir/src/geometryRhombus.cpp.o.d -o CMakeFiles/geometry.dir/src/geometryRhombus.cpp.o -c /home/fly/workspace/cmake-learning-code/recipe_08_compiler_option/src/geometryRhombus.cpp
[ 62%] Linking CXX static library libgeometry.a
/usr/bin/cmake -P CMakeFiles/geometry.dir/cmake_clean_target.cmake
/usr/bin/cmake -E cmake_link_script CMakeFiles/geometry.dir/link.txt --verbose=1
/usr/bin/ar qc libgeometry.a CMakeFiles/geometry.dir/src/geometryCircle.cpp.o CMakeFiles/geometry.dir/src/geometrySquare.cpp.o CMakeFiles/geometry.dir/src/geometryPolygon.cpp.o CMakeFiles/geometry.dir/src/geometryRhombus.cpp.o
/usr/bin/ranlib libgeometry.a
gmake[2]: Leaving directory '/home/fly/workspace/cmake-learning-code/recipe_08_compiler_option/build'
[ 62%] Built target geometry
/usr/bin/gmake  -f CMakeFiles/compute-areas.dir/build.make CMakeFiles/compute-areas.dir/depend
gmake[2]: Entering directory '/home/fly/workspace/cmake-learning-code/recipe_08_compiler_option/build'
cd /home/fly/workspace/cmake-learning-code/recipe_08_compiler_option/build &amp;&amp; /usr/bin/cmake -E cmake_depends "Unix Makefiles" /home/fly/workspace/cmake-learning-code/recipe_08_compiler_option /home/fly/workspace/cmake-learning-code/recipe_08_compiler_option /home/fly/workspace/cmake-learning-code/recipe_08_compiler_option/build /home/fly/workspace/cmake-learning-code/recipe_08_compiler_option/build /home/fly/workspace/cmake-learning-code/recipe_08_compiler_option/build/CMakeFiles/compute-areas.dir/DependInfo.cmake --color=
gmake[2]: Leaving directory '/home/fly/workspace/cmake-learning-code/recipe_08_compiler_option/build'
/usr/bin/gmake  -f CMakeFiles/compute-areas.dir/build.make CMakeFiles/compute-areas.dir/build
gmake[2]: Entering directory '/home/fly/workspace/cmake-learning-code/recipe_08_compiler_option/build'
[ 75%] Building CXX object CMakeFiles/compute-areas.dir/helloWorld.cpp.o
/usr/bin/c++   -fPIC -MD -MT CMakeFiles/compute-areas.dir/helloWorld.cpp.o -MF CMakeFiles/compute-areas.dir/helloWorld.cpp.o.d -o CMakeFiles/compute-areas.dir/helloWorld.cpp.o -c /home/fly/workspace/cmake-learning-code/recipe_08_compiler_option/helloWorld.cpp
[ 87%] Building CXX object CMakeFiles/compute-areas.dir/src/message.cpp.o
/usr/bin/c++   -fPIC -MD -MT CMakeFiles/compute-areas.dir/src/message.cpp.o -MF CMakeFiles/compute-areas.dir/src/message.cpp.o.d -o CMakeFiles/compute-areas.dir/src/message.cpp.o -c /home/fly/workspace/cmake-learning-code/recipe_08_compiler_option/src/message.cpp
[100%] Linking CXX executable compute-areas
/usr/bin/cmake -E cmake_link_script CMakeFiles/compute-areas.dir/link.txt --verbose=1
/usr/bin/c++ CMakeFiles/compute-areas.dir/helloWorld.cpp.o CMakeFiles/compute-areas.dir/src/message.cpp.o -o compute-areas  libgeometry.a
gmake[2]: Leaving directory '/home/fly/workspace/cmake-learning-code/recipe_08_compiler_option/build'
[100%] Built target compute-areas
gmake[1]: Leaving directory '/home/fly/workspace/cmake-learning-code/recipe_08_compiler_option/build'
/usr/bin/cmake -E cmake_progress_start /home/fly/workspace/cmake-learning-code/recipe_08_compiler_option/build/CMakeFiles 0

You can see that <span>geometryCircle.cpp</span>, <span>geometryPolygon.cpp</span>, <span>geometryRhombus.cpp</span>, and <span>geometrySquare.cpp</span> all include the <span>-fPIC -Wall -Wextra -Wpedantic</span> flags during compilation, indicating that these flags were successfully added to the <span>geometry</span> target. On the other hand, the compilation command for <span>compute-areas.cpp</span> only includes the <span>-fPIC</span> flag, successfully controlling the passing of compilation options through visibility options.

In addition to modifying the <span>CMakeLists.txt</span> file, you can also override global compilation flags through CMake command line options. This is useful for quickly testing different compilation configurations.

For example, to disable exceptions and runtime type information (RTTI), you can use the following command:

cmake -D CMAKE_CXX_FLAGS="-fno-exceptions -fno-rtti" ..

This command will add <span>-fno-exceptions</span> and <span>-fno-rtti</span> to the compilation commands of all C++ targets. If you want to control the compilation options of specific targets more finely, it is still recommended to use the <span>target_compile_options()</span> command and visibility options.

Note: If you want to control the compilation options of specific targets through global flags, you can use the following command, which will configure the <span>geometry</span> target with <span>-fno-exceptions -fno-rtti -fPIC -Wall -Wextra -Wpedantic</span>, while configuring the <span>compute-areas</span> target with <span>-fno-exceptions -fno-rtti -fPIC -Wall</span>.

cmake -D CMAKE_CXX_FLAGS="-fno-exceptions -fno-rtti" ..

Although it is possible to control compilation options through global variables, this guide recommends setting compilation options for each target. Using <span>target_compile_options()</span> not only allows for fine-grained control of compilation options but also integrates better with CMake’s more advanced features (such as configuring generator expressions). This approach makes the build process clearer, more maintainable, and scalable. At the same time, it avoids unnecessary compilation option pollution, making the compilation more precise and efficient.

4.3. More Content

When building cross-platform projects, a common challenge is how to handle the differences in compiler support for compilation options. Many compilers, such as GCC and Clang, provide compiler-specific feature flags. These flags are not supported by all compilers, and directly applying these flags to all compilers may lead to build failures. Therefore, in cross-platform projects, careful handling of compilation options is necessary to ensure that the project builds correctly in various compiler environments. There are three methods to address this issue.

The simplest and most straightforward method: directly modify <span>CMAKE_<LANG>_FLAGS_<CONFIG></span> (not recommended). Append the required compiler flags to the corresponding configuration type CMake variable <span>CMAKE_<LANG>_FLAGS_<CONFIG></span>. For example:

if(CMAKE_CXX_COMPILER_ID MATCHES GNU)
  list(APPEND CMAKE_CXX_FLAGS "-fno-rtti" "-fno-exceptions")
  list(APPEND CMAKE_CXX_FLAGS_DEBUG "-Wsuggest-final-types" "-Wsuggest-final-methods" "-Wsuggest-override")
  list(APPEND CMAKE_CXX_FLAGS_RELEASE "-O3" "-Wno-unused")
endif()

if(CMAKE_CXX_COMPILER_ID MATCHES Clang)
  list(APPEND CMAKE_CXX_FLAGS "-fno-rtti" "-fno-exceptions" "-Qunused-arguments" "-fcolor-diagnostics")
  list(APPEND CMAKE_CXX_FLAGS_DEBUG "-Wdocumentation")
  list(APPEND CXX_FLAGS_RELEASE "-O3" "-Wno-unused")
endif()

The advantages of this method are simplicity and clarity, but the drawbacks are also obvious:

  • Depends on the definition of <span>CMAKE_<LANG>_COMPILER_ID</span>: Not all compilers define the <span>CMAKE_<LANG>_COMPILER_ID</span> variable, which limits the portability of the code.

  • Modifying <span>CMAKE_<LANG>_FLAGS_<CONFIG></span> affects all targets, making fine-grained control difficult.

  • If a specific compiler does not support a particular flag or the flag has been deprecated, it requires modifying global variables, leading to higher maintenance costs.

  • Potential conflicts: Conflicts may occur with other CMake modules or user-defined settings.

The second method is to use custom variables and generator expressions (recommended but needs improvement). This method avoids directly modifying <span>CMAKE_<LANG>_FLAGS_<CONFIG></span> variables, instead defining project-specific variables to store compiler flags and then using generator expressions to select different sets of flags based on configuration types.

set(CXX_FLAGS)
set(CXX_FLAGS_DEBUG)
set(CXX_FLAGS_RELEASE)

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()

target_compile_options(compute-areas
  PRIVATE
    ${CXX_FLAGS}
    "$&lt;$&lt;CONFIG:Debug&gt;:${CXX_FLAGS_DEBUG}&gt;"
    "$&lt;$&lt;CONFIG:Release&gt;:${CXX_FLAGS_RELEASE}&gt;"
  )

This method is more flexible than the first method, allowing fine-grained control for each target. However, it still has the drawbacks related to <span>CMAKE_<LANG>_COMPILER_ID</span>. Additionally, this method still relies on manually maintaining the list of compiler flags and cannot handle compiler version updates or flag deprecations.

The third method: feature testing and setting compilation options (the most robust). This combines project-specific variables, <span>target_compile_options</span>, and generator expressions, making the solution very powerful. A further improvement is to use feature testing to verify whether the compiler supports specific flags before adding them to the compilation options. This method ensures that only valid flags are used in the project, thus improving the stability and reliability of the build.

Although the <span>CMAKE_<LANG>_COMPILER_ID</span> and <span>CMAKE_<LANG>_COMPILER_VERSION</span> variables are common for identifying compilers, they are not reliable. A more robust alternative is to use CMake’s <span>try_compile</span> or <span>CheckCXXCompilerFlag</span> module to check whether the compiler supports specific compilation options. The <span>CheckCXXCompilerFlag</span> is a built-in CMake module that can be used to check whether the compiler supports a specific compilation option. This method determines whether the compiler supports the flag by actually compiling a simple test program.

For example, if you want to use the <span>-fcolor-diagnostics</span> flag but are unsure whether all compilers support it, you can use the following code:

include(CheckCXXCompilerFlag)

check_cxx_compiler_flag("-fcolor-diagnostics" SUPPORTS_COLOR_DIAGNOSTICS)

if(SUPPORTS_COLOR_DIAGNOSTICS)
  list(APPEND CXX_FLAGS "-fcolor-diagnostics")
endif()

target_compile_options(my_target PRIVATE ${CXX_FLAGS})

This “check and set” pattern ensures that the flag is only added to the compilation options if the compiler supports it. This method avoids build failures due to the use of unsupported flags and enhances the robustness and portability of the code.

5. Conclusion

This article thoroughly analyzes the key elements of CMake compiler configuration, from background introduction, compiler variables, compiler selection to option settings, explaining the powerful capabilities of CMake in compiler configuration from shallow to deep.

By explicitly specifying the compiler, flexibly controlling compilation options, and adopting target property management strategies, you can build applications that are high-performance, highly portable, and easy to maintain.

Stop Using the Default Compiler? CMake Compiler Configuration Enables Code Compilation Anywhere!Lion Welcome to follow my public account Learn technology or submit articles

Leave a Comment