Relationships Between Targets
Learn about target types, and how the PUBLIC
, PRIVATE
, and INTERFACE
keywords control how properties are shared.
In the last lesson, we learned to treat CMake targets as self-contained objects, each with its own set of properties. We saw how to attach information like include paths and compile definitions to a specific library or executable.
But a project is more than a collection of isolated components; it's a network of dependencies. What happens when one target needs to use another? How does our application automatically know where to find the headers for the library it uses? Manually adding every library's include path to every application that uses it would be a repetitive and error-prone nightmare.
Thankfully, this is something that CMake can help with. We will fully explore the target_link_libraries()
command, seeing how it does much more than just link. We'll also cover the PUBLIC
, PRIVATE
, and INTERFACE
keywords, which control how usage requirements automatically flow from a library to its consumers, creating a robust and self-managing build system.
Understanding CMake Targets
A target is any output of the build system. The most common targets are executables and libraries, but they can also be custom commands for things like generating documentation, which we'll cover later. Everything in a modern CMake project revolves around defining targets and manipulating their properties.
We've already seen the commands for creating targets: add_executable()
and add_library()
. Let's review add_executable()
quickly, and then expand upon the add_library()
command.
Executable Targets using add_executable()
This is the most straightforward target type. It represents a standalone program that can be run.
add_executable(MyApp src/main.cpp src/app.cpp)
This creates a target named MyApp
that, when built, will produce an executable file (e.g., MyApp.exe
on Windows or MyApp
on Linux).
The add_library()
command can create two kinds of "real" libraries that are built from source files, as well as a more abstract "interface" library. Let's introduce all three.
Static Libraries
To specify our library should be static, we add the STATIC
keyword. The library's compiled code is then copied into any target that links against it at build time:
# Creates libPhysics.a or Physics.lib
add_library(Physics STATIC src/dynamics.cpp src/collision.cpp)
If we don't specify the type, CMake will create a STATIC
library by default:
# This will create a STATIC library by default
add_library(Physics src/dynamics.cpp src/collision.cpp)
This default behavior is is something we can change, and it is quite an important setting. We'll revisit this topic later in the chapter to explain how we can change this behavior, and why we'd want to.
Shared/Dynamic Libraries
These are created with the SHARED
keyword. The library is a separate file (.so
or .dll
) that is loaded by the operating system at runtime:
# Creates libRenderer.so or Renderer.dll
add_library(Renderer SHARED src/window.cpp src/shader.cpp)
Whilst this will build a shared library, it may not automatically place that library in a location where our executable can find it. We'll cover how to automate this process in more detail later in the course.
Interface Library Targets
An interface is a "virtual" target type that doesn't compile any source code and doesn't produce a library file on disk. So, what is it for?
An INTERFACE
library is a way to bundle together usage requirements. It's a target that exists only to have properties attached to it, which can then be inherited by other targets that "link" to it.
add_library(MyHeaderOnlyLib INTERFACE)
We'll see practical examples soon, but an INTERFACE
library is perfect for two common scenarios:
- Header-Only Libraries: A library that consists only of header files and has no source to compile. However, any target that uses it needs to have its
include
directory added to its search path. AnINTERFACE
library is the perfect way to model this relationship. We'll walk through a practical example of this later in the lesson. - Grouping Properties: You can create an
INTERFACE
library to represent a set of common properties, like a group of compiler settings. Any target that links to this interface library can inherit those properties. We'll walk through an example of this in the next lesson.
This is a little abstract so don't worry if interface libraries don't make much sense yet. We'll explain what "usage requirements" and "inheriting properties" means through the rest of this lesson, and see several practical examples of INTERFACE
libraries through the rest of the chapter.
The PUBLIC
, PRIVATE
, and INTERFACE
Keywords
This is one of the most important concepts in CMake. When you add a property (like an include directory or a compile definition) to a target, we will typically also specify the visibility of that property. This tells CMake how the property should propagate to other targets that depend on it.
Usage Requirements and Properties
When we use a command like target_include_directories()
, we're setting what CMake conceptually refers to as usage requirements. "If you want to use my library, you'll need to set these include directories".
From a lower level technical perspective, what we're doing is setting specific properties on that target.
The benefit of CMake is that it can automatically copy these usage requirements (by copying the underlying properties) to the targets that need them. For example, let's consider two commands in our current CMake configuration
GreeterLib
usestarget_include_directories()
to specify where its header files are. This is conceptually setting a usage requirement - "if something wants to useGreeterLib
, it will need to set these include directories"GreeterApp
usestarget_link_library()
to link toGreeterLib
. This is conceptually saying "GreeterApp
wants to useGreeterLib
"
CMake, seeing both of these commands, can jump into action and automatically ensure GreeterApp
does indeed set the include directories it will need to use GreeterLib
.
When we use a command like target_include_directories()
, we are setting properties on the target. We have the opportunity to specify the visibility of those properties as either PUBLIC
, PRIVATE
, or INTERFACE
. Using PRIVATE
, for example, would look like this:
target_include_directories(SomeLibrary PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)
We'll cover all three of these options with practical examples in the next section but, from a high level:
- A
PRIVATE
property applies only toSomeLibrary
- An
INTERFACE
property applies only to targets that link againstSomeLibrary
- A
PUBLIC
property applies both toSomeLibrary
and to targets that link againstSomeLibrary
Let's cover all three of these with some practical examples.
Practical Examples
Let's expand on our Greeter
project to understand these three keywords. Our project currently has two targets: the GreeterApp
executable and the GreeterLib
library.
GreeterApp
depends on GreeterLib
, and we have already informed CMake about this dependency using the target_link_libraries()
command:
greeter/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
add_executable(GreeterApp src/main.cpp)
target_link_libraries(GreeterApp GreeterLib)

Practical Example: PRIVATE
Imagine GreeterLib
has an internal helper header, src/InternalDetails.h
, that is used by our src/Greeter.cpp
:
greeter/src/InternalDetails.h
#pragma once
namespace InternalDetails {
inline const std::string Name{"the modular Greeting Library"};
}
greeter/src/Greeter.h
#include <greeter/Greeter.h>
// We should #include "InternalDetails.h" as the header
// is in the same directory as this source file. We'll
// use <> as we want the preprocessor to look only in the
// search paths defined by CMake for this contrived example
#include <InternalDetails.h>
std::string get_greeting() {
return "Hello from " + InternalDetails::Name + "!";
}
To make this #include
directive work, we need to add the /src
directory to GreeterLib
's include directories.
However, GreeterApp
, and any other target that might use our library in the future, does not need to know or care about this internal implementation detail.
As such, we'd make this target_include_directories()
command PRIVATE
. A PRIVATE
property applies only to the target it's set on. It is an internal implementation detail and is never seen by any target that uses our library:
greeter/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
add_library(GreeterLib src/Greeter.cpp)
# The 'include' directory continues to be public
# Consumers need to know these header files
target_include_directories(GreeterLib PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/include"
)
# The 'src' directory is a PRIVATE implementation detail.
# Only GreeterLib's own source files need this path.
target_include_directories(GreeterLib PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)
When we link GreeterApp
to GreeterLib
, GreeterApp
will not have greeter/src
directory added to its include paths, because that was a PRIVATE
detail. This is exactly what we want - we're encapsulating GreeterLib
's internal build requirements, without those requirements unnecessarily affecting other components.
Practical Example: INTERFACE
An INTERFACE
property applies only to consumers of a target. The target itself doesn't use the property when it's being built. This is the perfect keyword for header-only libraries.
Let's add a tiny header-only library to our project that can return the current day. We'll follow the "namespaced include" structure of our greeter
directory. In this case, our library is just a single header file - we don't need a /src
folder:
date/include/date/Today.h
#pragma once
#include <string>
#include <chrono>
#include <format>
namespace Date {
inline std::string Today() {
return std::format(
"{:%A}", std::chrono::system_clock::now()
);
}
}
As usual, we'll create a CMakeLists.txt
file for our library, making it easy to integrate into our build. Any consumer of our Date
library will need this date/include
directory added to its search paths.
However, the Date
target itself does not need this property. It's a header-only library, so it doesn't get directly compiled - all compiling is done by the targets that #include
our header. As such, it doesn't need include directories, only the targets that use it, so we'll mark these include directories as an INTERFACE
.
Additionally, our library requires C++20 for std::format()
, so any target that uses our library will need to support that standard. Let's express that requirement using target_compile_features()
. Again, we express this requirement with INTERFACE
visibility, because our Date
library doesn't get compiled directly, so it doesn't require this standard internally.
date/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
# No source files, so this is an INTERFACE library
add_library(Date INTERFACE)
# The include directory is an INTERFACE property.
# Date itself doesn't need to compile anything
# However, anyone who uses it needs this path.
target_include_directories(Date INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include"
)
# We're using std::format, which was added in C++20
# Anyone linking against us will need to support that
target_compile_features(Date INTERFACE cxx_std_20)
We can add this new CMakeLists.txt
file to our build in the usual way, with an add_subdirectory()
command:
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(Greeter VERSION "1.0")
add_subdirectory(greeter)
add_subdirectory(app)
add_subdirectory(date)
Now, any target that needs to use our library can link against it using target_link_libraries()
as usual:
app/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
add_executable(GreeterApp src/main.cpp)
target_link_libraries(GreeterApp GreeterLib)
target_link_libraries(GreeterApp Date)
Given that our Date
command used the INTERFACE
visibility when setting /date/include
as an include directory, our GreeterApp
target that linked against it has also inherited that include directory:
#include <iostream>
#include <greeter/Greeter.h>
#include <date/Today.h>
int main() {
std::cout << get_greeting();
std::cout << "\nHappy " << Date::Today();
return 0;
}
If we build everything and run GreeterApp
or GreeterApp.exe
(or Greeter
/ Greeter.exe
if you updated OUTPUT_NAME
property in the previous lesson) we should see everything working:
cmake --build .
./app/GreeterApp
Hello from the modular Greeting Library!
Happy Thursday
Practical Example: PUBLIC
A PUBLIC
property is simply a combination of PRIVATE
and INTERFACE
. The property is applied to the target itself and it's propagated to any consumers.
This is the most common keyword you'll use for a typical library's public headers. The library's own source files usually needs to #include
its own public headers, and so do any other targets that use the library.
We're already using a PUBLIC
property for our library's /greeter/include
directory:
greeter/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
add_library(GreeterLib src/Greeter.cpp)
# The 'include' directory contains public headers.
# GreeterLib needs it for its own compilation, AND
# GreeterApp needs it to use the library.
target_include_directories(GreeterLib PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/include"
)
This path is required by the GreeterLib
target itself for the #include
directive in greeter/src/Greeter.cpp
. As such, we shouldn't make it an INTERFACE
.
Any other target that wants to use this library, such as GreeterApp
, will also need to know about this include directory, so it shouldn't be PRIVATE
either.
The property is needed both by the target itself, and by targets that depend on it, so we declared that property as PUBLIC
.
As a consequence, when our GreeterApp
target linked against GreeterLib
using target_link_libraries(GreeterApp GreeterLib)
, it inherited that include directory automatically.
That is what allowed the highlighted #include
directive to work:
app/src/main.cpp
#include <iostream>
#include <greeter/Greeter.h>
#include <date/Today.h>
int main() {
std::cout << get_greeting();
std::cout << "\nHappy " << Date::Today();
return 0;
}
Summary
This lesson expanded on the core of CMake, covering how it establishes the relationship between our targets as a web of dependencies, and then automatically manages the indirect links in that web.
Four Target Types: Executables, STATIC
libraries, SHARED
libraries, and INTERFACE
libraries.
The Three Keywords: This is the most important concept.
PRIVATE
: For internal implementation details. Does not propagate to consumers.INTERFACE
: For consumers only. Used by the target that links to you.PUBLIC
: For both. Used by you and your consumers.
Indirect Relationships and the Dependency Graph
Discover how CMake's target_link_libraries()
command builds a dependency graph, and how it uses this graph to automatically manage transitive dependencies and link order.