Automated dependency handling with FetchContent

Objectives

  • Learn how to download your dependencies at configure-time with FetchContent.

  • Learn how fetched content can be used natively within your build system.

CMake offers two modules to satisfy missing dependencies on-the-fly: the ExternalProject and FetchContent modules.

Using ExternalProject
  • The download step happens at project build-time.

  • You can handle dependencies that do not use CMake.

  • You need to rewrite your whole build system as a superbuild.

Using FetchContent
  • The download step happens at project configure-time.

  • You can handle dependencies that do not use CMake.

  • It’s an well-delimited change to an existing CMake build system.

Both are extremely powerful mechanisms. In this episode, we will discuss the FetchContent module.

The FetchContent module

To fetch dependencies on-the-fly at configure-time you will include the built-in CMake module FetchContent. This module has been part of CMake since its 3.11 version and has been steadily improved since then.

There are two steps in a FetchContent-based workflow:

  1. Declaring the content to fetch with FetchContent_Declare. This can be a tarball (local or remote), a local folder, or a version control repository (Git, SVN, etc.).

  2. Populating the content with FetchContent_MakeAvailable. This commands adds the targets declared in the external content to your build system.

    Since targets from the external project are added to your own project, you will be able to use them in the same way you would when obtaining them through a call to find_package: you can use found and fetched content in the same exact way. If you need to set options for building the external project, you will set them as CMake variables before calling FetchContent_MakeAvailable.

Exercise: Unit testing with Catch2

Unit testing is a valuable technique in software engineering: it can help identify functional regressions with a very fine level of control, since each unit test is meant to exercise isolated components in your codebase. Equipping your codebase with integration and unit tests is very good practice.

There are many unit testing frameworks for the C++ language. Each of them stresses a slightly different approach to unit testing and comes with its own peculiarities in set up and usage. In this episode, we will show how to use Catch2 a very popular unit testing framework which emphasizes a test-driven development workflow. Catch2 is distributed as a single header file, which is one of its most appealing features: it can easily be included in any project. Rather than download the header file and adding it to our codebase, we can use FetchContent to satisfy this dependency for us when needed.

Exercise: Fetching and using Catch2 to test our code

We want to use the Catch2 unit testing framework for our test code (we stay with the example from Creating and running tests with CTest) and so we changed test.cpp to now contain:

#include "sum_integers.hpp"

// this tells catch to provide a main()
// only do this in one cpp file
#define CATCH_CONFIG_MAIN
#include <catch2/catch.hpp>

#include <vector>

TEST_CASE("Sum of integers for a short vector", "[short]") {
  auto integers = {1, 2, 3, 4, 5};
  REQUIRE(sum_integers(integers) == 15);
}

TEST_CASE("Sum of integers for a longer vector", "[long]") {
  std::vector<int> integers;
  for (int i = 1; i < 1001; ++i) {
    integers.push_back(i);
  }
  REQUIRE(sum_integers(integers) == 500500);
}

There are two tests now, they also have tags ([short] and [long]).

  1. Adapt the CMakeLists.txt with these new lines:

# set minimum cmake version
cmake_minimum_required(VERSION 3.14)

# project name and language
project(example LANGUAGES CXX)

# require C++14
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# example library
add_library(sum_integers sum_integers.cpp)

# main code
add_executable(sum_up main.cpp)
target_link_libraries(sum_up PRIVATE sum_integers)

# enable FetchContent
include(FetchContent)

# declare Catch2
FetchContent_Declare(Catch2                              # name of the content
  GIT_REPOSITORY https://github.com/catchorg/Catch2.git  # the repository
  GIT_TAG        v2.13.7                                 # the tag
  )

# make available
FetchContent_MakeAvailable(Catch2)

# testing binary
add_executable(cpp_test test.cpp)
target_link_libraries(cpp_test PRIVATE sum_integers Catch2::Catch2)

# enable testing functionality
enable_testing()

# define tests
add_test(
  NAME catch_test
  COMMAND $<TARGET_FILE:cpp_test> --success
  )
  1. Build the code and run the tests with:

$ ./cpp_test --success
  1. Also try to run only one of the two:

$ ./cpp_test [short] --success
  1. Now run them via CTest:

$ ctest --verbose
  1. How would you adapt CMakeLists.txt to run them as two separate tests in CTest? Try it out.

  2. Discuss the pros and cons of running them as one or as two from CMake/CTest perspective.

Keypoints

  • CMake lets you satisfy dependencies on-the-fly.

  • You can do so at build-time with ExternalProject, but you need to adopt a superbuild framework.

  • At configure-time, you can use the FetchContent module: it is more a appropriate with dependencies that also use CMake.