The Complete C++ Build System: CMake & Ninja Part 1

CodeInSeoul
7 min readJul 12, 2023

--

Introduction

As C++ programmers, we always struggle with building projects written in C++. Sometimes we clone a C++ project and spend a long time understanding how the project is built. And sometimes we need to build our own projects by ourselves. For C++, there are many choices related to build systems like Makefile, CMake, Ninja, Bazel, and Scons, to name a few. It’s not easy to find a perfect combination for our C++ project. This article will introduce you to the most stable, the most powerful, and the fastest build system for C++.

CMake is probably the most widely used and the most powerful build system generator for C++. Many large projects written in C++ are using CMake as their build system generator, so you may often use CMake and encounter files like CMakeLists.txt. And sometimes you need to open these CMake files to figure out how a project is organized and built. Or you may write your own CMake files to build your own project.

Somehow you manage to read and write CMake files for a project, then what? You would need a build system. Every build system has pros and cons, but build speed might be the most important factor when choosing a build system for your project. If you are working on a very large project, it would take a long time to build and test the project after you change a tiny piece of code. Ninja just might be the answer for you if speed matters. For instance, the LLVM framework, which has millions of Lines of Code (LoC), recommends its developers use the combination of CMake and Ninja.

If you are a C++ developer who wants to understand the CMake build system more, this is the right article for you. This article will teach you the best practice of reading and writing CMake files, so you can better understand and build projects. We also assume that you know nothing about CMake or Ninja. So don’t panic and buckle up!

Install

You can install CMake and Ninja on Windows, macOS, and Linux environments. Here, we explain how to install CMake and Ninja on a Ubuntu environment. You can install them on Ubuntu with the commands shown below.

Install CMake & ninja on a Ubuntu environment

Hello, World!

Let’s get started with the simplest form of a project, that has only one source file. Let’s build the old “Hello, World” project. You can find all of the code used in this article in our GitHub repo. The C++ code for printing “Hello, World!” is nothing special, as shown below.

hello_world.cc

CMake is a build system generator that is platform- and compiler-independent. That means you don’t need to rewrite code for CMake again when you want to port your project from one system to another. CMake also has its own language, called the CMake language. Thanks to its language, you can implement complicated logic in CMake files. You can find documentation for the CMake language on the official website.

Let’s write our first CMake files. The first thing you need to do is to create CMakeLists.txt with your favorite code editor. Then, copy and paste the code below. cmake_minimum_required in Line 1 specifies the minimum CMake version to build the project. If CMake version is lower than 3.5, CMake just aborts. project in Line 3 specifies the project name is hello_world and the language used for the project is CXX, which means C++. add_executable includes the source file hello_world.cc to the executable hello_world.

CMakeLists.txt

Everything is ready. Now, let’s build hello_world project! CMake generates lots of files related to the build system and we don’t want to mess up our code base with these automatically generated files, so we create build directory. Then, cmake .. -G Ninja command generates these files for Ninja. Finally, ninja command builds the project from the files generated by CMake. Most of the time, you will only need this series of commands when building your project.

Build the project

If you enter the build directory, you will find the hello_world binary. If you run the binary, then “Hello, World!” will be printed on the screen.

Libraries

A C++ project almost always has static or shared libraries to link. Again, we will reuse the old “Hello, World!” project, but this time, we will split the project into different files and directories. You can find all of the code used in this article in our GitHub repo as well.

First, we implement a tiny library, called message, which has print_message function that prints a message to the screen. To create a library, we prepare CMakeLists.txt. We don’t need to specify the minimum CMake version, because the version will be specified in the top-level CMakeLists.txt later. A library might have lots of files and directories inside, so often we need a way to include source files by using a regular expression. file(GLOB_RECURSE SRCS *.cc) in Line 1 recursively finds files with .cc extension and adds them to SRCS. Then, we create a library with add_library(message STATIC ${SRCS}), where message is the library name and STATIC means that we want to create a static library. If you change STATIC to SHARED, CMake creates a shared library instead.

lib/message/message.h
lib/message/message.cc
lib/message/CMakeLists.txt

Imagine lib directory has multiple libraries that we clone from GitHub, each of which has CMakeLists.txt. Since we want to build all libraries, we need a higher-level CMakeLists.txt that runs each CMakeLists.txt for each library. add_subdirectory(message) runs CMakeLists.txt in message folder, which we have just seen. If we have more libraries in lib folder, a list of add_subdirectory will appear in this CMakeLists.txt.

lib/CMakeLists.txt

We put our own source code in src directory. And we put wrapper functions that use message library in src/api directory. print_hello and print_bye functions print different messages to the screen by using the same message library. main function uses these functions to print messages to the screen.

It’s time to build our own source code with src/CMakeLists.txt. The first thing we need to do is to specify where we can find header files. For example, #include "message/message.h" in Line 2 of src/api/message_api.cc assumes that we can find header files of message library under message directory. We can specify locations where the build system looks for headers by using include_directories. The first include_directories(.) says that the build system should find headers in the current directory, which is src. And the second include_directories(../lib) says it should find headers in lib directory.

Then, CMakeLists.txt sets flags with set. set(CMAKE_CXX_COMPILER "/usr/bin/g++") sets CMAKE_CXX_COMPILER to "/usr/bin/g++", the path to g++ in our system. Later, when the build system chooses a C++ compiler, it looks up CMAKE_CXX_COMPILER variable and uses this compiler when building the project. Likewise, set(CMAKE_CXX_STANDARD 20) says that the C++ standard is 2020 and set(CMAKE_CXX_FLAGS "-O3") sets the optimization level to O3.

As we did before for lib/CMakeLists.txt, we recursively find source files in Line 4 and add them to the binary in Line 6. But how can we tell the build system to link which libraries? We can do that by using target_link_libraries. target_link_libraries(libraries message) says that the libraries executable should link to message library.

With lib/CMakeLists.txt and src/CMakeLists.txt, we could specify how we can build the libraries and our own source files. We need to connect the two CMakeLists.txt, so libraries and our own source files can be built together. Let’s look at the top-level CMakeLists.txt!

src/api/message_api.h
src/api/message_api.cc
src/main.cc
src/CMakeLists.txt

The top-level CMakeLists.txt specifies the minimum CMake version in Line 1, the name of the project in Line 3, and includes lib and src directories in Line 5 and Line 6, respectively.

CMakeLists.txt

When you type the commands shown below, this time, you can find the executable in build/src/libraries as well as the library at build/lib/message/libmessage.a. Isn’t it amazing that the combination of CMake and Ninja does everything for us? You can also create a shared library instead by following the instructions above—we leave this as an exercise.

Build the project

If you are working on small or medium size projects, the CMake skills covered in this article will be sufficient. But if you are working on large-scale projects, you might need more complex logic to build the project — for instance, you might want to build the project differently based on conditions. We are going to cover more advanced CMake skills in our next article!

Reference

  • Bast et al., “CMake Cookbook: Building, Testing, and Packaging Modular Software with Modern CMake”

--

--

CodeInSeoul

Articles on system programming and how computers work • Led by Computer Architecture PhD Student :: www.codeinseoul.com