Handling dependencies with CMake

Apr 17, 2025#  cmake, tutorial, C++, buildsystem

In the previous article we saw how to customize your project based on context and user input.

While this is enough to write a simple application, complex software often has dependencies to other projects, libraries, etc. In this blog post you will learn a few of the various ways 1 to consume such dependencies in CMake.

The simple (but less reliable) way

You’ve learned in the 1st article that one could use add_subdirectory to add a folder to your CMake project. Well this can effectively be used to add other projects as a dependency ! Simply write add_subdirectory(folder/of/dependency) and it will be added to your build.

If you need to configure a new default value for options of the dependency, you may do so by defining them before the call to add_subdirectory.

Let’s look at the following example of a CMakeLists.txt located in your external folder:

set(CMAKE_FOLDER external) # Tells CMake to regroup targets in the "external" folder for IDEs that support it.

# If you don't want to build/run the tests of our dependencies.
set(BUILD_TESTING_BCKP ${BUILD_TESTING}) # Back up the value
set(BUILD_TESTING OFF CACHE BOOL "Force disable of tests for external dependencies" FORCE)

# Those options are defined by tracy, but since we define them before
# the value we provide will be the default.
# It is advised to use the same comment since it will also be the one used.
option(TRACY_ON_DEMAND "On-demand profiling" ON)
option(TRACY_NO_SAMPLING "Disable call stack sampling" ON)
add_subdirectory(tracy) # Try it, it's a good profiler!

# Restore BUILD_TESTING back to its previous value
set(BUILD_TESTING ${BUILD_TESTING_BCKP} CACHE BOOL "Build tests (default variable for CTest)" FORCE)

As you can see, we start seeing some code to handle things we don’t really want in our build, there are quite a few limitations related to the usage of add_subdirectory:

I think this is fine for small projects that are not consumed by others, but if you have a big project or something you need to share (a library for example), then keep on reading.

Packages: The “right” way

Most package managers or other dependency ingestion mechanisms in CMake rely on something a bit more suited to the task: find_package. As the name hints, its purpose is to look for a (often prebuilt) package. This is the preferred way to look for your dependencies. While writing this article, it supports 3 different modes: Module, Config, and FetchContent redirection.

If simply consuming a package, the different modes do not really matter to you. Simply know that the Module mode is mostly used to find system/pre-installed libraries that do not natively support CMake, and are usually built-in scripts. The Config mode is for any dependency that you install (either yourself or through a package manager), and is the one you should prefer nowadays.

Looking for packages

Let’s first have a look at the most used parameters of the function: find_package(<PackageName> [version] [EXACT] [MODULE|CONFIG] [REQUIRED] [COMPONENTS].

Each package may expose targets or libraries in a different way, but the Modern CMake convention is to expose them under a namespace. For example let’s look for the fmt and TracyClient libraries:

find_package(fmt CONFIG REQUIRED)
find_package(Tracy CONFIG REQUIRED)

target_link_libraries(MyTarget 
  PRIVATE
    fmt::fmt # namespace is fmt, target is fmt
    Tracy::TracyClient # namespace is Tracy, target is TracyClient
)

Where does CMake look for packages?

It depends.

But the main rule is that it will look for a file named <lowercasePackageName>-config.cmake/<PackageName>Config.cmake for CMake in Config mode or FindXXXX.cmake in Module mode.

While the whole procedure is a bit complicated, it can be summed up by looking for those files in the directories pointed to by the variables:

  1. CMAKE_FIND_PACKAGE_REDIRECTS_DIR: Used by package managers to redirect to their own packages
  2. <PackageName>_ROOT: This lets you provide the location of a specific package
  3. CMAKE_PREFIX_PATH: If you install your dependencies in a single place, this is the easy way. (Can be a CMake variable, or an environment variable).
  4. Your PATH environment variable
  5. The CMake Package Registry: If you install/export a dependency it can be added to the registry if CMAKE_EXPORT_NO_PACKAGE_REGISTRY isn’t set.

You will most likely want to use the package manager option, or CMAKE_PREFIX_PATH.

Manual installation of a dependency

Assuming your dependency may be built with CMake and supports the install target, all you need to do is to configure (cmake -B buildtree), build (cmake --build buildtree) and install it: cmake --install buildtree.

You will probably want to specify the following parameters:

For example: cmake --install out/build --install out/install --config RelWithDebInfo.

Handling installation in your project will be the subject of a coming article.

Common Packge Specification

As I was writing those lines, Kitware announced a new standard for package descriptions: the Common Package Specification.

While still at the experimental stage, the good news is that the part about consuming packages didn’t change, you will still use find_package just as described here.


That’s it for this article ! In the next one you will learn how to create your own packages, and how to create installers with CMake.

Footnotes

  1. There are many ways to consume dependencies in CMake. Be it preinstalled Modules, FetchContent, ExternalProject, add_subdirectory or even using third party package manages such as CPM or VCPKG

  2. As mentionned in the previous article, CMAKE_*_FLAGS are meant to be modified only by the user or toolchain files!

photoClément GRÉGOIRE

Clément GRÉGOIRE

Performance & Optimization Expert
# C++PerformanceVideo GamesRendering