Quick start to CMake

CMake is a family of tools which mainly serves to build cross-platform applications and libraries written in any of C family languages (C, C++, C#, Objective-C, CUDA).

It is not a compiler, or a construction tool though. Its goal is to generate a solution which will be used to build it. It’s a template consisting information about source files, dependencies and compilation settings that should be used to build a project no matter a platform it is built for or IDE it is used.

The best way to use CMake is via terminal, but many IDEs like CLion or Visual Studio Code integrates CMake into their pipeline.

Does it sound like magic? Only at the beginning. After setting it up for the first time everything becomes extra clear! Let’s check the basics.

Hello, world!

Let’s create a directory and name it CMakeTut. This will be a directory where our new project will be placed. Then, create a main.cpp file inside:
#include <iostream>
int main() 
{
    std::cout << "Hello, world!\n";
    return 0;
}
Simple code, nothing crazy. Now, inside the same directory, create a CMakeLists.txt file:
cmake_minimum_required (VERSION 3.0.0)
project (CMakeTut)
add_executable (CMakeTut main.cpp)
  • The first line is not required but it is a good practice to determine what is a minimum version of installed CMake required to generate this project. 
  • The second line creates a project named CMakeTut.
  • The third line says that CMakeTut will be an executable project and adds main.cpp file to the list of source files.
That’s all! This short template is enough to generate a solution. Now it’s time to actually generate it and build a program!

Building on Linux

For Linux we will use Makefiles. To generate a solution type the following commands to the console:
mkdir build
cd build
cmake -G "Unix Makefiles" ..
This will generate a Unix Makefiles solution inside a newly created build directory. Next, run make command:
make
And finally, you can run a program!
./CMakeTut

Building on Windows

For Windows, assuming there is a Visual Studio 2017 installed, type the following commands in the console:
mkdir build
cd build
cmake -G "Visual Studio 15 2017 Win64" ..
This will generate a Visual Studio 2017 solution. You can open this solution in Visual Studio and simply build a program in it. You can also build it with MSBuild from command line:
set MSBUILD_PATH="C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\Bin\amd64\msbuild"
%MSBUILD_PATH% CMakeTut.sln /property:Configuration=Debug /property:Platform=x64
As you can see you can tell MSBuild for which platform you want to build and with what configuration. Finally, you can run a program!
cd Debug
CMakeTut.exe

Changing output

By default a final executable will be stored in a directory from which the cmake has been called (so in our case – in build directory). To change that add the following command BEFORE the add_executable:
set (CMAKE_RUNTIME_OUTPUT_DIRECTORY ../Output)
If you want to change a name of an executable app to not be the same as a project name, add the following command AFTER the add_executable
set_target_properties (CMakeTut PROPERTIES OUTPUT_NAME "MyAwesomeApp")

Adding more files

This is nice, but we’d like to add more files to the project. How to do it? Let’s add two files: math.h and math.cpp to the project's directory:
math.h
class Math
{
public:
    static int Sum(int A, int B);
};
math.cpp
#include "math.h"
int Math::Sum(int A, int B)
{
    return A + B;
}
Let’s use a Sum function in main.cpp
#include <iostream>
#include "math.h"
int main() 
{
    std::cout << "Sum: " << Math::Sum(5, 10) << "\n";
    return 0;
}
To tell CMake to compile math.h and math.cpp simply add them to the add_executable command:
add_executable (CMakeTut main.cpp math.h math.cpp)
For better readability you can write it in a different manner, like this:
add_executable (CMakeTut 
    main.cpp 
    math.h
    math.cpp)
Or you can gather files into one list and pass them all at once:
set (MATH_FILES math.h math.cpp)
add_executable (CmakeTut main.cpp ${MATH_FILES})
You can imagine that this will be tedious with a project with a huge amount of files! There is a solution for that. Use recursive search for files. First, just for a convenience, move all source files to a new directory, let’s call it src. Then modify the add_executable command to look like this:
file (GLOB_RECURSE SRC_FILES
    src/*.cpp
    src/*.h
)
add_executable (CMakeTut ${SRC_FILES})
The file command creates a list of files called SRC_FILES which includes all cpp and h files in src directory. The search is recursive, so it will get all files from all directories inside the src directory. Finally, the list is passed to the add_executable command.

Extra tip: Organizing solution

If you generate a project for a Visual Studio or for a XCode you might notice that all files in project explorer have the same depth. Information about their directories has gone. You can fix this by using the following function:
function (assign_source_group)
    foreach (source IN ITEMS ${ARGN})
        if (IS_ABSOLUTE ${source})
            file(RELATIVE_PATH source_rel ${CMAKE_CURRENT_SOURCE_DIR} ${source})
        else()
            set(source_rel ${source})
        endif()
        get_filename_component(source_path ${source_rel} PATH)
        string(REPLACE "/" "\\" source_path_msvc ${source_path})
        source_group(${source_path_msvc} FILES ${source})
    endforeach()
endfunction (assign_source_group)
 
assign_source_group(${SRC_FILES})

Trace log

Sometimes you want to log some data to the screen. CMake gives you a message command, so if you want, for example, to check the value of a SRC_FILES list add the following command:
message ("Src files: " ${SRC_FILES})

Getting project directory

If you want to get the full path to the project directory you can use the CMAKE_CURRENT_SOURCE_DIR value.
message (${CMAKE_CURRENT_SOURCE_DIR})

Debug and Release

When using IDEs like Visual Studio or XCode you select the build configuration in project properties, but when you generate a Makefile solution the configuration must be set during solution generation. To do it you must pass a proper value to CMake when calling it:
cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Debug ..
Usually you can use Debug, Release, RelWithDebInfo and MinSizeRel configurations.
Because the CMAKE_BUILD_TYPE is not explicitly defined CMake will not set any additional flags. It is a good practice to set it to some default value:
if (NOT CMAKE_BUILD_TYPE)
    set (CMAKE_BUILD_TYPE Release)
endif()

Selecting compiler

On Linux there are usually multiple compilers we can use to build a solution. If not explicitly specified, CMake will use the best compiler it found. To use a compiler of your choice you must set CC (C compiler) and CXX (C++ compiler) flags before running cmake, for example:
CC=gcc-7 CXX=g++-7 cmake -G "Unix Makefiles" ..
CC=gcc-5 CXX=g++-5 cmake -G "Unix Makefiles" ..
CC=x86_64-linux-gnu-gcc-7 CXX=x86_64-linux-gnu-g++-7 cmake -G "Unix Makefiles" ..
CC=clang-6.0 CXX=clang++-6.0 cmake -G "Unix Makefiles" ..

Building libraries

But what if we don’t want our project to be an executable but a library? We simply replace add_executable command with an add_library command! Notice that setting output directory changes too!
set (CMAKE_ARCHIVE_OUTPUT_DIRECTORY ../Output)
add_library (CMakeTut ${SRC_FILES})
This will build a static library. If you want to build a shared library use the following commands:
set (CMAKE_LIBRARY_OUTPUT_DIRECTORY ../Output)
add_library (CMakeTut SHARED ${SRC_FILES})

Linking with static libraries

Let’s say we want to link our project against a static library. Consider we have a library and it’s located as described in the following tree.
|- CMakeTut
    |- Libs
        |- libMyLib.a
            |- include
                |- math.h
After the add_executable has been called we can simply add the library into an executable target:
target_link_libraries (CMakeTut "../Libs/libMyLib.a")
Or, if you want to keep it more organized, create a library target and then add it to an executable target:
add_library (MyLib IMPORTED STATIC GLOBAL)
set_target_properties (MyLib PROPERTIES
    IMPORTED_LOCATION "../Libs/libMyLib.a"
)
target_link_libraries (CMakeTut MyLib)
Also remember to add paths to the include directories:
target_include_directories (CMakeTut PUBLIC "Libs/include")

Adding macro definitions

You can pass macro definitions to the preprocessor using the target_compile_definitions command:
target_compile_definitions (CMakeTut PUBLIC MACRO1=1 MACRO2=0 MACRO3)
When compiling such code:
#if MACRO1
    std::cout << "This should be shown\n";
#endif
 
#if MACRO2
    std::cout << "This should NOT be shown\n";
#endif
 
#ifdef MACRO3
    std::cout << "This also should be shown\n";
#endif
The result of running program will be:
This should be shown
This also should be shown

Setting up C++ standard

You can tell the compiler which standard of C++ you want to be used. If we want to use C++11 standard add the following command:
set_property(TARGET CMakeTut PROPERTY CXX_STANDARD 11)
Unfortunatelly the MSVC compiler is rather recalcitrant while setting up c++ standard and will always setup the latest version supported. If you really want to change the version use:
target_compile_options(CMakeTut PUBLIC /std:c++11)

Setting up compiler options

You can set some additional compiler options like this:
target_compile_options (CMakeTut PUBLIC -Wall)
Remember that different compilers might require different flags:
if(MSVC)
  target_compile_options(CMakeTut PUBLIC /W4)
else()
  target_compile_options (CMakeTut PUBLIC -Wall)
endif()
If you want to use different flags for debug and release builds you can use generator expressions to set them up:
target_compile_options(CMakeTut PUBLIC
   $<$<CONFIG:Debug>:-Wall>
   $<$<CONFIG:Release>:-w>
)
To set additional linker options use the following command:
target_link_options(CMakeTut PUBLIC LINKER:-z,defs)

Summary

I think this is enough for the beginning. I hope this article helped you start learning CMake and believe me, you can do a lot of magic with it!