Make is the classic way to build C — but write Makefiles long enough and you meet its sharp edges: header dependencies you must list by hand, tab-vs-space landmines, and a file that only works on Unix. CMake is the industry’s answer, and it’s what you’ll meet in almost every serious C/C++ codebase today. This tutorial takes the exact same three-file project from our Makefile tutorial and rebuilds it the CMake way, with every command’s actual output shown (CMake 3.28, gcc 13.3, Ubuntu 24.04).
What CMake Actually Is (Not a Build Tool)
The one idea that makes CMake click: CMake doesn’t build anything. It’s a generator — it reads your project description (CMakeLists.txt) and generates a native build system: a Makefile on Linux, an Xcode project on macOS, a Visual Studio solution on Windows. You describe the project once; CMake produces the right build files for wherever you are. That’s why it won: one description, every platform.
Step 1 — Install CMake
sudo apt install cmake
(macOS: brew install cmake. Windows: winget install Kitware.CMake or the installer from cmake.org.) Verify:
cmake --version
cmake version 3.28.3
The Project — Same Calculator, New Build
The same three files from the Makefile tutorial — utils.h declaring add() and multiply(), utils.c defining them, main.c calling them:
/* main.c */
#include <stdio.h>
#include "utils.h"
int main(void)
{
printf("3 + 4 = %d\n", add(3, 4));
printf("3 * 4 = %d\n", multiply(3, 4));
return 0;
}
Next to them, create a file named exactly CMakeLists.txt (capital C, capital M, capital L — CMake is picky about the name). The whole thing is five lines:
cmake_minimum_required(VERSION 3.16)
project(calc C)
add_executable(calc main.c utils.c)
target_compile_options(calc PRIVATE -Wall -Wextra)
Line by line:
cmake_minimum_required— the oldest CMake version this file supports. Every CMakeLists.txt must start with it.project(calc C)— names the project and says it’s C (CMake assumes C++ otherwise, and will demand a C++ compiler).add_executable(calc main.c utils.c)— the heart of it: build an executable calledcalcfrom these sources. Notice utils.h isn’t listed — hold that thought.target_compile_options(calc PRIVATE -Wall -Wextra)— warnings on, always.PRIVATEmeans the flags apply to this target only, not to anything that later links against it.
Step 2 — Configure, Then Build
CMake is a two-step tool, and the modern commands are symmetrical:
cmake -B build # configure: generate the build system into ./build
cmake --build build # build: run it
The configure step inspects your machine and writes everything into a build/ directory:
$ cmake -B build -- The C compiler identification is GNU 13.3.0 -- Detecting C compiler ABI info -- Detecting C compiler ABI info - done -- Check for working C compiler: /usr/bin/cc - skipped -- Detecting C compile features -- Detecting C compile features - done -- Configuring done (0.1s) -- Generating done (0.0s) -- Build files have been written to: /work/calc/build
This is called an out-of-source build: every generated file lands in build/, your source directory stays clean, and “start over” is just rm -rf build. Peek inside and you’ll find CMake kept its promise about being a generator:
$ ls build CMakeCache.txt CMakeFiles Makefile calc cmake_install.cmake
There’s a real Makefile in there — on Linux, CMake generated one for you (you can even cd build && make and it works). Now build and run:
$ cmake --build build [ 33%] Building C object CMakeFiles/calc.dir/main.c.o [ 66%] Building C object CMakeFiles/calc.dir/utils.c.o [100%] Linking C executable calc $ ./build/calc 3 + 4 = 7 3 * 4 = 12
Step 3 — The Feature That Beats Hand-Written Makefiles
Rebuild with nothing changed — CMake does nothing, like Make:
$ cmake --build build [100%] Built target calc
Touch one source file — only that file recompiles, like Make:
$ touch utils.c $ cmake --build build [ 33%] Building C object CMakeFiles/calc.dir/utils.c.o [ 66%] Linking C executable calc [100%] Built target calc
Now the payoff. In the Makefile tutorial, we had to manually list utils.h as a prerequisite — and “edited a header but nothing rebuilt” is the classic bug when you forget. Our CMakeLists.txt never mentions utils.h anywhere. Touch it anyway:
$ touch utils.h $ cmake --build build [ 33%] Building C object CMakeFiles/calc.dir/main.c.o [ 66%] Building C object CMakeFiles/calc.dir/utils.c.o [100%] Built target calc
Both files that #include "utils.h" recompiled — automatically. CMake asks the compiler which headers each source actually includes and tracks them for you. An entire category of stale-build bugs, gone. This alone is why large projects moved off hand-written Makefiles.
Step 4 — Debug and Release Builds
With Make you’d edit CFLAGS by hand. With CMake you configure two build directories with different build types and they coexist:
cmake -B build-debug -DCMAKE_BUILD_TYPE=Debug
cmake -B build-release -DCMAKE_BUILD_TYPE=Release
Run the builds with --verbose and you can see exactly what each type adds to the compile line. Debug gets -g (debugger-ready — see our GDB tutorial):
/usr/bin/cc -g -Wall -Wextra ... -c /work/calc/main.c
Release gets full optimization and strips assert()s:
/usr/bin/cc -O3 -DNDEBUG -Wall -Wextra ... -c /work/calc/main.c
One caveat worth knowing early: the default build type is empty — neither -g nor -O. Always pass -DCMAKE_BUILD_TYPE explicitly.
Step 5 — Growing Up: Targets and Libraries
CMake’s model is targets — executables and libraries — connected by target_link_libraries. Here’s the same project restructured the way real codebases do it, with the utilities as a reusable static library:
cmake_minimum_required(VERSION 3.16)
project(calc C)
add_library(utils STATIC utils.c)
target_include_directories(utils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
add_executable(calc main.c)
target_link_libraries(calc PRIVATE utils)
target_compile_options(calc PRIVATE -Wall -Wextra)
$ cmake -B build && cmake --build build [ 25%] Building C object CMakeFiles/utils.dir/utils.c.o [ 50%] Linking C static library libutils.a [ 50%] Built target utils [ 75%] Building C object CMakeFiles/calc.dir/main.c.o [100%] Linking C executable calc $ ./build/calc 3 + 4 = 7 3 * 4 = 12
Two things to notice. First, CMake built a genuine libutils.a and linked it — the same artifact you’d hand-craft with ar. Second, the PUBLIC on target_include_directories means “anyone who links utils also gets its include path” — the library carries its own requirements with it. That’s the idea (“modern CMake”) that scales from this toy to projects with hundreds of targets.
Common Issues
| Problem | Fix |
|---|---|
Error: could not load cache |
You ran cmake --build . in the source directory — point it at the build dir: cmake --build build |
CMake Error: The source directory ... does not appear to contain CMakeLists.txt |
File must be named CMakeLists.txt exactly, in the directory you pass to cmake |
No CMAKE_CXX_COMPILER could be found on a C-only project |
Declare the language: project(name C) — without it CMake also demands a C++ compiler |
| Generated files polluting your source directory | You ran cmake . (in-source build) — delete CMakeCache.txt and CMakeFiles/, then use cmake -B build |
| Changed CMakeLists.txt — do I reconfigure? | No — cmake --build build notices and re-runs the configure step automatically |
| Binary has no debug info / runs slow | Default build type is empty — configure with -DCMAKE_BUILD_TYPE=Debug (or Release) |
Make or CMake — Which Should You Learn?
Both, in that order. Make teaches you what a build is — targets, dependencies, timestamps — in a file you can read in one sitting (start with the Makefile tutorial). CMake is what you’ll actually use on multi-platform and multi-library projects, and on Linux it literally generates the Makefile for you. For a single-directory personal project, a Makefile is still perfectly fine; the moment you add a second platform, a third-party library, or a teammate on Windows, reach for CMake.
What’s Next
Your Debug build already compiles with -g, so it’s ready for the debugger — How to Use GDB to Debug C Programs picks up exactly there, and Valgrind completes the workflow by catching the leaks your tests miss.
New to the Linux C toolchain? Start from installing GCC on Ubuntu or the complete C environment setup guide.
As an Amazon Associate we earn from qualifying purchases.
Recommended Book
Build systems exist to compile well-structured programs — and program structure is what The C Programming Language by Kernighan & Ritchie (Amazon.com) teaches better than any book since. Chapter 4 on functions and program structure is the “why” behind every multi-file build.