XClose

COMP0210: Research Computing with C++

Home
Menu

Estimated reading time: 45 minutes

Building and running the ‘hello_world’ project

‘Out-of-Source’ vs ‘In-source’ builds

The project is now ready to be built. However, there is one final aspect that needs to be discussed before we proceed with the build viz. the location of the build artefacts (intermediate/final outputs generated by the build process).

It is possible to invoke CMake at the project root without specifying a directory intended to hold build artefacts. This is known as an in-source build. While CMake does not explicitly forbid this (it can be coaxed into not allowing this), it is frowned upon to do so. CMake is a build system generator and its output is intended to be consumed by a native build system such as make. The generation and build process may produce a lot of artefacts, a real hodgepodge of replicated source files, intermediate build system files, binary blobs etc.

The clean project repository that we started with shall be left in a polluted state of mess with all such extraneous output strewn all over the place starting at the root of the project’s filesystem. Worse, these extraneous files scattered throughout shall make it harder for us to ignore them in version control systems. Finally, this will severely hamper our ability to have multiple types of builds (e.g. Debug vs Release), since each build will overwrite the generated files, and some types of builds produce artefacts not applicable to other build types, leaving the user in confusion. In an out-of-source build, the build tree is separate to the source tree. All the build-related files shall be located within that designated directory, which can easily be ignored in version control systems, and deleted when no longer needed. Multiple build types can easily be accomplished by nesting under a common build directory. Hence, projects must always use out-of-source builds.

Build process

Configure stage

Invoke the executable cmake at the root of the project, specifying a build directory as follows.

$ cmake -S . -B build_dir

This instructs cmake that the top level CMakeLists.txt (the source folder indicated by the -S flag) is in the current folder (referenced by .), and that the build artefacts need to be located within a directory named build_dir (which is passed as argument to the -B flag). CMake shall create the build directory if it does not exist already. If the directory exists, it simply reuses it. This step is known as the CMake configure stage during which CMake merely generates the build system for the target platform. Syntax errors or other errors pertaining to incorrectly describing the build shall be flagged here, interrupting the configure process.

During the configure stage, CMake queries the system and environment to infer the languages, libraries and toolchains to be invoked within the build system. If not explicitly specified through a CMake predefined variable CMAKE_BUILD_TYPE, the default build is a “non-release, non debug” empty build type, which is acceptable for now. When CMake completes the configure stage, we are ready to build our project.

Build stage

At the end of the configure step, we obtain a platform-native build system which can be invoked directly to perform the build. However, this requires us to move into the executable directory (which may be deeply nested if multiple build types are organised under the same build root). We can stay at the root of the project repository, and invoke the build through CMake itself as follows:

$ cmake --build build_dir

This will build a target called all which is a special target provided by CMake to build all build artefacts. In this case, we have only one target, an executable named hello_world. If we have multiple targets and if building all takes a long time for large-scale projects, we can build just the target of interest instead, as follows:

$ cmake --build build_dir --target hello_world

The --target flag may be abbreviated to -t.

The build process invokes the toolchain (typically the compiler and linker) with the requisite flags, and produces the project’s desired outputs (typically libraries and executables). If the build process was successful, we now have an hello_world executable that can be run. Let’s do that next!

Invoking the build outputs

Somewhat frustratingly, the final build outputs are typically deeply embedded in the build directory tree, resulting in very long absolute paths. This is because CMake project outputs are not typically designed to be run directly from the build tree. Libraries and executables produced by such projects are typically intended for downstream consumption. CMake provides detailed facilities to package up a project’s outputs for installation into standard locations on the native platform, as well as package them up for wider distribution.

To follow the classical edit-build-debug cycle, it is of course, necessary to run the executable program to verify its behaviour. Some IDEs have provided abilities to invoke the final build outputs through their graphical user interfaces that are conditionally activated by parsing CMake projects. We have ameliorated this problem a bit by specifying in our top level CMakeLists.txt that the final output shall reside in the bin/ directory at the root of the build tree. Thus, at the command line, from the root of the project we can invoke:

$ ./build_tree/bin/hello_world

At this point, we should see “Hello World!” printed to our console. Whew, success!

Optional: Easy invocation of built executable through CMake

CMake allows us to build custom targets that allows us to run any available commands in the user’s path using the add_custom_target() command. We may set up a custom target called run_program within the src/CMakeLists.txt file as follows:


add_custom_target(run_program
  ALL
  COMMAND "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/hello_world"
  DEPENDS hello_world # name of the target this depends on
  COMMENT "Run the built executable")

By default, custom targets are not built unless we explicitly ask CMake for it. The optional keyword ALL ensures that this run_program target will be built when the default pseudo-target all is built.

This custom target invokes a command by providing the path to the hello_world executable that can be easily accessed by dereferencing the CMAKE_RUNTIME_OUTPUT_DIRECTORY (which we configured in the top-level CMakeLists.txt file). More crucially, this target lists the hello_world target as a dependency using the DEPENDS keyword. Thus, building this run_program target implies that the target hello_world must be up to date. If the hello_world executable is not present, then CMake will first build the target, and then run our command. Neat!

Thus, we can directly run the output immediately after the configure stage by simply building the default target as follows:


$ cmake --build build_dir

This will build and run hello_world, resulting in “Hello World!” being printed on the console!

Rebuilds in the event of source changes

One of the biggest advantages of using a build system generator like CMake is the easiness with which the project can be rebuilt in the event of changes to sources.

(Exercise) If the file hello.cpp is modified, we can skip the configure stage and simply proceed to the build stage with the usual invocation:

$ cmake --build build_dir

and the new executable will be built to correspond to the updated hello.cpp file.

If any of the CMakeLists.txt files are modified, then directly invoking the build step shall automatically trigger the configuration stage, and the resulting executable accounts for all such changes. This is because, when CMake initially generates the native build system, it implicitly declares the CMakeLists.txt files themselves as dependencies to the built executable.

(Exercise) Try adding a custom header file to hello.cpp and declare it as a dependency of the hello_world executable using the target_include_directories() command.

We can now reap the rewards of having a hierarchically organised project structure, with modular separation of project functionality in source code, together with a properly configured build system. This upfront effort helps the project to be flexible enough to evolve with minimal scalability challenges to tackle future needs.