CMake FetchContent vs. ExternalProject

Generally, using code from external project is better done by the build system e.g. CMake or Meson instead of Git submodules. CMake can bring together code or libraries from multiple projects using CMake functionality from:

Let’s discuss the highest-level approach of each. We’ll call the master project “parent” and the other project “child”. Suppose “parent” wants to use “libfoo” from “child”, and that both are CMake projects. The master Fortran project has an executable use_ext.f90 that uses the Fortran module inside a target named ext in child project.

A live example of this is: https://github.com/scivision/fortran2018-examples/ submodules directory

For reference, Meson subprojects work somewhat like CMake ExternalProject.

ExternalProject

ExternalProject populates content from the other project at build time. This means the other project’s libraries are not visible until the master project is built. Thus, after defining the external project, we must add a manual dependency as in:

“master” project CMakeLists.txt:

cmake_minimum_required(VERSION 3.13)
project(master Fortran)

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)
set(child_BINARY_DIR ${BINARY_DIR})  # to avoid overwriting value elsewhere

add_executable(useext use_ext.f90)
add_dependencies(useext child)
target_link_directories(useext PRIVATE ${child_BINARY_DIR})
target_link_libraries(useext PRIVATE ext)
target_include_directories(useext PRIVATE ${child_BINARY_DIR})
add_test(NAME ExternalSubmodule COMMAND useext)

“child” project CMakeLists.txt:

cmake_minimum_required(VERSION 3.13)
project(child Fortran)

add_library(ext ext.f90)
set_target_properties(ext PROPERTIES Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR})
  • set_target_properties(... PROPERTIES Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}) avoids having to introspect the directory tree of the other project.

FetchContent

A key distinction from ExternalProject is that FetchContent populates content from the other project at configure time instead of at build time like ExternalProject.

FetchContent allows directly using code from the other project in the current project’s CMakeLists.txt:

cmake_minimum_required(VERSION 3.11)
project(master Fortran)

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_GetProperties(child)

if(NOT child_POPULATED)
  FetchContent_Populate(child)

  add_subdirectory(${child_SOURCE_DIR} ${child_BINARY_DIR})
endif()

add_executable(useext use_ext.f90)
target_link_libraries(useext PRIVATE ext)
add_test(NAME ExternalSubmodule COMMAND useext)
  • add_subdirectory() command could be other commands directly using the source code as if it were part of the master CMake project.

Which one to use?

A simple way to decide between CMake FetchContent and ExternalProject is:

  • need to use other project code as if it were part of this project: FetchContent
  • only need to link other project as a library: ExternalProject