Getting Started with the CMake Build System

Note: This article was written in 2021, and targets CMake version 3.19. Details may differ for newer versions of CMake.

CMake is a cross-platform build system primarily for the C++ language. While CMake itself does not compile C++ code, it does invoke the compiler and linker of your choice. But it can do much, much more than just delegate to any C++ compiler. It also provides cross-platform access to most essential operating system tasks, such as managing directories and copying or processing resource files. The FetchContent module introduced in 2018 provides dependency management features that make it much easier to pull in third-party libraries than in the past. And to top it off, the CTest utility lets you run a suite of automated tests.

If you're still creating custom makefiles for a specific C++ compiler, or managing project properties in Visual Studio, you might want to pause your current project in order to learn about CMake.

This post will walk you through creating your first CMake project. I'll be referencing my Learning CMake video series over on Youtube that I created earlier this year.

CMake in Visual Studio

You can download and install CMake separately, but it does come packaged with Visual Studio 2019. Go to Tools, then Get Tools and Features. Find the CMake tools for Windows option.

Visual Studio comes packaged with CMake support!

You can verify your current version by going to Tools, then Command Line, then Developer Command Prompt. Run cmake --version.

CMake Version

When creating a new project, you can now pick the CMake Project template.

Create CMake Project

This will produce a shell project that includes a starter CMakeLists.txt file. This filename is case-sensitive, and provides the script that CMake will use to generate build files. Visual Studio supports directly invoking CMake to produce its proprietary solution (.sln) and project (.vcxproj) files. You can see a list of all the compilers that CMake supports by running cmake --help, and scrolling down to the Generators section.

Target any platform you want by choosing the appropriate Generator, with the -G option.

CMake's Two-Phase Build Process

CMake builds a project in two separate phases.

The generator phase will update your project's build files, for your chosen compiler. By default, Visual Studio will use its own proprietary compiler. If you right-click on the CMakeLists.txt file, you'll see a Configure MyProject option. This will invoke the generator phase. By default, the build files will be placed in an /out/build/ folder, so that they're kept separate from your source code.

The build phase will invoke your chosen compiler and linker, using the build files created by the generator phase.

This two-step process is what allows CMake to be cross-platform. If you wanted to build your project using a different configuration, a different compiler, or targeting a different platform, you can do so by using a different generator.

Building on the Command Line

Building your project from within Visual Studio is convenient for everyday programming. However, when it comes time to release your project, I recommend being familiar with the command-line interface.

I'll use a simple example project that you can find on my cmake-testing-grounds repository. I'll explain its CMakeLists.txt script in the next section, but for now just focus on the commands you can use to build the project:

mkdir build
cd build
mkdir Win64
cd Win64
cmake -S ../../ -B .
cmake --build .

The first four lines are there just to create a build folder. I am in the practice of placing the build files from the command-line in a separate /build/ folder, just to isolate them from the build files that are automatically created by Visual Studio.

The line cmake -S ../../ -B . invokes the generator phase. The -S option specifies the path to the root source folder, and is where your CMakeLists.txt file is expected to reside. The -B option specifies the path to the output build folder. If you wanted to target a different platform, you would pass the -G option with the name of the generator in quotes. For Visual Studio, you can also pass the -A Win32 option to target 32-bit Windows. Keep in mind that if you want to use a different compiler, its location will need to be added to your PATH environment variable.

The line cmake --build . invokes the build phase, and specifies the path to the build folder.

Example CMakeLists.txt Build File

Now to review the example CMakeLists.txt file from my cmake-testing-grounds repository:

cmake_minimum_required(VERSION 3.19)

project(LearnCMakePart1 VERSION 1.2)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED true)

string(TIMESTAMP BUILD_DATE "%Y%m%d")
string(TIMESTAMP BUILD_TIME "%H%M%S")

configure_file(configure/about.txt resources/about.txt)

add_executable(
 LearnCMakePart1
 main.cpp
)

add_custom_command(
 TARGET LearnCMakePart1 POST_BUILD
 COMMAND ${CMAKE_COMMAND} -E copy_directory
  ${CMAKE_SOURCE_DIR}/resources $<TARGET_FILE_DIR:LearnCMakePart1>/resources
)

enable_testing()

add_test(NAME GreetingTest COMMAND LearnCMakePart1)
set_tests_properties(
 GreetingTest
 PROPERTIES PASS_REGULAR_EXPRESSION "Hello, CMake, from our resource file!"
)

add_test(NAME VersionTest COMMAND LearnCMakePart1 -v)
set_tests_properties(
 VersionTest
 PROPERTIES PASS_REGULAR_EXPRESSION "Version: ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}"
)

The cmake_minimum_required command specifies the minimum version of CMake needed to build the project. The project command specifies a name and version. Both of these commands are required.

The two set commands specify the C++ language standard.

The string commands are used to set local variable values that will be referenced later in the script. In this example, they are used to store two timestamp variables with the date and time that the generator phase was run. The configure_file command is used not only to copy a file, but also to replace its contents with these variable values. Here's the source for the about.txt file:

version=@LearnCMakePart1_VERSION_MAJOR@.@LearnCMakePart1_VERSION_MINOR@
build=@BUILD_DATE@ @BUILD_TIME@

The add_executable command is used to create an executable target. You provide the name of the executable file, and the list of source files that should be compiled. Most projects will also need a target_link_libraries command, which is used to list third-party libraries for the C++ linker.

The enable_testing command is necessary if you plan to write automated tests. The add_test command can then be used to specify a test to be run. Each test is given a name, which will appear in the results when running ctest on the command line. For this example, the set_tests_properties commands are used to verify that the console output matches a particular regular expression.

The official documentation provides a complete list of CMake commands here.

Learning CMake Video Series

For a more comprehensive dive into CMake with a particular focus on getting started for game development, you can check out my Learning CMake video series on Youtube. This starts out by following the official CMake Tutorial, but provides some additional information. You'll find details on an appropriate .gitignore file, copying resource files, building libraries, managing third-party dependencies, and using Github Actions to help automate your build process.

In my next post, I'll be exploring how to use CMake to pull in SFML as a third-party dependency for game development.