Professional CMake:

A Practical Guide

Learn to use CMake effectively with practical advice from a CMake co-maintainer. You can also have the author work directly with your team!

CMake targets with detailed dependencies

If only software build systems would do what we intuitively expect! I’m sure many of you have your own horror stories of having to unravel convoluted scripts, project settings, compiler bugs, etc. in order to get code to build, despite the project requirements seeming to be relatively simple. If you work with cross-platform software, this is probably a pain point you are particularly familiar with. This article demonstrates some more recent features of CMake which greatly reduce that pain.

Specifying target dependencies

If you are using CMake to control your builds, then versions 2.8.11 and later have added some very useful features which make specifying dependencies between build targets much simpler and more robust. Instead of carrying around compiler flags, linker options, header search paths, etc. in CMake variables, you can now attach that information directly to the targets themselves and let CMake add them as needed for anything that links to that target. This greatly simplifies projects, especially those with a large number of target dependencies spread across multiple directories.

The most relevant commands that have been added or extended to support these features are the following:

These commands all follow a similar pattern:

target_XXX_YYY(targetName
  [PUBLIC ...]
  [PRIVATE ...]
  [INTERFACE ...]
)

Things specified as PUBLIC will apply to both targetName and anything that links to it. PRIVATE specifies things that only apply to targetName, meaning anything that links to it doesn’t need to know about it. INTERFACE applies only to targets that link to targetName but which are not needed by targetName itself.

A concrete example best demonstrates how these commands and keywords are used. Let’s say we have a project with some libraries which make use of an external package, Boost:

  • Static library foo will use Boost features internally when the useBoost pre-processor symbol is defined, but does not expose anything related to Boost in its interface. When enabled, Boost libraries need to be linked in for any target using foo.
  • Shared library bar always uses parts of Boost which only require its headers but do not require Boost libraries to be linked in.
  • Library gumby does not use Boost in any way, but it uses foo internally and it also uses parts of bar in its own public interface.

We can define these libraries as follows:

# Let CMake find Boost for us.
# Details omitted for brevity.
find_package(Boost ...)

# Basic definition of library targets
add_library(foo STATIC foo.cpp)
add_library(bar SHARED bar.cpp)
add_library(gumby gumby.cpp)

# Dependency details for the libraries
target_compile_definitions(foo
  PRIVATE useBoost=true
)
target_link_libraries(foo
  INTERFACE ${Boost_LIBRARIES}
)
target_include_directories(bar
  PUBLIC ${Boost_INCLUDE_DIRS}
)
target_link_libraries(gumby
  PRIVATE foo
PUBLIC bar
)

Now if we have other targets which need to link against these libraries, even if those targets are in different directories, it is trivial to do so and CMake will take care of the dependencies for us.

add_executable(appA appA.cpp)
target_link_libraries(appA foo)

add_executable(appB appB.cpp)
target_link_libraries(appB gumby)

In the above, we only explicitly link appA against the static library foo. The dependencies we set up for foo, however, will ensure that appA is also linked against the Boost libraries.

For appB, we only specified that it should link against gumby, but CMake will also do the following for us automatically:

  • Add bar to the linked in libraries because bar is a PUBLIC dependency of gumby.
  • Add the Boost header search paths when compiling appB.cpp because they are a PUBLIC dependency of bar.
  • Add Boost libraries to those linked in because foo is a dependency of gumby. Even though this is a PRIVATE dependency, CMake still honours the requirement that anything linking to foo must also link to the Boost libraries.

Real world example: Qt

The above example was fairly contrived to show how the CMake commands are used. In practice, targets may be defined in different directories or come from entirely separate third-party projects. A particularly powerful example of how the above CMake features can be exploited to very good effect is the way they are used with recent Qt versions. Qt has a number of different modules, each one requiring its own header search paths, libraries to be linked and pre-processing symbols to be defined, with a lot of this information varying from platform to platform. In the past, these things all had to be specified via CMake variables and they were awkward to use for multiple targets which used different Qt modules. Using the new target dependency commands, it is now just a simple matter of linking to the required Qt modules and CMake takes care of the rest. For example:

# List the Qt modules required by any target
find_package(QtNetwork REQUIRED)
find_package(QtGui REQUIRED)

# Create some executable targets
add_executable(consoleApp ...)
add_executable(openGLApp WIN32 ...)

# Make each executable depend only on the Qt modules they need
target_link_libraries(consoleApp Qt5::Network)
target_link_libraries(openGLApp Qt5::Network Qt5::Gui)

Those two target_link_libraries commands hide a great deal of dependency information that we mere mortals do not need to know about. We just want our targets to be able to use those Qt modules and not have to care about the details. Behind the scenes, these commands will add header search paths, compiler flags and other Qt libraries and system libraries required by the Qt module(s) that we did actually specify. In the case of openGLApp, it even handles adding a special static library (qtmain) which is only required for Windows GUI Qt applications. Furthermore, the above shows how two different targets can use different sets of Qt modules. Those developers who have worked with Qt prior to CMake 2.8.11 will appreciate just how much simpler this is compared to before!

For the interested reader, a more complete description of using CMake with Qt5 is now maintained as part of Qt itself.


Have a CMake maintainer work on your project

Get the book for more CMake content

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.