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
:
- You will always need to build the dependency as part of your normal build.
- This may not scale well for big projects.
- You may not want to build all targets of your dependencies (such as tests, see the gymnastics in the example).
- Subprojects may have side-effects in their CMakeLists.txt
- They may use a custom list of configurations
- Someone down the line may mess with cache variables such as
CMAKE_CXX_FLAGS
(Please don’t! 2). - Conflicts when dependencies are shared between dependencies (doesn’t scale well)
- And the list goes on…
- You need to access to the source code of the dependency, and it must use CMake
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]
.
Version
can be used to only allow a compatible version (compatibility is defined by the package).- For a specific version, you can use the following format:
major[.minor[.patch[.tweak]]]
- For a version range you can use
versionMin...[<]versionMax
. Bounds are included by default, but you may use excludeversionMax
by specifying<
- If you need the exact version, add
EXACT
- For a specific version, you can use the following format:
REQUIRED
CMake will stop with an error if the package is not found- Otherwise, you can rely on the value of
<PackageName>_FOUND
.
- Otherwise, you can rely on the value of
COMPONENTS
Can be used to look for specific parts of a package. This is most useful for big dependencies such asQt
,Boost
, etc.- [MODULE|CONFIG] Force either
CONFIG
orMODULE
mode.CONFIG
can be useful if the dependency provides its own package configuration file, and you want to make sure not to use the built-in module.
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:
CMAKE_FIND_PACKAGE_REDIRECTS_DIR
: Used by package managers to redirect to their own packages<PackageName>_ROOT
: This lets you provide the location of a specific packageCMAKE_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).- Your
PATH
environment variable - 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:
- Destination directory:
--prefix <directory>
- Configuration to install:
--config <cfg>
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
-
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 ↩
-
As mentionned in the previous article,
CMAKE_*_FLAGS
are meant to be modified only by the user or toolchain files! ↩