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 theuseBoost
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 usingfoo
. - 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 usesfoo
internally and it also uses parts ofbar
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 becausebar
is aPUBLIC
dependency ofgumby
. - Add the Boost header search paths when compiling
appB.cpp
because they are aPUBLIC
dependency ofbar
. - Add Boost libraries to those linked in because
foo
is a dependency ofgumby
. Even though this is aPRIVATE
dependency, CMake still honours the requirement that anything linking tofoo
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.