Skip to content

Latest commit

 

History

History
472 lines (339 loc) · 15.9 KB

targets.rst

File metadata and controls

472 lines (339 loc) · 15.9 KB

Target-based build systems with CMake

  • How can we handle more complex projects with CMake?
  • What exactly are targets in the CMake domain-specific language (DSL)?
  • 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 into executables and/or libraries. In the vast majority of cases, you will be faced with projects comprising hundreds of source files sprawling in a complex source tree. Using modern CMake helps you keep the complexity of the build system in check.

It's all about targets and properties

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

Targets

A target is declared by either or : thus, in broad terms, a target maps to a build artifact in the project.1 Any 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.

A target is the basic element in the CMake DSL. Each target has properties, which can be read with and modified with . Compile options, definitions, include directories, source files, link libraries, and link options are properties of targets.

A target is the basic element in the CMake DSL. Each target has properties, which can be read with and modified with . Compile options, definitions, include directories, source files, link libraries, and link options are properties of targets.

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 first argument. In our pseudo-code, tgtB will only be used to build tgtA but not be propagated as a dependency to other targets consuming tgtA.
  • INTERFACE. The property will only be used to build targets that consume the target given as first argument. In our pseudo-code, tgtC will only be propagated as a dependency to other targets consuming tgtA.
  • PUBLIC. The property will be used both to build the target given as first argument and targets that consume it. In our pseudo-code, tgtD will be used to build tgtA and will be propagated as a dependency to any other targets consuming tgtA.

Properties on targets have visibility levels, which determine how CMake should propagate them between interdependent targets.

Properties on targets have visibility levels, which determine how CMake should propagate them between interdependent targets.

The five most used commands used to handle targets are:

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> [BEFORE]
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

Use it to specify which compiler flags to use.

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> [SYSTEM] [BEFORE]
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

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

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 INTERFACE visibility levels a little less abstract.

You can find the file with the complete source code and solution in the content/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 source and one header file. The header file account.hpp and the shared library are needed to produce the bank executable. We also want to use the -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 INTERFACE visibility:

    target_include_directories(account
      INTERFACE
        ${CMAKE_CURRENT_SOURCE_DIR}
      )

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

Rule of thumb for visibility settings

When working out which visibility settings to use for the properties of your targets 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 hello-ctest). CMake lets you set properties at many different levels of visibility across the project:

  • Global scope. These are equivalent to variables set in the root CMakeLists.txt. Their use is, however, more powerful as they can be set from 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(<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(<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: a source 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 command: variables and targets declared in the root have effectively global scope. Remember also that will point to the folder containing the root CMakeLists.txt. In order to move between the root and a leaf or between leaves, you will use the command:

add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])

Typically, you only need to pass the first argument: the folder within the build tree will be automatically computed by CMake. We can declare targets at any level, not necessarily the root: a target is visible 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 multiple folders. We will implement a relatively simple code to compute and print to screen elementary cellular automata. We separate the sources into src and external to simulate a nested project which reuses an external project. Your goal is to:

  • Build a library out of the contents of external and each subfolder of src. Use together with and, for C++, . Think carefully about the visibility levels.
  • Build the main executable. Where is it located in the build tree? Remember that CMake generates a build tree mirroring the source tree.
  • The executable will accept 3 arguments: the length, number of steps, and automaton rule. You can run it with:

    $ automata 40 5 30

    This is the output:

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

C++

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 of ? If yes, which visibility level should you use?
  2. In , 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.

Fortran

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

automata-f/
├── external
│   └── conversion.f90
└── src
    ├── evolution
    │   ├── ancestors.f90
    │   ├── empty.f90
    │   └── evolution.f90
    ├── initial
    │   └── initial.f90
    ├── io
    │   └── io.f90
    ├── main.f90
    └── parser
        └── parser.f90
  1. The empty.f90 source declares, as the name suggests, an empty Fortran module. This module is only used within the evolution subfolder: what visibility level should it have in ?
  2. Note that CMake can understand the compilation order imposed by the Fortran modules without further intervention. Where are the .mod files?

A working example is in the solution subfolder.

Bonus

You can decide where executables, static and shared libraries, and Fortran .mod files will be stored within the build tree. The relevant variables are:

  • CMAKE_RUNTIME_OUTPUT_DIRECTORY, for executables.
  • CMAKE_ARCHIVE_OUTPUT_DIRECTORY, for static libraries.
  • CMAKE_LIBRARY_OUTPUT_DIRECTORY, for shared libraries.
  • CMAKE_Fortran_MODULE_DIRECTORY, for Fortran .mod files.

Modify your CMakeLists.txt to output the automata executable in build/bin and the libraries in build/lib.

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
The dependencies between targets in the cellular automata project.The dependencies between targets in the cellular automata project.
  • Using targets, you can achieve granular control over how artifacts are built 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 the visibility levels PRIVATE, INTERFACE, PUBLIC and let CMake figure out the details.
  • Use to inquire and to modify values of properties.
  • To keep the complexity of the build system at a minimum, each folder in a multi-folder project should have its own CMake script.

Footnotes


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