CMake

2025/09/06

CMake is a powerful build tool that can make building C/C++ projects simple. I cover how to build a small project in simple modern CMake, including how to compile and link source files and header files, and using dependencies (viz. libraries) that use CMake.

Basic Structure of CMakeLists.txt

We start a project by creating a CMakeLists.txt file with the following content.

cmake_minimum_required(VERSION 3.31)
project(my-proj)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Generate a `compile_commands.json` for e.g. clangd.

add_executable("${PROJECT_NAME}" main.cpp other.cpp)
target_compile_features("${PROJECT_NAME}" PRIVATE cxx_std_20)
target_compile_options("${PROJECT_NAME}" PRIVATE -Wall -Wextra)

The add_executable() command tells CMake that we want to compile some source files into an executable with the same name as the project1. This command also creates a target which can be passed to many CMake commands to set options and modify how the target should be built.

Invoking cmake

The project is built in two steps. First we generate the build system.

cmake -S . -B ./build -G Ninja -DCMAKE_BUILD_TYPE=Debug

The -S option specifies the source directory (where CMakeLists.txt is), and -B specifies where CMake should output the build artifacts.

Second, we run the generated build scripts.

cmake --build ./build

I like to use a simple Makefile.

EXEC_NAME := my-proj
BUILD_DIR := ./build

.PHONY: build
build:
	cmake -S . -B ${BUILD_DIR} -G Ninja -DCMAKE_BUILD_TYPE=Debug
	cmake --build ${BUILD_DIR}

.PHONY: run
run: build
	${BUILD_DIR}/${EXEC_NAME}

.PHONY: clean
clean:
	cmake --build ${BUILD_DIR} --target clean

If you use a language server such as clangd, you may have to configure it to look for the generated compile_commands.json in the build directory.

CompileFlags:
    CompilationDatabase: build

Build Types and Compiler Flags

There are four default build types or configurations: Debug, Release, RelWithDebInfo and MinSizeRel. These should be used instead of passing options such as -g or -O2 to the compiler directly using target_compile_options(). For the specific options enabled by each of these build configurations by default, see this answer on Stack Overflow.

The desired build configuration is usually passed as a flag to CMake (see Invoking CMake above).

Generator Expressions

We can use generator expressions to pass certain compile options only for certain build types. The basic syntax for a generator expression is $<...>. For example, we might write the following.

target_compile_options("${PROJECT_NAME}" PRIVATE $<$<CONFIG:Debug>:-g -Og>)

This is a nested generator expression. The expression $<CONFIG:Debug> evaluates to 1 if the build type/configuration is Debug, otherwise 0. The outer expression is in the form $<condition:text_if_true>, and evaluates to the right-hand-side if the left-hand-side is 1, otherwise it evaluates to the empty string, i.e., nothing. Therefore, the options listed will be passed to the compiler only in Debug builds.

Using Dependencies

There are two approaches that both work well for including a CMake-using dependency to your project.

Git Submodules

Add your dependency as a Git submodule to your project. Then you can add the dependency as a subdirectory, which will make the dependency’s targets available in your project. It remains to link the desired library to the executable.

set(GLFW_BUILD_WAYLAND OFF CACHE BOOL "" FORCE)

add_subdirectory("${PROJECT_SOURCE_DIR}/external/glfw" EXCLUDE_FROM_ALL)
target_link_libraries("${PROJECT_NAME}" PRIVATE glfw)

Note that we set options before adding the dependency as a subdirectory. If you prefer to use the CMake GUI, you can set the options there.

The FetchContent CMake Module

include(FetchContent) # Import the FetchContent module.

# Set any desired options before fetching.
set(GLFW_BUILD_WAYLAND OFF CACHE BOOL "" FORCE)

FetchContent_Declare(
    glfw
    GIT_REPOSITORY https://github.com/glfw/glfw.git
    GIT_TAG e7ea71be039836da3a98cea55ae5569cb5eb885c
    EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(glfw)

target_link_libraries("${PROJECT_NAME}" PRIVATE glfw)

  1. PROJECT_NAME is a built-in variable set by the project() command. ↩︎