C++ Build System

Required Software

In order to use the described build system for C++ you need the following software on your computer:

Brief Overview

We will demonstrate how to set up a C++ project using the build tool CMake, which seems to be one of the most-used choices in the recent years for C++. CMake is based on a declarative scripting language, which makes it flexible to use and well-understandable for programmers accustomed to C-like programming languages. The most important files for CMake are called CMakeLists.txt. These files contain the scripting commands to set up your build system.

In this tutorial we will build a shared library with some reusable functions. The library will be installed in a way that downstream projects can use it. Additionally, we will build an executable which uses the shared library. This executable will be installed so that it can be called from the command line.

We assume basic knowledge of C++ and the general way how C++ code can be deployed. That means executable and shared and static libraries as well as the use of header files. Additionally, we assume some basic knowledge of CMake. In cases you have never used CMake before, please read the first part of How CMake simplifies the build process, which explains how to use the CMake command line and some very basics of the langauge.

Note

The presented build system uses several CMake macros provided by the RSC library, which is a dependency of RSB. Hence, RSC is available for all projects using RSB. In cases where you want to create a project without RSB dependency and you want to avoid the dependency on RSC you are free to copy and modify the RSC CMake macros according to the license terms of RSC.

Folder Layout

The folder layout of our tutorial project will look as follows. All RSX projects use this structure.

  • build-system-essentials-cpp: top-level folder of our project
    • CMakeLists.txt: Main CMake declarations and dependency handling.
    • README.txt, COPYING.txt, …: Files with additional textual information like license, contact information etc.
    • build-system-essentials-cpp-config.cmake.in: CMake configuration file for exposing shared libraries to downstream projects.
    • build-system-essentials-version.cmake.in: CMake configuration file for exposing the version of shared libraries to downstream projects.
    • build-system-essentials-build-tree-settings.cmake.in: CMake configuration to expose shared libraries to downstream projects from the build tree of our own project.
    • src: Contains the source code of our project laid out with one directory corresponding to each C++ namespace. Header files (h extension) and implementation files (cpp extension) are mixed inside this tree.
      • CMakeLists.txt CMake declarations to build the shared libraries and end executables of the project.
      • build-system-essentials
        • library: This subdirectory contains code from which a shared library will be created.
          • MagicNumberTransformer.h: Example header file; part of the shared library.
          • MagicNumberTransformer.cpp: Example implementation file; part of the shared library.
        • binary.cpp: Example implementation file from which the executable program will be built.
    • test: Contains the unit tests for the project in a parallel hierarchy to the src folder
      • CMakeLists.txt CMake declarations to build the unit tests of the project.

Declaring a CMake Project and Finding Upstream Dependencies

See also

CMake online documentation
Comprehensive documentation of all CMake commands as well as links to tutorials and examples
manpage cmake(1)
Manpage of CMake with the same content as the manual on the website

We start by declaring the basic project in CMake in the top-level CMakeLists.txt. The first two lines here are canonical and make explicit the expected version of CMake and declare the project name:

1
2
3
CMAKE_MINIMUM_REQUIRED(VERSION 2.6)

PROJECT("build-system-essentials-cpp")

The next thing we define is an explicit user option, whether to build the unit tests or not. Further options might be added here:

1
OPTION(BUILD_TESTS "Decides whether the unit tests will be built." ON)

Note

User options in CMake can only be boolean flags. If you need other data types, e.g. to gather a file system path, you can use variables stored in the CMake cache. Please refer to the CMake documentation how to use them.

For all binaries we are going to create in our own project we want to set a fixed rpath (TODO link), which defines were the dynamic linker will automatically search for shared libraries when executing the binary. The setting here searches relative to the location of the binary in the filesystem. This setting usually does not cause any trouble but can help very often if software is installed in a single prefix (the UNIX way):

1
SET(CMAKE_INSTALL_RPATH "\$ORIGIN/../lib:\$ORIGIN/")

One important thing to handle in the build system is finding upstream dependencies of your own project. We recommend to perform the handling of these dependencies consistently in the main CMakeLists.txt to easily have an overview of required software packages. In the case of the example project we search for RSB and its upstream dependency RSC:

1
2
3
4
5
6
FIND_PACKAGE(RSC 0.7 REQUIRED)
FIND_PACKAGE(RSB 0.7 REQUIRED)

INCLUDE_DIRECTORIES(BEFORE SYSTEM ${RSB_INCLUDE_DIRS})

LIST(INSERT CMAKE_MODULE_PATH 0 ${RSC_CMAKE_MODULE_PATH})

RSC and RSB are essential dependencies, hence they are marked REQUIRED and the CMake configuration fails if either of them cannot be found. With INCLUDE_DIRECTORIES the header locations of RSC and RSB are added to the compiler include paths. The last line makes the CMake modules provided by RSC available to our own build system.

Note

By using BEFORE in the INCLUDE_DIRECTORIES command the include directories are passed to the compiler before any others and we make sure that the dependencies we find are used before any potentially matching ones that are accidentally on the compiler command line. SYSTEM tells the compiler that these are headers from our system installation and hence cannot be changed by us easily. Therefore, the compiler will not emit warnings for these headers and our build log will be much cleaner.

Note

To learn more about how CMake actually finds dependencies, please refer to the official documentation of find_package and the wiki article CMake:How To Find Libraries.

Now that the RSC CMake macros are made available we include some of them for our own use:

1
2
3
4
INCLUDE(InstallFilesRecursive)
INCLUDE(DefineProjectVersion)
INCLUDE(GenerateDoxygen)
INCLUDE(PedanticCompilerWarnings)

In detail these are:

  • InstallFilesRecursive: Used to install the header files of our own project
  • DefineProjectVersion: Used to define version variable in CMake including information from version control systems like SVN or GIT
  • GenerateDoxygen: Used to generate doxygen source code documentation
  • PedanticCompilerWarnings: Install some compiler flags which increase the compilation warnings to a reasonable level

Using the DefineProjectVersion macro from RSC we next define the version of our project and a utility variable to attach a version to the generated shared library of our project.

1
2
DEFINE_PROJECT_VERSION("ESSENTIALS_" 0 1 0 "archive")
SET(SO_VERSION "${ESSENTIALS_VERSION_MAJOR}.${ESSENTIALS_VERSION_MINOR}")

After the DEFINE_PROJECT_VERSION_CALL several variables like ESSENTIALS_VERSION_MAJOR or ESSENTIALS_VERSION_MINOR are available for further use. Please refer to the documentation of the macro for further details.

Finally, to conclude with the preamble of the top-level CMake file, we define the names of targets (that means libraries and binaries built by our project). These names are required several times throughout the remaining CMake logic.

1
2
3
SET(LIBRARY_NAME "build-system-essentials")
SET(BINARY_NAME "cpp-tester")
SET(EXPORT_NAME "build-system-essentials-exports")

The first two lines define the names of the shared library and binary built by our project. The last line defines the name of a target which CMake uses to expose the own code to downstream projects which want to use the shared library of our own project.

Note

For exposing our own library to possible downstream projects we recommend the use of CMake’s “export” features. These features are very versatile but currently lack a good documentation. Some further hints besides what will be explained in this tutorial can be found in the CMake manual and on these websites:

Building Targets

Now that all necessary libraries have been found and useful variables like version and target names have been defined, we can actually declare the targets to be built by the project. As these definitions usually take some space and we decided to organize the source code in separate folders, it is useful to factor them out to specific CMake listings in these folders. In case of the usual production source code this is easily done with the following directive:

1
ADD_SUBDIRECTORY(src)

For the unit test, some more work is necessary. First, we gave the user the option to decide whether the tests shall be built or not. Additionally, if the test shall be built, we need to make available the unit testing framework we are going to use. For this purpose we recommend Google Test and Google Mock, which provide a very good feature set. As Google Mock already contains Google Test, we will only talk about Google Mock in the remainder of this document. RSC has specific support to include these framework:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
ENABLE_TESTING()
IF(BUILD_TESTS)
    INCLUDE(ProvideGoogleMock)
    IF(GMOCK_AVAILABLE)
        FIND_PACKAGE(Threads REQUIRED)
        ADD_SUBDIRECTORY(test)
    ELSE()
        MESSAGE(WARNING "Could not build unit tests even though desired because Google Mock could not be installed.")
    ENDIF()
ENDIF()

In line 1 we first enable CMake’s testing features. In cases where the unit tests shall be built, line 3 triggers a download of the Google Mock sources using the RSC macro. As this might fail, we need to test for success afterwards. Finally, Google Mock requires thread support. Hence, we search for the correct threading library like pthreads in line 5 and afterwards finally include the test sub-tree.

We will now look in detail how the targets in the src directory are build. Therefore we continue with looking at the definitions in src/CMakeLists.txt.

The first thing we are going to build in CMake is the shared library:

1
2
3
4
SET(LIBRARY_SOURCES "build-system-essentials/library/MagicNumberTransformer.cpp")
SET(LIBRARY_HEADERS "build-system-essentials/library/MagicNumberTransformer.h")
ADD_LIBRARY(${LIBRARY_NAME} SHARED ${LIBRARY_SOURCES} ${LIBRARY_HEADERS})
SET_TARGET_PROPERTIES(${LIBRARY_NAME} PROPERTIES VERSION ${SO_VERSION})

First we define two variables containing respectively the source files and the header files for our projects. Afterwards, we declare a shared library using ADD_LIBRARY. Please note that we also the header files should be added to the target, even if they do not need to be compiled. With the last line, we declare the version of the shared library. For all these definitions we have used the variables declared in the main CMake listing. As our shared library does not have any external dependencies, we do not need to link it against any other libraries.

The next step is to define the executable we are going to build:

1
2
ADD_EXECUTABLE(${BINARY_NAME} "build-system-essentials/binary.cpp")
TARGET_LINK_LIBRARIES(${BINARY_NAME} ${LIBRARY_NAME} ${RSB_LIBRARIES})

The executable is declared using ADD_EXECUTABLE. If there were additional headers files only meant for this executable, they should also be added to this target. In our case, the executable uses the library we have built just before and RSB etc. Therefore, we have to link it against these libraries using the TARGET_LINK_LIBRARIES function.

Finally, the executable as well as the shared library have to be installed to the file system so that they can be used (by downstream projects).

1
2
3
4
5
6
INSTALL(TARGETS ${LIBRARY_NAME} ${BINARY_NAME}
        EXPORT ${EXPORT_NAME}
        RUNTIME DESTINATION bin
        LIBRARY DESTINATION lib
        ARCHIVE DESTINATION lib)
INSTALL_FILES_RECURSIVE("include" LIBRARY_HEADERS)

The main command to install things in CMake is INSTALL. It has several signatures depending on what is going to be installed. In this case, we first install the two targets. For this purpose we have to tell CMake where to install these target with the various XXX DESTINATION declarations. Binaries will be installed to RUNTIME DESTINATION and shared libraries to LIBRARY DESTINATION. On Windows, shared libraries are split into a dll and lib file. While the lib file will go to LIBRARY DESTINATION comparable to Linux, the dll will be installed to RUNTIME DESTINATION. Hence, you should at least mention these two destinations in each INSTALL call. ARCHIVE DESTINATION would be used for static libraries.

Note

The given paths in the DESTINATION variables are always relative to CMAKE_INSTALL_PREFIX. So there is no need to construct such a thing manually or explicitly mention CMAKE_INSTALL_PREFIX.

Attention

It is important to include the EXPORT statement in the INSTALL call to make the targets available for downstream projects using the aforementioned export mechanism.

Finally, the headers need to be installed so that downstream projects can be compiled against our own library. Our headers are arranged in a deeper directory hierarchy but CMake does not natively come with a function to preserve this hierarchy during installation. Therefore, we use the RSC macro INSTALL_FILES_RECURSIVE.

Building Unit Tests

The unit tests are separated in a different folder (test) and test files are organized in parallel hierarchy to the production source code. Build definitions for the tests can be found in the respective CMake listing test/CMakeLists.txt.

For integrating the unit testing with e.g. continuous integration server it is a good idea to have machine-readable output from the unit tests. For this purpose we start with defining a location for this output, in our case XML documents:

1
SET(TEST_RESULT_DIR "${CMAKE_BINARY_DIR}/testresults")

For compiling the unit tests, we need to have our unit testing framework Google Mock in the include path as well as our own production source code that we are actually testing:

1
2
INCLUDE_DIRECTORIES(BEFORE SYSTEM ${GMOCK_INCLUDE_DIRS})
INCLUDE_DIRECTORIES(BEFORE "${CMAKE_SOURCE_DIR}/src")

Finally we can declare our unit test:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
SET(TEST_NAME "${LIBRARY_NAME}-test")

SET(TEST_SOURCES "build-system-essentials/library/MagicNumberTransformerTest.cpp")
ADD_EXECUTABLE(${TEST_NAME} ${TEST_SOURCES})

TARGET_LINK_LIBRARIES(${TEST_NAME}
                      ${LIBRARY_NAME}
                      ${GMOCK_LIBRARIES}
                      ${CMAKE_THREAD_LIBS_INIT})
                      
ADD_TEST(${TEST_NAME} ${TEST_NAME} "--gtest_output=xml:${TEST_RESULT_DIR}/")

We first define a name for the generated executable which runs the unit tests in TEST_NAME. Afterwards, we build an executable with our test code and link it. The test executable needs to be linked against our own library and against Google Mock. As Google Mock requires a threading library, it also needs to be linked against this library, which is declared in CMAKE_THREAD_LIBS_INIT from the find_package call in the top-level CMakeLists.txt.

Finally, we register the unit test in CMake’s own testing framework CTest, which has the benefit that we can easily call make test to trigger the tests. For this purpose, CMake also needs to know that the unit test needs to be called with a special command line argument to generate the desired XML output.

Exposing the Project to Downstream Projects

In order to make the project usable we for downstream projects we need to generate and install a few more files. This generation is done again in the top-level CMakeLists.txt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
CONFIGURE_FILE(build-system-essentials-config.cmake.in
               "${CMAKE_BINARY_DIR}/build-system-essentials-config.cmake"
               @ONLY)
CONFIGURE_FILE(build-system-essentials-config-version.cmake.in
               "${CMAKE_BINARY_DIR}/build-system-essentials-config-version.cmake"
               @ONLY)
CONFIGURE_FILE(build-system-essentials-build-tree-settings.cmake.in
               "${CMAKE_BINARY_DIR}/build-system-essentials-build-tree-settings.cmake"
               @ONLY)
               
INSTALL(FILES "${CMAKE_BINARY_DIR}/build-system-essentials-config.cmake"
              "${CMAKE_BINARY_DIR}/build-system-essentials-config-version.cmake"
        DESTINATION "share/build-system-essentials")

EXPORT(TARGETS ${LIBRARY_NAME} FILE "${CMAKE_BINARY_DIR}/${EXPORT_NAME}.cmake")
INSTALL(EXPORT ${EXPORT_NAME}
        DESTINATION "share/build-system-essentials")

The three generated files are necessary to work with the export import mechanism for CMake targets and the configuration we are using in this example project closely resembles the descriptions in the wiki article CMake/Tutorials/Exporting and Importing Targets. In case you are adapting the build structure of your project significantly, you should check all three files for changed variable or target names. Roughly described, build-system-essentials-config.cmake.in is the file which find_package searches for the find our project. build-system-essentials-config-version.cmake.in implements the version check for find_package in case someone requests a specific version of our library. Finally, build-system-essentials-build-tree-settings.cmake.in declares special settings, in cases a downstream project uses our code directly from our build tree, instead of an installed version. This is handy, because it avoids the installation step in cases where you are working constantly on this project and the downstream project in parallel.

In a last step, the CMake export mechanism requires the installation of another file, which is automatically generated by CMake on the call to EXPORT.

Finally, we also want to generate an API documentation for the project so that downstream developers have a reference to work with. This is easily accomplished with the macro provided by RSC:

1
GENERATE_DOXYGEN(VERSION "${ESSENTIALS_VERSION}")

How to Build and Use the Project

You should perform out-of-source builds:

cd PATH/build-system-essentials/cpp

# go to a directory different from the project's root folder, e.g.
mkdir build
cd build

# let CMake do the job
cmake -DCMAKE_BUILD_TYPE=debug -DCMAKE_INSTALL_PREFIX=/your/prefix ..

# build
make

# run tests
make test
# or to get output for non-failing tests
ctest -V

# build documentation
make doc

# install
make install

# clean generated files
make clean

Note

In case one of the dependencies like RSC is not found, you can point cmake to the installation location of these by adding -DRSC_DIR=path/to/RSCConfig.cmake to the cmake call.

Further Readings

This tutorial should have shown you how to construct a reliable build system using cmake. However, there are many more valuable techniques and important practices you should follow when modifying the build system. We recommend the following articles and blog postings for further information: