CMake is a de facto standard build tool for C++. There are those who love it, and there are those who hate it. The tool had a few problems in the past, but Modern CMake solved most of them and continued to evolve and add more and more features.

Watch Effective CMake talk by Daniel Pfeifer and Using Modern CMake Patterns to Enforce a Good Modular Design by Mathieu Ropert to get an idea and hints on modern CMake.

In this blog post, I describe the approach I use to manage external dependencies in C++ projects.

The dependencies have to have a valid CMake configuration, i.e. they have to export their targets as described in Daniel Pfeifer’s "Effective CMake" starting at 37:34, Henry Schreiner’s Modern CMake: Exporting and Installing, and It’s Time To Do CMake Right by Pablo Arias.

Typical project layout

Let’s review the layout of one of my C++ projects:

$ outpost-cpp git:(master) tree -L 2
.
├── build
├── cmake
│   ├── Catch.cmake
│   ├── CatchAddTests.cmake
│   └── ParseAndAddCatchTests.cmake
├── conker
│   ├── include
│   ├── src
│   └── CMakeLists.txt
├── extern
│   └── CMakeLists.txt
├── outpost
│   ├── assets
│   ├── include
│   ├── src
│   └── CMakeLists.txt
├── test
│   ├── src
│   └── CMakeLists.txt
├── CMakeLists.txt
└── Doxyfile

12 directories, 9 files

There are a few items in the root directory of the project:

Directory Purpose

.

Project root with the root CMakeLists.txt

build

That’s where you do cd build && cmake .. && make to build the project

cmake

Consists of custom CMake modules

conker

A subproject for a library

extern

External dependencies

outpost

A subproject for a main application

test

A subproject for unit-tests

Describing dependencies

The extern/CMakeLists.txt file is a central repository for the dependencies. Make it available from the root CMakeLists.txt:

CMakeLists.txt
add_subdirectory(extern)

Let’s take a closer look at the dependencies file:

extern/CMakeLists.txt
cmake_minimum_required(VERSION 3.11 FATAL_ERROR)

include(FetchContent)

# ------------------------------------------------------------------------------
# A modern, C++-native, header-only, test framework for unit-tests,
# TDD and BDD - using C++11, C++14, C++17 and later
FetchContent_Declare(
  extern_catch2

  GIT_REPOSITORY https://github.com/catchorg/Catch2.git
  GIT_TAG        v2.2.3)

FetchContent_GetProperties(extern_catch2)
if(NOT extern_catch2_POPULATED)
  FetchContent_Populate(extern_catch2)
  add_subdirectory(
    ${extern_catch2_SOURCE_DIR}
    ${extern_catch2_BINARY_DIR}
    EXCLUDE_FROM_ALL)
endif()

# ------------------------------------------------------------------------------
# A multi-platform library for OpenGL, OpenGL ES, Vulkan, window and
# input http://www.glfw.org/
FetchContent_Declare(
  extern_glfw

  GIT_REPOSITORY https://github.com/glfw/glfw.git)

FetchContent_GetProperties(extern_glfw)
if(NOT extern_glfw_POPULATED)
  FetchContent_Populate(extern_glfw)
  add_subdirectory(
    ${extern_glfw_SOURCE_DIR}
    ${extern_glfw_BINARY_DIR}
    EXCLUDE_FROM_ALL)
endif()

# ------------------------------------------------------------------------------
# https://devblogs.nvidia.com/linking-opengl-server-side-rendering/
find_package(
  OpenGL     REQUIRED
  COMPONENTS OpenGL)

The script uses CMake’s FetchContent module which enables populating content at configure time. That’s how you describe a dependency:

extern/CMakeLists.txt
FetchContent_Declare(
  extern_catch2

  GIT_REPOSITORY https://github.com/catchorg/Catch2.git
  GIT_TAG        v2.2.3)

FetchContent_GetProperties(extern_catch2)
if(NOT extern_catch2_POPULATED)
  FetchContent_Populate(extern_catch2)
  add_subdirectory(
    ${extern_catch2_SOURCE_DIR}
    ${extern_catch2_BINARY_DIR}
    EXCLUDE_FROM_ALL)
endif()

In other words, you declare where and how to get the dependency first and then check if it is already available. If it is not avialable yet, then you fetch it and include into the build using the add_subdirectory command.

Importing dependencies in subprojects

FetchContent makes exported targets available to the build so that you can use them in target_link_libraries:

outpost/CMakeLists.txt
target_link_libraries(outpost
  conker
  glfw
  )

Here, I added the glfw as a public dependency to the outpost target.

Another example:

test/CMakeLists.txt
target_link_libraries(conker_tests
  PRIVATE Catch conker)

Hierarchical dependency management

There are few ways to organise dependency management.

Keep all dependencies in the same file

It’s good enough for simple projects that live in the same repository, and there are no plans to move subprojects outside of it. That’s what I described in the previous sections.

├── CMakeLists.txt
├── extern
│   └── CMakeLists.txt
├── subproject_a
│   └── CMakeLists.txt
├── subproject_b
│   └── CMakeLists.txt
└── subproject_c
      └── CMakeLists.txt

Each subproject describes its dependencies

In this case, you make subprojects independent by adding their own external/CMakeLists.txt. Then you can extract any of them from the source tree and build it without any further configuration if needed.

├── CMakeLists.txt
├── subproject_a
│   ├── extern
│   │   └── CMakeLists.txt
│   └── CMakeLists.txt
├── subproject_b
│   ├── extern
│   │   └── CMakeLists.txt
│   └── CMakeLists.txt
└── subproject_c
    ├── extern
    │   └── CMakeLists.txt
    └── CMakeLists.txt

Combination of the previous methods

You declare dependencies for each subproject in their own external/CMakeLists.txt. Then you declare (i.e. duplicate) dependencies on one level higher than the subprojects. This is how you override population of content specified anywhere lower in the project hierarchy.

├── CMakeLists.txt
├── extern
│   └── CMakeLists.txt
├── subproject_a
│   ├── extern
│   │   └── CMakeLists.txt
│   └── CMakeLists.txt
├── subproject_b
│   ├── extern
│   │   └── CMakeLists.txt
│   └── CMakeLists.txt
└── subproject_c
    ├── extern
    │   └── CMakeLists.txt
    └── CMakeLists.txt

The end

I hope this helps.