Properties
Broadly speaking CMake projects are customized with two major facilities – cache variables (which we will return to at some point) and properties. Whereas variables, be it normal or cached, have for the most part, global scope, properties are attached to an entity – they specify some aspect of it within the project. Properties may be specified for a wide range of scopes – from GLOBAL, through DIRECTORY and TARGET down do individual files. Today we’ll focus on TARGET properties – as these are what will help us with using targets to structure our projects well.
Properies may be operated on directly using the most general commands – set_property and get_property. These commands can set or fetch the value of any property in any scope. However, as the result of the generality they’re the most complicated to use. Most often it’s more convenient to use the more specialized alternatives, like set_target_properties and get_target_property. In fact, even these commands are usually too big of a hammer for the task. Instead, the most used commands operate on a very limited set of properties – usually just one or two. In fact, we’ve already used a command that operates on properties – target_link_libraries! If a command operates on a target, and does not create one, it most likely modifies its properties.
Project setup
In the previous part of the series we started working on a simple library that exposed a single add function. Expanding this example will help us illustrate the concept of properties. First of all, we will isolate the library code from the application code. For this purpose we create a subdirectory named add, move the library source files – calc_status.h, add.h, add.cpp into that directory, and create a CMakeLists.txt within it. We end up with the following directory structure:
.
├── add
│ ├── add.cpp
│ ├── add.h
│ ├── calc_status.h
│ └── CMakeLists.txt
├── CMakeLists.txt
└── main.cpp
All that needs to be done in the Add library CMakeLists.txt is to declare the library target:
add_library(Add
calc_status.h
add.h
add.cpp
)
We now need to incorporate this directory into our project so that we may use it. The top-level CMakeLists.txt now looks as follows:
cmake_minimum_required(VERSION 3.19)
project(Fundamentals)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_VERBOSE_MAKEFILE ON)
add_subdirectory(add)
add_executable(Main main.cpp)
target_link_libraries(Main
PUBLIC
Add
)
add_subdirectory
To include a directory into a project we introduce a new command – add_subdirectory The add_subdirectory command does exactly what it says – it adds the specified directory to the project. The command expects that the directory contains a CMakeLists.txt file, and processes it immediately – all of the targets defined within the listing will be visible across the entire project. The add_subdirectory command allows us to keep the project-defining CMakeLists.txt files as close to the actual code as possible, this improves readability and maintainability, and actually helps with good design – a subdirectory in this case isolates the library as a separate component or a module. The Add target defined in the subdirectory is then linked into the application code. As a reminder, the main.cpp looks as follows.
#include <iostream>
#include <limits>
#include "add/add.h"
#include "add/calc_status.h"
void add(int a, int b)
{
calc_status cs{};
auto const result = add(a, b, cs);
if (cs == calc_status::success)
{
std::cout << a << " + " << b << " = " << result << "\n";
}
else
{
std::cout << "Error calculating " << a << " + " << b << "\n";
}
}
int main()
{
const int a{42};
const int b{102};
add(a, b);
add(a, std::numeric_limits<int>::max());
}
To include the Add library headers we now need to go down into the add directory.
Inspecting properties
Let’s now inspect how the target_link_libraries command affects the properties of the Main target. For that purpose we will create our first CMake function.
CMake functions serve the same purpose as in any other language – they facilitate code reuse. Function definitions are enclosed in a function() block. For our needs we will define a function that fetches the value of some target properties we’re interested in, and echoes them to the screen.
function(PrintTargetLibraries target msg)
get_target_property(linkedLibs ${target} LINK_LIBRARIES)
get_target_property(interfaceLibs ${target} INTERFACE_LINK_LIBRARIES)
message(STATUS "${msg}")
message(STATUS "${target} LINK_LIBRARIES: ${linkedLibs}")
message(STATUS "${target} INTERFACE_LINK_LIBRARIES: ${interfaceLibs}")
endfunction()
The above listing defines a function named PrintTargetLibraries that accepts arguments target and msg. We see a new piece of syntax here – variables are dereferenced by enclosing them in ${}. The get_target_property command fetches the value of LINK_LIBRARIES and INTERFACE_LINK_LIBRARIES properties and stores them in variables linkedLibs and interfaceLibs, respectively. The value is then printed to the screen using the message command. We will gloss over some of the details here as usual.
Our function can be called same as any other CMake command. We use it to print the value of the selected properties on the Main target, before, and after linking the Add library:
add_executable(Main main.cpp)
PrintTargetLibraries(Main "Before linking Add:")
target_link_libraries(Main
PRIVATE
Add
)
PrintTargetLibraries(Main "After linking Add:")
Let’s now configure the project to see the output:
$ cmake -B build -S .
-- Before linking Add:
-- Main LINK_LIBRARIES: linkedLibs-NOTFOUND
-- Main INTERFACE_LINK_LIBRARIES: interfaceLibs-NOTFOUND
-- After linking Add:
-- Main LINK_LIBRARIES: Add
-- Main INTERFACE_LINK_LIBRARIES: interfaceLibs-NOTFOUND
We see that before linking the Add library, the variables are set to linkedLibs-NOTFOUND and interfaceLibs-NOTFOUND values respectively. This is a common pattern in CMake – when fetching something that doesn’t exist, the destination variable is set to <variableName>-NOTFOUND. Here – these properties have not been set yet, and that how this is signalled by get_target_property. After linking the library the LINK_LIBRARIES is set to Add – as expected. The INTERFACE_LINK_LIBRARIES however is still empty. To understand why, we will need to discuss the difference between the LINK_LIBRARIES and INTERFACE_LINK_LIBRARIES properties. We will do so, while also discussing the PRIVATE, INTERFACE and PUBLIC keywords.
PRIVATE, INTERFACE, PUBLIC
The PRIVATE, INTERFACE and PUBLIC keywords may bring to mind access specifiers familiar to anyone aquainted with object-oriented languages. This may seem applicable here at first, however at closer inspection it isn’t precisely right. Instead, I’d suggest to think in terms of transitivness of dependencies of a particular target. For exapmle, given target Foo, the semantic meaning of these keywords is:
PRIVATE – fully encapsulated dependencies of Foo – they are required to build Foo, but using Foo does not expose the user to the private dependencies
INTERFACE – dependencies required to use Foo – but NOT required to build Foo itself, Users of Foo will need also these dependencies
PUBLIC – transitive dependencies of Foo – they are required to build Foo, and users of Foo will also depend on these targets indirectly
This has been difficult to grasp intuitively for me at first. What has helped me greatly is to learn, that these keywords are implemented in terms of properties – all they do is specify which properties are modified by a given command. Going back to our target_link_libraries example – it operates on the LINK_LIBRARIES and INTERFACE_LINK_LIBRARIES properties.
The LINK_LIBRARIES property specifies a list of libraries that need to be linked to the given target in order to build it. In this case the target Add is added to the list – CMake will handle dereferencing the target and linking the actual library at build time. This property can be thought of as a list of requirements necessary to build the given target.
The INTERFACE_LINK_LIBRARIES property specifies a list of libraries that need to be linked to the targets which use (link) the given target. In other words, these are transitive dependencies – if the use of library A also requires linking of library B we could ensure that this is done properly by setting this property to B on library A. This may seem somewhat abstract at the moment – we will see an example later that puts this into perspective.
Thus all these keywords do, is specify if one, or both of these properties should be affected by the target_link_libraries call. The exact mapping is presented in the table below:
Keyword | Properties affected |
PRIVATE | LINK_LIBRARIES |
INTERFACE | INTERFACE_LINK_LIBRARIES |
PUBLIC | LINK_LIBRARIES, INTERFACE_LINK_LIBRARIES |
Given our Add target library and Main target executable that uses it, let’s inspect how specifying different keywords would affect these properties. We’ve already seen the example of using PRIVATE. Let’s now try using INTERFACE:
add_executable(Main main.cpp)
PrintTargetLibraries(Main "Before linking Add:")
target_link_libraries(Main
INTERFACE
Add
)
PrintTargetLibraries(Main "After linking Add:")
Let’s now configure the project to see the output:
$ cmake -B build -S .
-- Before linking Add:
-- Main LINK_LIBRARIES: linkedLibs-NOTFOUND
-- Main INTERFACE_LINK_LIBRARIES: interfaceLibs-NOTFOUND
-- After linking Add:
-- Main LINK_LIBRARIES: linkedLibs-NOTFOUND
-- Main INTERFACE_LINK_LIBRARIES: Add
Note that attempting to actually build the project (rather than just configure it) would fail:
$ cmake --build build
/usr/bin/c++ -O3 -DNDEBUG CMakeFiles/Main.dir/main.cpp.o -o Main
/usr/bin/ld: CMakeFiles/Main.dir/main.cpp.o: in function `add(int, int)':
main.cpp:(.text+0x30): undefined reference to `add(int, int, calc_status&)'
This is because calling target_link_libraries with INTERFACE won’t actually link the library into the target – and since Main directly uses code from the Add library, it needs to be linked.
The last keyword we need to inspect is PUBLIC. As described – the Add library should be added to both properties:
add_executable(Main main.cpp)
PrintTargetLibraries(Main "Before linking Add:")
target_link_libraries(Main
PUBLIC
Add
)
PrintTargetLibraries(Main "After linking Add:")
$ cmake -B build -S .
-- Before linking Add:
-- Main LINK_LIBRARIES: linkedLibs-NOTFOUND
-- Main INTERFACE_LINK_LIBRARIES: interfaceLibs-NOTFOUND
-- After linking Add:
-- Main LINK_LIBRARIES: Add
-- Main INTERFACE_LINK_LIBRARIES: Add
And it is. Since the library is now actually linked into the Main target, the project can be built successfully now:
$ cmake --build build
[ 75%] Building CXX object CMakeFiles/Main.dir/main.cpp.o
/usr/bin/c++ -o CMakeFiles/Main.dir/main.cpp.o -c /home/user/cmake_fundamentals/main.cpp
[100%] Linking CXX executable Main
/usr/bin/c++ -O3 -DNDEBUG CMakeFiles/Main.dir/main.cpp.o -o Main add/libAdd.a
[100%] Built target Main
Note that in this case using PUBLIC does not actually make any sense – since this is an executable target, that isn’t, won’t and can’t be linked into any other targets, therefore appending dependencies to INTERFACE_LINK_LIBRARIES has no practical use.
General-purpose property commands
At the start of this post I had mentioned that there are multiple, general-purpose commands for operating on targets. As a small aside, and curiosity let’s see how these commands could be used instead of taret_link_libraries to achieve the same goal.
First we’ll look at the more specialized set_target_properties. With this command it is relatively easy to modify both LINK_LIBRARIES and INTERFACE_LINK_LIBRARIES in a single call:
add_executable(Main main.cpp)
PrintTargetLibraries(Main "Before linking Add:")
set_target_properties(Main
PROPERTIES
LINK_LIBRARIES Add
INTERFACE_LINK_LIBRARIES Add
)
PrintTargetLibraries(Main "After linking Add:")
The above call is equivalent to calling target_link_libraries with the PUBLIC specifier. Configuring and building the project would result in the same output. Note however, that here we set these properties, instead of appending to them – had they contained any values already, we’d clobber them. That’s why using target_link_libraries is both more convenient and readable.
And for completeness let’s now see how we could achieve the same thing using set_property – the most general command for property manipulation.
add_executable(Main main.cpp)
PrintTargetLibraries(Main "Before linking Add:")
set_property(TARGET Main
APPEND
PROPERTY
LINK_LIBRARIES Add
)
set_property(TARGET Main
APPEND
PROPERTY
INTERFACE_LINK_LIBRARIES Add
)
PrintTargetLibraries(Main "After linking Add:")
With set_property we need to specify explicitly that we’d like to manipulate properties on a TARGET. As an improvement, compared to set_target_properties we get to specify APPEND mode – the existing values won’t be overriden, but rather appended to. The trade-off here is that we can modify only a single property at a time, so to imitate a call to target_link_libraries with the PUBLIC specifier, two calls are necessary.
This was just for demonstration purposes – in real code prefer the right tool for the job, in this case – target_link_libraries.
Summary
In this part of the series we introduced a few new commands, but most importantly we got the chance to understand exactly what the PRIVATE, INTERFACE and PUBLIC keywords do and how they’re implemented. We learned enough about properties to be able to reason about what to expect from commonly used target-commands, and we’re now ready to explore the concept further.
You may have noticed that even though the Add library was isolated into a separate directory it was not fully encapsulated – we still used relative imports in the main.cpp, which could be seen as a leaky abstraction. In the next part of the series we address this issue and discuss modular design. We will also see practical examples of using INTERFACE and PUBLIC keywords, and we extract some guidelines for when to use each specifier.
Did you learn anything new from what we discussed in this post? Do you expect that any of it will be directly applicable to your own code and daily work?
References
- Git Gist
- set_property command documentation
- get_property command documentation
- set_target_properties command documentation
- get_target_property command documentation
- LINK_LIBRARIES property documentation
- INTERFACE_LINK_LIBRARIES property documentation
This is very useful article. It reveals some crucial concepts of cmake.
Jeremi, this is an excellent guide. While “Professional CMake” by Craig Scott (@crascit on X) is the best CMake reference I’ve found, yours is the best CMake tutorial I’ve found. Thank you!
Tank you for the kind words!