Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 63

Building C++ Executables with CMake

Recommended resources

Hello! In our last lesson, we saw how auto can simplify our code, especially when dealing with complex types. So far, we've been compiling our single-file programs directly with g++, like g++ main.cpp -o my_program. While this works for simple cases, it quickly becomes unmanageable as soon as a project grows to include multiple source files and dependencies.

This lesson directly addresses this challenge and fulfills the learning outcome: Write a basic CMakeLists.txt file to build a C++ executable.

We will explore CMake, the de facto standard build system generator for C++. It's a fundamental tool for any serious C++ development, from desktop applications to embedded systems—two of your key interest areas.

Today, you will learn:

  • What a build system is and why CMake is essential for C++ projects.
  • The core commands needed to create a minimal CMakeLists.txt file.
  • How to configure and build a C++ project from the command line using an "out-of-source" build.
  • How to expand your build script to handle multiple source files.

1. Why Do We Need a Build System?

As a project grows, you'll split your code into multiple .cpp and .h files. You might also need to link against external libraries. Manually running g++ with a long list of files and flags becomes tedious and error-prone. A build system automates this process.

CMake is not a build system itself, but a meta-build system or a build system generator. It doesn't compile your code. Instead, it reads a configuration file you write (CMakeLists.txt) and generates the actual build files for a native build tool like Make on Linux/macOS or Visual Studio on Windows.

This approach gives you a single, cross-platform way to define your project.

CMake Tutorial: Building Your First C++ Project & Beyond

This article provides a fantastic mental model for understanding CMake. The key takeaway is that with CMake, you don't tell it how to compile; you describe what you want to build.

Please read the first two sections, 'What is CMake?' and 'Why CMake is Confusing? (And How to Fix That)'. Focus on the core idea that CMake generates native build files and that you should think in terms of describing your project, not scripting the compilation.

This two-phase process is a cornerstone of CMake:

  1. Configure Phase: You run cmake to parse your CMakeLists.txt and generate the native build files (e.g., a Makefile).
  2. Build Phase: You run the native build tool (e.g., make) or use a generic cmake command to execute the build, which compiles and links your code.

This flowchart clearly visualizes these two distinct phases.

CMake Build Process Overview
This flowchart shows the two main phases of a CMake build. The 'Configure' phase generates build files from your CMakeLists.txt, and the 'Build' phase uses those files to compile and link your executable.

2. Your First CMakeLists.txt

Every CMake project is defined by a plain text file named CMakeLists.txt. For a simple executable, you only need three commands.

Let's start by looking at the official CMake tutorial to understand these essential commands.

Step 1: Getting Started with CMake

The official CMake documentation is the ultimate source of truth. This first step of their tutorial introduces the most fundamental commands required to build an executable.

Read the sections 'Background' and 'Exercise 1 - Building an Executable'. Don't worry about the TODOs or file structure in the tutorial itself. Focus on understanding the purpose of these three commands: cmake_minimum_required() project() add_executable()

To summarize, these are the three pillars of a basic CMakeLists.txt:

  1. cmake_minimum_required(VERSION 3.10): Specifies the minimum version of CMake required to build your project. This ensures that the CMake features you're using are available. Using a relatively recent version like 3.10 or higher is a good practice.
  2. project(MyAwesomeApp): Declares the name of your project. This also sets up default configurations and checks for a working C++ compiler.
  3. add_executable(MyAwesomeApp main.cpp): This is the command that does the work. It defines an executable target. Here, it says: "Create an executable named MyAwesomeApp from the source file main.cpp."

3. Building a Single-File Project

Let's put this into practice. Imagine you have a folder with two files:

main.cpp




#include <iostream>

int main() {
    std::cout << "Hello, CMake!" << std::endl;
    return 0;
}

CMakeLists.txt




# 1. Set the minimum required version of CMake
cmake_minimum_required(VERSION 3.10)




# 2. Set the project name
project(HelloCMake)




# 3. Add an executable target
add_executable(HelloCMake main.cpp)

Now, how do we build this? The best practice is to perform an out-of-source build. This means all the files generated by CMake and the compiler (Makefiles, object files, the final executable) are placed in a separate directory, typically named build, keeping your source directory clean.

Here is the command-line workflow:

  1. Create and navigate to a build directory:

    mkdir build
    cd build
    
  2. Run CMake to configure the project:

    
    
    
    # This tells cmake to look for CMakeLists.txt in the parent directory (..)
    cmake ..
    

    This command reads your CMakeLists.txt, checks your compiler, and generates a Makefile inside the build directory.

  3. Run the build tool:

    
    
    
    # This invokes the underlying build tool (e.g., 'make')
    make
    

    This compiles main.cpp and links it into an executable named HelloCMake.

  4. Run your program:

    ./HelloCMake
    

The following video demonstrates this exact process. Although it uses a C program, the workflow is identical for C++.

CMake Tutorial for Absolute Beginners - From GCC to CMake including Make and Ninja

This video from 'Gary Explains' gives a great practical demonstration of the out-of-source build process from the command line.

Watch from 12:22 to 14:22. Observe how he creates a build directory, runs cmake .. from within it to generate the Makefile, and then runs make to compile the program. This visualizes the workflow we just discussed.

A note on modern syntax: The official CMake tutorial (LINK, section 2) now recommends a slightly different set of commands that achieve the same result without needing to cd into the build directory:




# Configure from the project's root directory
cmake -S . -B build




# Build from the project's root directory
cmake --build build

Here, -S . specifies the source directory (current directory) and -B build specifies the build directory. Both workflows are common and valid.


4. Handling Multiple Source Files

Real projects are rarely a single file. Let's expand our project.

Suppose we have this structure:

  • main.cpp
  • functions.h
  • functions.cpp
  • CMakeLists.txt

functions.h




#ifndef FUNCTIONS_H
#define FUNCTIONS_H

void print_message();




#endif //FUNCTIONS_H

functions.cpp




#include "functions.h"
#include <iostream>

void print_message() {
    std::cout << "This is a message from another file!" << std::endl;
}

main.cpp




#include "functions.h"

int main() {
    print_message();
    return 0;
}

How do we tell CMake about functions.cpp? We just add it to the add_executable command. For projects with many files, it's cleaner to group them in a variable first using the set() command.

CMake Tutorial: Building Your First C++ Project & Beyond

This section of the tutorial shows a clean and scalable way to handle multiple source files by creating a variable to hold the list of source files.

Read the section 'Compiling Multiple Source Files Into One Program'. Focus on how set(SOURCE_FILES ...) is used to create a variable and how ${SOURCE_FILES} is used in the add_executable command.

Our updated CMakeLists.txt would look like this:

CMakeLists.txt (for multiple files)

cmake_minimum_required(VERSION 3.10)
project(MultiFileProject)




# Set the C++ standard to C++17
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)




# Group all your .cpp source files into a variable
set(SOURCE_FILES
    main.cpp
    functions.cpp
)




# Add the executable using the variable
add_executable(MultiFileProject ${SOURCE_FILES})

Notice two new lines: set(CMAKE_CXX_STANDARD 17) and set(CMAKE_CXX_STANDARD_REQUIRED ON). These are important for ensuring your project is compiled using the correct C++ standard (in this case, C++17).

The build process remains exactly the same. CMake is smart enough to see the updated CMakeLists.txt and rebuild things correctly.


Conclusion

You now have the fundamental knowledge to manage a C++ project build with CMake. This is a huge step up from single-file compilation and is the standard practice in the industry.

Key Takeaways:

  • CMake is a meta-build system that generates native build files (like Makefiles) from a central CMakeLists.txt script.
  • The core commands for a simple project are cmake_minimum_required(), project(), and add_executable().
  • Out-of-source builds are the standard workflow: cmake -B build to configure, and cmake --build build to compile. This keeps your source directory clean.
  • You can easily manage multiple source files by listing them in the add_executable command, often by grouping them with set().
  • You can enforce a specific C++ standard using variables like CMAKE_CXX_STANDARD.

Next Up

Our project is still entirely self-contained. Most real-world applications, however, depend on third-party libraries for things like graphics, networking, or advanced math. In our next lesson, we will learn how to configure a CMake project to find and link against an external library. This will unlock the ability to build much more powerful and feature-rich applications.