Instead of maintaining a Visual-Studio .sln file, and a makefile, and an XCode project CMake will generate these for you, and then some!

If you don’t have CMake already installed on your system, you can download it from the official site or if you are on Ubuntu run sudo apt install cmake

Basic CMakeList.txt

Let’s add CMakeLists.txt file to our project:

cmake_minimum_required(VERSION 3.8.0)

project(fractal VERSION 1.0.0 LANGUAGES CXX)

add_library(fractal src/fractal.cpp include/fractal/fractal.h)

target_compile_features(fractal PRIVATE cxx_std_11)

target_include_directories(fractal PUBLIC include)

This file will describe our project structure to CMake:

  • It is customary to begin every CMakeLists.txt file with minimum require version of CMake. For this tutorial, we will go with 3.8.0.
  • We will define out project name and version (it will come handy later), as well as the default language. This gives away the fact that CMake is not limited just to C++, but can work with a variatey of languages.
  • Next, we define a CMake target named fractal (not to be confused with our high-level CMake project). Targets are main actors participating in the build. Targets can be executables, libraries, or logical sets of constraints and commands.
  • We can hint the compiler that our code needs C++ 11 using new target_compile_features keyword.
  • Finally, we can configure include search path for the project.

In context of CMake, PUBLIC and PRIVATE keywords hint at how constraints should be composed when combining targets. PRIVATE constraints only affect the target they are defined on, while PUBLIC constraints propogate to any target dependent on it.

Now, from terminal / command-line we can run:

mkdir build && cd build
cmake ..
cmake --build .

This will result in our project being built using the native compiler our OS has to offer:

$ cmake ..
-- The CXX compiler identification is GNU 7.3.0
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/dorodnic/Desktop/fractal/build
$ cmake --build .
[ 50%] Building CXX object CMakeFiles/fractal.dir/src/fractal.cpp.o
[100%] Linking CXX static library libfractal.a
[100%] Built target fractal

Since CMake will auto-generate a lot of new files for the build, it is a good idea to run CMake not in the root directory of our project. Some people go for the ./build/ convention (with properly configured .gitignore), while others prefer to run CMake outside the project tree entirely.

To build the project with optimisation enabled, run:

mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build . --config Release

Code so-far

Growing the Project

CMake is meant to be modular. Let’s say we want to add a UI application to our project.

We will create new tools folder, with all the application logic:

In our tools/CMakeLists.txt we will specify everything required to build the tool:

cmake_minimum_required(VERSION 3.8.0)

project(fractal-tools)

set(FRACTAL_UI_INCLUDES ../include)
set(FRACTAL_UI_DEPENDENCIES fractal glfw3)

find_package(OpenGL)
if(NOT OPENGL_FOUND)
    message(FATAL_ERROR "OpenGL package not found!")
endif()
list(APPEND FRACTAL_UI_DEPENDENCIES ${OPENGL_LIBRARIES})

...

add_executable(fractal-ui fractal-ui.cpp)

target_compile_features(fractal-ui PRIVATE cxx_std_11)

target_include_directories(fractal-ui PRIVATE ${FRACTAL_UI_INCLUDES})
target_link_libraries(fractal-ui ${FRACTAL_UI_DEPENDENCIES})

One of the key benefits of using CMake is the ability to find_package. Here we are saying - this application needs OpenGL, and CMake will figure out how to satisfy this requirement on whatever system we happen to be.

Now in our main CMakeLists.txt, we can make tools part of our bigger project:

option(BUILD_TOOLS "Build UI Tools" ON)
if(BUILD_TOOLS)
    add_subdirectory(tools)
endif()

This will introduce a new variable into our top-level CMake, that we can control by running cmake .. -DBUILD_TOOLS=true or false. When enabled CMake will build our tools as well.

So after running cmake .. -DBUILD_TOOLS=true && cmake --build . we get:

Code so-far

User Side

Now let’s consider how someone developing a C++ application would consume our API.

Ideally, we would want him to just do find_package(fractal) and be good to go.

First step toward that goal is generating packageConfig.cmake file. This file will be auto-generated during our CMake process and capture all the targets and locations that were used during CMake.

The way to do this is to add fractalConfig.cmake.in template file:

@PACKAGE_INIT@

set(fractal_VERSION_MAJOR "@PROJECT_VERSION_MAJOR@")
set(fractal_VERSION_MINOR "@PROJECT_VERSION_MINOR@")
set(fractal_VERSION_PATCH "@PROJECT_VERSION_PATCH@")

set_and_check(fractal_INCLUDE_DIR "@PACKAGE_CMAKE_INSTALL_INCLUDEDIR@")

include("${CMAKE_CURRENT_LIST_DIR}/fractalTargets.cmake")
set(fractal_LIBRARY fractal::fractal)

And ask CMake to generate fractalConfig.cmake from it during CMake process:

set(CMAKECONFIG_INSTALL_DIR lib/cmake/fractal)

include(CMakePackageConfigHelpers)
configure_package_config_file(fractalConfig.cmake.in fractalConfig.cmake
    INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR}
    INSTALL_PREFIX ${CMAKE_INSTALL_PREFIX}/bin
    PATH_VARS CMAKE_INSTALL_INCLUDEDIR
)
write_basic_package_version_file("${CMAKE_CURRENT_BINARY_DIR}/fractalConfigVersion.cmake"
    VERSION "${PROJECT_VERSION}" COMPATIBILITY AnyNewerVersion)

Now every time we run CMake, fractalConfig.cmake is being generated in our build directory.

Now, to connect with our library all the user needs to know is the location of fractalConfig.cmake. So, if someone now will call find_package(fractal), CMake will ask for fractal_DIRECTORY location. After pointing this variable to our build folder, CMake will pick up the fractalConfig.cmake and compete the configuration process.

Installation

Ideally, we don’t want to keep track of where each library was built. Once it is built, we’d like to copy all library artifacts to some well-known (OS-specific) location.

This is called install in CMake terms.

We need to add a bit of CMake code to define what are our artifacts and what do we want to install:

set_target_properties(fractal
    PROPERTIES
    PUBLIC_HEADER
    "include/fractal/fractal.h"
    )

install(TARGETS fractal
    EXPORT fractalTargets
    RUNTIME DESTINATION bin
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
    PUBLIC_HEADER DESTINATION "${CMAKE_INSTALL_PREFIX}/include/fractal"
)

set(CMAKE_INSTALL_INCLUDEDIR "${CMAKE_INSTALL_PREFIX}/include")

install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/fractal DESTINATION include)

install(EXPORT fractalTargets FILE fractalTargets.cmake NAMESPACE fractal::
    DESTINATION ${CMAKECONFIG_INSTALL_DIR})
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/fractalConfig.cmake"
    DESTINATION ${CMAKECONFIG_INSTALL_DIR})
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/fractalConfigVersion.cmake"
    DESTINATION ${CMAKECONFIG_INSTALL_DIR})

Now we can execute the install step by running sudo make install or building the INSTALL target in Visual Studio (note that it will fail unless you run Visual Studio as an Admin, since it will try to install artifacts to C:/Program Files/)

The good news is that now find_package(fractal) will succeed without any extra steps, and anyone who wants to use our library will be able to easily do so.

Code so-far

Packaging

Of course, not all users want the hastle of building your library from source and installing it via CMake.

Depending on the platform, users expect some type of standard packaging, be it Debian package on Ubuntu, and .msi installer on Windows, or many other packaging types.

Furtunately, just like CMake is a cross-platform generator for build-systems, CPack is a cross-platform generator for packaging-systems.

This way, we can specify our prefered package-generator to CMake (for example, on Ubuntu you might want to pass -DCPACK_GENERATOR=DEB to generate a Debian package) and run cpack . to build the installer.

Code so-far

In the next part, we will learn how to set-up a cross-platform CI environment in the cloud