Build with CMake
Table of contents
Introduction
So far we have used command line commands to directly compile programs. For small-scale projects, this is sufficient. However, for large-scale C++ software codebase, we need a better and more automatic tool to compile, organize and install all the required files and binaries. In addition, since C++ does not have a centralized distribution of an official compiler, for different platforms we might need to use different compilers. And each of those compilers might have different options and settings that we need to account for. Luckily, we have these so called build systems that give developers a nearly unified interface across different platforms. Users of such systems usually need to write some configurations files, from which the build environments will then generate native makefiles for compilation.
CMake is one of such systems. With CMake, we write CMakeLists.txt
files to inform it where to find source code and header files. We then call CMake to generate makefiles, which are then used to compile and generate libraries and executables.
An Example
Let’s try creating our first CMake project to see how CMake works. Create a new folder, with the following files in it:
- CMakeLists.txt
- main.cpp
- my_lib.hpp
- my_lib.cpp
In CMakeLists.txt
, put down the following text:
cmake_minimum_required(VERSION 3.10)
project(
hello_world
VERSION 1.0
LANGUAGES CXX)
add_library(mylib my_lib.cpp my_lib.hpp)
add_executable(main main.cpp)
target_link_libraries(main PRIVATE mylib)
Let’s go over this line by line.
First, we have cmake_minimum_required(VERSION 3.10)
. This statement enforces that the minimum version required for CMake to generate build files for this project is 3.10. If your installed CMake has version lower than 3.10, then CMake will generate an error and abort the build process. By default, the CMake version installed on Ubuntu 20.04 is 3.16.
Then, we have the project(...)
statement. The arguments say that our project is named hello_world
, is versioned at 1.0, and is in C++. This is something that you should define yourselves based on your projects.
The next three lines define the actual library and executable. We first create a library called mylib
, which consists of the files my_lib.cpp
and my_lib.hpp
. We then create our executable main
, which consists of the file main.cpp
. We then link main
with our library mylib
. The PRIVATE
keyword means that this linking is not transitive: mylib
is not exposed to other binaries that link against main
. While this is less important for an executable, for libraries that depend on other libraries it is important to use PRIVATE
so that downstream users are not burdened with unnecessary libraries.
In main.cpp
, put down the following text:
#include <iostream>
#include "my_lib.hpp"
int main() {
std::cout << "Hello world!" << std::endl;
std::cout << my_lib_function() << std::endl;
return 0;
}
In my_lib.cpp
, put down the following:
#include <string>
#include "my_lib.hpp"
std::string my_lib_function() {
return "In library";
}
And put down the following in my_lib.hpp
:
#pragma once
#include <string>
std::string my_lib_function();
Now, we are ready to use CMake. We are going to do an out-of-source build, which is the recommended way of compiling software with CMake. Make a directory called build
, and enter it using cd build
. Then, run cmake ..
. You should see outputs similar to this:
-- The CXX compiler identification is GNU 7.5.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/jingnanshi/code/VNAV2023/example-project/build
Let’s go over the outputs together.
- The first line is self-explanatory: it is saying that CMake idenfities the C++ compiler be GNU
gcc
7.5.0. - The next two lines talk about C++ Application Binary Interface (ABI). We won’t dive into this here; just remember that ABI specifies how compiled code interacts with the underlying machine, and that different compiler/platform combinations might have different ABIs.
- The next three lines are CMake reporting its results of checking the available C++ compiler and its supported features. If your compiler is too old, and that you are specifing a new standard (such as C++ 20), then CMake might abort due to lack of compiler support.
- The next two lines show the two stages CMake perform its actions in. The first is the configuring stage, during which CMake builds an internal representation of the project. The second is the generation stage, during which CMake generates the necessary build files.
- Last but not least, CMake gives us the directory in which the generated build files live in.
If you cd
into build
, you should see the following files and directories:
CMakeCache.txt CMakeFiles cmake_install.cmake Makefile
CMakeCache.txt
is a text file containing the values of CMake cached variables. Usually we don’t need to touch this file.CMakeFiles/
is a directory containing CMake related files. Again we probably don’t need to touch it.cmake_install.cmake
is a CMake install script for our project. If we are developing a library, then this will become relevant.Makefile
is a text file for themake
tool to digest.make
will read this file, and compile our source code. This is generated automatically by CMake so we don’t need to write it by hand for every platform.
We can then run make
, and there will be two more files in the build folder: libmylib.a
which is our my_lib
library, and main
which is our executable. If you run main
, you should see the following two lines:
Hello world!
In library
Next Steps
You can find a good introductory tutorial to CMake here. While CMake is used in ROS (which we will cover in Lab 2), usually the accompanying CMakeLists.txt
files are relatively easy to figure out. So don’t stress too much if you are not familiar with it.