Redundancy in CMake-Based Build Systems

One of the predicates underlying the Statement of Need section is that CMake-based build systems are redundant. The redundancy in turn contributes to the verboseness of the resulting build system, and its inability to satisfy DRY. To better illustrate this point, this page showcases some example CMake-based build systems. Note that source code for all examples are available in CMaize’s GitHub repository in the tests/docs directory.

Bare-Minimum CMake Build System

Defining the “minimal CMake-based build system” is a seemingly innocuous task. In theory, we are after the shortest CMake script which will build a simple C++ project. In practice, the shortest CMake script will depend on exactly what we mean by “build” and what constitutes a “simple” C++ project. The latter is perhaps easier to define. To that end, consider a C++ project which has no dependencies (aside from the standard C++ library) and let the project build a single executable from a single source file. The complexity of the contents of the source file are irrelevant, so long as the contents adhere to the stated assumptions; thus a standard “Hello World” example suffices:

1#include <iostream>
2
3int main() {
4  std::cout << "Hello World!" << std::endl;
5  return 0;
6}

Assuming the above source code resides in a source file hello_world.cpp, which resides next to the CMakeLists.txt for building it, i.e., assuming a project structure like:

hello_world/
├─ CMakeLists.txt
├─ hello_world.cpp

then the minimal CMakeLists.txt file looks like:

1add_executable(hello_world hello_world.cpp)

Note this will not configure warning free, nor will you be able to install the resulting executable. If we want to be warning free we need to expand the CMakeLists.txt to:

1cmake_minimum_required(VERSION 3.5)
2project(hello_world)
3
4add_executable(hello_world hello_world.cpp)

and if we also want to be able to install the executable, the minimal CMakeLists.txt is then:

1cmake_minimum_required(VERSION 3.5)
2project(hello_world)
3
4add_executable(hello_world hello_world.cpp)
5
6install(TARGETS hello_world)

In our opinion, the above is a reasonable candidate (vide infra) for the “simplest” CMake-based build system for our simple C++ project. Of note the build system:

  • is warning free,

  • builds the executable,

  • installs the executable,

  • is configurable (it respects variables meant to be set by the user, like CMAKE_INSTALL_PREFIX and CMAKE_CXX_FLAGS), and

  • can be included by other CMake-based build systems via CMake’s FetchContent module.

The “candidate” moniker is because this build system still does not adhere to all CMake best practices. In particular, the installed package does not provide CMake configuration files to facilitate finding the package with CMake’s find_package function. To be fair, such files are far more important for libraries which are meant to be consumed by other (CMake-based) build systems. Thus, whether the CMake-based build system here needs to generate configuration files is up for debate. Nevertheless, for completeness, we can also generate the configuration files by using the CMakeLists.txt:

 1cmake_minimum_required(VERSION 3.5)
 2project(hello_world VERSION 1.0.0)
 3
 4add_executable(hello_world hello_world.cpp)
 5
 6include(GNUInstallDirs)
 7set(target_name hello_world)
 8install(
 9    TARGETS ${target_name}
10    EXPORT ${PROJECT_NAME}Targets
11    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
12)
13
14include(CMakePackageConfigHelpers)
15
16set(install_cmake_dir ${CMAKE_INSTALL_LIBDIR}/cmake)
17install(
18    EXPORT ${PROJECT_NAME}Targets
19    FILE ${PROJECT_NAME}-targets.cmake
20    NAMESPACE ${PROJECT_NAME}::
21    DESTINATION ${install_cmake_dir}/${PROJECT_NAME}
22)
23
24set(version_file ${PROJECT_NAME}-config-version.cmake)
25set_property(
26    TARGET ${target_name}
27    PROPERTY VERSION ${${PROJECT_NAME}_VERSION}
28)
29set_property(
30    TARGET ${target_name}
31    PROPERTY
32        INTERFACE_${target_name}_MAJOR_VERSION ${${PROJECT_NAME}_VERSION_MAJOR}
33)
34set_property(
35    TARGET ${target_name}
36    APPEND PROPERTY
37    COMPATIBLE_INTERFACE_STRING ${target_name}_MAJOR_VERSION
38)
39write_basic_package_version_file(
40    "${CMAKE_CURRENT_BINARY_DIR}/${version_file}"
41    VERSION ${${PROJECT_NAME}_VERSION}
42    COMPATIBILITY SameMajorVersion
43)
44
45set(config_file ${PROJECT_NAME}-config.cmake)
46configure_package_config_file(
47    ${CMAKE_CURRENT_SOURCE_DIR}/${config_file}.in
48    "${CMAKE_CURRENT_BINARY_DIR}/${config_file}"
49    INSTALL_DESTINATION ${install_cmake_dir}/${PROJECT_NAME}
50)
51install(
52    FILES ${CMAKE_CURRENT_BINARY_DIR}/${config_file}
53          ${CMAKE_CURRENT_BINARY_DIR}/${version_file}
54    DESTINATION ${install_cmake_dir}/${PROJECT_NAME}
55
56)

CMake also requires us to write a template configuration file, i.e., a <project-name>-config.cmake.in file. The contents of our hello_world-config.cmake.in file is:

1@PACKAGE_INIT@
2
3set(_hw_package_name hello_world)
4set(_hw_target_file ${_hw_package_name}-targets.cmake)
5include(${CMAKE_CURRENT_LIST_DIR}/${_hw_target_file})
6
7check_required_components(${_hw_package_name})

As can be seen, needing to generate configuration files dramatically lengthens the build system. However, as we tried to show in the above snippets by the use of variables, most of of the required code is boilerplate. It should be noted, this boilerplate was adopted from CMake’s official importing/exporting guide 5; the point being, if there is a more succinct way to package an executable, CMake is not currently advertising it (and we are not aware of it).

Some readers may argue that this is still not the “simplest CMake-based build system” because the build system still does not address a number of software development best practices, e.g., documentation, testing, and/or deployment. While there are benefits which come from integrating these tasks into the build system (mainly the ability to utilize configuration information), many of these remaining tasks are conventionally handled by tools outside CMake (though CMake may provide support for these tools, e.g., via CTest or the Doxygen CMake module). Regardless of what exactly constitutes the “simplest CMake-based build system”, already with the previous example we have begun to see redundancy. The vast majority of the boilerplate could have been filled in for the developer provided the:

  • name of the target to install,

  • the name of the project, and

  • the project version.

Furthermore, the latter two on the list are available from CMake as long as the developer calls the project() command with a version.

Bare-Minimum CMaize Build System

Note

Since CMaize is a CMake extension, its location is not known to CMake by default. CMaize examples on this page assume that the build system knows where CMaize is located. There are a number of ways to accomplish this (see Obtaining CMaize).

We would be remiss if we did not take this opportunity to demonstrate what the equivalent CMaize-based build system looks like. Said build system is:

1cmake_minimum_required(VERSION 3.5)
2project(hello_world VERSION 1.0.0)
3
4include(cmaize/cmaize)
5
6cmaize_add_executable(hello_world SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR})
7cmaize_add_package(hello_world)

It should be noted that the above CMaize-based build system:

  • is warning free,

  • builds the executable,

  • installs the executable,

  • is configurable (it respects variables meant to be set by the user, like CMAKE_INSTALL_PREFIX and CMAKE_CXX_FLAGS),

  • can be included by other CMake-/CMaize-based build systems via CMake’s FetchContent module, and

  • will generate the configuration files necessary for another CMake-/CMaize- based build system to leverage an installed version.

Admittedly the brevity of the CMaize-based build system comes from making a number of assumptions about default values (see CMaize Assumptions for a full list). However, we expect that these assumptions are already true for the majority of CMake-based projects and/or most projects would be fine adopting these conventions in exchange for the much simpler build system.