Finding Dependencies using find_package()
Learn to find and use pre-installed libraries with find_package()
, understanding Module vs. Config mode and the power of imported targets
So far, we've built our projects using only the source code we've written ourselves. But the power of C++ comes from its vast ecosystem of third-party libraries. We don't want to reinvent the wheel for every task.
But how do we incorporate a third party library? This will be the focus of this chapter. By the end of this chapter, we'll unlock the ability for our projects to automatically download the libraries we need from the internet.
However, in this first lesson, we'll focus on libraries that are already installed on our machine, and CMake just needs to find them. This is the job of the find_package()
command. We'll learn how it works, explore its two modes of operation, and master the modern, target-based approach that makes using external libraries clean and simple.
The find_package()
Command
When you call find_package(SomeLib)
, CMake embarks on a search to find SomeLib
. It has two distinct strategies for finding the library, which it tries in order.
Module Mode (The Old Way)
First, CMake checks if it has a special script called FindSomeLib.cmake
. It looks in two places:
- Its own installation directory, which contains
Find<...>.cmake
modules for many libraries that are popular in the C++ world. This includes the popular Boost libraries, which we'll use in this lesson. - Any directories listed in the
CMAKE_MODULE_PATH
variable. This means that if CMake doesn't have aFindSomeLib.cmake
script itself, we can help it out and write our own. That script will then be used when we use the commandfind_package(SomeLib)
.
A Find
script is an active, searching script. It runs a series of commands (find_path()
, find_library()
) to hunt for the library's header files and binary files in standard system locations. If it's successful, it sets a series of variables for you to use:
SomeLib_FOUND
: Set to true if the library was found.SomeLib_INCLUDE_DIRS
: A path to the library's include directories.SomeLib_LIBRARIES
: A path to the library files to link against.
You would then use these variables to configure your target in the normal way, using target_include_directories()
and target_link_Libraries()
:
target_include_directories(MyApp PRIVATE ${SomeLib_INCLUDE_DIRS})
target_link_libraries(MyApp PRIVATE ${SomeLib_LIBRARIES})
This pattern is still common in older projects, but it's considered legacy. It's verbose and requires you to manually plumb variables into your targets.
Config Mode (The Modern Way)
If CMake can't find a FindSomeLib.cmake
module, it switches to Config mode. This is the modern, preferred approach. We can also explicitly ask CMake to skip the module mode search and jump straight to config mode by passing the CONFIG
keyword to find_package()
. We'll use this in our example later in the lesson.
In config mode, CMake searches for a file that the library itself installed, trying names like SomeLibConfig.cmake
and somelib-config.cmake
. When this file is found and executed, it doesn't just set variables - it defines IMPORTED
targets. Like any CMake target, these have all the power and flexibility we've seen in previous chapters.
As we learned in our lesson on , an IMPORTED
target is a CMake target that represents a pre-existing binary on disk. The Config.cmake
file tells CMake: "Here is a target called SomeLib
. I've already attached all its properties, like its include directories and link dependencies."
This means you don't need to juggle variables. You just link to the provided target, and all the usage requirements are propagated automatically.
A Practical Example with Boost
Let's see this in action by adding a Boost library to our build. Boost has over 160 components covering a huge range of utility and it is the most used third-party library in the C++ world.
Installing Boost
Remember, find_package()
can only find what's already on our disk. So, lets start by installling the Boost development libraries on our system
- Windows (MSVC): Download the pre-built binaries from the official Boost website. Make sure to get a version that matches your compiler (e.g., MSVC 14.3 for Visual Studio 2022).
- Windows (MSYS2 UCRT64):
pacman -S mingw-w64-ucrt-x86_64-boost
- macOS (Homebrew):
brew install boost
- Linux (Ubuntu/Debian):
sudo apt-get install libboost-all-dev
The CMakeLists.txt
Let's update our GreeterApp to uses Boost's program_options
library. This makes it easier for our program to accept options on the command line. In much the same way that cmake
accepts arguments such as --config Debug
or --build .
, we'll update our GreeterApp
to accept an argument like --greeting Howdy
:
./app/GreeterApp --greeting Howdy
This greeting
argument will affect our program's behaviour:
Howdy from the GreeterApp
The code to make this happen is included in main.cpp
below but, for this course, we'll focus on the CMakeLists.txt
file rather than how to use a Boost library. Let's start by adding a find_package()
command:
Files
Let's break down the find_package()
command:
find_package(Boost ...)
: We're looking for the Boost libraries.REQUIRED
: This keyword makes the search critical. If Boost is not found, CMake will stop with a fatal error. If you omit this, the script will proceed, but Boost won't be available. We'll see how to deal withfind_package()
failures later in the lesson.COMPONENTS program_options
: Many packages, including Boost, have a range of components that we can include. In this case, we're requesting theprogram_options
component.CONFIG
: This asks CMake to skip the legacy "module mode" search and jump straight to the modern "config mode" described above.
We've now found the library, but we're not using it yet. Before our build works, our GreeterApp
target still needs to link to the library, and have its search paths updated to make its #include <boost/program_options.hpp>
directive work. We'll do that in the next section.
Legacy Linking Using Variables
After the find_package()
call, the FindBoost.cmake
module has set several variables for us. We could use the legacy pattern:
cmake_minimum_required(VERSION 3.16)
add_executable(GreeterApp src/main.cpp)
find_package(Boost REQUIRED COMPONENTS program_options)
# LEGACY: Manually use the variables
target_include_directories(GreeterApp PRIVATE
${Boost_INCLUDE_DIRS}
)
target_link_libraries(GreeterApp PRIVATE
${Boost_LIBRARIES}
)
This works, but it's not ideal. We have to know the specific variable names that the library's CMake file sets. In this case, it's Boost_INCLUDE_DIRS
and Boost_LIBRARIES
, but a different library might set different variables.
Modern Linking Using Imported Targets
Fortunately, Boost has been updated to also provide modern, imported targets. A common convention is for a find_package()
result to provide an alias for the target in the format Package::Component
.
Boost adopts this convention too, so our target will be available as Boost::program_options
. We link to it like any other target:
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
add_executable(GreeterApp src/main.cpp)
find_package(Boost REQUIRED COMPONENTS program_options)
target_include_directories(GreeterApp PRIVATE
${Boost_INCLUDE_DIRS}
)
# MODERN: Link to the IMPORTED target
target_link_libraries(GreeterApp PRIVATE
${Boost_LIBRARIES}
Boost::program_options
)
As usual with a well configured target, this target_link_libraries()
command will hook up everything we need:
- It links our
GreeterApp
to the actual Boostprogram_options
library file. - It automatically adds the necessary include directories to our target because they are an
INTERFACE
property of theBoost::program_options
imported target. - It handles any other transitive dependencies.
Let's build and run our program:
cmake --build .
./app/GreeterApp --greeting Howdy
Howdy from the GreeterApp
Optional Packages
What happens when find_package()
cannot find what we requested? If we set the package as REQUIRED
, our script will fail with an error:
find_package(SomeLib REQUIRED)
cmake ..
CMake Error at app/CMakeLists.txt:8 (find_package):
By not providing "FindSomeLib.cmake" in CMAKE_MODULE_PATH this project has
asked CMake to find a package configuration file provided by "SomeLib", but
CMake did not find one.
Could not find a package configuration file provided by "SomeLib" with any
of the following names:
SomeLibConfig.cmake
somelib-config.cmake
-- Configuring incomplete, errors occurred!
Our error output is likely to document CMake's attempt to find the package in both module mode and config mode, along with some suggestions on how we might help it. We'll cover how to help CMake find the packages we're looking for later in the chapter.
In most cases, failing with an error is what should happen. However, in some circumstances, it may be acceptable that CMake fails to find a package, as we may be able to recover. To prevent CMake from erroring on find_package()
failures, we remove the REQUIRED
keyword:
find_package(SomeLib REQUIRED)
find_package(SomeLib)
This will downgrade our error to a warning, and allow our script to continue. We can suppress the warning by adding the QUIET
keyword, if preferred:
find_package(SomeLib QUIET)
The find_package(PackageName)
command will set a PackageName_FOUND
variable if it found our package. We can use this to react as needed to success or failures:
if(NOT SomeLib_FOUND)
message(STATUS "SomeLib was not found, but I can fix it...")
# ...
endif()
-- SomeLib was not found, but I can fix it...
We'll learn some useful approaches to dealing with these optional find_package()
failures soon, including having CMake just download the missing package automatically from the internet.
Setting the Required Version
We can specify the minimum version of the library that our program requires by providing it as an argument to find_package()
after the library name:
find_package(
Boost
1.5
REQUIRED
COMPONENTS program_options
CONFIG
)
We can require an exact version of a library by adding the EXACT
keyword:
find_package(
Boost
1.5 EXACT
REQUIRED
COMPONENTS program_options
CONFIG
)
We shouldn't overspecify version requirements unless necessary. If any version of the library will work, we shouldn't specify a version requirement. If the feature we need was added in version 1.5
and continues to be available, we should set that as a minimum requirement rather than an EXACT
requirement.
Setting spurious requirements makes our code more difficult to integrate with other projects, especially if those projects have their own version requirements that might conflict with ours.
From CMake 3.19 onwards, we can specify both a minimum and maximum version using the ...
syntax:
cmake_minimum_required(VERSION 3.16)
cmake_minimum_required(VERSION 3.19)
# ...
find_package(
Boost
# Any version from 1.5 to 1.6 (inclusive) is okay
1.5 ... 1.6
REQUIRED
COMPONENTS program_options
CONFIG
)
The main use case for this is when the feature we rely on was removed from the library at some point. In that case, we'd typically add a <
token to exclude that breaking version from the acceptable range:
find_package(
Boost
# The feature we need was added in version 1.5
# It was removed in 2.0
1.5 ... <2.0
REQUIRED
COMPONENTS program_options
CONFIG
)
In professional projects, CMake is often combined with a package manager such as vcpkg or Conan. These integrate seamlessly with the find_package()
command, making it a lot easier to use, and helping ensure versions are consistent across our team. We cover package managers in the next chapter.
Summary
The find_package()
command is the bridge between your project and the wider C++ ecosystem. By understanding how it works, you can integrate third-party libraries in a clean, portable, and modern way.
- Two Modes:
find_package()
can operate in Module mode (using aFindSomeLib.cmake
script) or Config mode (using aSomeLibConfig.cmake
file provided by the library). Config mode is the modern standard. - Imported Targets are Preferred: A modern
find_package()
call providesIMPORTED
targets (e.g.,SomeLib::Component
). - Link to Targets, Not Variables: Where possible, prefer
target_link_libraries()
over manually using variables like${Package_LIBRARIES}
. This encapsulates all usage requirements.
Creating a Consumable Package
Learn how to make your libraries consumable by other projects using CMake by turning your build-tree into a distributable install-tree.