Contents

Creating super projects with CMake's ExternalProject

Some time ago, I wrote a piece about CMake. It was more of a rant, written in the heat of the moment, after being frustrated by some of CMake’s idiosyncrasies. Ironically, it has become one of the most read posts on this blog, which is a bit disappointing. I would like to believe that there are far more interesting and useful posts available here but it is what it is.

My concluding issue in that post was related to dependency management between a set of CMake projects which comprised a “stack”. Recently, having to work with cmake once again, I’ve discovered CMake’s ExternalProject - which mitigates that problem significantly.

Recap

To cut the long story short, CMake identifies three stages:

  • project configuration
  • build system generation
  • project build

In configure stage, it tries to resolve all targets and dependencies but some of these might be provided as a result of the build stage. So, it’s a bit of a catch 22 situation.

As I mentioned, there are many hacks to solve this problem. ExternalProject might be classified as one as well however, it feels like a clean solution to the problem.

Problem definition

Consider the following example project structure.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
$ tree .
.
├── CMakeLists.txt
├── foo
│   ├── CMakeLists.txt
│   └── main.cpp
├── liba
│   ├── a.cpp
│   ├── CMakeLists.txt
│   ├── Config.cmake.in
│   └── include
│       └── liba
│           └── a.h
└── libb
    ├── b.cpp
    ├── CMakeLists.txt
    ├── Config.cmake.in
    └── include
        └── libb
            └── b.h

The dependecies would be:

/cmake_external_project/deps.png

The global CMakeLists.txt would contain:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
cmake_minimum_required(VERSION 3.28)
project(global LANGUAGES CXX)

include(ExternalProject)
ExternalProject_Add(
    liba
    PREFIX ${CMAKE_BINARY_DIR}/liba
    SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/liba
    CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX}
    BUILD_COMMAND make
    INSTALL_COMMAND make install
)

ExternalProject_Add(
    libb
    PREFIX ${CMAKE_BINARY_DIR}
    SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libb
    CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX}
    BUILD_COMMAND make
    INSTALL_COMMAND make install
    DEPENDS liba
)

ExternalProject_Add(
    foo
    PREFIX ${CMAKE_BINARY_DIR}
    SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/foo
    CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX}
    BUILD_COMMAND make
    INSTALL_COMMAND make install
    DEPENDS libb
)

ExternalProject is executed and installed during the configure stage. So, from the perspective of the global, super project build, there are no targets to produce. That’s all right though. All the magic happens within the build system for each individual subproject.

I won’t quote or discuss any of the individual subproject’s CMakeLists.txt as they are quite verbose. For reference, I’m providing a repo with the discussed project layout.