Step 4: In-Depth CMake Target Commands

There are several target commands within CMake we can use to describe requirements. As a reminder, a target command is one which modifies the properties of the target it is applied to. These properties describe requirements needed to build the software, such as sources, compile flags, and output names; or properties necessary to consume the target, such as header includes, library directories, and linkage rules.

Note

As discussed in Step1, properties required to build a target should be described with the PRIVATE scope keyword, those required to consume the target with INTERFACE, and properties needed for both are described with PUBLIC.

In this step we will go over all the available target commands in CMake. Not all target commands are created equal. We have already discussed the two most important target commands, target_sources() and target_link_libraries(). Of the remaining commands, some are almost as common as these two, others have more advanced applications, and a couple should only be used as a last resort when other options are not available.

Background

Before going any further, let's name all of the CMake target commands. We'll split these into three groups: the recommended and generally useful commands, the advanced and cautionary commands, and the "footgun" commands which should be avoided unless necessary.

Common/Recommended

Advanced/Caution

Esoteric/Footguns

target_compile_definitions() target_compile_features() target_link_libraries() target_sources()

get_target_property() set_target_properties() target_compile_options() target_link_options() target_precompile_headers()

target_include_directories() target_link_directories()

Note

There's no such thing as a "bad" CMake target command. They all have valid use cases. This categorization is provided to give newcomers a simple intuition about which commands they should consider first when tackling a problem.

We'll demonstrate most of these in the following exercises. The three we won't be using are get_target_property(), set_target_properties() and target_precompile_headers(), so we will briefly discuss their purpose here.

The get_target_property() and set_target_properties() commands give direct access to a target's properties by name. They can even be used to attach arbitrary property names to a target.

add_library(Example)
set_target_properties(Example
  PROPERTIES
    Key Value
    Hello World
)

get_target_property(KeyVar Example Key)
get_target_property(HelloVar Example Hello)

message("Key: ${KeyVar}")
message("Hello: ${HelloVar}")
$ cmake -B build
...
Key: Value
Hello: World

The full list of target properties which are semantically meaningful to CMake are documented at cmake-properties(7), however most of these should be modified with their dedicated commands. For example, it is unnecessary to directly manipulate LINK_LIBRARIES and INTERFACE_LINK_LIBRARIES, as these are handled by target_link_libraries().

Conversely, some lesser-used properties are only accessible via these commands. The DEPRECATION property, used to attach deprecation notices to targets, can only be set via set_target_properties(); as can the ADDITIONAL_CLEAN_FILES, for describing additional files to be removed by CMake's clean target; and other properties of this sort.

The target_precompile_headers() command takes a list of header files, similar to target_sources(), and creates a precompiled header from them. This precompiled header is then force included into all translation units in the target. This can be useful for build performance.

Exercise 1 - Features and Definitions

In earlier steps we cautioned against globally setting CMAKE_<LANG>_STANDARD and overriding packagers' decision concerning which language standard to use. On the other hand, many libraries have a minimum required feature set they need in order to build, and for these it is appropriate to use the target_compile_features() command to communicate those requirements.

target_compile_features(MyApp PRIVATE cxx_std_20)

The target_compile_features() command describes a minimum language standard as a target property. If the CMAKE_<LANG>_STANDARD is above this version, or the compiler default already provides this language standard, no action is taken. If additional flags are necessary to enable the standard, these will be added by CMake.

Note

target_compile_features() manipulates the same style of interface and non-interface properties as the other target commands. This means it is possible to inherit a language standard requirement specified with INTERFACE or PUBLIC scope keywords.

If language features are used only in implementation files, then the respective compile features should be PRIVATE. If the target's headers use the features, then PUBLIC or INTERFACE should be used.

For C++, the compile features are of the form cxx_std_YY where YY is the standardization year, e.g. 14, 17, 20, etc.

The target_compile_definitions() command describes compile definitions as target properties. It is the most common mechanism for communicating build configuration information to the source code itself. As with all properties, the scope keywords apply as we have discussed.

target_compile_definitions(MyLibrary
  PRIVATE
    MYLIBRARY_USE_EXPERIMENTAL_IMPLEMENTATION

  PUBLIC
    MYLIBRARY_EXCLUDE_DEPRECATED_FUNCTIONS
)

It is neither required nor desired that we attach -D prefixes to compile definitions described with target_compile_definitions(). CMake will determine the correct flag for the current compiler.

Goal

Use target_compile_features() and target_compile_definitions() to communicate language standard and compile definition requirements.

Helpful Resources

Files to Edit

  • Tutorial/CMakeLists.txt

  • MathFunctions/CMakeLists.txt

  • MathFunctions/MathFunctions.cxx

  • CMakePresets.json

Getting Started

The Help/guide/tutorial/Step4 directory contains the complete, recommended solution to Step3 and relevant TODOs for this step. Complete TODO 1 through TODO 8.

Build and Run

We can run CMake using our tutorial preset, and then build as usual.

cmake --preset tutorial
cmake --build build

Verify that the output of Tutorial is what we would expect for std::sqrt.

Solution

First we add a new option to the top-level CML.

TODO 1: Click to show/hide answer
TODO 1: CMakeLists.txt
option(TUTORIAL_BUILD_UTILITIES "Build the Tutorial executable" ON)
option(TUTORIAL_USE_STD_SQRT "Use std::sqrt" OFF)

Then we add the compile feature and definitions to MathFunctions.

TODO 2-3: Click to show/hide answer
TODO 2-3: MathFunctions/CMakeLists.txt
target_compile_features(MathFunctions PRIVATE cxx_std_20)

if(TUTORIAL_USE_STD_SQRT)
  target_compile_definitions(MathFunctions PRIVATE TUTORIAL_USE_STD_SQRT)
endif()

And the compile feature for Tutorial.

TODO 4: Click to show/hide answer
TODO 4: Tutorial/CMakeLists.txt
target_compile_features(Tutorial PRIVATE cxx_std_20)

Now we can modify MathFunctions to take advantage of the new definition.

TODO 5-6: Click to show/hide answer
TODO 5: MathFunctions/MathFunctions.cxx
#include <cmath>
#include <format>
#include <iostream>
TODO 6: MathFunctions/MathFunctions.cxx
double sqrt(double x)
{
#ifdef TUTORIAL_USE_STD_SQRT
  return std::sqrt(x);
#else
  return mysqrt(x);
#endif
}

Finally we can update our CMakePresets.json. We don't need to set CMAKE_CXX_STANDARD anymore, but we do want to try out our new compile definition.

TODO 7-8: Click to show/hide answer
TODO 7-8: CMakePresets.json
"cacheVariables": {
  "TUTORIAL_USE_STD_SQRT": "ON"
}