Target-based build systems with CMake — CMake Workshop (2024)

Questions

  • How can we handle more complex projects with CMake?

  • What exactly are targets in the CMake domain-specific language (DSL)?

Objectives

  • Learn that the basic elements in CMake are not variables, but targets.

  • Learn about properties of targets and how to use them.

  • Learn how to use visibility levels to express dependencies between targets.

  • Learn how to work with projects spanning multiple folders.

  • Learn how to handle multiple targets in one project.

Real-world projects require more than compiling a few source files intoexecutables and/or libraries. In the vast majority of cases, you will be facedwith projects comprising hundreds of source files sprawling in a complex sourcetree. Using modern CMake helps you keep the complexity of the build system incheck.

It’s all about targets and properties

With the advent of CMake 3.0, also known as Modern CMake, there has been asignificant shift in the way the CMake domain-specific language (DSL) isstructured. Rather than relying on variables to convey information in aproject, we should shift to using targets and properties.

Targets

A target is declared by either add_executable or add_library: thus, in broadterms, a target maps to a build artifact in the project. 1Any target has a collection of properties, which define:

  • how the build artifact should be produced, and

  • how it should be used by other targets in the project that depend on it.

Target-based build systems with CMake — CMake Workshop (1)

It is much more robust to use targets and properties than using variables.Given a target tgtA, we can invoke one command in the target_* family as:

target_link_libraries(tgtA PRIVATE tgtB INTERFACE tgtC PUBLIC tgtD )

the use of the visibility levels will achieve the following:

  • PRIVATE. The property will only be used to build the target given as firstargument. In our pseudo-code, tgtB will only be used to build tgtAbut not be propagated as a dependency to other targets consuming tgtA.

  • INTERFACE. The property will only be used to build targets that consumethe target given as first argument. In our pseudo-code, tgtC will only bepropagated as a dependency to other targets consuming tgtA.

  • PUBLIC. The property will be used both to build the target given asfirst argument and targets that consume it. In our pseudo-code, tgtDwill be used to build tgtA and will be propagated as a dependency toany other targets consuming tgtA.

Target-based build systems with CMake — CMake Workshop (2)

The five most used commands used to handle targets are:

target_sources

target_sources(<target> <INTERFACE|PUBLIC|PRIVATE> [items1...] [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

Use it to specify which source files to use when compiling a target.

target_compile_options

target_compile_options(<target> [BEFORE] <INTERFACE|PUBLIC|PRIVATE> [items1...] [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

Use it to specify which compiler flags to use.

target_compile_definitions

target_compile_definitions(<target> <INTERFACE|PUBLIC|PRIVATE> [items1...] [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

Use it to specify which compiler definitions to use.

target_include_directories

target_include_directories(<target> [SYSTEM] [BEFORE] <INTERFACE|PUBLIC|PRIVATE> [items1...] [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

Use it to specify which directories will contain header (for C/C++) andmodule (for Fortran) files.

target_link_libraries

target_link_libraries(<target> <PRIVATE|PUBLIC|INTERFACE> <item>... [<PRIVATE|PUBLIC|INTERFACE> <item>...]...)

Use it to specify which libraries to link into the current target.

There are additional commands in the target_* family:

$ cmake --help-command-link | grep "^target_"

Understanding visibility levels

Let’s make the difference between PRIVATE, PUBLIC, and INTERFACEvisibility levels a little less abstract.

You can find the file with the complete source code and solution in thecontent/code/day-2/29_visibility-levels/solution folder.

Here we want to compile a C++ library and an executable:

  • The library code is in the account subfolder. It consists of one sourceand one header file. The header file account.hpp and the sharedlibrary are needed to produce the bank executable. We also want to usethe -ffast-math compiler flag and propagate it throughout the project.

  • The executable code is in bank.cpp. It includes account.hpp.

Thus:

  1. The account target declares the account.cpp source file as PRIVATE:

    target_sources(account PRIVATE account.cpp )

    since it is only needed to produce the shared library.

  2. The -ffast-math is instead PUBLIC:

    target_compile_options(account PUBLIC "-ffast-math" )

    since it needs to be propagated to all targets consuming account.

  3. The account folder is an include directory with INTERFACEvisibility:

    target_include_directories(account INTERFACE ${CMAKE_CURRENT_SOURCE_DIR} )

    since only targets consuming account need to know whereaccount.hpp is located.

Rule of thumb for visibility settings

When working out which visibility settings to use for the properties of yourtargets you can refer to the following table:

Who needs?

Others

Target

YES

NO

YES

PUBLIC

PRIVATE

NO

INTERFACE

N/A

Properties

So far we have seen that you can set properties on targets, but also on tests(see Creating and running tests with CTest).CMake lets you set properties at many different levels of visibility across theproject:

  • Global scope. These are equivalent to variables set in the rootCMakeLists.txt. Their use is, however, more powerful as they can be setfrom any leaf CMakeLists.txt.

  • Directory scope. These are equivalent to variables set in a given leaf CMakeLists.txt.

  • Target. These are the properties set on targets that we discussed above.

  • Test.

  • Source files. For example, compiler flags.

  • Cache entries.

  • Installed files.

For a complete list of properties known to CMake:

$ cmake --help-properties | less

You can get the current value of any property with:

get_property

get_property(<variable> <GLOBAL DIRECTORY [<dir>] TARGET <target> SOURCE <source> [DIRECTORY <dir> | TARGET_DIRECTORY <target>] INSTALL <file> TEST <test> CACHE <entry> VARIABLE PROPERTY <name> [SET | DEFINED | BRIEF_DOCS | FULL_DOCS])

and set the value of any property with:

set_property

set_property(<GLOBAL DIRECTORY [<dir>] TARGET [<target1> ...] SOURCE [<src1> ...] [DIRECTORY <dirs> ...] [TARGET_DIRECTORY <targets> ...] INSTALL [<file1> ...] TEST [<test1> ...] CACHE [<entry1> ...] [APPEND] [APPEND_STRING] PROPERTY <name> [<value1> ...])

Multiple folders

Each folder in a multi-folder project will contain a CMakeLists.txt: asource tree with one root and many leaves.

project/├── CMakeLists.txt <--- Root├── external│ ├── CMakeLists.txt <--- Leaf at level 1└── src ├── CMakeLists.txt <--- Leaf at level 1 ├── evolution │ ├── CMakeLists.txt <--- Leaf at level 2 ├── initial │ ├── CMakeLists.txt <--- Leaf at level 2 ├── io │ ├── CMakeLists.txt <--- Leaf at level 2 └── parser └── CMakeLists.txt <--- Leaf at level 2

The root CMakeLists.txt will contain the invocation of the projectcommand: variables and targets declared in the root have effectively globalscope. Remember also that PROJECT_SOURCE_DIR will point to the foldercontaining the root CMakeLists.txt.In order to move between the root and a leaf or between leaves, you will use theadd_subdirectory command:

add_subdirectory

add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])

Typically, you only need to pass the first argument: the folder within the buildtree will be automatically computed by CMake.We can declare targets at any level, not necessarily the root: a target isvisible at the level at which it is declared and all higher levels.

Exercise 21: Cellular automata

Let’s move beyond “Hello, world” and work with a project spanning multiplefolders. We will implement a relatively simple code to compute and print toscreen elementary cellular automata.We separate the sources into src and external to simulate a nested projectwhich reuses an external project.Your goal is to:

  • Build a library out of the contents of external and each subfolder ofsrc. Use add_library together with target_sources and, for C++,target_include_directories. Think carefully about the visibilitylevels.

  • Build the main executable. Where is it located in the build tree? Rememberthat CMake generates a build tree mirroring the source tree.

  • The executable will accept 3 arguments: the length, number of steps, andautomaton rule. You can run it with:

    $ automata 40 5 30

    This is the output:

    length: 40number of steps: 5rule: 30 * *** ** * ** **** ** * * ** **** ***

The scaffold project is in content/code/day-2/21_automata-cxx.The sources are organized in a tree:

automata-cxx/├── external│ ├── conversion.cpp│ └── conversion.hpp└── src ├── evolution │ ├── evolution.cpp │ └── evolution.hpp ├── initial │ ├── initial.cpp │ └── initial.hpp ├── io │ ├── io.cpp │ └── io.hpp ├── main.cpp └── parser ├── parser.cpp └── parser.hpp
  1. Should the header files be included in the invocation oftarget_sources? If yes, which visibility level should you use?

  2. In target_sources, does using absolute(${CMAKE_CURRENT_LIST_DIR}/parser.cpp) or relative(parser.cpp) paths make any difference?

A working example is in the solution subfolder.

The internal dependency tree

You can visualize the dependencies between the targets in your project with Graphviz:

$ cd build$ cmake --graphviz=project.dot ..$ dot -T svg project.dot -o project.svg
Target-based build systems with CMake — CMake Workshop (3)

Keypoints

  • Using targets, you can achieve granular control over how artifacts arebuilt and how their dependencies are handled.

  • Compiler flags, definitions, source files, include folders, link libraries,and linker options are properties of a target.

  • Avoid using variables to express dependencies between targets: use thevisibility levels PRIVATE, INTERFACE, PUBLIC and let CMakefigure out the details.

  • Use get_property to inquire and set_property to modify values ofproperties.

  • To keep the complexity of the build system at a minimum, each folder in amulti-folder project should have its own CMake script.

Footnotes

1

You can add custom targets to the build system with add_custom_target.Custom targets are not necessarily build artifacts.

Target-based build systems with CMake — CMake Workshop (2024)

References

Top Articles
Latest Posts
Article information

Author: Otha Schamberger

Last Updated:

Views: 6190

Rating: 4.4 / 5 (55 voted)

Reviews: 86% of readers found this page helpful

Author information

Name: Otha Schamberger

Birthday: 1999-08-15

Address: Suite 490 606 Hammes Ferry, Carterhaven, IL 62290

Phone: +8557035444877

Job: Forward IT Agent

Hobby: Fishing, Flying, Jewelry making, Digital arts, Sand art, Parkour, tabletop games

Introduction: My name is Otha Schamberger, I am a vast, good, healthy, cheerful, energetic, gorgeous, magnificent person who loves writing and wants to share my knowledge and understanding with you.