CMake zero to hero
This path starts with one C++ file and ends with a project that can build, test, install, package, configure options, handle versions, copy non-C++ files, and cross-compile.
The goal is to learn modern CMake.
Modern CMake means:
- think in targets
- prefer
target_*commands - keep dependencies attached to the target that needs them
- avoid global project settings when possible
- keep source files and generated build files separate
Big picture
flowchart TD
A[Source files<br/>CMakeLists.txt] --> B[Configure<br/>cmake -S . -B build]
B --> C[Build files<br/>Ninja or Makefiles]
C --> D[Build<br/>cmake --build build]
D --> E[Run tests<br/>ctest --test-dir build]
E --> F[Install<br/>cmake --install build]
F --> G[Package<br/>cpack --config build/CPackConfig.cmake]
Stage 1: one executable
Start with the smallest useful project.
Project layout:
CMakeLists.txt:
Build:
Learn here:
cmake_minimum_requiredprojectadd_executabletarget_compile_features- configure step vs build step
Stage 2: use a clean build folder
Always keep generated files outside the source tree.
Good:
Avoid:
The build/ folder can be deleted and recreated. Your source files should not
depend on anything inside it.
Stage 3: split code into a library
Large projects should not put everything in one executable.
Project layout:
CMakeLists.txt:
Learn here:
add_librarytarget_include_directoriestarget_link_librariesPUBLIC,PRIVATE, andINTERFACE
Stage 4: understand PUBLIC, PRIVATE, INTERFACE
These keywords control how settings move between targets.
PRIVATE means only this target needs it.
PUBLIC means this target needs it, and users of this target need it too.
INTERFACE means this target does not need it for itself, but users need it.
Common rule:
- use
PRIVATEfor implementation details - use
PUBLICfor headers or libraries exposed by your public API - use
INTERFACEfor header-only libraries or usage requirements
Stage 5: add project options
Use option() to let the user configure the build.
Use the option:
Configure with parameters:
Learn here:
optionif- cache variables
-DNAME=value
Stage 6: handle project version
Put the version in the project() command.
CMake creates variables:
Generate a version header:
cmake/version.hpp.in:
CMakeLists.txt:
Learn here:
project(... VERSION ...)configure_file- generated headers
- build directory include paths
Stage 7: include non-C++ files
Projects often need config files, shaders, YAML files, launch files, or other runtime assets.
For development, copy files next to the executable:
For install/package, install the files:
Learn here:
add_custom_command${CMAKE_COMMAND} -Einstall(DIRECTORY ...)- runtime file location
Stage 8: add tests
CMake uses CTest for running tests.
Minimal test:
Run:
Learn here:
include(CTest)BUILD_TESTINGadd_testctest- test output on failure
Stage 9: use external dependencies
Prefer imported targets from find_package.
Example:
Good modern dependency usage usually looks like:
Learn here:
find_package- imported targets
- package config files
- system dependencies vs vendored dependencies
Stage 10: install the project
Install rules describe what should be copied to an install prefix.
Run:
Learn here:
install(TARGETS ...)install(DIRECTORY ...)- install prefix
- executable, shared library, and static library destinations
Stage 11: package with CPack
CPack creates archives or install packages from install rules.
Run:
Common generators:
TGZ:.tar.gzZIP: zip archiveDEB: Debian packageRPM: RPM package
Learn here:
CPack- package metadata
- package generators
- how install rules become package contents
Stage 12: use CMakePresets.json
Presets save common configure, build, and test commands.
CMakePresets.json:
Run:
Learn here:
- configure presets
- build presets
- test presets
- repeatable local and CI builds
Stage 13: cross compile
Cross-compiling means building for a different target system than the machine running CMake.
Example: build on x86 Linux, target ARM Linux.
Use a toolchain file.
cmake/toolchains/arm-linux.cmake:
Configure:
Build:
Learn here:
- toolchain files
- sysroot
- target compiler
- host tools vs target libraries
CMAKE_SYSTEM_NAMECMAKE_FIND_ROOT_PATH
Stage 14: grow into a large project
A larger project should be split into directories and targets.
Example layout:
Top-level CMakeLists.txt:
Learn here:
add_subdirectory- one target per library or executable
- small focused
CMakeLists.txtfiles - keeping install and package rules near the targets they describe
Important issues to know
- Do not put generated files in the source tree.
- Do not use
file(GLOB ...)for source files when learning. List sources explicitly so changes are clear. - Prefer
target_compile_features(my_target PRIVATE cxx_std_17)over global C++ standard variables. - Prefer imported targets like
fmt::fmtover raw include paths and library variables. - Use
PRIVATEunless the dependency is part of your public API. - Do not make every include directory global.
- Keep build options few and meaningful.
- Use presets when command lines become long.
- Run tests from the build folder with
ctest --test-dir build. - Make install rules before packaging. CPack packages what you install.
- Cross-compiling is mostly about the toolchain file and dependency locations.
Recommended learning order
- executable
- clean build folder
- library target
- include directories
- link libraries
- options and cache variables
- version header
- non-C++ runtime files
- tests with CTest
- dependencies with
find_package - install rules
- packages with CPack
- presets
- cross-compiling
- multi-directory project structure
Final target
At the end, you should be able to build a project with commands like:
That means your project can be configured, built, tested, installed, and packaged in a repeatable way.