Building with make and CMake
Typing long compiler commands gets old quickly, and recompiling every file after each small change wastes time. make reads a Makefile describing how files depend on each other and rebuilds only what changed. For larger and cross-platform projects, CMake generates the build for you.
A first Makefile
For the three-file program from the previous lesson:
CC = cc CFLAGS = -std=c17 -Wall -Wextra -pedantic -g LDLIBS = -lm weather: main.o stats.o $(CC) $(CFLAGS) -o weather main.o stats.o $(LDLIBS) main.o: main.c stats.h $(CC) $(CFLAGS) -c main.c stats.o: stats.c stats.h $(CC) $(CFLAGS) -c stats.c clean: rm -f weather *.o .PHONY: clean
Each rule has three parts:
target: prerequisites <TAB>command to build the target
- Target — the file to build (
weather,main.o). - Prerequisites — the files it's built from. If any is newer than the target, the target is rebuilt.
- Commands — the shell commands that build it. They must be indented with a tab character, not spaces — the most common Makefile error ("missing separator").
Using it
make # builds the first target: weather make # nothing changed: "weather is up to date" touch stats.c # pretend stats.c was edited make # recompiles only stats.o, then relinks make clean # removes the built files
make compares file timestamps to decide what's out of date. Listing stats.h as a prerequisite of both object files means changing the header rebuilds both. .PHONY marks clean as a command name rather than a file.
Variables and pattern rules
Real Makefiles avoid repeating themselves. A pattern rule says how to build any .o from the matching .c:
CC = cc CFLAGS = -std=c17 -Wall -Wextra -pedantic -g SRCS = main.c stats.c OBJS = $(SRCS:.c=.o) weather: $(OBJS) $(CC) $(CFLAGS) -o $@ $^ %.o: %.c stats.h $(CC) $(CFLAGS) -c $< -o $@ clean: rm -f weather $(OBJS) .PHONY: clean
| Symbol | Means |
|---|---|
$@ | The target |
$< | The first prerequisite |
$^ | All prerequisites |
$(SRCS:.c=.o) | The list with .c replaced by .o |
You can override variables from the command line, for example make CFLAGS="-O2" for an optimised build. GCC and Clang can also generate the header dependencies automatically with -MMD -MP.
CMake
A Makefile is tied to Unix-style tools. CMake describes the project at a higher level and generates the real build files — Makefiles, Ninja files or Visual Studio projects — for whatever platform you're on. It's the most widely used build system for C and C++ projects:
cmake_minimum_required(VERSION 3.20) project(weather C) set(CMAKE_C_STANDARD 17) set(CMAKE_C_STANDARD_REQUIRED ON) add_executable(weather main.c stats.c) target_compile_options(weather PRIVATE -Wall -Wextra -pedantic)
cmake -S . -B build # configure: generate build files in ./build cmake --build build # compile ./build/weather
Keeping the build in a separate build/ folder keeps generated files out of your sources (and out of Git — add build/ to .gitignore). IDEs such as CLion and Visual Studio open CMake projects directly.
Other tools
- Meson — a modern, fast build system with a simple syntax.
- Ninja — a very fast low-level build runner, usually generated by CMake or Meson rather than written by hand.
- Package managers such as Conan and vcpkg fetch C and C++ libraries for you.
Try it yourself
Write a Makefile for a two-file project of your own, run make twice to see the "up to date" message, then touch one source file and check that only it is recompiled.
Built with make!