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.
- Specify a minimum C/C++ standard using
target_compile_features(). See CMAKE_C_KNOWN_FEATURES and CMAKE_CXX_KNOWN_FEATURES for available features for C and C++ respectively. - Enable warnings or specify other compile options with
target_compile_options(). Note that these options are not used when linking; usetarget_link_options()in that case. - Specify include directories (tell the compiler where header files are) using
target_include_directories(). - Set certain properties of the target using
set_target_properties().
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)
-
PROJECT_NAMEis a built-in variable set by theproject()command. ↩︎