The FetchContent module enables populating content at configure time. In our case, we have fetched a Git repository with a well defined Git tag:
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.8.0
)
The FetchContent module supports fetching content via any method supported by the ExternalProject module - in other words, via Subversion, Mercurial, CVS, or HTTP(S). The content name "googletest" was our choice and with this we will be able to reference the content when querying its properties, when populating directories, and later also when configuring the subproject. Before populating the project, we checked whether the content was already fetched, otherwise FetchContent_Populate() would have thrown an error if it was called more than once:
if(NOT googletest_POPULATED)
FetchContent_Populate(googletest)
# ...
endif()
Only then did we configure the subdirectory, which we can reference with the googletest_SOURCE_DIR and googletest_BINARY_DIR variables. They were set by FetchContent_Populate(googletest) and constructed based on the project name we gave when declaring the content:
add_subdirectory(
${googletest_SOURCE_DIR}
${googletest_BINARY_DIR}
)
The FetchContent module has a number of options (see https://cmake.org/cmake/help/v3.11/module/FetchContent.html) and here we can show one: how to change the default path into which the external project will be placed. Previously, we saw that by default the content is saved to ${CMAKE_BINARY_DIR}/_deps. We can change this location by setting FETCHCONTENT_BASE_DIR:
set(FETCHCONTENT_BASE_DIR ${CMAKE_BINARY_DIR}/custom)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.8.0
)
FetchContent has become a standard part of CMake in its 3.11 version. In the following code, we will try to emulate FetchContent using ExternalProject_Add at configure time. This will not only be practical for older CMake versions, it will hopefully also give us more insight into what is happening underneath the FetchContent layer and provide an interesting alternative to the typical build-time fetching of projects included using ExternalProject_Add. Our goal will be to write a fetch_git_repo macro and place it in fetch_git_repo.cmake so that we can fetch the content like this:
include(fetch_git_repo.cmake)
fetch_git_repo(
googletest
${CMAKE_BINARY_DIR}/_deps
https://github.com/google/googletest.git
release-1.8.0
)
# ...
# adds the targets: gtest, gtest_main, gmock, gmock_main
add_subdirectory(
${googletest_SOURCE_DIR}
${googletest_BINARY_DIR}
)
# ...
This feels similar to the use of FetchContent. Under the hood, we will use ExternalProject_Add. Let us now lift the hood and inspect the definition of fetch_git_repo in fetch_git_repo.cmake:
macro(fetch_git_repo _project_name _download_root _git_url _git_tag)
set(${_project_name}_SOURCE_DIR ${_download_root}/${_project_name}-src)
set(${_project_name}_BINARY_DIR ${_download_root}/${_project_name}-build)
# variables used configuring fetch_git_repo_sub.cmake
set(FETCH_PROJECT_NAME ${_project_name})
set(FETCH_SOURCE_DIR ${${_project_name}_SOURCE_DIR})
set(FETCH_BINARY_DIR ${${_project_name}_BINARY_DIR})
set(FETCH_GIT_REPOSITORY ${_git_url})
set(FETCH_GIT_TAG ${_git_tag})
configure_file(
${CMAKE_CURRENT_LIST_DIR}/fetch_at_configure_step.in
${_download_root}/CMakeLists.txt
@ONLY
)
# undefine them again
unset(FETCH_PROJECT_NAME)
unset(FETCH_SOURCE_DIR)
unset(FETCH_BINARY_DIR)
unset(FETCH_GIT_REPOSITORY)
unset(FETCH_GIT_TAG)
# configure sub-project
execute_process(
COMMAND
"${CMAKE_COMMAND}" -G "${CMAKE_GENERATOR}" .
WORKING_DIRECTORY
${_download_root}
)
# build sub-project which triggers ExternalProject_Add
execute_process(
COMMAND
"${CMAKE_COMMAND}" --build .
WORKING_DIRECTORY
${_download_root}
)
endmacro()
The macro receives the project name, download root, Git repository URL, and a Git tag. The macro defines ${_project_name}_SOURCE_DIR and ${_project_name}_BINARY_DIR, and we use a macro instead of a function since ${_project_name}_SOURCE_DIR and ${_project_name}_BINARY_DIR need to survive the scope of fetch_git_repo because we use them later in the main scope to configure the subdirectory:
add_subdirectory(
${googletest_SOURCE_DIR}
${googletest_BINARY_DIR}
)
Inside the fetch_git_repo macro, we wish to use ExternalProject_Add to fetch the external project at configure time and we achieve this with a trick in three steps:
- First, we configure fetch_at_configure_step.in:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(fetch_git_repo_sub LANGUAGES NONE)
include(ExternalProject)
ExternalProject_Add(
@FETCH_PROJECT_NAME@
SOURCE_DIR "@FETCH_SOURCE_DIR@"
BINARY_DIR "@FETCH_BINARY_DIR@"
GIT_REPOSITORY
@FETCH_GIT_REPOSITORY@
GIT_TAG
@FETCH_GIT_TAG@
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
TEST_COMMAND ""
)
Using configure_file, we generate a CMakeLists.txt file in which the previous placeholders are replaced by values defined in fetch_git_repo.cmake. Note that the previous ExternalProject_Add command is constructed to only fetch, not to configure, build, install, or test.
- Second, we trigger the ExternalProject_Add at configure time (from the perspective of the root project) using a configure step:
# configure sub-project
execute_process(
COMMAND
"${CMAKE_COMMAND}" -G "${CMAKE_GENERATOR}" .
WORKING_DIRECTORY
${_download_root}
)
- Third and final trick triggers a configure-time build step in fetch_git_repo.cmake:
# build sub-project which triggers ExternalProject_Add
execute_process(
COMMAND
"${CMAKE_COMMAND}" --build .
WORKING_DIRECTORY
${_download_root}
)
One nice aspect of this solution is that since the external dependency is not configured by ExternalProject_Add, we do not need to channel any configuration settings to the project via the ExternalProject_Add call. We can configure and build the module using add_subdirectory as if the external dependency was part of our project source tree. Brilliant disguise!