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.
In the previous lesson, we saw how to define targets and attach properties to them. We learned that the PUBLIC
, PRIVATE
, and INTERFACE
keywords control how these properties are shared with direct consumers. A consumer that links to our library will inherit its PUBLIC
and INTERFACE
properties, but not its PRIVATE
ones.
This is the key to building modular components, but it's only half the story. Real-world projects are not simple, linear chains of dependencies. They are complex webs, where an application might depend on a library, which in turn depends on several other libraries, and so on.
This lesson explores how CMake manages these indirect relationships. We'll see how target_link_libraries()
does more than just specify a link; it builds a dependency graph.
By understanding this graph, you'll see how CMake automatically handles transitive dependencies and solves the age-old problem of link order, freeing you from one of the most tedious aspects of C++ development.
Indirect Dependencies through Targets
In addition to properties being PUBLIC
, PRIVATE
, or INTERFACE
, the links between our targets can also have these visibility settings. For example, when we specify that GreeterApp
depends on GreeterLib
, we can declare that relationship PUBLIC
:
target_link_libraries(GreeterApp PUBLIC GreeterLib)
Setting the visibility of the relationships between targets is what enables CMake to do a lot of heavy lifting once our projects get large. It allows us to define only the direct dependencies, whilst CMake handles all the indirect dependencies automatically.
For example, let's imagine we introduce another header-only library, Calendar
, that depends on Date
. Compiling Date
's header files requires C++20, so Calendar
needs to keep track of that requirement. Our Date
properly specifies that requirement using a target_compile_features()
with INTERFACE
visibility, so it will automatically be propagated to Calendar
when we link them using target_link_libraries()
.
However, what if another library, such as GreeterLib
links to Calendar
?

Now, GreeterLib
also requires C++20 to compile successfully. This is true even when:
GreeterLib
doesn't directly link toDate
, it only links toCalendar
. It may not even know thatDate
exists and it shouldn't need to. FromGreeterLib
's perspective, it just wants to useCalendar
and doesn't care about its internal implementation details.Calendar
doesn't directly specify that it needs C++20. It only inherits that requirement fromDate
.
In spite of both of these constraints, GreeterLib
still has a transitive dependency on Date
, so it needs to respect its requirements and use C++20.
These webs of dependencies can get extremely complex in larger projects, with dozens of targets and hundreds of direct and indirect connections between them. Managing this dependency graph is one of the strengths of CMake.
Visibility of Relationships
The target_link_libraries()
command is the engine that allows CMake to construct this dependency graph. Let's imagine we have this command:
target_link_libraries(Calendar PUBLIC Date)
When you write this, you are declaring that Calendar
depends on target Date
. Accordingly, CMake will now ensure that Calendar
inherits all of Date
's PUBLIC
and INTERFACE
properties, including the C++20 requirement.
Furthermore, because we used the PUBLIC
keyword in the target_link_libraries()
call itself, that means this relationship between Calendar
and Date
is also visible and inheritable. It means that any target that links to Calendar
will now also be transitively linked to Date
, and will also inherit Date
's PUBLIC
and INTERFACE
properties in much the same way that Calendar
did.
Let's continue to build our graph by declaring that GreeterLib
depends on Calendar
:
target_link_libraries(Calendar PUBLIC Date)
target_link_libraries(GreeterLib Calendar)
Not only will CMake understand it needs to link GreeterLib
to Calendar
, it will also understand that there is now an indirect, transitive relationship between GreeterLib
and Date
that it needs to manage.

Default Visibility of Relationships
When we do not specify the visibility of a relationship within target_link_libraries()
, the relationship is PRIVATE
by default:
# These are equivalent
target_link_libraries(B C)
target_link_libraries(B PRIVATE C)
This means that targets that link to B will not be aware that B links to C, and will therefore not inherit C's properties:
# A PRIVATE relationship
target_link_libraries(B C)
# This links A to B, but does NOT link A to C
# This is because B's relationship to C is PRIVATE
target_link_libraries(A B)
We walk through a practical example of where PRIVATE
relationships are useful in the next lesson.
Automatic Link Ordering
One of the most frustrating problems in traditional C/C++ builds is managing the link order. When you call the linker directly, the order in which you list your libraries matters. If library A
uses functions from library B
, you typically need to link them in the order A
, then B
.
Manually tracking these dependencies and ensuring the correct order for a project with dozens of libraries is a tedious and fragile process.
CMake completely solves this problem. It doesn't think in terms of a linear list of libraries. As we've seen earlier in the chapter, it thinks in terms of a dependency graph.

Every time you use target_link_libraries(A B)
, you are adding a connection to this graph, from A
to B
. When it's time to generate the final link command, CMake analyses the graph to generate a linear list of all the libraries. The algorithm that generate this list of targets ensures that they are ordered in a way that satisfies all of the dependencies.
Our only job is to accurately describe the direct dependencies for each target. CMake handles the rest. If App
links to Renderer
, and Renderer
links to Core
, CMake knows that the final link command must list them in an order that respects that chain.
Circular Dependencies
If our dependency graph contained a cycle, that would represent a circular dependency. For example, if Physics
depends on Core
, which depends on Math
, which depends on Physics
, then we introduce a cycle:

CMake, and most other build systems, cannot resolve circular dependencies. If we create one, we will get an error:
target_link_libraries(A B)
target_link_libraries(B A)
CMake Error: The inter-target dependency graph contains the following cycle:
- "A" links to "B"
- "B" links to "A"
This is not allowed.
Circular dependencies like this are almost always a sign of a design flaw in your project, which should be fixed regardless of any CMake limitations. Breaking a circular dependency typically involve one of the following options:
- If the two components are so tightly coupled that reliance on this circular dependency is pervasive through one or both of the libraries, they can be merged into a single component
- If the circular dependency is required for only a small piece of functionality, that part can be refactored so it exists entirely within one of the libraries, or within a new, third library that they both can use
Summary
This lesson pulled back the curtain on how CMake manages the complex web of relationships in a project. By thinking in terms of a graph rather than a list, CMake automates some of the most difficult parts of C++ build configuration.
- Dependency Graph: Every
target_link_libraries()
call adds a directed edge to a graph. CMake uses this graph to understand the complete structure of your project. - Transitive Dependencies: When a link is
PUBLIC
, the dependency is passed on to consumers. IfApp
links toLibA
, andLibA
publicly links toLibB
,App
automatically gets linked toLibB
as well. - Automatic Link Order: CMake performs a topological sort on the dependency graph to determine the correct order for libraries on the linker command line, solving a common and frustrating build problem.
- No Circular Dependencies: A direct or indirect dependency of a target on itself forms a cycle in the graph. CMake will report this as a fatal error, which usually indicates a flaw in your project's design.
Using INTERFACE
, ALIAS
, and IMPORTED
Libraries
Learn to use abstract target types like INTERFACE
, ALIAS
, and IMPORTED
to model complex project needs, organize build properties, and integrate pre-compiled binaries.