Build with CMake, Part 1

CMake: Get to Know this Open Source Tool

By Chris Rizzitello

CMake is an open-source, cross-platform family of tools designed to build, test and package software. According to the CMake website, CMake is used to "control the software compilation process using simple platform and compiler-independent configuration files, and generate native makefiles and workspaces that can be used in the compiler environment of your choice."

It is widely used because CMake allows developers to more easily create, tailor and test software by simplifying some of the most challenging aspects of the process, including system introspection and executing complex builds. CMake is intended to be used in conjunction with the native build environment, which differentiates it from many cross-platform systems. 

To get you started with CMake, I’ve created this series. Over the course of three blog posts I'll show you how to build a basic CMake project, as well as find libraries and link them to your application. And I’ll walk you through a detailed example that builds a complex library project to show you how to get your project set up for use downstream. Today’s Part 1 explains how to build a basic CMake project. 

Creating a Basic CMake Project

CMake uses simple configuration files in each source directory to generate standard build files. When CMake runs, it locates files, libraries and executables. It may also encounter optional build directives. The build process is controlled by creating one or more CMakeLists.txt files in each directory (including subdirectories) that make up a project.

When you install CMake several binaries are part of the installation. The one you will work with most often is cmake. There are also cpack and ctest executables, which can be used directly or called as part of your main cmake file. In this blog, I’ll focus on the cmake application as this is the one you will interact with the most often.

Configure Your Build

The first step to building an application that uses CMake is to configure the build for your system. This is no different than working with any other build system. Some systems provide scripts or other special commands to call when setting the configuration. CMake uses the cmake command for this. 

To set various options for the build, I’ll use some of the switches in the cmake command. When setting your own build, there are only a few switches you will need to know for project configuration, which I’ve described below. For many projects, it is often simply running the cmake command in the directory of the CMakeLists.txt file.

The switches you should know for configuration are:

  • -S PATH sets the input directory to PATH. Default '.'
  • -B PATH sets the output directory to PATH. Default '.'
  • -DOPTION=VALUE sets OPTION to VALUE.
  • -G FORMAT to generate output in a specific format, use -G to see what formats your system supports.

Example: CMake configuration command

> cmake -B build -DCMAKE_BUILD_TYPE=Debug

This will build a native "Makefile" for your system. It does not build the code. For that you have two choices: either use the native tool directly or have cmake do it for you.

> cmake -B build -G ninja

Generates build files to be used with the ninja build system

The -D switch is used to configure cmake and project variables. Some of them may be overridden by the CMakeLists.txt file.

Many times you only will use these two common variables:

  • CMAKE_BUILD_TYPE This sets the build type, The default is Release. Two other modes Debug and RelWithDebInfo are also provided by default build types.
  • CMAKE_INSTALL_PREFIX Controls the path that cmake will use as the base directory when the install target is used. The default for this depends on the underlying system but for the common desktop systems it is /usr/local, Program Files and /Applications. Be sure to set this when you want to install to another base path.

Build Your Code

Once you have configured the project for building you can now build the code. Building with CMake requires you only know two switches:

  • --build PATH build the files in PATH where PATH contains output from cmake configuration step
  • --target TARGET build the target TARGET
> cmake --build build

Builds the output of the previous configuration The default target of ALL will be used.

> cmake --build build --target install

Builds the output of the previous configuration, as well as builds the target "install". Same as `make install`. Remember that unless you set the CMAKE_INSTALL_PREFIX this will install to a location you may not have access to and that can cause the command to fail. You can also break this up into two commands.

> cmake --build build
> cmake --install build –prefix installDir

The first one will build our configured project from the “build” folder and the second will install the project to the prefix “installDir” this path is relative to the path the command is run in. This is a good way to test your install steps if you forgot to set the CMAKE_INSTALL_PREFIX.

The CMakeLists.txt File

While there are many things I can put in my CMakeLists.txt, today I’ll focus on a few basic items in order to build a very simple C++ application.

Application Source Code

For our CMake project we will use a standard C++ Hello World application.

#include <iostream>
  int main()
  {
   std::cout << "Hello World";
   return 0;
  }

In order to build the application "hello" using g++ we would use the command:

> g++ main.cpp -o hello

If I wanted to build this on Windows or MacOs I would need to know the commands for the compilers on those systems. As our application becomes more complex the command(s) to build will also become more complex. In my simple example, you can already see how cmake can at the very least make build commands more consistent no matter the compiler or system.

The Simplest CMakeList.txt

The first thing I need in my CMakeLists.txt is something to tell CMake the version needed to parse this file. CMake often adds new commands for each release, which means this line is essential so the system knows whether or not it can parse the file.

cmake_minimum_required(VERSION 3.21)

After I’ve set a minimum version I can create a project.

project(hello VERSION 0.0.1
              DESCRIPTION "A Simple Hello World Application"
              LANGUAGES CXX
)

This will create a basic project. It also populates the a few variables that CMake can use later, such as ${CMAKE_PROJECT_NAME} (hello) and its version ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}. The ${CMAKE_PROJECT_DESCRIPTION} and the LANGUAGES that this project will use to build itself. 

Note that I can put new lines between the various parts of the project command, which can greatly enhance readability.

With a project now defined, I can make targets where I want to make an executable the next line.

add_executable(hello main.cpp)

Since the sample application does not use any external libraries, I don’t have to provide any linking information. But don’t worry; I’ll show you how to use external libraries later in the blog series.

For today’s example, the last task is to provide install information. To do this, I first must import a module that is aware of GNUInstallDirs. (Despite its name, GNUInstallDirs is aware of paths for all operating systems.)

include(GNUInstallDirs)
install(TARGETS hello
        RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

The first line, include, will include the cmake module GNUInstallDirs, which is included with CMake. I will then use the install command to install TARGETS using the same name used earlier when I called add_executable.

In this example, the EXE target is named “hello”. The install command can install targets and these targets can be of several different types, such as RUNTIME, LIBRARY and a few others. Each of these have their own DESTINATION. That is where files of that type for the listed targets will be deployed.

GNUInstallDirs provides the variable ${CMAKE_INSTALL_BINDIR}${CMAKE_INSTALL_PREFIX}.

With cmake I can use the following to build and install the EXE target ‘hello’:

> cmake -B build -DCMAKE_INSTALL_PREFIX=installDir
> cmake --build build --target install

I will build and then install to the "installDir" directory in our source tree. You’ll be able to see what is installed and where.

> ls -R installDir/
installDir/:
bin

installDir/bin:
hello

Conclusion

As you’ve seen, with just a few simple lines I can make a CMake project for a basic case. In Part 2 of this series, I’ll show you how to find libraries and link them to your application. In the meanwhile you can read more on CMake in our blogs How to Create a Qt/CMake Project That Easily Supports Unit Testing and Revisiting the Qt Installer Framework with CMake.