Makefile syntax, targets, dependencies, pattern rules, and best practices for building C/C++ and multi-language projects with Make.
Last Updated: 2025-10-26
Use this skill when:
Alternatives: CMake (cross-platform), Ninja (speed), Bazel (monorepos)
Prerequisites: Basic shell scripting, compiler toolchains (gcc, clang)
# Comments start with #
# Target: Dependencies
# <TAB> Commands
target: dependencies
commands
# Variables
CC = gcc
CFLAGS = -Wall -Wextra -O2
SOURCES = main.c utils.c
OBJECTS = $(SOURCES:.c=.o)
# Default target (first target in file)
all: program
# Executable target
program: $(OBJECTS)
$(CC) $(OBJECTS) -o program
# Pattern rule for .o files
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Phony targets (not real files)
.PHONY: all clean install
clean:
rm -f $(OBJECTS) program
install: program
cp program /usr/local/bin/
# $@ - The target name
# $< - First dependency
# $^ - All dependencies
# $? - Dependencies newer than target
# $* - Stem of pattern rule
%.o: %.c %.h
@echo "Target: $@" # e.g., "main.o"
@echo "First dep: $<" # e.g., "main.c"
@echo "All deps: $^" # e.g., "main.c main.h"
$(CC) -c $< -o $@
# Simple assignment (evaluated immediately)
VAR := value
# Recursive assignment (evaluated when used)
VAR = value
# Conditional assignment (only if not set)
VAR ?= default_value
# Append to variable
VAR += additional_value
# Example
CC := gcc
CFLAGS = -Wall
CFLAGS += -g -O2
PREFIX ?= /usr/local
# Use variables
$(CC) $(CFLAGS) main.c -o main
##
Pattern Rules and Implicit Rules
# General pattern: %.target: %.source
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Multiple patterns
%_test: %_test.c
$(CC) $(CFLAGS) $< -o $@ -lcheck
# Static pattern rules
$(OBJECTS): %.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Pattern with multiple extensions
%.pdf: %.tex
pdflatex $<
pdflatex $< # Run twice for references
Make has built-in implicit rules:
# Built-in rule for C compilation
# %.o: %.c
# $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
# Override implicit variables
CC = clang
CFLAGS = -Wall -Wextra -std=c11
# Disable implicit rules
.SUFFIXES:
# Or cancel specific implicit rule
%.o: %.c
# Project structure:
# src/
# include/
# build/
# tests/
CC := gcc
CFLAGS := -Wall -Wextra -std=c11 -I./include
SRCDIR := src
BUILDDIR := build
TESTDIR := tests
SOURCES := $(wildcard $(SRCDIR)/*.c)
OBJECTS := $(SOURCES:$(SRCDIR)/%.c=$(BUILDDIR)/%.o)
DEPS := $(OBJECTS:.o=.d)
TARGET := myprogram
.PHONY: all clean test dirs
all: dirs $(TARGET)
dirs:
@mkdir -p $(BUILDDIR)
$(TARGET): $(OBJECTS)
$(CC) $(OBJECTS) -o $@
# Auto-generate dependencies
$(BUILDDIR)/%.o: $(SRCDIR)/%.c
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
# Include dependency files
-include $(DEPS)
clean: # Cleans build artifacts only - safe to run
rm -rf $(BUILDDIR) $(TARGET)
test: $(TARGET)
$(MAKE) -C $(TESTDIR)
# Debug vs Release builds
DEBUG ?= 0
ifeq ($(DEBUG), 1)
CFLAGS += -g -O0 -DDEBUG
else
CFLAGS += -O2 -DNDEBUG
endif
# Platform-specific
UNAME := $(shell uname)
ifeq ($(UNAME), Darwin)
LIBS += -framework CoreFoundation
else ifeq ($(UNAME), Linux)
LIBS += -lrt
endif
# Compiler detection
CC ?= gcc
ifeq ($(CC), clang)
CFLAGS += -Qunused-arguments
endif
# Top-level Makefile
SUBDIRS := src tests docs
.PHONY: all $(SUBDIRS) clean
all: $(SUBDIRS)
$(SUBDIRS):
$(MAKE) -C $@
# Ensure tests depend on src
tests: src
clean:
for dir in $(SUBDIRS); do \
$(MAKE) -C $$dir clean; \
done
# Enable parallel builds with make -j
# Serialize certain targets
.NOTPARALLEL: install
# Or use order-only prerequisites
program: | $(BUILDDIR)
# Build program only after builddir exists
$(BUILDDIR):
mkdir -p $@
# Limit parallelism for specific target
test:
$(MAKE) -C tests -j1 # Serial tests
# Built-in functions
# String substitution
SOURCES := main.c utils.c
OBJECTS := $(SOURCES:.c=.o)
# Or: $(patsubst %.c,%.o,$(SOURCES))
# Wildcard
SOURCES := $(wildcard src/*.c)
# Directory/basename
DIRS := $(dir $(SOURCES)) # Extract directories
BASES := $(notdir $(SOURCES)) # Extract basenames
STEMS := $(basename $(SOURCES)) # Remove extension
# Filter
CFILES := $(filter %.c,$(SOURCES))
NON_TEST := $(filter-out %_test.c,$(SOURCES))
# Shell command
GIT_HASH := $(shell git rev-parse --short HEAD)
NUM_CORES := $(shell nproc)
# Conditional
DEBUG_FLAG := $(if $(DEBUG),-g)
# Example: Version from git
VERSION := $(shell git describe --tags --always --dirty)
CFLAGS += -DVERSION=\"$(VERSION)\"
# Define function with $(call)
define compile_template
$(1)_objs := $$($(1)_srcs:.c=.o)
$(1): $$($(1)_objs)
$$(CC) $$($(1)_objs) -o $(1)
endef
# Use function
app1_srcs := main.c utils.c
$(eval $(call compile_template,app1))
app2_srcs := test.c helpers.c
$(eval $(call compile_template,app2))
# Include configuration
include config.mk
# Include dependencies (silent failure with -)
-include $(DEPS)
# Conditional include
ifeq ($(USE_SSL), 1)
include ssl.mk
endif
# Example config.mk
# CC = clang
# CFLAGS = -Wall
# PREFIX = /opt/local
.PHONY: all clean install uninstall test help
# Default target
all: program
# Clean build artifacts - safe to run
clean:
rm -f $(OBJECTS) $(TARGET)
rm -rf $(BUILDDIR)
# Install to system
install: $(TARGET)
install -d $(PREFIX)/bin
install -m 755 $(TARGET) $(PREFIX)/bin/
# Uninstall
uninstall:
rm -f $(PREFIX)/bin/$(TARGET)
# Run tests
test: $(TARGET)
@echo "Running tests..."
@./run_tests.sh
# Help message
help:
@echo "Available targets:"
@echo " all - Build program (default)"
@echo " clean - Remove build artifacts"
@echo " install - Install to $(PREFIX)"
@echo " test - Run test suite"
@echo ""
@echo "Variables:"
@echo " DEBUG=1 - Enable debug build"
@echo " PREFIX - Install prefix (default: /usr/local)"
# Automatic dependency generation with -MMD -MP
CC := gcc
CFLAGS := -Wall -std=c11
DEPFLAGS = -MMD -MP
SOURCES := $(wildcard *.c)
OBJECTS := $(SOURCES:.c=.o)
DEPS := $(OBJECTS:.o=.d)
program: $(OBJECTS)
$(CC) $^ -o $@
# Compile with dependency generation
%.o: %.c
$(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@
# Include generated dependencies
-include $(DEPS)
clean:
rm -f $(OBJECTS) $(DEPS) program
# Explicit dependencies
main.o: main.c main.h utils.h config.h
utils.o: utils.c utils.h config.h
config.o: config.c config.h
# Or generate with makedepend
depend:
makedepend -Y -- $(CFLAGS) -- $(SOURCES)
# Silent mode (@)
clean:
@echo "Cleaning..."
@rm -f $(OBJECTS)
# Verbose mode (V=1)
V ?= 0
ifeq ($(V), 1)
Q =
else
Q = @
endif
%.o: %.c
$(Q)echo "CC $<"
$(Q)$(CC) $(CFLAGS) -c $< -o $@
# Or use .SILENT
.SILENT: clean
# Cross-compile for ARM
CROSS_COMPILE ?= arm-linux-gnueabihf-
CC := $(CROSS_COMPILE)gcc
AR := $(CROSS_COMPILE)ar
STRIP := $(CROSS_COMPILE)strip
# Platform-specific flags
ARCH ?= x86_64
ifeq ($(ARCH), arm)
CFLAGS += -march=armv7-a
else ifeq ($(ARCH), x86_64)
CFLAGS += -m64
endif
# Example usage: make ARCH=arm
# WRONG: Using spaces instead of tabs
target:
echo "This will fail" # Spaces, not tab
# CORRECT: Use tab character
target:
echo "This works" # Tab character
# WRONG: Infinite recursion
CFLAGS = $(CFLAGS) -Wall
# CORRECT: Use different variable or :=
CFLAGS := $(CFLAGS) -Wall
# Or
BASE_CFLAGS = -Wall
CFLAGS = $(BASE_CFLAGS) -g
# WRONG: If file named "clean" exists, won't run
clean:
rm -f *.o
# CORRECT: Declare as phony
.PHONY: clean
clean:
rm -f *.o
# WRONG: Hardcoded paths
install:
cp program /usr/local/bin/
# CORRECT: Use variables
PREFIX ?= /usr/local
install:
cp program $(PREFIX)/bin/
# WRONG: No header dependencies
program: main.o utils.o
gcc $^ -o $@
# CORRECT: Include dependencies or use -MMD
%.o: %.c
$(CC) $(CFLAGS) -MMD -c $< -o $@
-include *.d
make # Build default target
make target # Build specific target
make -j4 # Parallel build (4 jobs)
make -n # Dry run (show commands)
make -B # Force rebuild all
make -k # Keep going on errors
make V=1 # Verbose output
make DEBUG=1 # Set variable
make -C dir # Run in directory
make --print-data-base # Print rules
CC # C compiler (gcc)
CXX # C++ compiler (g++)
CFLAGS # C compiler flags
LDFLAGS # Linker flags
LDLIBS # Libraries to link
AR # Archive tool (ar)
INSTALL # Install command
%.o: %.c # C object files
%.so: %.o # Shared libraries
%_test: %_test.c # Test executables
$(BUILD)/%.o: $(SRC)/%.c # With directories
cmake-patterns.md - Modern cross-platform build systembuild-system-selection.md - Choosing between Make, CMake, Bazelcross-platform-builds.md - Multi-platform build strategiesbuild-optimization.md - Incremental builds and cachingzig-build-system.md - Zig's build.zig as Make alternativecicd/ci-optimization.md - Optimizing Make in CI pipelinesMake is the ubiquitous UNIX build tool with a steep learning curve but powerful capabilities:
Key Takeaways:
%.o: %.c for reusable rules$@, $<, $^ for concise Makefiles-MMD -MP to auto-track header dependencies.PHONY for non-file targets:= for immediate, = for recursive, ?= for defaultsmake -j for faster buildsMake excels at incremental builds and is the foundation for many modern build systems. While CMake and Bazel offer more features, Make remains the lightweight, portable choice for C/C++ projects.