Intuition The ONE core idea
Your source code is not a program yet — it must travel through a pipeline of tools (preprocessor, compiler, assembler, linker) to become a runnable binary, and each tool needs the right flags (extra command-line switches) in the right order. CMake is a machine that writes those instructions for you : you describe what to build and what depends on what , and CMake generates the exact, platform-correct commands.
Before you can read a single line of a CMakeLists.txt, you need the vocabulary the parent note quietly assumes. This page builds each idea from nothing — plain words first, then a picture, then why the topic needs it . Read top to bottom; every item leans on the one above it.
Definition Source code vs. binary
Source code is the text you type: main.cpp. A computer cannot run text — it runs machine code (raw numbers the CPU understands), stored in a file called a binary (an executable).
Building = the whole process that turns text into a runnable binary.
The picture below shows why this is not one step but several. This is the compilation pipeline , and CMake exists to drive it.
Definition Flag (compiler / linker flag)
A flag is a small text switch you append to a tool's command line to change what it does — e.g. -I math tells the compiler "also search the math/ folder", -O2 says "optimise", -lmath tells the linker "pull in library math". Flags are how each arrow in the pipeline is told to behave; getting them wrong (or in the wrong order) breaks the build.
Intuition Why does the pipeline matter for CMake?
CMake never touches your code. It only arranges who runs which tool, with which flags, in which order . If you do not know the pipeline exists, phrases like "link order" and "include path" are meaningless. Every CMake command ultimately turns into flags on one arrow in that picture.
Let us name the two arrows CMake cares about most.
Taking one source file (add.cpp) and turning it into an object file (add.o) — machine code for that file alone , with gaps where it calls code defined elsewhere. Picture a jigsaw piece with tabs sticking out.
Fitting all the object-file jigsaw pieces together (plus libraries) into one finished binary, connecting each "tab" to the matching "slot". If a call has no matching definition anywhere, you get the famous undefined reference error.
Intuition Why the topic needs these two words
add_executable / add_library decide what gets compiled .
target_link_libraries decides what gets linked together and in what order.
The parent's whole "link order is automatic" claim only makes sense once you see linking as the piece-fitting step above.
.h)
A file that declares what functions exist ("there is a function add(int,int)") without saying how they work. It is a promise ; the .cpp file is the fulfilment . Other files #include the header to learn the promise.
Definition Include path (include directory)
The list of folders the compiler searches when it meets an #include. There are two include forms , and they search differently:
#include "add.h" (quotes ) — search your own project folders first : the current file's directory, then the dirs you added (via -I / target_include_directories).
#include <vector> (angle brackets ) — search only the system include paths : the standard-library and installed-package folders the compiler already knows about. This is the edge case people trip on — a header you installed system-wide is reached with <...>, a header inside your project with "...".
If the folder holding a header is on none of these lists, compiling fails with file not found .
See Header files and Include Guards for why a header can be included many times safely. The one thing to hold here:
target_include_directories exists
When the parent writes target_include_directories(mathlib PUBLIC math), it is saying: "add the math/ folder to the include path — for me, and for anyone who links me." Under the hood this just emits an -I math flag . Without the notion of an include path , that command is pure noise. The header is the interface of a library; the include dir is where the compiler finds that interface .
A bundle of already-compiled object code meant to be reused by many programs, instead of being recompiled each time.
There are two flavours, and the parent's STATIC | SHARED keyword picks between them:
Definition Static library (
.a / .lib)
Its machine code is copied into each program that links it at build time. Picture photocopying a chapter into every book. Bigger binaries, but self-contained.
Definition Shared library (
.so / .dll)
Kept as a separate file; the program borrows it at run time. Picture one library book many readers share. Smaller binaries, but the .so must be present when you run.
Intuition Why the STATIC vs SHARED choice matters
in CMake
One keyword in add_library(mathlib STATIC ...) decides the whole shape of your deliverable:
STATIC → each program that links mathlib gets its own copy baked in. The binary runs anywhere with no extra files, but fixing a bug in mathlib means rebuilding every program , and shared code is duplicated on disk.
SHARED → one .so/.dll is loaded at run time. Many programs share it, and you can patch the library without recompiling them — but that file must be found at run time or the program won't start.
Because CMake propagates the linking rules automatically, changing this single word ripples through every target that links the library. That is why it is a design decision, not a detail.
Full detail lives in Static vs Dynamic Linking . For CMake, this is the choice you hand to add_library.
Definition CMakeLists.txt
CMakeLists.txt is the script CMake reads — an ordinary text file of commands (project(), add_executable(), target_link_libraries(), …) that CMake runs top-to-bottom to learn about your project. The name is fixed and case-sensitive : CMake looks for a file called exactly CMakeLists.txt in the folder you point it at, and one in every subfolder you pull in with add_subdirectory. Rename it and CMake will not find it.
Intuition Why one fixed filename
A build tool must know where to start without you telling it every time. By convention CMake always opens CMakeLists.txt first — the root one describes the whole project, and each subdirectory's own CMakeLists.txt describes its piece. Everything the parent note shows lives inside such a file.
A target is one thing CMake knows how to build: an executable or a library. Give it a name (an internal handle like calc or mathlib) and CMake tracks everything about it — its sources, its include dirs, its dependencies.
Intuition Why "target-centric" matters
Old build scripts set global settings that leaked into everything. Modern CMake attaches every setting to a target with target_* commands. Think of a target as a box: each box carries its own labelled compartments (sources, includes, links), so boxes do not contaminate each other.
Definition Dependency graph
A drawing where each target is a dot, and an arrow A → B means "A depends on B". CMake reads all your target_link_libraries lines, builds this graph, and computes a safe build/link order automatically.
Definition Transitive dependency
A dependency you get through another one. If app → core and core → util, then util may reach app transitively — but only if the core → util link is marked so it propagates .
Intuition Why the PUBLIC / PRIVATE / INTERFACE keywords exist
They are labels on the arrows that tell CMake whether a dependency flows onward :
PRIVATE — the arrow stops here (used only inside my .cpp).
PUBLIC — the arrow flows to whoever links me (used by me and exposed in my header).
INTERFACE — I do not use it, but it flows to my users (header-only case).
Without the graph picture, "propagation" is an abstract word. With it, propagation is simply: does the arrow continue past this node?
Definition Configure phase
CMake reads your CMakeLists.txt and generates native build files (Makefiles, Ninja files). No compiling happens yet. Command: cmake -S . -B build.
Intuition Why two phases and why "meta"
CMake is a meta build-system : it does not build, it builds the builder . The output of configure is fed to a real engine like Makefiles or the Ninja build system . This is exactly why the parent says CMake "does not compile your code."
The diagram below reads top-to-bottom : an arrow means "you need the upper idea before the lower one makes sense." Start at Source vs binary (the very first thing on this page), follow the arrows down, and you end at the actual CMake commands — every node is a section above.
Compiling to object files
Header files and include path
Target as a box of settings
CMakeLists.txt is the script
PUBLIC PRIVATE INTERFACE propagation
CMakeLists commands you can now read
Configure vs Build phases
How to read it: the two base facts are Source vs binary and CMakeLists.txt is the script. The pipeline splits into compile and link; those feed header/include and static/shared ; all of that pours into the idea of a target , which enables the dependency graph and its propagation keywords . Once you hold every node, the bottom box — the CMake commands — is just plain reading.
A program is text; the CPU runs machine code stored in a binary — so text must be built first.
A compiler/linker flag is a command-line switch (like -I math or -lmath) that tells a tool how to behave.
The pipeline stage that makes one .o from one .cpp is called compiling.
The pipeline stage that fits all .o pieces into one binary is called linking.
A header (.h) file contains a declaration — a promise that a function exists, not its body.
#include "add.h" searchesyour own project folders first, then the added include dirs.
#include <vector> searchesonly the system include paths the compiler already knows.
A STATIC library's code is copied into each program that links it, at build time.
A SHARED library's code is kept separate and borrowed at run time (must be present to run).
CMakeLists.txt is the fixed-name text script CMake reads to learn about your project.
A CMake target is one buildable thing (executable or library) that carries its own settings.
An arrow A to B in the dependency graph means A depends on B.
A transitive dependency is one you get through another dependency, when the link propagates.
PRIVATE / PUBLIC / INTERFACE control whether a dependency arrow propagates to your users.
CMake's two phases are configure (generate build files) then build (run the compiler).
CMake is called meta because it does not compile; it generates the files a real builder runs.