cpp-introduccion-cmake

Introduction to CMake for C++ Projects

  • 5 min

CMake is a tool that generates build systems from a unified configuration for our project.

When we start programming in C++, we usually have a single main.cpp file. Compiling it is easy: you press the IDE’s run button or type g++ main.cpp in the console.

The situation changes as the project grows:

  • You have 20 .cpp and 20 .h files.
  • You use external libraries.
  • You want your code to compile both on Windows (with Visual Studio) and Linux (with GCC).

Doing this manually is a nightmare. That’s why we use CMake.

CMake has a reputation for being difficult, and its syntax is sometimes quirky, but it is the tool that ties the entire C++ ecosystem together.

What is CMake really?

CMake is an open-source, cross-platform software build tool that simplifies the creation and management of C++ projects.

It uses configuration files called CMakeLists.txt to define how a project should be built.

There is a very common misconception: CMake is not a compiler. It does not compile your code; rather, it is a build system generator.

Think of it this way:

  1. You write a “recipe” in a file named CMakeLists.txt.
  2. CMake reads that recipe.
  3. CMake generates the necessary files for your preferred build tool.
    • On Windows, it can generate a Visual Studio solution (.sln).
    • On Linux, it can generate files for Make or Ninja.
    • On macOS, it can generate an Xcode project.

Thanks to this, you only maintain a single configuration file, and CMake handles adapting the project to any operating system or compiler.

Your first CMakeLists.txt

Every CMake project starts with a text file, mandatorily named CMakeLists.txt (case-sensitive).

Let’s create the most basic one possible for a “Hello World”.

# 1. Minimum version of CMake required (Good practice)
cmake_minimum_required(VERSION 3.10)

# 2. Project name and language
project(MiProyectoHolaMundo CXX)

# 3. Define the C++ standard (e.g., C++17)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# 4. Create the executable
# Syntax: add_executable(TargetName SourceFiles...)
add_executable(MiApp main.cpp)
Copied!

Commands in CMake (project, add_executable) are case-insensitive, but the modern convention is to write them in lowercase. Variables (CMAKE_CXX_STANDARD) are Case Sensitive.

Structure of a project with CMake

A typical CMake project structure might look like this:

mi_proyecto/ ├── CMakeLists.txt # Main project configuration. ├── src/ # Source file directory. │ ├── CMakeLists.txt │ ├── main.cpp │ └── utils.cpp └── include/ # Header file directory. ├── CMakeLists.txt └── utils.h

The Workflow

One of the best practices in CMake is to do Out-of-Source Builds.

This means we don’t want to clutter our source code folder with temporary build files (.o, .obj, .exe). We want all the “junk” to go into a separate folder, usually called build.

The standard terminal process is as follows:

Configuration

We tell CMake to read the CMakeLists.txt and generate the project files inside the build folder.

# -S .      -> Source (where the CMakeLists.txt is located, i.e., the current directory)
# -B build  -> Build (where to generate the temporary files)
cmake -S . -B build
Copied!

Compilation

Now we ask CMake to invoke the actual compiler (GCC, MSVC, Clang) to compile what is in build.

# --build build -> Compiles whatever you have configured in that folder
cmake --build build
Copied!

If all goes well, your executable will be inside the build folder (or build/Debug on Windows).

Working with multiple files

A real project never has a single file. Let’s imagine we have main.cpp, calculadora.cpp, and calculadora.h.

We could list them all in the executable:

add_executable(MiApp main.cpp calculadora.cpp)
Copied!

However, in modern C++, we prefer to organize code into Libraries.

We can group calculadora.cpp into an internal library. This makes the project more modular.

cmake_minimum_required(VERSION 3.10)
project(ProyectoMatematico CXX)

# 1. Create a static library called "Calculos"
add_library(Calculos calculadora.cpp)

# 2. Create the main executable with only main.cpp
add_executable(MiApp main.cpp)

# 3. Link the library to the executable
# This tells CMake: "MiApp needs to use the code from Calculos"
target_link_libraries(MiApp PUBLIC Calculos)
Copied!

The concept of Target is fundamental in modern CMake. MiApp is a target. Calculos is a target. Instead of using global variables, we define relationships between targets: “This target depends on that other one”.

Managing includes

If your .h files are in a separate folder (e.g., include/), you need to tell the compiler where to find them.

Do not use compiler flags (-I include). Use the semantic CMake command:

# Tell the target 'Calculos' that its headers are in the 'include' folder
target_include_directories(Calculos PUBLIC include)
Copied!

By using PUBLIC, any other executable (like MiApp) that links against Calculos will automatically inherit that include path. CMake handles propagating this configuration.