Introduction to modern CMake for beginners (2024)

— Written by Triangles on September 04, 2020 • updated on September 08, 2020 • ID 83 —

A look at one of the most popular build systems for C and C++.

CMake is a collection of open-source and cross-platform tools used to build and distribute software. In recent years it has become a de-facto standard for C and C++ applications, so the time has come for a lightweight introductory article on the subject. In the following paragraphs we will understand what CMake is exactly, its underlying philosophy and how to use it to build a demo application from scratch. Mind you, this won't be the definitive CMake bible. Rather, just a practical, ongoing introduction to the tool for humble enthusiasts like me.

What is CMake exactly

CMake is known as a meta build system. It doesn't actually build your source code: instead, it generates native project files for the target platform. For example, CMake on Windows will produce a solution for Visual Studio; CMake on Linux will produce a Makefile; CMake on macOS will produce a project for XCode and so on. That's what the word meta stands for: CMake builds build systems.

A project based on CMake always contains the CMakeLists.txt file. This special text file describes how the project is structured, the list of source files to compile, what CMake should generate out of it and so on. CMake will read the instructions in it and will produce the desired output. This is done by the so-called generators, CMake components responsible for creating the build system files.

Another nice CMake feature is the so-called out-of-source build. Any file required for the final build, executables included, will be stored in a separated build directory (usually called build/). This prevents cluttering up the source directory and makes it easy to start over again: just remove the build directory and you are done.

A toy project to work with

For this introduction I will be using a dummy C++ project made up of few source files:

myApp/ src/ engine.hpp engine.cpp utils.hpp utils.cpp main.cpp

To make things more interesting, later on I will spice up the project with an external dependency and some parameters to pass at build stage to perform conditional compilation. But first let's add a CMakeLists.txt file and write something meaningful in it.

Understanding the CMakeLists.txt file

A modern CMake's CMakeLists.txt is a collection of targets and properties. A target is a job of the building process or, in other words, a desired outcome. In our example, we want to build the source code into a binary executable: that's a target. Targets have properties: for example the source files required to compile the executable, the compiler options, the dependencies and so on. In CMake you define targets and then add the necessary properties to them.

Let's start off by creating the CMakeLists.txt file in the project directory, outside the src/ directory. The folder will look like this:

myApp/ src/ engine.hpp engine.cpp utils.hpp utils.cpp main.cpp CMakeLists.txt

Then open the CMakeLists.txt file with the editor of your choice and start editing it.

Define the CMake version

A CMakeLists.txt file always starts with the cmake_minimum_required() command, which defines the CMake version required for the current project. This must be the first thing inside a CMakeLists.txt file and it looks like this:

cmake_minimum_required(VERSION <version-number>)

where <version-number> is the desired CMake version you want to work with. Modern CMake starts from version 3.0.0 onwards: the general rule is to use a version of CMake that came out after your compiler, since it needs to know compiler flags, etc, for that version. Generating the project with a CMake older than the required version will result in an error message.

Set the project name

The second instruction a CMakeLists.txt file must contain is the project name, defined by the project() command. This command may take multiple options such as the version number, the description, the homepage URL and much more. The full list is available in the documentation page. A pretty useful one is the programming language the project is written in, specified with the LANGUAGES flag. So in our example:

project(myApp VERSION 1.0 DESCRIPTION "A brief CMake experiment" LANGUAGES CXX)

where CXX stands for C++.

Define the executable target

We are about to add our first CMake target: the executable. Defined by the add_executable() command, it tells CMake to create an executable from a list of source files. Suppose we want to call it myApp, the command would look like this:

add_executable(myApp src/engine.hpp src/engine.cpp src/utils.hpp src/utils.cpp src/main.cpp)

CMake is smart enough to construct the filename according to the target platform conventions: myApp.exe on Windows, myApp on macOS and Linux and so on.

Set some target properties

As said earlier, targets have properties. They are set by a bunch of commands that start with the target_ suffix. These commands also require you to define the scope: how properties should propagate when you include the project into other CMake-based parent projects. Since we are working on a binary executable (not a library), nobody will include it anywhere so we can stick to the default scope called PRIVATE. In the future I will probably write another article about CMake libraries to fully cover this topic.

Set the C++ standard in use

Let's assume our dummy project is written in modern C++20: we have to instruct the compiler to act accordingly. For example, on Linux it would mean to pass the -stdc++=20 flag to GCC. CMake takes care of it by setting a property on the myApp target with the target_compile_features() command, as follows:

target_compile_features(myApp PRIVATE cxx_std_20)

The full list of available C++ compiler features is available here.

Set some hardcoded preprocessor flags

Let's also assume that our project wants some additional preprocessor flags defined in advance, something like USE_NEW_AUDIO_ENGINE. It's a matter of setting another target property with the target_compile_definitions() command:

target_compile_definitions(myApp PRIVATE USE_NEW_AUDIO_ENGINE)

The command will take care of adding the -D part for you if required, so no need for it.

Set compiler options

Enabling compiler warnings is considered a good practice. On GCC I usually go with the common triplet -Wall -Wextra -Wpedantic. Such compiler flags are set in CMake with thetarget_compile_options() command:

target_compile_options(myApp PRIVATE -Wall -Wextra -Wpedantic)

However, this is not 100% portable. In Microsoft's Visual Studio for example, compiler flags are passed in a completely different way. The current setup works fine on Linux, but it's still not cross-platform: we will see how to improve it in a few paragraphs.

Running CMake to build the project

At this point the project is kind of ready to be built. The easiest way to do it is by invoking the cmake executable from the command line: this is what I will do in the rest of this article. A graphical wizard is also available, which is usually the preferred way on Windows: the official documentation covers it in depth. On Unix ccmake is also available: same thing for Windows but with a text-based interface.

As said earlier, CMake supports out-of-source builds, so the first thing to do is to create a directory — sometimes called build but the name is up to you — and go in there. The project folder will now look like this:

myApp/ build/ src/ engine.hpp engine.cpp utils.hpp utils.cpp main.cpp CMakeLists.txt

From inside the new folder invoke the CMake as follows:

cmake ..

This will instruct CMake to read the CMakeLists.txt file from above, process it and churn out the result in the build/ directory. Once completed, you will find your generated project files in there. For example, assuming I'm running CMake on Linux, my build/ directory will contain a Makefile ready to be run.

What we have seen so fare is a barebone yet working CMake configuration. We can do better, though: keep reading for additional improvements.

Add multiplatform support: Linux, Windows and macOS

CMake gives you the ability to detect which platform you are working on and act accordingly. This is done by inspecting CMAKE_SYSTEM_NAME, one of the many variables that CMake defines internally. CMake also supports conditionals, that is the usual if-else combination. With this tools in place, the task is pretty easy. For example, assuming we want to fix the portability issue we had before with the compiler options:

if (CMAKE_SYSTEM_NAME STREQUAL "Windows") target_compile_options(myApp PRIVATE /W4)elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") target_compile_options(myApp PRIVATE -Wall -Wextra -Wpedantic)elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin") # other macOS-specific flags for Clangendif()

Notice how STREQUAL is the CMake way for comparing strings. A list of possible CMAKE_SYSTEM_NAME values is available here. You can also check for additional information such as the operating system version, the processor name and so on: full list here.

Passing command line variables to CMake

In our current configuration we have a hardcoded preprocessor definition: USE_NEW_AUDIO_ENGINE. Why not giving users the ability to enable it optionally while invoking CMake? You can do it by adding the option() command anywhere in the CMakeLists.txt file. The syntax is the following:

option(<variable> "<help_text>" [value])

The optional [value] can be ON or OFF. If omitted, OFF is used. This is how it would look like in our dummy project:

option(USE_NEW_AUDIO_ENGINE "Enable new experimental audio engine" OFF)

To use it, just run CMake as follows:

cmake -DUSE_NEW_AUDIO_ENGINE=ON ..

or:

cmake -DUSE_NEW_AUDIO_ENGINE=OFF ..

This is also how internal CMake variables and other options are passed from the cmake executable. More generally:

cmake [options and flags here] <path to CMakeLists.txt>

Debug versus release builds

Sometimes you want to build an executable with debugging information and optimizations turned off for testing purposes. Some other times an optimized build ready for release is just fine. CMake supports the following build types:

  • Debug — debugging information, no optimization;
  • Release — no debugging information and full optimization;
  • RelWithDebInfo — same as Release, but with debugging information;
  • MinSizeRel — a special Release build optimized for size.

How build types are handled depends on the generator that is being used. Some are multi-configuration generators (e.g. Visual Studio Generators): they will include all configurations at once and you can select them from your IDE.

Some are single-configuration generators instead (e.g. Makefile Generators): they generate one output file (e.g. one Makefile) per build type. So you have to tell CMake to generate a specific configuration by passing it the CMAKE_BUILD_TYPE variable. For example:

cmake -DCMAKE_BUILD_TYPE=Debug ..

In such case it's useful to have multiple build directories, one for each configuration: build/debug/, build/release/ and so on.

Dependency management

Real world programs often depend on external libraries but C++ still lacks of a good package manager. Luckily, CMake can help in multiple ways. What follows is a brief overview of the commands available in CMake for dependency management, pretending that our project depends on SDL (a cross-platform development library).

1: the find_library() command

The idea here is to instruct CMake to search the system for the required library and then link it to the executable if found. The search is performed by the find_library() command: it takes the name of the library to look for and a variable that will be filled with the library path, if found. For example:

find_library(LIBRARY_SDL sdl)

You then check the correctness of LIBRARY_SDL and then pass it to target_link_libraries(). This command is used to specify the libraries or flags to use when linking the final executable. Something like this:

if (LIBRARY_SDL) target_link_libraries(myApp PRIVATE ${LIBRARY_SDL})else() # throw an error or enable compilation without the libraryendif()

Notice the use of the ${...} syntax to grab the variable content and use it as a command parameter.

2: the find_package() command

The find_package() command is like find_library() on steroids. With this command you are using special CMake modules that help in finding various well-known libraries and packages. Such modules are provided by the library authors or CMake itself (and you can also write your own). You can see the list of available modules on your machine by running cmake --help-module-list. Modules that start with the Find suffix are used by the find_package() command for its job. For example, the CMake version we are targeting is shipped with the FindSDL module, so it's just a matter of invoking it as follows:

find_package(SDL)

Where SDL is a variable defined by the FindSDL module. If the library is found, the module will define some additional variables to be used in your CMake script as shown in the previous method. If you can, always prefer this method over find_library().

3: the ExternalProject module

The two previous commands assume the library is already available and compiled somewhere in your system. The ExternalProject module follows a different approach: it downloads, builds and prepares the library for use in your CMake project. ExternalProject can also interface with popular version control systems such as Git, Mercurial and so on. By default it assumes the dependency to be a CMake project but you can easily pass custom build instructions if necessary.

Using this module is about calling the ExternalProject_Add(<name> [<option>...]) command, something like this:

include(ExternalProject) # Needs to be included firstExternalProject_Add(sdl GIT_REPOSITORY https://github.com/SDL-mirror/SDL.git)

This will download the SDL source code from the GitHub repository, run CMake on it — SDL is a CMake project — and then build it into a library ready to be linked. By default the artifacts will be stored in the build directory.

One thing to keep in mind: the download step is performed when you build the project (e.g. when invoking make on Linux), so CMake is not aware of the library presence while it generates the project (e.g. when you invoke the cmake command). The consequence is that you can't obtain the right path and flags for your library with commands like find_library() or find_package(). One way is to assume that the dependency is already in place because it will be downloaded and built sooner or later, so you just pass its full path to target_link_libraries() instead of a variable as we did before.

4: the FetchContent module (CMake 3.14+)

This module is based on the previous one. The difference here is that FetchContent downloads the source code in advance while generating the project. This lets CMake know that the dependency exists and to treat it as a child project. A typical usage looks like this:

include(FetchContent) # Needs to be included firstFetchContent_Declare(sdl GIT_REPOSITORY https://github.com/SDL-mirror/SDL.git)FetchContent_MakeAvailable(sdl)

In words: you first declare what you want to download with FetchContent_Declare, then you include the dependency with FetchContent_MakeAvailable to set up your project for the required library. The dependency will be automatically configured and compiled while building the final executable, before the linkage.

The FetchContent module assumes CMake-based dependencies. If so, including it in your project is as easy as seen above. Otherwise you need to explicitly tell CMake how to compile it, for example with the add_custom_target() command. More on this in future episodes.

As you can see, there are multiple ways to deal with external dependencies in CMake. Choosing the right one really depends on your taste and your project requisites. CMake can also interface with external package managers such as Vcpkg, Conan and existing git submodules directly.

Read more

In this article I've just scratched the surface of the huge CMake universe, while there are tons of interesting features that deserve a mention: macros and functions to write reusable CMake blocks; variables and lists, useful for storing and manipulating data; generator expressions to write complex generator-specific properties; continuous integration test support with ctest. Stay tuned for more!

Sources

CLion manual — Quick CMake tutorial
saoe.net — Using CMake with External Projects
CGold: The Hitchhiker’s Guide to the CMake
An Introduction to Modern CMake
Mirko Kiefer’s blog — CMake by Example
Pablo Arias — It's Time To Do CMake Right
Kuba Sejdak — Modern CMake is like inheritance
Preshing on Programming — How to Build a CMake-Based Project
Jason Turner — C++ Weekly - Ep 78 - Intro to CMake
Jason Turner — C++ Weekly - Ep 208 - The Ultimate CMake / C++ Quick Start
How To Write Platform Checks
CMake manual — Using Dependencies Guide
foonathan::​blog() — Tutorial: Easy dependency management for C++ with CMake and Git
Using external libraries that CMake doesn't yet have modules for
StackOverflow — Package vs Library
StackOverflow — CMake target_include_directories meaning of scope
StackOverflow — How to build an external library downloaded with CMake FetchContent?

Introduction to modern CMake for beginners (2024)

FAQs

Is CMake still relevant? ›

Having worked with build systems for large projects in the past, the developers designed CMake to address these needs. Since then CMake has continuously grown in popularity, with many projects and developers adopting it for its ease-of-use and flexibility.

What is CMake for beginners? ›

CMake stands for cross-platform make. It is a tool designed to manage the build process of software using compiler-independent methods. It was created to support complex directory hierarchies and applications that depend on several libraries.

Is there something better than CMake? ›

Explore other competing options and alternatives. Other important factors to consider when researching alternatives to CMake include projects. The best overall CMake alternative is GNU Make. Other similar apps like CMake are SCons, GNU Automake, Leiningen, and FinalBuilder.

How did you learn CMake? ›

Mastering CMake is a textbook published by Kitware that you can purchase from Amazon or read the open source version online. Kitware offers live CMake training courses throughout the year. Learn from the CMake developers at Kitware through interactive, hands-on lessons.

Do we really need CMake? ›

Using only CMake to manage dependencies, whether by FetchContent or CPM has following advantages: An easier project configuration, you just call cmake , and more or less understand how it will setup project and its dependencies; Configuration becomes much more reproducible since it is self-contained.

Why use CMake instead of Visual Studio? ›

CMake is a better build system than Visual Studio projects and solutions. It is compact and much more easier to maintain even for Windows only projects. See CMake for Visual Studio Developers for some reasons.

Is CMake only for C++? ›

CMake supports an extensive list of compilers, including: Apple Clang, Clang, GNU GCC, MSVC, Oracle Developer Studio, and Intel C++ Compiler.

What is the difference between CMake and CMake? ›

CMake and Make work on different principles. With CMake you specify build targets, and assign source files and various parameters. With Make, you create a set of build rules. Each rule has a target file, a set of dependencies (to the right of the colon), and the commands needed to perform the rule's build task.

Does CMake need a compiler? ›

If you do not find precompiled binaries for your system, then you can build CMake from source. To build CMake, you will need a modern C++ compiler and the source distribution from the CMake Download page or Kitware's GitLab instance. To build CMake, follow the instructions in README. rst at the top of the source tree.

Why use CMake with Python? ›

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.

Is CMake an API? ›

CMake provides a file-based API that clients may use to get semantic information about the buildsystems CMake generates. Clients may use the API by writing query files to a specific location in a build tree to request zero or more Object Kinds.

Does CMake create an executable? ›

Exercise 1 - Building a Basic Project. The most basic CMake project is an executable built from a single source code file. For simple projects like this, a CMakeLists.

Why do people use CMake? ›

CMake allows developers to describe how to build simple and very complicated software systems with a single set of input files. This can be used to build the software on multiple platforms, from Android to iOS to High-Performance Computing systems.

How do I start a CMake project? ›

The CMake Tools extension can create the files for a basic CMake project for you.
  1. Open the Command Palette (Ctrl+Shift+P) and run the CMake: Quick Start command:
  2. Enter a project name, and select and C++ as the project language. ...
  3. Select CTest as an additional option to add support for testing.
May 29, 2024

Is CMake an IDE? ›

cmake-ide is a package to enable IDE-like features on Emacs for CMake projects. It also supports non-CMake projects as long as a compilation database is generated out-of-band. This includes autocompletion and on-the-fly syntax checking in Emacs for CMake projects with minimal configuration.

Is CMake server 7 deprecated? ›

how i can repair this? CMake server mode is an old way of communicating between CMake and your IDE. It has been deprecated in favor of the file API. You're receiving this message because your IDE is still using server mode and hasn't been updated for file API.

Should I use CMake for C++? ›

Yes, if you are writing complex C++ programs, cmake is great. CMake allows you to build and test your program on the fly. Learning curve isn't high, not much different from gcc or gdb. Of course, if you wish to understand how CMake works and all the files that are being generated, then you may find it challenging.

Is CMake bloated? ›

cmake (written in C++) - so huge and bloated, compilation takes longer than compiling GCC (!). It's not even possible to create freestanding Makefiles, since the generated Makefiles call back into the cmake binary itself. Usage of cmake requires learning a new custom scripting language with very limited expressiveness.

Does Google use CMake? ›

GoogleTest comes with a CMake build script (CMakeLists. txt) that can be used on a wide range of platforms ("C" stands for cross-platform.).

References

Top Articles
Regeneration Potions Terraria
Jose Apocalypse Costume
Spasa Parish
The Machine 2023 Showtimes Near Habersham Hills Cinemas
Gilbert Public Schools Infinite Campus
Rentals for rent in Maastricht
159R Bus Schedule Pdf
11 Best Sites Like The Chive For Funny Pictures and Memes
Finger Lakes 1 Police Beat
Craigslist Pets Huntsville Alabama
Paulette Goddard | American Actress, Modern Times, Charlie Chaplin
Red Dead Redemption 2 Legendary Fish Locations Guide (“A Fisher of Fish”)
What's the Difference Between Halal and Haram Meat & Food?
Rugged Gentleman Barber Shop Martinsburg Wv
Jennifer Lenzini Leaving Ktiv
Havasu Lake residents boiling over water quality as EPA assumes oversight
Justified - Streams, Episodenguide und News zur Serie
Epay. Medstarhealth.org
Olde Kegg Bar & Grill Portage Menu
Half Inning In Which The Home Team Bats Crossword
Amazing Lash Bay Colony
Cyclefish 2023
Truist Bank Open Saturday
What’s Closing at Disney World? A Complete Guide
New from Simply So Good - Cherry Apricot Slab Pie
Ohio State Football Wiki
Find Words Containing Specific Letters | WordFinder®
FirstLight Power to Acquire Leading Canadian Renewable Operator and Developer Hydromega Services Inc. - FirstLight
Webmail.unt.edu
When Is Moonset Tonight
2024-25 ITH Season Preview: USC Trojans
Metro By T Mobile Sign In
Restored Republic December 1 2022
Dl 646
Apple Watch 9 vs. 10 im Vergleich: Unterschiede & Neuerungen
12 30 Pacific Time
1084 Sadie Ridge Road, Clermont, FL 34715 - MLS# O6240905 - Coldwell Banker
Kino am Raschplatz - Vorschau
Classic Buttermilk Pancakes
Pick N Pull Near Me [Locator Map + Guide + FAQ]
'I want to be the oldest Miss Universe winner - at 31'
Gun Mayhem Watchdocumentaries
Ice Hockey Dboard
Infinity Pool Showtimes Near Maya Cinemas Bakersfield
Dermpathdiagnostics Com Pay Invoice
A look back at the history of the Capital One Tower
Alvin Isd Ixl
Maria Butina Bikini
Busted Newspaper Zapata Tx
2045 Union Ave SE, Grand Rapids, MI 49507 | Estately 🧡 | MLS# 24048395
Upgrading Fedora Linux to a New Release
Latest Posts
Article information

Author: Sen. Ignacio Ratke

Last Updated:

Views: 5297

Rating: 4.6 / 5 (76 voted)

Reviews: 83% of readers found this page helpful

Author information

Name: Sen. Ignacio Ratke

Birthday: 1999-05-27

Address: Apt. 171 8116 Bailey Via, Roberthaven, GA 58289

Phone: +2585395768220

Job: Lead Liaison

Hobby: Lockpicking, LARPing, Lego building, Lapidary, Macrame, Book restoration, Bodybuilding

Introduction: My name is Sen. Ignacio Ratke, I am a adventurous, zealous, outstanding, agreeable, precious, excited, gifted person who loves writing and wants to share my knowledge and understanding with you.