Relationships Between Targets

Learn about target types, and how the PUBLIC, PRIVATE, and INTERFACE keywords control how properties are shared.

Greg Filak
Updated

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).

Static and Shared Library Targets using add_library()

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:

  1. 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. An INTERFACE library is the perfect way to model this relationship. We'll walk through a practical example of this later in the lesson.
  2. 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 uses target_include_directories() to specify where its header files are. This is conceptually setting a usage requirement - "if something wants to use GreeterLib, it will need to set these include directories"
  • GreeterApp uses target_link_library() to link to GreeterLib. This is conceptually saying "GreeterApp wants to use GreeterLib"

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 to SomeLibrary
  • An INTERFACE property applies only to targets that link against SomeLibrary
  • A PUBLIC property applies both to SomeLibrary and to targets that link against SomeLibrary

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.
Next Lesson
Lesson 21 of 25

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.

Have a question about this lesson?
Answers are generated by AI models and may not be accurate