Dependency hell?
One thing that has eluded me in the past was how to efficiently manage dependencies of different components within one CMake project. I’d use the include_directories, add_definitions and add_compile_options command in the top-level or in mid-level CMakeLists.txt
files just to get the whole thing to compile. Of course, this is all heavily order-dependent – so the build system breaks as soon as you make an ever so subtle change to the directory layout. I’ve seen projects tackle this problem in various ways – for example by defining specifically named variables for each library and using that for their clients. Other projects defined “interface” files for each library that could be included by other targets. All these homegrown solutions work, but they are rather clumsy and don’t work well when integrating libraries not written in that same convention.
target_link_libraries to the rescue!
It turns out there’s actually a pretty elegant solution built into CMake, which centers around target_link_libraries. But information on this is pretty scarce on the web. The ones that initially put me on the right track were The Ultimate Guide to Modern CMake and CMake – Introduction and best practices. Of course, it’s all in the CMake documentation, but mentioned implicitly at best.
The gist is this: Using target_link_libraries to link A to an internal target B will not only add the linker flags required to link to B, but also the definitions, include paths and other settings – even transitively – if they are configured that way.
To do this, you need to use target_include_directories and target_compile_definitions with the PUBLIC
or INTERFACE
keywords on your targets. There’s also the PRIVATE
keyword that can be used to avoid adding the settings to all dependent targets.
A simple example
Here’s a small example of a library that uses Boost in its headers and therefore wishes to have its clients setup those directories as well:
set(TARGET_NAME cool_lib) add_library(${TARGET_NAME} STATIC cool_feature.cpp cool_feature.hpp) target_include_directories(${TARGET_NAME} INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(${TARGET_NAME} SYSTEM PUBLIC ${Boost_INCLUDE_DIR})
Now here’s a program that wants to use that:
set(TARGET_NAME cool_tool) add_executable(cool_tool main.cpp) target_link_libraries(cool_tool PRIVATE cool_lib)
cool_tool
can just #include "cool_feature.hpp"
without knowing exactly where it is located in the source tree or without having to worry about setting up the boost includes for itself! Pretty neat!
PRIVATE, PUBLIC and INTERFACE
Typically, you’d use the PRIVATE
keyword for includes and definitions that are exclusively used in you implementation, i.e. your *.cpp
and *.c
files and internal headers. It’s good practice to favor PRIVATE
to avoid “leaking” dependencies so they won’t stack up in the dependent libraries and bring down your compile times. The INTERFACE
keyword is a bit more curious: For example, with definitions, you can use it to define your .dll interface differently for compilation and usage. For include directories, one common usage is to set the own source directory with INTERFACE
if you keep your headers and source files in the same folder. The PUBLIC
keyword is used when definitions and includes are relevant for the own and dependent libraries. It pretty much is the combination of PRIVATE
and INTERFACE
– whenever you’re temped to put something in both of those, put it in PUBLIC
instead. It is probably the most common option.
The future!
I hope that all open-source libraries switch to this style sooner rather than later so you can easily include them in your build-trees. Just don’t use the old commands that add properties for all following targets like add_definitions
, include_directories
etc. and use the commands with the target_
prefix!
That is all nice if you bundle your dependencies inside the source tree but does not help if you have depend on libraries installed globally. So I don’t know if your last statement makes that much sense.
Well I’d like to have the option to do both. Nothing prevents you from also providing sane installation and find scripts.
Thanks for pointing out the “target_include_directories” and “target_compile_definitions”, I wasn’t aware of them.
In large projects with lots of internal dependences, it’s a much saner way of handling include paths: e.g. in a three-layered project I’m working on I ended up having to include 10-15 directories in the upper levels.
It would be nice to have the same mechanism in the find scripts, but it doesn’t seem to be common practice.