cmake (when make isn't enough)
CMake is not a compiler and not the build tool that usually executes each compile command.
It is a build generator. You describe targets, source files, include paths, compile
features, libraries, install rules, and options in CMakeLists.txt; CMake configures a
build directory and writes native build files for Make, Ninja, Xcode, Visual Studio, or
another backend. The value appears when the project outgrows hand-maintained Makefiles:
multiple platforms, optional dependencies, generated files, tests, installs, and library
boundaries.
The reset: make executes a dependency graph. CMake writes that graph for a chosen generator from a higher-level target model.
How it really works
CMake has two main phases. The configure/generate phase reads CMakeLists.txt, detects
compilers and platform features, evaluates options, and writes build-system files under a
separate build directory. The build phase delegates to the generated backend:
cmake -S . -B build
cmake --build build
Modern CMake is target-oriented. You avoid global piles of flags and instead attach requirements to targets:
| CMake command | Meaning |
|---|---|
add_library(message STATIC message.c) |
build a library target |
add_executable(demo main.c) |
build an executable target |
target_include_directories(message PUBLIC ...) |
consumers of message inherit include paths |
target_link_libraries(demo PRIVATE message) |
demo links to message without exporting that dependency |
target_compile_options(demo PRIVATE -Wall -Wextra) |
attach compile flags to one target |
Out-of-source builds are the norm: source files stay clean, while generated build files,
objects, caches, and binaries live in build/. That makes it easy to throw away a build
directory, configure Debug and Release side by side, or build the same source tree with
different compilers.
When it beats hand-written make
Make is excellent when you want exact control over a small graph. CMake becomes attractive when the graph must adapt: feature checks, library discovery, platform-specific flags, installation layout, test integration, IDE project generation, cross-compilation toolchain files, and packaging. It is also the lingua franca for many C/C++ dependencies, so knowing the target model matters even if you prefer simpler tools for tiny projects.
CMake is not free. It has its own language, cache model, generator-specific behavior, and old global-style patterns that still appear in tutorials. The decision rule is simple: if your build is one executable and two objects, a Makefile is clearer. If you need portable targets across machines and toolchains, CMake earns its keep.
Executable artifact: target model plus local fallback
The example lives in examples/toolchain-and-linking/cmake-when-make-isnt-enough/. It
contains a real CMakeLists.txt, but cmake is not installed in this local environment, so
the script truthfully falls back to compiling and running the same sources with cc.
cmake_minimum_required(VERSION 3.16)
project(cmake_demo C)
add_library(message STATIC message.c)
target_include_directories(message PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}")
target_compile_options(message PRIVATE -Wall -Wextra)
add_executable(demo main.c)
target_link_libraries(demo PRIVATE message)
target_compile_options(demo PRIVATE -Wall -Wextra)
The C code is ordinary:
#include <stdio.h>
#include "message.h"
int main(void) {
printf("%s result: %d\n", message_label(), message_value());
return 0;
}
Run it:
cd examples/toolchain-and-linking/cmake-when-make-isnt-enough
./run.sh
Real output from this machine:
== cmake configure/build if available ==
cmake: not installed on this machine
== fallback compile/run with same sources ==
cmake target result: 42
On a machine with CMake installed, the intended commands are cmake -S . -B build, then
cmake --build build, then ./build/demo. That would verify the same target graph instead
of the fallback compile line.
Failure modes & trade-offs
- In-source builds pollute the tree. Prefer
cmake -S . -B buildso generated files are disposable. - Global flags leak. Old patterns such as editing
CMAKE_C_FLAGSglobally make targets harder to compose. Prefer target properties. - Cache state can lie.
CMakeCache.txtrecords detected paths and options. When the build behaves impossibly, delete the build directory and reconfigure. - Generator differences exist. Make, Ninja, Xcode, and Visual Studio backends do not expose identical behavior.
- It can be too much tool. For a tiny one-platform exercise, CMake may obscure more than it helps.
- Cross-compilation needs explicit toolchain files. Guessing from the host is wrong for an OS kernel or embedded target.
In practice
- Think in targets. Libraries and executables own their includes, compile definitions, flags, and link dependencies.
- Keep source and build directories separate.
build/should be safe to delete. - Use CMake when portability pressure is real. Platform checks and dependency discovery are where it pays for itself.
- Do not hide compiler basics. You still need to understand objects, archives, shared libraries, and linker flags because CMake will generate those commands.
- For the OS project, use a toolchain file. Freestanding targets need a cross-compiler, sysroot assumptions, and linker behavior that do not match the host.
Connects to: Toolchain & Linking · make and the build dependency graph · gcc vs clang and the compiler driver model · OS from Scratch
Sources
- CMake Tutorial — official introduction to configure/build workflow and target creation. https://cmake.org/cmake/help/latest/guide/tutorial/index.html
- CMake
cmake-buildsystem(7)manual — target properties, usage requirements, and the buildsystem model. https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html - CMake
add_librarycommand — library target creation, static/shared choices, and source lists. https://cmake.org/cmake/help/latest/command/add_library.html - CMake
target_link_librariescommand — target link dependencies and PUBLIC/PRIVATE/INTERFACE propagation. https://cmake.org/cmake/help/latest/command/target_link_libraries.html - CMake Toolchains manual — cross-compilation and toolchain-file model. https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html