Guide to CMake Files
CMake files are text files that describe how a software project should be configured,
built, tested, and sometimes packaged. The main file is usually CMakeLists.txt,
and CMake uses it to generate build files for tools like Make, Ninja, Visual Studio,
or Xcode.
What CMake Does
CMake is not usually the thing that compiles your code directly. Instead, it acts as a build system generator: it reads your project description and creates the native files needed by your platform's build tool.
That means one CMake project can work across Windows, macOS, and Linux without rewriting the build setup for each environment.
What CMake Files Are For
A CMake file tells the build system things like:
- Which source files to compile.
- Which libraries or executables to build.
- Which options users can turn on or off.
- Which dependencies to search for.
- How to handle tests, install rules, and packaging.
This makes build logic easier to maintain in one place instead of duplicating it for different platforms or IDEs.
Why People Use CMake
The biggest reason is portability. You write the build rules once, and CMake can generate project files for different tools on different operating systems.
It is also useful for larger projects because it supports:
- Out-of-source builds.
- Dependency detection.
- Optional features.
- Custom code generation.
- Shared and static libraries.
- Testing and packaging.
Common Files
| File | Purpose |
|---|---|
CMakeLists.txt | Main project instructions file |
*.cmake | Reusable helper files, modules, package config |
CMakePresets.json | Shareable configure/build/test presets |
cmake/ folder | Project-specific .cmake helper files |
Simple Example
A very small CMake project often looks like this:
cmake_minimum_required(VERSION 3.20)
project(HelloWorld)
add_executable(hello main.cpp)
This says: require a certain CMake version, name the project, and build an executable
called hello from main.cpp. That is enough for CMake to generate the proper build
files for your platform.
The Build Pipeline
Understanding how CMake fits into the overall workflow helps avoid confusion:
Your source code + CMakeLists.txt
│
│ cmake .. ← Configure step
▼
Build system files
(Makefile / .sln / build.ninja)
│
│ cmake --build . ← Build step
▼
Compiled binaries
(hello, libfoo.a, libbar.so)
│
│ ctest ← Test step (optional)
▼
Test results
│
│ cmake --install . ← Install step (optional)
▼
Installed files
Always build out-of-source — keep generated files away from your source tree:
mkdir build
cd build
cmake ..
cmake --build .
This way, git clean removes everything generated without touching your source.
Core Commands
cmake_minimum_required
cmake_minimum_required(VERSION 3.20)
Always put this first. It tells CMake which features your project relies on and prevents older CMake versions from silently producing wrong results.
project
project(MyApp VERSION 1.2.3 LANGUAGES CXX C)
Defines the project name, version, and languages. CMake will detect compilers for
each listed language. The version is accessible via ${PROJECT_VERSION}.
add_executable
add_executable(myapp main.cpp utils.cpp parser.cpp)
Creates a binary executable from the listed source files.
add_library
add_library(mylib STATIC foo.cpp bar.cpp) # .a / .lib
add_library(mylib SHARED foo.cpp bar.cpp) # .so / .dll
add_library(mylib OBJECT foo.cpp bar.cpp) # compiled objects only, no archive
target_link_libraries
target_link_libraries(myapp PRIVATE mylib)
Links myapp against mylib. The keyword controls propagation:
| Keyword | Meaning |
|---|---|
PRIVATE | Only myapp links against mylib |
PUBLIC | myapp and anything linking myapp gets mylib |
INTERFACE | Only consumers of myapp get mylib, not myapp itself |
target_include_directories
target_include_directories(mylib PUBLIC include/)
Makes include/ available as a header search path. PUBLIC means any target
that links mylib also gets this include path automatically.
target_compile_options
target_compile_options(myapp PRIVATE -Wall -Wextra -O2)
Passes compiler flags to a specific target. Prefer this over the older
add_compile_options() which applies globally.
Variables
set(MY_VAR "hello")
message(STATUS "Value is: ${MY_VAR}")
CMake variables are strings or lists. Lists are semicolon-separated internally:
set(SOURCES main.cpp foo.cpp bar.cpp)
# Equivalent to: "main.cpp;foo.cpp;bar.cpp"
add_executable(myapp ${SOURCES})
Useful Built-in Variables
| Variable | Description |
|---|---|
CMAKE_SOURCE_DIR | Root of the source tree |
CMAKE_BINARY_DIR | Root of the build tree |
CMAKE_CURRENT_SOURCE_DIR | Source dir of the current CMakeLists.txt |
PROJECT_NAME | Name set by project() |
PROJECT_VERSION | Version set by project() |
CMAKE_BUILD_TYPE | Debug, Release, RelWithDebInfo, MinSizeRel |
CMAKE_CXX_STANDARD | C++ standard: 11, 14, 17, 20, 23 |
BUILD_SHARED_LIBS | If ON, add_library() defaults to SHARED |
Cache Variables
Cache variables persist between CMake runs and appear in cmake-gui or ccmake:
option(ENABLE_TESTS "Build unit tests" ON)
if(ENABLE_TESTS)
add_subdirectory(tests)
endif()
Users can override them at configure time:
cmake .. -DENABLE_TESTS=OFF
A Real-World Project Layout
MyProject/
├── CMakeLists.txt ← root
├── CMakePresets.json
├── cmake/
│ └── FindFoo.cmake ← custom find module
├── include/
│ └── mylib/
│ └── api.h
├── src/
│ ├── CMakeLists.txt ← library target
│ ├── foo.cpp
│ └── bar.cpp
├── apps/
│ ├── CMakeLists.txt ← executable target
│ └── main.cpp
└── tests/
├── CMakeLists.txt ← test target
└── test_foo.cpp
Root CMakeLists.txt:
cmake_minimum_required(VERSION 3.20)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
option(BUILD_TESTS "Build tests" ON)
add_subdirectory(src)
add_subdirectory(apps)
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
src/CMakeLists.txt:
add_library(mylib STATIC foo.cpp bar.cpp)
target_include_directories(mylib PUBLIC
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_compile_options(mylib PRIVATE -Wall -Wextra)
apps/CMakeLists.txt:
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE mylib)
Finding Dependencies
find_package
The idiomatic way to find a library:
find_package(OpenSSL REQUIRED)
target_link_libraries(myapp PRIVATE OpenSSL::SSL OpenSSL::Crypto)
REQUIRED makes CMake error out if the package isn't found.
Modern CMake packages expose imported targets (like OpenSSL::SSL) instead of
raw OPENSSL_LIBRARIES variables — prefer imported targets.
Common Packages and Their Targets
| Package | Target |
|---|---|
| OpenSSL | OpenSSL::SSL, OpenSSL::Crypto |
| Threads | Threads::Threads |
| zlib | ZLIB::ZLIB |
| Boost | Boost::filesystem, Boost::system, … |
| fmt | fmt::fmt |
| GTest | GTest::gtest, GTest::gtest_main |
FetchContent — Download Dependencies Automatically
No need to install libraries system-wide; CMake can fetch them at configure time:
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)
target_link_libraries(my_tests PRIVATE GTest::gtest_main)
The first configure clones the repo into the build directory and builds it. Subsequent configures use the cached copy.
Generator Expressions
Generator expressions are evaluated at build time, not configure time.
They look like $<...> and are essential for configuration-specific settings:
# Add -g only in Debug builds
target_compile_options(myapp PRIVATE $<$<CONFIG:Debug>:-g>)
# Different include paths for build vs install
target_include_directories(mylib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
# Link against a library only on Windows
target_link_libraries(myapp PRIVATE $<$<PLATFORM_ID:Windows>:ws2_32>)
Testing with CTest
# In root or tests/CMakeLists.txt
enable_testing()
add_executable(test_foo test_foo.cpp)
target_link_libraries(test_foo PRIVATE mylib GTest::gtest_main)
include(GoogleTest)
gtest_discover_tests(test_foo)
Run tests:
cd build
ctest --output-on-failure
ctest -R test_foo # run tests matching a pattern
ctest -j4 # run 4 tests in parallel
Installing
install(TARGETS mylib myapp
RUNTIME DESTINATION bin # executables
LIBRARY DESTINATION lib # shared libraries
ARCHIVE DESTINATION lib # static libraries
)
install(DIRECTORY include/
DESTINATION include
)
Build and install:
cmake --build .
cmake --install . --prefix /usr/local
CMake Presets (CMakePresets.json)
Presets let you commit common configure/build options to version control so everyone on the team uses the same settings:
{
"version": 6,
"configurePresets": [
{
"name": "debug",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"BUILD_TESTS": "ON"
}
},
{
"name": "release",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"BUILD_TESTS": "OFF"
}
}
],
"buildPresets": [
{ "name": "debug", "configurePreset": "debug" },
{ "name": "release", "configurePreset": "release" }
]
}
Use them:
cmake --preset debug
cmake --build --preset debug
ctest --preset debug
Cross-Compilation
To compile for a different architecture, pass a toolchain file:
cmake .. -DCMAKE_TOOLCHAIN_FILE=toolchain-arm.cmake
A minimal ARM toolchain file:
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)
set(CMAKE_FIND_ROOT_PATH /usr/arm-linux-gnueabihf)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
Common Pitfalls
Using directory-level commands instead of target-level
# Bad — affects everything in the directory and below
include_directories(include/)
add_compile_options(-Wall)
# Good — scoped to this target only
target_include_directories(mylib PUBLIC include/)
target_compile_options(mylib PRIVATE -Wall)
Globbing source files
# Bad — CMake won't re-run when you add a new file
file(GLOB SOURCES src/*.cpp)
# Good — list files explicitly so CMake detects additions
add_executable(myapp src/main.cpp src/foo.cpp src/bar.cpp)
Not specifying the C++ standard
# Fragile — relies on whatever the compiler defaults to
add_executable(myapp main.cpp)
# Correct
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) # use -std=c++17, not -std=gnu++17
Hardcoding paths
# Bad — only works on one machine
target_include_directories(myapp PRIVATE /home/user/libs/boost/include)
# Good — use find_package or FetchContent
find_package(Boost REQUIRED COMPONENTS filesystem)
target_link_libraries(myapp PRIVATE Boost::filesystem)
Quick Reference Cheat Sheet
cmake_minimum_required(VERSION 3.20)
project(Name VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Targets
add_executable(app src/main.cpp)
add_library(lib STATIC src/lib.cpp)
# Properties
target_include_directories(lib PUBLIC include/)
target_compile_options(lib PRIVATE -Wall -Wextra)
target_link_libraries(app PRIVATE lib)
# Dependencies
find_package(OpenSSL REQUIRED)
target_link_libraries(app PRIVATE OpenSSL::SSL)
# Tests
enable_testing()
add_test(NAME mytest COMMAND myapp --run-tests)
# Install
install(TARGETS app lib RUNTIME DESTINATION bin ARCHIVE DESTINATION lib)
install(DIRECTORY include/ DESTINATION include)
Configure → Build → Test → Install
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
│
▼ reads CMakeLists.txt, detects compilers, finds packages
cmake --build build --parallel
│
▼ compiles source files, links targets
ctest --test-dir build --output-on-failure
│
▼ runs registered tests, reports failures
cmake --install build --prefix /usr/local
│
▼ copies binaries, headers, and libraries to the prefix
Once you understand this pipeline and the target-centric model — where
target_* commands attach properties to named targets rather than
polluting global state — CMake becomes much easier to reason about
and maintain across large codebases.
Comments
(0)Loading comments...