Dependency management with CMake and FetchContent

How to handle lightweight dependencies using CMake and FindContent

Especially when writing cross-platform compatible programs, dependency management can become a real pain in C++. While most Linux distributions have a nice package manager, Windows still relies on either very different concepts or manual installation steps. Especially for small things like Catch2 or yaml-cpp, this can become a real pain to maintain and document.

One nice solution for this problem is offered through FetchContent in CMake. It allows you to define external repositories and include into your project. A simple example of how you could for example graph Catch2 is shown below:

include(FetchContent)

FetchContent_Declare(
  Catch2
  GIT_REPOSITORY https://github.com/catchorg/Catch2.git
  GIT_TAG        v2.13.5
)

FetchContent_GetProperties(Catch2)
if(NOT catch2_POPULATED)
  FetchContent_Populate(Catch2)
  add_subdirectory(${catch2_SOURCE_DIR} ${catch2_BINARY_DIR})
endif()

set(Catch2_INCLUDE_DIRS "${catch2_SOURCE_DIR}/;${catch2_SOURCE_DIR}/src/;" CACHE PATH "Path to include folder for Catch2")
set(Catch2_LIBRARIES "Catch2::Catch2" CACHE STRING "Library for Catch2")
set(Catch2_FOUND TRUE)

The advantage of this method over many other ways is that you can specify the version of the library as well through the git tag which allows you to make sure that the build is always consistent (any system wide installation could have a non-compatible version of that library).

I would recommend packaging each of those dependencies in a Find<Package>.cmake file and position them in your cmake/modules/ subdirectory. Afterwards, you can include the dependencies from your main CMakeLists.txt as follows:

set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/modules")
find_package(Catch2 REQUIRED)

Like this you do not pollute your main configuration file with too much dependencies. The last three variables defined (Catch2_INCLUDE_DIRS, Catch2_LIBRARIES, Catch2_FOUND) will then be available from the main context (no need to add PARENT_SCOPE in that case). There is a more detailed list of standard variable names which should be defined available in the CMake developer guide.

Notes:

  • this method is highly unsuitable when we are talking about large dependencies because you will of course need to build all your dependencies from scratch
  • between the FetchContent_Populate and add_subdirectory you can add commands which should be respected by the build of your submodule. you could for example disable building some not required components.

Common alternatives:

  • using git submodules
  • using ExternalProject

A few examples of libraries I use oftern

Catch2

include(FetchContent)

FetchContent_Declare(
  catch2
  GIT_REPOSITORY https://github.com/catchorg/Catch2.git
  GIT_TAG        v2.13.5 # or a later release
)

FetchContent_GetProperties(catch2)
if(NOT catch2_POPULATED)
  FetchContent_Populate(catch2)
  add_subdirectory(${catch2_SOURCE_DIR} ${catch2_BINARY_DIR})
endif()

set(catch2_INCLUDE_DIR "${catch2_SOURCE_DIR}/;${catch2_SOURCE_DIR}/src/;" CACHE PATH "Path to include folder for Catch2")
set(catch2_LIBRARIES "Catch2::Catch2" CACHE STRING "Library for Catch2")
set(catch2_FOUND TRUE CACHE BOOL "Catch2 found")

Leave a Reply

Your email address will not be published. Required fields are marked *