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
andCMAKE_CXX_FLAGS
), andcan 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
andCMAKE_CXX_FLAGS
),can be included by other CMake-/CMaize-based build systems via CMake’s
FetchContent
module, andwill 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.