CMake has been created in 2000 by Bill Hoffman from Kitware. During the last 20 years, as of the time of this publication, it’s been constantly evolving by adding new features and expanding its support for third-party libraries. But the most significant additions were released in version 3.0 and are commonly called “Modern CMake”. Despite being available for more than 5 years, many programmers are still not using them!
Today I will show you one of the greatest “Modern CMake” feature, that behaves almost like C++ inheritance. But before we get there, let me briefly explain a few fundaments.
Modern CMake = targets + properties
Target is a fundamental concept in CMake. It generally represents one “job” of the building process. Most commonly used targets are executables and libraries. Let’s see the syntax for creating and using them:
add_executable(myExecutable
main.cpp
)
add_library(libA
sourceA.cpp
)
add_library(libB
sourceB.cpp
sourceB_impl.cpp
)
add_library(libC
sourceC.cpp
)
target_link_libraries(myExecutable libA)
target_link_libraries(libA libB)
target_link_libraries(libB libC)
This is a classic way of creating targets. Here we have an executable called myExecutable
and three libraries libA
, libB
and libC
. For now we know, that in order to build myExecutable
we have to link with libA
, for building libA
we have to link with libB
and for building libB
we have to link with libC
. The last one has no dependencies.
Let’s keep in mind, that those are not the only possible targets that can be defined in CMake. We can create also custom targets, whose only role is to invoke some command:
add_custom_target(firmware.bin
COMMAND ${CMAKE_OBJCOPY} -O binary firmware firmware.bin
DEPENDS firmware
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
)
In the snippet above we are defining a custom target named firmware.bin
, whose only objective is to create a BIN file from the original executable using GNU objcopy (if you are not familiar with objcopy, then think of this as some conversion of the binary file). Here we explicitly say, that firmware.bin
depends on firmware
. This is natural, because in order to convert firmware file it must be already built!
Note
We could also skip the line with DEPENDS
and add add_dependencies(firmware.bin firmware)
instead as a separate statement. But in my opinion, it is less elegant.
Each target can have its own set of properties, that will be used in proper contexts. Here is a list of the most popular ones:
- compilation flags,
- linking flags,
- preprocessor flags,
- C/C++ standard,
- include directories.
All of the above properties are stored in the special CMake variables and are automatically used by the build system, when the given target appears in the certain context. For example, include directories property is automatically added to the compilation flags when the target is being compiled. Linking flags are automatically passed to the linker, when this target is being linked. You may say, that all these properties are either CFLAGS
or LDFLAGS
so there is no need to extract them into individual entities. But we will see in a moment, that this separation can be quite handy in Modern CMake projects.
Setting properties: include directories, preprocessor, compilation and linking flags
Target properties can be set in at least few ways. Before CMake 3.x we could either set raw CMake flags (e.g. CMAKE_C_FLAGS
, CMAKE_LINKER_FLAGS
) or use directory-oriented commands, which set given property for all targets in the current directory and its subdirectories. Modern CMake introduced a new set of target-oriented commands, which allow us to set properties for targets individually.
Below you can find a comparison of the old directory-oriented commands and their new recommended alternatives in Modern CMake:
Directory-oriented old commands | Target-oriented new commands |
---|---|
include_directories(<include_path>) | target_include_directories(<target> [VISIBILITY] <include_path>) |
add_definitions(<preprocessor_flags>) | target_compile_definitions(<target> [VISIBILITY] <preprocessor_flags>) |
set(CMAKE_CXX_FLAGS <compilation_flags>) | target_compile_options(<target> [VISIBILITY] <compilation_flags>) |
set(CMAKE_LINKER_FLAGS <linker_flags>) | target_link_options(<target> [VISIBILITY] <linker_flags>) |
Modern CMake introduced also new keywords, that specify visibility of the given target property: PRIVATE
, PUBLIC
, INTERFACE
. Their meanings are as follows:
PRIVATE
property is only for the internal usage by the property owner,PUBLIC
property is for both internal usage by the property owner and for other targets that use it (link with it),INTERFACE
property is only for usage by other libraries.
This is very similar to the access specifiers in a C++ class! Each of the new commands allow visibility specification. If none is provided, then PUBLIC
is assumed. Note, that each command can have multiple properties set at different visibility level:
target_include_directories(<target>
INTERFACE <include_path_1> <include_path_2> <include_path_3>
PUBLIC <include_path_4> <include_path_5> <include_path_6>
PRIVATE <include_path_7> <include_path_8> <include_path_9>
)
Note
Targets that don’t produce any binaries (e.g. header-only libraries) can have only INTERFACE
properties and can only use INTERFACE
linking. This is quite understandable, because there is no “internal” part in such targets, so PRIVATE
keyword doesn’t mean anything.
Using (linking with) libraries behaves like inheritance
In order to link libraries together we use the expression:
target_link_libraries(<TARGET_A> <TARGETS...>)
Modern CMake extended this command with the visibility specifier like this:
target_link_libraries(<TARGET_A> [VISIBILITY] <TARGETS...>)
Again, visibility can be one of PRIVATE
, PUBLIC
and INTERFACE
. If none is provided then PUBLIC
is used by default. But what does it mean in terms of linking?
CMake 3.x introduced a very important “side effect” of linking with targets: linked target is passing all its PUBLIC
and INTERFACE
properties to the library that it links with. So for example, if libA
is linking with libB
then libA
gets all PUBLIC
and INTERFACE
properties of libB
. PRIVATE
properties are still not accessible. Another question is: what is the visibility of the newly obtained set of properties (by libA
)? Answer is simple: it’s the same as the specifier used in target_link_libraries()
for that target. So if libA
links as PRIVATE
with libB
, then all PUBLIC
and INTERFACE
properties of libB
become PRIVATE
properties of libA
. Similarly, if it links as PUBLIC
, then all PUBLIC
and INTERFACE
properties of libB
become PUBLIC
in libA
. The same goes for INTERFACE
linking.
Can you see now, that this looks almost identical as inheritance in C++? Private inheritance makes all public and protected members private in the derived class and public inheritance keeps the visibility unchanged.
In order to understand it better, let’s see some use cases.
Example 1: avoiding header dependencies
Let’s assume the following directory structure:
libA/
- include/
- libA/
- sourceA.h
- privateHeaderA1.h
- privateHeaderA2.h
- sourceA.cpp
libB/
- include/
- libB/
- sourceB.h
- submodule/
- submodule.h
- submodule.cpp
- privateHeaderB1.h
- privateHeaderB2.h
- sourceB.cpp
- sourceB_impl.h
- sourceB_impl.cpp
libC/
- include/
- libC/
- sourceC.h
- privateHeaderC1.h
- privateHeaderC2.h
- sourceC.cpp
main.cpp
and the following include dependencies in code:
// sourceB.cpp
#include "libC/sourceC.h"
#include "submodule.h"
// ...
// sourceA.h
#include "libB/sourceB.h”
// ...
// main.cpp
#include "libA/sourceA.h"
#include "libC/sourceC.h"
// ...
Also let’s define a rule, that we don’t want any library to be able to use “private” headers of the other libraries: e.g. the code below shouldn’t compile:
// main.cpp
#include "privateHeaderC2.h” // should fail as "no such file or directory"
This restriction is a good architectural practice, that can keep the code clean from unwanted dependencies. How to make that compile without a messy config?
First we have to check each target and determine the include paths that it is “creating”. By this I mean which include paths belong to this particular target. Then for each path in a given target we have to decide, if it should be accessible by others (PUBLIC
) or not (PRIVATE
). Finally we will use new target-oriented commands to set the include properties for each library.
add_executable(myExecutable
main.cpp
)
add_library(libA
sourceA.cpp
)
target_include_directories(libA
PUBLIC include
PRIVATE . # "dot" is redundant, because local headers are always available in C/C++.
)
add_library(libB
sourceB.cpp
submodule/submodule.cpp
)
target_include_directories(libB
PUBLIC include
PRIVATE . submodule/ # "dot" is redundant, because local headers are always available in C/C++.
)
add_library(libC
sourceC.cpp
)
target_include_directories(libC
PUBLIC include
PRIVATE . # "dot" is redundant, because local headers are always available in C/C++.
)
All these targets have one thing in common: the only PUBLIC
include path is the include
directory. This means, that if other libraries call only target_link_libraries()
to both get include paths and link with library, then no private header will ever leak unintentionally outside the containing library.
Now its time to properly link the libraries:
target_link_libraries(myExecutable
PRIVATE libA libC
)
target_link_libraries(libA
PUBLIC libB
)
target_link_libraries(libB
PRIVATE libC
)
Observe the following things:
- Executable doesn’t need to specify linking type (because nothing can link with exec), but we define it for consistency.
libA
links publicly withlibB
, because it is using header fromlibB
in its own public header. So it has to provide this path to its clients.libB
link privately withlibC
, because it is using header fromlibC
only in its internal implementation and its clients shouldn’t even be aware of this.target_link_libraries()
means in Modern CMake two things: use library (get its properties) at compilation stage and link with it at linking stage. Hence maybe a bit better name for it would betarget_use_libraries()
but it would break the backward compatibility.
Example 2: defining header-only libraries
Sometimes we have to deal with libraries, that don’t produce any binaries. For example, they are just a set of headers that your application needs to include. In such a case they are called a header-only libraries.
An excellent example would be the Catch2 library, which implements the popular C++ testing framework. It consists of only one file catch.hpp
which is stored in catch2
directory. First, it would be convenient for us, to still have a CMake target that provides path to that file once someone links with it. Secondly, Catch2 allows some behavior customization via the define directives. For example, we can disable usage of POSIX signals and exceptions in favor of a call to std::terminate()
. This is particularly crucial on embedded systems, where we can’t use any of them. So our target should also be able to detect the environment and provide the proper defines accordingly.
In Modern CMake it could be expressed like this:
add_library(catch2 INTERFACE)
target_include_directories(catch2
INTERFACE catch2
)
if (<some_condition_to_detect_embedded_platform>)
target_compile_definitions(catch2
INTERFACE CATCH_CONFIG_NO_POSIX_SIGNALS CATCH_CONFIG_DISABLE_EXCEPTIONS
)
endif ()
Note the usage of INTERFACE
keyword. When add_library()
contains the INTERFACE
specifier, then it tells CMake, that this target doesn’t produce any binary. In such a case it doesn’t contain any source files.
As mentioned before, all properties of the INTERFACE
target also have to be marked as INTERFACE
. This is understandable, because header-only libraries don’t have any private implementation. Everything is always accessible to the client. If this is still confusing for you then just remember, that INTERFACE
target enforces INTERFACE
properties. But later linking with such a target can be of any type:
add_library(myTestingModule source.cpp)
target_link_libraries(myTestingModule
PRIVATE catch2
)
Summary
CMake provides a new target-oriented way of specifying various compiler options and other properties. Once you link with a target, you immediately inherit (obtain) its INTERFACE
and PUBLIC
properties and make it your own with the access level specified in the linking command. This mechanism resembles C++ inheritance, thus should be easy to understand.
If you are using CMake 3.x and above, then use the this rule as your guide for creating targets:
Library designed and built with Modern CMake should provide its clients with everything they need to compile and use it, without the need to check its internal implementation.
If you find yourself checking what is the path to the missing include in some library then it means that you are doing something wrong:
- either CMake configuration of that library is bad,
- or you are trying to access files that are explicitly hidden from you.
Subscribe to get notified about new content
Thanks for taking your time to read it. If you like it or have other opinions, please share it in the comments. You can also subscribe to my newsletter to get latest news about my content. Happy coding and let’s stay connected!