CMake Fundamentals Part 5

by | Feb 22, 2021 | CMake | 3 comments

In part 4 of the series we’ve had a first look at modular design. While we were at it I described how properties work and gave examples of using the PRIVATE and PUBLIC transitivity keywords. The INTERFACE keyword was mentioned, but left without a fully fleshed out exmaple. In this post I present a concrete use case for INTERFACE targets, discuss how to handle compiler and linker flags and introduce the include command.

Interface targets

CMake uses the command add_library to define all kinds of library-related targets. When dealing with static or shared libraries this makes sense and is totally intuitive – it immediately brings into mind the correlation between the declared target and the resulting binary library file. However, the remaining “library” types – OBJECT and INTERFACE may not be as obvious. Quite the opposite, the add_library command name may actually be a little misleading, or at least suggesting limited use. I already discussed OBJECT “libraries” in part 2 of the series, but only mentioned INTERFACE libraries briefly in the previous post. I had given one of the use cases for INTERFACE libraries there – modularizing header-only libraries. In that use case, the INTERFACE library encapsulates a set of properties that describe the library headers – most importantly the include paths, but also possibly compiler defines and flags/switches, and whatever else is necessary. While that is an important and common use case of INTERFACE libraries it is far from being the only one.

Instead of “interface library” I like to use the term “interface target” to remove the limiting suggestion that the feature is applicable only to libraries. As a matter of fact all of the CMake-level entities declared with add_library or add_executable (plus some other commands I have not introduced yet) ARE targets. Targets may represent libraries, but the uses are much broader than that, especially for INTERFACE targets. I honestly wouldn’t mind seeing add_target being introduced as a command alternative to add_library.

What are the other use cases then? The key thing to notice is that a target is just a set of properties. Those properties may describe a library, but certainly don’t have to. They may carry any information useful during the build process. One major example that comes to mind is encapsulating compiler and linker configuration. Instead of setting the compiler and linker flags and options globally, or on a per-directory basis, the configuration could be gathered into an interface target, that is then “linked” into other targets as needed. This promotes encapsulation and allows for a much more fine-grained control over the compilation process. We will look at such an example shortly. But first…

Compiler flags

A common question everyone getting started with CMake has is “How do I set the compiler flags?”. This of course comes out of necessity – either adjusting the optimization level, warnings, linker flags, target architecture, or what have you.

One way to do it, that’s often described in older CMake guides is to directly alter CMAKE_CXX_FLAGS

# Bad - discards flags specified on the command line
set(CMAKE_CXX_FLAGS "-Wall -Werror")
# Append - better, but still - don't to it
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror")

The problem with modifying the CMAKE_<LANG>_FLAGS variables directly is that there’s many pitfalls awaiting the user. It’s easy to make the mistake of overwriting any flags specified on the command line or set previously (as shown in the example above). Then there’s an issue of granularity – specifying different sets of flags for different targets. More issues arise when a project needs to be incorporated as a subproject into another, larger project. It’s just best to stay away from modifying these variables directly altogether. The only exception might be toolchain files, but those come with a set of variables dedicated specifically to this purpose so it’s a different story (that we might get to in the future).

Modern CMake moves the focus from variables into targets and properties. It shouldn’t come as a surprise that there’s a dedicated set of commands for manipulating compiler and linker flags and defines. The commands that you should be aware of are:

Command Affected Properties Purpose
target_compile_options COMPILE_OPTIONS, INTERFACE_COMPILE_OPTIONS Add compiler flags like “-Wall”, “-O3”, etc.
target_compile_definitions COMPILE_DEFINITIONS, INTERFACE_COMPILE_DEFINITIONS Add compiler defines like “-DNDEBUG”
target_compile_features COMPILE_FEATURES, INTERFACE_COMPILE_FEATURES Specify features required to be supported by the compiler to compile the target
target_link_options LINK_OPTIONS, INTERFACE_LINK_OPTIONS Add linker-specific flags

 
In this post I’ll limit the discussion to target_compile_options. The other commands work exactly the same.

These commands could be used directly on each target in a project. By the example of the project we’ve been working on the past few posts, the -Wall -Werror flags could be added to the Maths target (unsurprisingly) as follows:

add_library(Maths
    maths/maths.h
    maths.cpp
)
# ...
target_compile_options(Maths
    PRIVATE
        -Wall
        -Wextra
)

The same could be done for every target in the project. That’s a lot of code duplication though. There must be a better way.

Project configuration interface targets

After that long-winded intro let’s get to the point of this post. An INTERFACE target can be used to encapsulate compiler configuration – all of the warning flags, optimization level and whatever else necessary – and later be “linked” into other targets to ensure consistent configuration, while still leaving the option of easily adding more flags on per-target basis if needed.

This is achieved very simply, by declaring an interface target dedicated to just this task (if you want to follow along add this to the top-level CMakeLists.txt for now):

add_library(ProjectConfiguration INTERFACE)
target_compile_options(ProjectConfiguration
    INTERFACE
        -Wall
        -Werror
        $<$<COMPILE_LANGUAGE:CXX>:-Weffc++>  # generator expression
)
target_compile_features(ProjectConfiguration
    INTERFACE
        cxx_std_17
)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

The call to add_library declares a ProjectConfiguration INTERFACE library target, the properties of this target are then configured using the target_compile_options and target_compile_features commands. Note that since ProjectConfiguration is an INTERFACE target the only valid transitivity keyword that can be used is INTERFACE – this means that these properties will propagate to targets ProjectConfiguration is “linked” into.

The target_compile_options aside for setting the -Wall and -Werror flags also sets a -Weffc++ flag, but does so using a new piece of syntax – a generator expression. This specific generator expression adds the given -Weffc++ flag only if the language infered for the target being compiled is C++. This is necessary because we have C source files in our project and -Weffc++ is a C++ specific flag, which would cause compilation of a C program to fail. The same could be achieved with an if-else block, however it would have been much more long-winded. Generator expressions are great once you get used to them, but can be a little hard to understand at first (which seems to be a common theme with CMake features). I will definitely be returning to generator expressions in the future, as they’re a large and important subject that deserves a post of its own.

The target_compile_features configures cxx_std_17. This is a “meta-feature”. Meaning that it’s not an actual feature of C++ or a particular compiler. It enforces the C++17 standard on the compiler, which will cause -std=c++17 to be added to the compiler call. Well, almost – to ensure that the standard is enforced two additional variables need to be set CMAKE_CXX_STANDARD_REQUIRED=ON and CMAKE_CXX_EXTENSIONS=OFF. The first one makes the specified standard a strict requirement – otherwise it’s a “kind request”. The second one ensures no non-standard extensions. Forgetting to set this variable to OFF would likely result in -std=gnu++17. It of course depends on your needs, but for strictly standard-compliant code prefer the shown configuration. In projects aiming for wide compiler version compatibility the target_compile_features command could be used to specify required compiler features on a very fine grained level. But this is beyond the scope of this post.

The ProjectConfiguration target can be then “linked” into all the other targets within the project:

add_library(Maths
    maths/maths.h
    maths.cpp
)
target_link_libraries(Maths
    PUBLIC
        Add
    PRIVATE
        Subtract
        ProjectConfiguration
)

It is added in the PRIVATE section to avoid propagating the configuration – this allows to maintain fine-grained control over all targets.

I keep putting “linked” in quotes because there’s nothing to link here – the only thing that’s happening, is property propagation. This is yet another example of confusing legacy command naming – we’re dealing with targets and properties, but the command name suggests use cases limited to libraries and linking. That’s hardly intuitive…

Assuming that ProjectConfiguration is added to all other targets building the project would result in the following output (relevant parts only):

/usr/bin/cc  -I/home/user/cmake_fundamentals/subtract -g -Wall -Wextra -o CMakeFiles/Subtract.dir/subtract.c.o -c /home/user/cmake_fundamentals/subtract/subtract.c
/usr/bin/c++  -I/home/user/cmake_fundamentals/add -g -Wall -Wextra -Weffc++ -std=c++17 -o CMakeFiles/Add.dir/add.cpp.o -c /home/user/cmake_fundamentals/add/add.cpp
/usr/bin/c++  -I/home/user/cmake_fundamentals/maths -I/home/user/cmake_fundamentals/add -I/home/user/cmake_fundamentals/subtract -g -Wall -Wextra -Weffc++ -std=c++17 -o CMakeFiles/Maths.dir/maths.cpp.o -c /home/user/cmake_fundamentals/maths/maths.cpp
/usr/bin/c++  -I/home/user/cmake_fundamentals/maths -I/home/user/cmake_fundamentals/add -g -Wall -Wextra -Weffc++ -std=c++17 -o CMakeFiles/Main.dir/main.cpp.o -c /home/user/cmake_fundamentals/main.cpp

This achieves the goal, however, the top-level CMakeLists is getting a little messy. Let’s see how it could be cleaned up a little.

include command

In part 2 of the series I introduced the add_subdirectory command. It is used to add a directory that contains a CMakeLists.txt file into the project. The contained CMakeLists is processed immediately upon adding the subdirectory. There’s one more command which offers a very similar functionality – include – it is used to, well, include a specified cmake script (file) into the project.

The major differences between add_subdirectory and include are:

  • include expects a file name, rather than a directory. This should be some form of a cmake script
  • include does not introduce a new variable scope, whereas add_subdirectory does.
  • include does not affect the value of CMAKE_CURRENT_SOURCE_DIR and CMAKE_CURRENT_BINARY_DIR

The include command is designed to allow a certain degree of both encapsulation and code reuse. Instead of defining the ProjectConfiguration target in the top-level CMakeLists.txt, all of the code could be moved to a dedicated file – project_configuration.cmake. Since we’re cleaning up the CMakeLists.txt let’s also move the PrintTargetLibraries function into a dedicated file as well – print_target_libraries.cmake. To avoid polutting the root project directory, let’s introduce a cmake subdirectory and move these scripts there. This results in the following tree:

.
├── cmake
│   ├── print_target_libraries.cmake
│   └── project_configuration.cmake

project_configuration.cmake contains the following code:

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_VERBOSE_MAKEFILE ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

add_library(ProjectConfiguration INTERFACE)
target_compile_options(ProjectConfiguration
    INTERFACE
        -Wall
        -Wextra
        $<$:-Weffc++>  # generator expression
)
target_compile_features(ProjectConfiguration
    INTERFACE
        cxx_std_17
)

and print_target_libraries.cmake:

function(PrintTargetLibraries target msg)
    get_target_property(linkedLibs ${target} LINK_LIBRARIES)
    get_target_property(interfaceLibs ${target} INTERFACE_LINK_LIBRARIES)
    message(STATUS "${msg}")
    message(STATUS "${target} LINK_LIBRARIES: ${linkedLibs}")
    message(STATUS "${target} INTERFACE_LINK_LIBRARIES: ${interfaceLibs}")
endfunction()

These files are now included in the top-level CMakeLists.txt. It now looks as follows:

cmake_minimum_required(VERSION 3.19)
project(Fundamentals)

include(cmake/project_configuration.cmake)
include(cmake/print_target_libraries.cmake)

add_subdirectory(add)
add_subdirectory(subtract)
add_subdirectory(maths)

add_executable(Main main.cpp)
PrintTargetLibraries(Main "Before linking Maths:")
target_link_libraries(Main
    PRIVATE
        Maths
        ProjectConfiguration
)
PrintTargetLibraries(Main "After linking Maths:")

Much cleaner than before, isn’t it? However, there’s still one minor improvement that could be made. The form of the include command used here takes a path to a file literally. There’s another form, that accepts a name of a cmake module. Simplifying a little a module is any CMake script ending with a .cmake extension that is found on one of the paths specified in the CMAKE_MODULE_PATH variable. To take advantage of this, we’d need to modify the CMAKE_MODULE_PATH appropriately. Doing so would make the top-level CMakeLists.txt include calls independent of the project directory structure. The included scripts wouldn’t even need to be part of the project anymore and could be managed externally – e.g. installed system-wide or imported by a dependency-manager, as long as they can be found on one of the paths listed in CMAKE_MODULE_PATH we’re good!

The first few lines of the CMakeLists.txt after introducing the described changes:

project(Fundamentals)

list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
include(project_configuration)
include(print_target_libraries)

Note that the include calls now specify only the name of the module – without the extension. Also, another new command is introduced here – list – it is used to conveniently manipulate list variables. I’ll leave you to study the documentation on this one for now, and as usual, I might return to it in the future.

And that’s it! The project now has a well-defined configuration target, feel free to extend it with any additional flags to meet your needs.

Summary

Some CMake command names – add_library and target_link_libraries exemplified in this post – suggest limited use cases, either due to legacy reasons or the most common uses they’re designed around. This might make some concepts seem more difficult than they really are. In this post you’ve seen how interface libraries targets can be utilised to encapsulate arbitrary set of properties. You also learned about how to specify compiler configuration the modern-cmake-way. Finally the include command was introduced, which I’m sure you will come to rely on when maintaining larger projects.

I have the usual questions for you – do you see any applications for the discussed topics in your own code? Did you find the interface libraries equally confusing as I did initially, or were they, and their uses immediately obvious? Feel free to share in the comments section.

 

References

3 Comments

  1. Diego

    very good article ,finally the target_compile_options target_compile_features and cmake module make sense to me

    Reply
  2. Borislav

    Yeah, good work!
    Thank you

    Reply
  3. mustfa

    Thank you It is nice to see good practice examples

    Reply

Submit a Comment

Your email address will not be published. Required fields are marked *

Share This