what is cmake?

CMake is not a build system — it's a build system generator. You describe your project once, in a CMakeLists.txt file, and CMake reads it and generates the actual build files for whatever tool you want to build with: Unix Makefiles, Ninja files, a Visual Studio solution, an Xcode project. That's the whole point of it: one project description, buildable on Linux, macOS, and Windows, with whichever compiler and build tool each platform prefers, without maintaining separate Makefiles by hand for each.

This is also why plain make or ninja alone can't build a CMake project directly from its CMakeLists.txt — there's a required generation step first (the "configure" step below) that turns the CMakeLists.txt into the Makefile or build.ninja file those tools actually understand.

installation via terminal

sudo apt update
sudo apt install cmake -y
cmake --version | | shows the installed CMake version. Ubuntu's apt package often lags behind — for the latest release, install via pip (`pip install cmake`) or the official installer at cmake.org/download |'cm1'
sudo apt install ninja-build | | installs Ninja, a faster alternative build tool to make that CMake can generate for instead |'cm2'

core concepts & the configure/build/install workflow

Two directories matter: the source tree (where your CMakeLists.txt and source files live) and the build tree (where CMake writes generated build files and compiled output). Always use an out-of-source build — a separate build directory, never mixed in with your source files. It keeps generated clutter out of version control, and lets you have multiple independent build configurations (a Debug build and a Release build, say) from the same source tree at once.

Every CMake project goes through three distinct steps:
cmake -S . -B build | | configure: reads CMakeLists.txt in the source dir (-S) and generates build files into the build dir (-B), creating it if needed |'cm3'
cmake --build build | | build: invokes the underlying build tool (make, ninja, msbuild, ...) — this generator-agnostic form works no matter which one CMake chose, so you don't need to remember to type make vs ninja yourself |'cm4'
cmake --install build | | install: copies the built binaries, libraries, and headers to their install locations (see install() below) |'cm5'
cmake --build build --target <name> | | builds one specific target instead of everything |'cm6'
cmake --build build -j 8 | | builds using 8 parallel jobs |'cm7'
cmake --build build --clean-first | | removes previous build output before rebuilding, without deleting the whole build directory |'cm8'

a minimal CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(MyApp VERSION 1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(myapp src/main.cpp)
cmake_minimum_required must be the first line — it also sets which CMake behaviors ("policies") are active, so an old CMakeLists.txt keeps building the same way even on a newer CMake. project() declares the project name, version, and languages, and must come before any target is defined.

targets: executables & libraries

Modern CMake is built around targets, not global variables. A target is an executable or a library, and everything about how to build it — its sources, include paths, compile definitions, linked libraries — is attached directly to that target rather than set globally for the whole project.
add_executable(<name> <sources...>) | | defines an executable target built from the given source files |'cm9'
add_library(<name> STATIC <sources...>) | | defines a static library (.a / .lib) |'cm10'
add_library(<name> SHARED <sources...>) | | defines a shared library (.so / .dll) |'cm11'
add_library(<name> INTERFACE) | | defines a header-only library — no sources of its own, just properties (include paths, flags) it hands to whatever links it |'cm12'
target_link_libraries(<target> PRIVATE <lib>) | | links a library into a target, and (via PRIVATE/PUBLIC/INTERFACE) controls whether that dependency propagates to whoever links this target in turn |'cm13'
target_include_directories(<target> PRIVATE <dir>) | | adds a header search path scoped to one target, instead of the old project-wide include_directories() |'cm14'
target_compile_definitions(<target> PRIVATE <DEFINE>) | | adds a preprocessor #define, scoped to one target |'cm15'
target_compile_options(<target> PRIVATE -Wall -Wextra) | | adds raw compiler flags to one target |'cm16'
target_compile_features(<target> PRIVATE cxx_std_17) | | requests a specific language feature/standard for one target — a more portable alternative to setting CMAKE_CXX_STANDARD globally |'cm17'

PUBLIC, PRIVATE & INTERFACE

These three keywords appear on every target_* command above, and they answer one question: does this property also apply to anything that links this target?
  • PRIVATE — used only to build this target itself; consumers linking it never see it. Use this for implementation details (an internal helper library, a private include path).
  • INTERFACE — not used to build this target at all, only passed on to whoever links it. The natural fit for header-only libraries, which have nothing to compile themselves.
  • PUBLIC — both: used to build this target, and passed on to consumers. Use this when your public headers themselves depend on it (e.g. a header includes <boost/optional.hpp>, so consumers need that include path too).
Getting this right is what lets `target_link_libraries(app PRIVATE mylib)` automatically pull in everything mylib needs (its own PUBLIC and INTERFACE dependencies) without app having to know or re-declare any of mylib's internals.

variables, the cache & options

set(&lt;VAR&gt; &lt;value&gt;) | | sets a normal (non-cached) variable, scoped to the current CMakeLists.txt and anything it includes |'cm18'
option(&lt;NAME&gt; "description" ON) | | declares a boolean cache variable a user can toggle from the command line, without editing CMakeLists.txt |'cm19'
cmake -B build -D&lt;VAR&gt;=&lt;value&gt; | | sets a cache variable (or overrides an option()) at configure time |'cm20'
cmake -B build -DCMAKE_BUILD_TYPE=Release | | sets the optimization/debug-info profile: Debug, Release, RelWithDebInfo, or MinSizeRel. Only meaningful for single-config generators like Make/Ninja — see generators below |'cm21'
cmake -B build -DCMAKE_INSTALL_PREFIX=/usr/local | | sets where `cmake --install` copies files to |'cm22'
cmake -B build -DCMAKE_CXX_COMPILER=clang++ | | selects which compiler to use, instead of CMake's autodetected default |'cm23'
Cache variables are written to CMakeCache.txt in the build directory and persist across reconfigures — that's how `cmake -B build` remembers your -D flags on the next plain `cmake --build build` without you repeating them. Deleting the build directory (or CMakeCache.txt specifically) resets everything back to defaults.

finding dependencies

find_package(&lt;Pkg&gt; REQUIRED) | | locates an already-installed library and its CMake config, failing the configure step if it isn't found (omit REQUIRED to make it optional) |'cm24'
find_package(Boost REQUIRED COMPONENTS filesystem) | | finds a package with specific sub-components; modern packages expose imported targets like Boost::filesystem you link directly |'cm25'
target_link_libraries(app PRIVATE Boost::filesystem) | | links against the imported target find_package() found — this alone brings in the right include paths and compile flags too, nothing else to configure manually |'cm26'
find_library(&lt;VAR&gt; NAMES &lt;name&gt;) | | a lower-level search for a specific library file, for dependencies with no CMake package config to find_package() against |'cm27'
find_program(&lt;VAR&gt; NAMES &lt;name&gt;) | | locates an executable on the system, e.g. an external code generator your build needs to run |'cm28'
cmake -B build -DCMAKE_PREFIX_PATH=/opt/mylib | | adds an extra location for find_package()/find_library() to search, for dependencies installed somewhere non-standard |'cm29'

fetching dependencies with FetchContent

When a dependency isn't already installed on the system, FetchContent downloads it (usually from git) and builds it as part of your own project — no separate install step, no version mismatch between what you tested against and what's on the machine.
include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        v1.14.0
)
FetchContent_MakeAvailable(googletest)

target_link_libraries(tests PRIVATE gtest_main)
FetchContent_MakeAvailable downloads the dependency once (cached for later builds), then runs its CMakeLists.txt as if it were add_subdirectory()'d in — its targets (gtest_main above) become directly linkable, same as any target you defined yourself.

multi-directory projects

add_subdirectory(&lt;dir&gt;) | | processes another CMakeLists.txt in a subdirectory, folding its targets into the overall project. The standard way to split a large project (a library folder, an app folder, a tests folder) into independently-organized pieces |'cm30'
add_library(core::utils ALIAS core_utils) | | creates a namespaced alias for a target, so consumers link core::utils regardless of which subdirectory actually defined core_utils — the same style find_package() imported targets use |'cm31'

testing with CTest

CTest is CMake's bundled test runner — it doesn't run your tests itself, it just knows how to invoke whatever test executables you register, collect their pass/fail exit codes, and report a summary.
enable_testing() | | turns on CTest support for this project; call once, near the top level |'cm32'
add_test(NAME &lt;name&gt; COMMAND &lt;target&gt;) | | registers a test: a name, plus the executable (and arguments) CTest should run and check the exit code of |'cm33'
ctest | | runs from inside the build directory: executes every registered test and prints a summary |'cm34'
ctest --output-on-failure | | also prints each failing test's full output, instead of just pass/fail — almost always what you actually want |'cm35'
ctest -R &lt;regex&gt; | | runs only tests whose name matches a pattern |'cm36'
ctest -j 8 | | runs independent tests in parallel |'cm37'

generators & build types

cmake -B build -G Ninja | | generates Ninja build files instead of the platform default — Ninja is generally faster than make, especially for incremental rebuilds |'cm38'
cmake -B build -G "Unix Makefiles" | | generates traditional Makefiles (the default on Linux/macOS if no -G is given) |'cm39'
cmake --help | | lists every generator available on the current system, near the bottom of the output |'cm40'
Generators split into two kinds, and it changes how CMAKE_BUILD_TYPE works:
  • Single-config generators (Makefiles, Ninja) pick one build type — Debug, Release, etc. — at configure time, via CMAKE_BUILD_TYPE. To switch, reconfigure (or use a separate build directory per config).
  • Multi-config generators (Visual Studio, Xcode) can hold every build type at once in one build directory. CMAKE_BUILD_TYPE is ignored; you choose the config at build time instead with `cmake --build build --config Release`.

advanced: presets & cross-compiling

A CMakePresets.json file (CMake 3.19+) lets a team commit named, shareable configure/build/test configurations to version control, instead of everyone remembering (or scripting) their own long strings of -D flags.
{
  "version": 6,
  "configurePresets": [
    {
      "name": "release",
      "binaryDir": "${sourceDir}/build/release",
      "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" }
    }
  ]
}
cmake --preset release | | configures using the named preset from CMakePresets.json |'cm41'
cmake --build --preset release | | builds using a matching build preset |'cm42'
cmake --list-presets | | lists every preset defined in CMakePresets.json |'cm43'
cmake -B build -DCMAKE_TOOLCHAIN_FILE=&lt;file.cmake&gt; | | points CMake at a toolchain file describing a different target platform/compiler than the host — how cross-compiling (e.g. building for an embedded ARM target on an x86 machine) is configured |'cm44'
$&lt;CONFIG:Debug&gt; | | a generator expression — evaluated at build time rather than configure time, so it can depend on which config a multi-config generator ends up building. Used inside target_compile_options() etc. to apply a flag only in specific configs |'cm45'
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON | | generates compile_commands.json, the database clangd/clang-tidy and most IDE tooling use for accurate autocomplete and static analysis. Only supported by the Makefiles and Ninja generators |'cm46'

advanced: install, export & CPack

install(TARGETS &lt;target&gt; DESTINATION bin) | | declares that `cmake --install` should copy this target's built output to &lt;prefix&gt;/bin |'cm47'
install(FILES &lt;header.h&gt; DESTINATION include) | | installs arbitrary files (typically public headers) alongside the built targets |'cm48'
install(TARGETS mylib EXPORT MyLibTargets ...) | | in addition to installing the binary, records the target under an export name for the next step |'cm49'
install(EXPORT MyLibTargets FILE MyLibTargets.cmake DESTINATION lib/cmake/MyLib) | | writes out a CMake package config file, which is what lets someone else's project simply find_package(MyLib) and link MyLib::mylib — the same mechanism Boost::filesystem above relies on |'cm50'
include(CPack) | | pulls in CMake's packaging module, which can bundle your `install()`-ed output into a .zip, .tar.gz, .deb, .rpm, or a platform installer |'cm51'
cpack -G DEB | | (after building) generates a .deb package from what install() would have installed |'cm52'

related topics

C++ Testing & Tooling — the test frameworks (GoogleTest, Catch2) CTest is usually wired up to run.
Docker Cheat Sheet — building a project's CMake configure/build/install steps inside a container for reproducible builds.
Git Cheat Sheet — the GIT_REPOSITORY/GIT_TAG pins FetchContent pulls dependencies from.
VS Code Debugging — attaching a debugger to a binary CMake just built.

reference

cmake.org documentation
CMake buildsystem reference (targets, PUBLIC/PRIVATE/INTERFACE)
CTest documentation