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:

MAKE Makefile
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:

TXT
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:

MAKE Makefile
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
SymbolMeans
$@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:

TXT CMakeLists.txt
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.

Try it yourself · main.c
Edit the code, copy it into main.c, then compile and run it with cc -std=c17 -Wall main.c -o main && ./main. How to set up a C compiler
Output
Built with make!