Back to Cmake

Step 4: In-Depth CMake Target Commands

Help/guide/tutorial/In-Depth CMake Target Commands.rst

4.3.217.4 KB
Original Source

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 :ref:scope keyword <Target Command Scope>, 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, :command:target_sources and :command: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 | +=========================================+======================================+=======================================+ | :command:target_compile_definitions | :command:get_target_property | :command:target_include_directories | | :command:target_compile_features | :command:set_target_properties | :command:target_link_directories | | :command:target_link_libraries | :command:target_compile_options | | | :command:target_sources | :command:target_link_options | | | | :command:target_precompile_headers | | +-----------------------------------------+--------------------------------------+---------------------------------------+

.. 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 :command:get_target_property, :command:set_target_properties and :command:target_precompile_headers, so we will briefly discuss their purpose here.

The :command:get_target_property and :command: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.

.. code-block:: cmake

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}")

.. code-block:: console

$ cmake -B build ... Key: Value Hello: World

The full list of target properties which are semantically meaningful to CMake are documented at :manual: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 :command:target_link_libraries.

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

The :command:target_precompile_headers command takes a list of header files, similar to :command: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 :variable: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 :command:target_compile_features command to communicate those requirements.

.. code-block:: cmake

target_compile_features(MyApp PRIVATE cxx_std_20)

The :command:target_compile_features command describes a minimum language standard as a target property. If the :variable: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:: :command: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 :command: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.

.. code-block:: cmake

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 :command:target_compile_definitions. CMake will determine the correct flag for the current compiler.

Goal

Use :command:target_compile_features and :command:target_compile_definitions to communicate language standard and compile definition requirements.

Helpful Resources

  • :command:target_compile_features
  • :command:target_compile_definitions
  • :command:option
  • :command:if

Files to Edit

  • CMakeLists.txt
  • 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.

.. code-block:: console

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.

.. raw:: html

<details><summary>TODO 1: Click to show/hide answer</summary>

.. literalinclude:: Step5/CMakeLists.txt :caption: TODO 1: CMakeLists.txt :name: CMakeLists.txt-TUTORIAL_USE_STD_SQRT :language: cmake :start-at: option(TUTORIAL_BUILD_UTILITIES :end-at: option(TUTORIAL_USE_STD_SQRT

.. raw:: html

</details>

Then we add the compile feature and definitions to MathFunctions.

.. raw:: html

<details><summary>TODO 2-3: Click to show/hide answer</summary>

.. literalinclude:: Step5/MathFunctions/CMakeLists.txt :caption: TODO 2-3: MathFunctions/CMakeLists.txt :name: MathFunctions/CMakeLists.txt-target_compile_features :language: cmake :start-at: target_compile_features :end-at: endif()

.. raw:: html

</details>

And the compile feature for Tutorial.

.. raw:: html

<details><summary>TODO 4: Click to show/hide answer</summary>

.. literalinclude:: Step5/Tutorial/CMakeLists.txt :caption: TODO 4: Tutorial/CMakeLists.txt :name: Tutorial/CMakeLists.txt-target_compile_features :language: cmake :start-at: target_compile_features :end-at: target_compile_features

.. raw:: html

</details>

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

.. raw:: html

<details><summary>TODO 5-6: Click to show/hide answer</summary>

.. literalinclude:: Step5/MathFunctions/MathFunctions.cxx :caption: TODO 5: MathFunctions/MathFunctions.cxx :name: MathFunctions/MathFunctions.cxx-cmath :language: c++ :start-at: cmath :end-at: format :append: #include <iostream>

.. literalinclude:: Step5/MathFunctions/MathFunctions.cxx :caption: TODO 6: MathFunctions/MathFunctions.cxx :name: MathFunctions/MathFunctions.cxx-std-sqrt :language: c++ :start-at: double sqrt(double x) :end-at: }

.. raw:: html

</details>

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.

.. raw:: html

<details><summary>TODO 7-8: Click to show/hide answer</summary>

.. code-block:: json :caption: TODO 7-8: CMakePresets.json :name: CMakePresets.json-std-sqrt

"cacheVariables": { "TUTORIAL_USE_STD_SQRT": "ON" }

.. raw:: html

</details>

Exercise 2 - Compile and Link Options ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Sometimes, we need to exercise specific control over the exact options being passed on the compile and link line. These situations are addressed by :command:target_compile_options and :command:target_link_options.

.. code:: cmake

target_compile_options(MyApp PRIVATE -Wall -Werror) target_link_options(MyApp PRIVATE -T LinksScript.ld)

There are several problems with unconditionally calling :command:target_compile_options or :command:target_link_options. The primary problem is compiler flags are specific to the compiler frontend being used. In order to ensure that our project supports multiple compiler frontends, we must only pass compatible flags to the compiler.

We can achieve this by checking the :variable:CMAKE_<LANG>_COMPILER_FRONTEND_VARIANT variable which tells us the style of flags supported by the compiler frontend.

.. note:: Prior to CMake 3.26, :variable:CMAKE_<LANG>_COMPILER_FRONTEND_VARIANT was only set for compilers with multiple frontend variants. In versions after CMake 3.26 checking this variable alone is sufficient.

However this tutorial targets CMake 3.23. As such, the logic is more complicated than we have time for here. This tutorial step already includes correct logic for checking the compiler variant for MSVC, GCC, Clang, and AppleClang on CMake 3.23.

Even if a compiler accepts the flags we pass, the semantics of compiler flags change over time. This is especially true with regards to warnings. Projects should not turn warnings-as-error flags by default, as this can break their build on otherwise innocuous compiler warnings included in later releases.

.. note:: For errors and warnings, consider placing flags in :variable:CMAKE_<LANG>_FLAGS for local development builds and during CI runs (via preset or :option:-D <cmake -D> flags). We know exactly which compiler and toolchain are being used in these contexts, so we can customize the behavior precisely without risking build breakages on other platforms.

Goal

Add appropriate warning flags to the Tutorial executable for MSVC-style and GNU-style compiler frontends.

Helpful Resources

  • :command:target_compile_options

Files to Edit

  • Tutorial/CMakeLists.txt

Getting Started

Continue editing files in the Step4 directory. The conditional for checking the frontend variant has already been written. Complete TODO 9 and TODO 10 to add warning flags to Tutorial.

Build and Run

Since we have already configured for this step, we can build with the usual command.

.. code-block:: cmake

cmake --build build

This should reveal a simple warning in the build. You can go ahead and fix it.

Solution

We need to add two compile options to Tutorial, one MSVC-style flag and one GNU-style flag.

.. raw:: html

<details><summary>TODO 9-10: Click to show/hide answer</summary>

.. literalinclude:: Step5/Tutorial/CMakeLists.txt :caption: TODO 9-10: Tutorial/CMakeLists.txt :name: Tutorial/CMakeLists.txt-target_compile_options :language: cmake :start-at: if( :end-at: endif()

.. raw:: html

</details>

Exercise 3 - Include and Link Directories ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. note:: This exercise requires building an archive using a compiler directly on the command line. It is not used in later steps. It is included only to demonstrate a use case for :command:target_include_directories and :command:target_link_directories.

If you cannot complete this exercise for whatever reason feel free to treat it as informational-only, or skip it entirely.

It is generally unnecessary to directly describe include and link directories, as these requirements are inherited when linking together targets generated within CMake, or from external dependencies imported into CMake with commands we will cover in later steps.

If we happen to have some libraries or header files which are not described by a CMake target which we need to bring into the build, perhaps pre-compiled binaries provided by a vendor, we can incorporate with the :command:target_link_directories and :command:target_include_directories commands.

.. code-block:: cmake

target_link_directories(MyApp PRIVATE Vendor/lib) target_include_directories(MyApp PRIVATE Vendor/include)

These commands use properties which map to the -L and -I compiler flags (or whatever flags the compiler uses for link and include directories).

Of course, passing a link directory doesn't tell the compiler to link anything into the build. For that we need :command:target_link_libraries. When :command:target_link_libraries is given an argument which does not map to a target name, it will add the string directly to the link line as a library to be linked into the build (prepending any appropriate flags, such a -l).

Goal

Describe a pre-compiled, vendored, static library and its headers inside a project using :command:target_link_directories and :command:target_include_directories.

Helpful Resources

  • :command:target_link_directories
  • :command:target_include_directories
  • :command:target_link_libraries

Files to Edit

  • Vendor/CMakeLists.txt
  • Tutorial/CMakeLists.txt

Getting Started

You will need to build the vendor library into a static archive to complete this exercise. Navigate to the Help/guide/tutorial/Step4/Vendor/lib directory and build the code as appropriate for your platform.

Typical commands for a GCC toolchain on Unix-like systems are:

.. code-block:: console

g++ -c Vendor.cxx ar rvs libVendor.a Vendor.o

Likewise, sample commands for an MSVC toolchain on Windows are:

.. code-block:: console

cl -c Vendor.cxx lib -out:Vendor.lib Vendor.obj

Here, since you're directly invoking cl and lib, make sure to use a Developer Command Prompt for your version of Visual Studio with the same target architecture used by this CMake project.

Then complete TODO 11 through TODO 14.

.. note:: VendorLib is an INTERFACE library, meaning it has no build requirements (because it has already been built). All of its properties should also be interface properties.

We'll discuss INTERFACE libraries in greater depth during the next step.

Build and Run

If you have successfully built libVendor, you can rebuild Tutorial using the normal command.

.. code-block:: console

cmake --build build

Running Tutorial should now output a message about the acceptability of the result to the vendor.

Solution

We need to use the target link and include commands to describe the archive and its headers as INTERFACE requirements of VendorLib.

.. raw:: html

<details><summary>TODO 11-13: Click to show/hide answer</summary>

.. code-block:: cmake :caption: TODO 11-13: Vendor/CMakeLists.txt :name: Vendor/CMakeLists.txt

target_include_directories(VendorLib INTERFACE include )

target_link_directories(VendorLib INTERFACE lib )

target_link_libraries(VendorLib INTERFACE Vendor )

.. raw:: html

</details>

Then we can add VendorLib to Tutorial's linked libraries.

.. raw:: html

<details><summary>TODO 14: Click to show/hide answer</summary>

.. code-block:: cmake :caption: TODO 14: Tutorial/CMakeLists.txt :name: Tutorial/CMakeLists.txt-VendorLib

target_link_libraries(Tutorial PRIVATE MathFunctions VendorLib )

.. raw:: html

</details>