CMake is a great tool for building C++ projects. Once you learn the syntax it's easy to work with, it's used extensively in production grade projects around the world, and it's reliable and moderately easy to maintain.
Unfortunately CMake can be a little bit esoteric at first. This tutorial provides what I consider to be the basic setup that will actually satisfy most projects.
Our project structure is quite basic:
.
├── bin
├── static
├── readme.md
└── src
├── CMakeLists.txt
├── main.cpp
└── lib
├── CMakeLists.txt
├── counter.cpp
└── counter.h
We have a root directory (the dot at the top) which contains a readme.md file, a src directory, and then bin and static directories. All of the commands we execute are performed in the root directory. The bin and static directories contain the build output and aren't part of the main project structure -- everything we care about is inside src.
There are three main components to this tutorial:
Perhaps the simplest thing you can do in C++ is build a single source file into an executable that you can then run locally and distribute externally. In this simple example all we're going to do is print something to the screen, so that you can see the mechanics of what is going on, rather than focussing on any implementation details.
The following code should go in main.cpp, which is in the src directory. It's considered best practice to keep all the source and header files inside a directory (which of course can contain other directories) and then the build outputs go inside a separate directory at the same level as src. This is why in the project tree above we can see that the src, bin, and static directories are all on the same level.
// ./src/main.cpp
#include
int main(int argc, char **argv)
{
std::cout << "Hello, universe!" << std::endl;
return EXIT_SUCCESS;
}
We have a single main function that prints Hello, universe!
to the console and then exits with no
error, easy! To build this into something we can run we need a _CMakeLists.txt_ file. This file should always have
the same name, and there should be one in every directory that you consider to be "something you want to build" .
At
this stage we are only building a single file, but the CMakeLists.txt in the root directory could include any
number
of source files, any number of libraries that are part of the same project, and also external dependencies. In
larger projects it's this root CMakeLists.txt file that will contain the build setup description for your main
executable. Our basic CMakeLists.txt contains only the following lines:
# ./CMakeLists.txt
cmake_minimum_required(VERSION 3.28)
project(basic_executable)
set (CMAKE_CXX_STANDARD 20)
add_executable ( basic_executable main.cpp )
The cmake_minimum_required
line is required at the start of every CMakeLists.txt file. The version
number listed should be as recent as possible, without compromising your ability to build and distribute your
project as necessary. In this case I am simply using the version of cmake that is available on my local machine,
which I found by executing cmake --version
on the command line. For me this returns
cmake version 3.28.1
. Next we have the project
specification. This doesn't do a lot
other
than set an internal variable that we don't need to worry about (PROJECT_NAME
, and some project
specific paths), _however_ we must always declare the project in our top-level CMakeLists.txt, otherwise cmake
will
generate a project name for us. CMakeLists.txt files in sub directories do not need to set the
project
command. The project
command can also be used to set some other variables specific to the project,
for
instance the language used and a description of the project, however these are optional and have been skipped
here.
Next we call set
to specify the version of the C++ standard that we wish to use. This is an optional
command, but is helpful because otherwise the compiler will use its default language variant. For modern
clang
(>16.0) this is fine as it will default to c++17
, however clang version 16 is very
new and not prevalent on most machines.
We then come to the most important line of our CMakeLists.txt file: add_executable()
. As is perhaps
expected this line takes the list of files defined (in this case just main.cpp) and compiles them into an
executable
called basic_executable. Note that the name we give to our executable must be unique within the project. This
means
that we can't have an executable called basic_executable and then a library also called basic_executable. On MacOS
this command will generate a file called basic_executable, but on other platforms such as Windows it may add .exe
to
the filename. This depends on the convention of the native platform we're building on.
To actually build anything we just need to execute two commands:
$ cmake -S src -B bin
$ cmake --build bin
Remember to do these in the root directory. Note that the bin directory doesn't need to exist before executing
these, and if you run into any problems or want to start the build from scratch you can simply delete the bin
directory (i.e. with rm -rf bin
). It's also typical to exclude this directory from version control,
so
you can
add "/bin" to your .gitignore file.
The first line cmake -S src -B bin
_configures_ the project and only needs to be run once, while the
second line cmake --build bin
generates the actual build. The final executable will be
bin/basic_executable, which you can run with ./bin/basic_executable
. Note that these directories can
have any names you like, however I stick with the common src / bin idiom (it's probably more common to use build
instead of bin, however I like to use bin for executables as we may have other build targets in separate output
directories later, so bin directly signifies that this is contains a *_binary*).
We should see the following output:
$ ./bin/basic_executable
> Hello, universe!
That's fun, but what if we want to move some of our code into a separate library and build it independently? This might seem trivial for a small example like this, but the need can actually arise pretty quickly. We might have some different code for iOS vs Android for instance, or perhaps we want to include some specific code that's used for visualisation during debug that we don't want to compile into our final release build.
In this simple example we're going to build a library that provides two functions: increment()
and
decrement()
. Since we've only got one library in this example we're going to put all the source code
inside the
lib directory, which itself is inside the src directory. Since this is a separate 'module' that we want to build
we're also going to put a CMakeLists.txt file inside the lib directory, but this one will be a bit different from
the file we used to compile our executable originally.
To start, let's write the library code. Our header file is:
// ./src/lib/counter.h
#pragma once
struct Counter
{
int count = 0;
void increment(const int amount);
void decrement(const int amount);
};
and our source file is:
// ./src/lib/counter.cpp
#include "counter.h"
void Counter::increment(const int amount)
{
count += amount;
}
void Counter::decrement(const int amount)
{
count -= amount;
}
Using this struct we can define a counter object that either increments or decrements its internal value by the specified amount. Let's update main.cpp to do this:
// ./src/main.cpp
#include
#include "counter.h"
int main(int argc, char **argv)
{
std::cout << "Linking _count_ library into executable" << std::endl;
Counter counter;
std::cout << "Count: " << counter.count << std::endl;
counter.increment(20);
std::cout << "Count: " << counter.count << std::endl;
counter.decrement(9);
std::cout << "Count: " << counter.count << std::endl;
return EXIT_SUCCESS;
}
Now, to build count and make it availble to our executable we're going to perform two steps. (1) we're going to compile a library from the counter code, and (2) we're going to link this library into our executable. This is what we need in the counter CMakeLists.txt:
cmake_minimum_required(VERSION 3.28)
add_library( Counter counter.cpp )
Again we use the same cmake_minimum_required
, and this time we're skipping declaring the
project
or
specifying the language version. Importantly, rather than calling add_executable
we're now calling
add_library
. The first parameter to the add_library
function is the name of the library,
which in this case
is "Counter" . We then specify a list of source files that we want to be included in the library. Note that we
don't need to include the header files, but we can if we want to. Note also that it's considered best practice
_not_ to use file globbing to define the files for compilation, but rather to define all the files manually. At
this stage we aren't going to invoke this CMakeLists.txt ourselves, but rather we're going to refer to it from the
root CMakeLists.txt. Replace the root CMakeLists.txt with the following:
cmake_minimum_required(VERSION 3.28)
project(basic_executable_with_library)
set (CMAKE_CXX_STANDARD 20)
add_executable (basic_executable_with_library main.cpp )
add_subdirectory( lib )
target_link_libraries(basic_executable_with_library Counter)
target_include_directories(basic_executable_with_library PRIVATE lib )
Most of this is the same as before, but there's three new functions we're calling: add_subdirectory
,
target_link_libraries
, and target_include_directories
. The first new function,
add_subdirectory
, steps down
into the directory specified (in this case: "lib" ) and looks for a CMakeLists.txt in that directory. If found it
adds the commands from there into the build graph. We then _link_
the static library that will be built from the add_subdirectory
call by calling
target_link_libraries
and
passing in our _target_ (i.e. the thing we are currently building, which happens to be the executable) and the
library we want to link against, which in this case is called "Counter" . These two steps essentially provide
access
to the _symbols_ that get defined in our library (i.e. the actual source code itself), however we still need to
know
where the header files are. This is achieved by calling target_include_directories
and first
specifying
the target
(again our executable), then specifying PRIVATE
as the _scope_ (don't worry about this, just use
PRIVATE) and
finally specifying the path to the directory that contains the header files. We invoke cmake the same way we did
before, by running:
bash $ cmake -S src -B bin $ cmake --build bin
Remember that you only need to call cmake -S src -B bin
once (although it doesn't matter if it gets
called
multiple times). As expected, running this gives the following output:
Linking _count_ library into executable Count: 0 Count: 20 Count: 11
What about if we want to distribute our library for someone else to use, or for us to use somewhere other than
the
current project? Easy -- we just distribute the static library we've built. Behind the scenes cmake has built the
Counter library for us, and put it inside the bin directory -- this is what we've been linking against in the
previous example. Specifically, the library is the ./bin/lib/libCounter.a
file (static libraries on
macos have the
.a file extension and 'lib' at the start). You can share this library as you wish (don't forget to share the
header
files as well), or we can independently build this library, without changing the code or cmake setup. To build
this
library independantly of the executable we're just going to tell cmake to build what's inside lib, rather than
what's inside src -- easy!
$ cmake -S src/lib -B static
$ cmake --build static
First we tell cmake that the source directory is actually src/lib rather than just src, and then we tell it to
put
everything into a build folder called static (this can be any name you like). Finally, we tell cmake to
--build
the static directory, which will generate our library. When we do this we will get two warnings from cmake. The
first is telling us that we didn't define a project name, and the second is regarding "default member
initialisation" in our header file. These two warnings can be fixed by adding in the following lines to the
CMakeLists.txt file in src/lib after the cmake_minimum_required
call.
project( Counter )
set (CMAKE_CXX_STANDARD 20)