CMake FetchContent vs. ExternalProject

Making multiple software projects work together is usually better done by the build system:

instead of Git submodule.

Meson subproject and CMake ExternalProject keep project namespaces separate. Meson subproject and CMake FetchContent download and configure all projects at configure time. CMake FetchContent comingles the CMake project namespaces. FetchContent can be easier to use than ExternalProject if you control both software projects’ CMake scripts. If you don't control the “child” project, it may be better to use ExternalProject instead of FetchContent.

For these examples, suppose we have a top-level project “parent” and a “child” project containing a library that is desired in parent. Suppose the child project can be built standalone (by itself) but also may be used directly from other CMake projects.

projectCMAKE_SOURCE_DIRCMAKE_BINARY_DIRPROJECT_SOURCE_DIR
parent~/foo~/foo/build~/foo
child: standalone~/bar~/bar/build~/bar
child: CMake ExternalProject~/foo/build/child-prefix/src/child~/foo/build/child-prefix/src/child-build~/foo/build/child-prefix/src/child
child: CMake FetchContent~/foo~/foo/build~/foo/build/_deps/child-src

FetchContent

FetchContent populates content from the other project at configure time. FetchContent comingles the variables from both projects, so care must be taken when using the same variables used in projects via FetchContent. FetchContent brings variables and targets from “child” project namespace into the “parent” project namespace as if it were part of the same project.

From “parent” project CMakeLists.txt:

project(parent ...)

include(FetchContent)
FetchContent_Declare(child
  GIT_REPOSITORY https://github.invalid/username/child.git
  GIT_TAG master   # it's much better to use a specific Git revision or Git tag for reproducibility
)

FetchContent_MakeAvailable(child)
FetchContent_MakeAvailable
make “child” code configure, populating variables and targets as if it were part of “parent” CMake project.

suppose “child” project CMakeLists.txt contains:

project(child ...)

add_library(ext STATIC ext.f90)
set_target_properties(ext PROPERTIES
  Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}
  ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}
  LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})

The child project CMAKE_BINARY_DIR and CMAKE_SOURCE_DIR will be those of parent project. That is, if the parent project is in ~/foo and the build directory is ~/foo/build, then the child project in ~/childcode called by FetchContent will also have CMAKE_SOURCE_DIR of ~/foo and CMAKE_BINARY_DIR of ~/foo/build. So be careful in the child project when using such variables that may be defined by parent projects. This is why projects that aren't specifically designed to work together may be better joined by ExternalProject. A typical technique within the child project that can operate standalone is to refer to CMAKE_CURRENT_SOURCE_DIR instead of CMAKE_SOURCE_DIR as the latter will break when used from FetchContent.

ExternalProject

ExternalProject populates content from the other project at build time. This means the other project's libraries are not visible until the parent project is built. Since ExternalProject does not combine the project namespaces, ExternalProject can be a better choice in general, especially if you don't control the other projects.

ExternalProject will not download, configure or build without the add_dependencies() statement. Upon cmake --build of the parent project, ExternalProject downloads, configures and builds.

From “parent” project CMakeLists.txt:

project(parent ...)

include(ExternalProject)
ExternalProject_Add(child
  GIT_REPOSITORY https://github.invalid/username/child.git
  GIT_TAG master  # it's much better to use a specific Git revision or Git tag for reproducibility
  INSTALL_COMMAND ""  # this disables the install step for the external project
)

ExternalProject_Get_Property(child BINARY_DIR)

# this imported library is used like any other library
add_library(ext STATIC IMPORTED GLOBAL)
add_dependencies(ext child)
set_target_properties(ext PROPERTIES
  IMPORTED_LOCATION ${BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}ext${CMAKE_STATIC_LIBRARY_SUFFIX}
  INTERFACE_INCLUDE_DIRECTORIES ${BINARY_DIR})
add_dependencies()
make ExternalProject always update and build first

The imported library ext is used in the “parent” project just like any other library.


“child” project CMakeLists.txt includes:

project(child ...)

add_library(ext STATIC ext.f90)
set_target_properties(ext PROPERTIES
  Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}
  ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}
  LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})

Configure “child” Fortran_MODULE_DIRECTORY so that it's not necessary for “parent” to introspect “child” directory structure.

Live examples

Caveats

Since the ExternalProject is built by itself and generally is unaware of the consuming “parent”, this does NOT work to detect use as an ExternalProject:

project(child ...)

# "is_fetched" is:
# * ExternalProject: false--does not detect
# * FetchContent: true

set(is_fetched (NOT CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR))

Note that the PARENT_DIRECTORY property is NOT useful for detecting if the “child” is being used as an ExternalProject.


  • target_link_directories() is generally NOT preferred because library name collisions can occur, particularly with system libraries.