Complete makefile toolkit with generation and validation capabilities
97
97%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Advisory
Suggest reviewing before use
This guide covers techniques for optimizing Makefile performance, including parallel builds, dependency tracking, incremental builds, caching strategies, and performance profiling.
# Run with 4 parallel jobs
make -j4
# Use all CPU cores
make -j$(nproc)
# Unlimited parallel jobs (careful!)
make -j# WRONG: Multiple rules write to same file
target1:
echo "data1" >> shared.log
target2:
echo "data2" >> shared.log
# With -j2, file corruption likely!# RIGHT: Serialize access with dependencies
target2: target1
# Or use separate files
target1:
echo "data1" > target1.log
target2:
echo "data2" > target2.log# Disable parallel builds for this Makefile
.NOTPARALLEL:
# Disable parallelism for specific targets
.NOTPARALLEL: install clean
# Serialize specific targets
install: build
# Install runs after build completesGNU Make 4.4 (released October 2022) introduced new features for fine-grained parallel control. These are becoming part of the upcoming POSIX standard.
The .WAIT target provides explicit ordering without creating artificial dependencies:
# .WAIT ensures prerequisites to its left complete
# before starting prerequisites to its right
all: compile .WAIT link .WAIT package
# Equivalent behavior without .WAIT would require:
# link: compile
# package: link
# But .WAIT is cleaner when targets are independent conceptuallyUse Cases for .WAIT:
# Build phases with explicit ordering
build: setup .WAIT compile .WAIT test .WAIT package
@echo "Build complete"
# Parallel within phases, serial between phases
ci: lint fmt .WAIT test.unit test.integration .WAIT build
# lint and fmt run in parallel
# then unit and integration tests run in parallel
# finally build runs
# Database migrations before tests
test: migrate .WAIT run-tests
@echo "Tests complete"Important Notes:
.WAIT only affects parallel builds (make -j).WAIT has no effect.WAIT doesn't create actual dependencies, just orderingmake --version)In Make 4.4+, .NOTPARALLEL can take specific targets as prerequisites:
# Traditional: Disable ALL parallel execution
.NOTPARALLEL:
# NEW in 4.4: Serialize only specific targets
.NOTPARALLEL: install deploy cleanup
# This implicitly adds .WAIT between each prerequisite
# of the listed targetsWhen to Use .NOTPARALLEL with Prerequisites:
# Deployment must be serial (avoid race conditions)
.NOTPARALLEL: deploy
deploy: deploy-database deploy-backend deploy-frontend
@echo "Deployment complete"
# deploy-database -> deploy-backend -> deploy-frontend (serial)
# But compilation can still be parallel
build: $(OBJECTS)
$(CC) $^ -o $(TARGET)
# Object files compile in parallel (unaffected by .NOTPARALLEL: deploy)Check Make version before using 4.4+ features:
# Check Make version (4.4 = 4.4, need >= 4.4)
MAKE_VERSION_MAJOR := $(word 1,$(subst ., ,$(MAKE_VERSION)))
MAKE_VERSION_MINOR := $(word 2,$(subst ., ,$(MAKE_VERSION)))
# Simple version check
ifeq ($(shell expr $(MAKE_VERSION_MAJOR) \>= 4),1)
ifeq ($(shell expr $(MAKE_VERSION_MINOR) \>= 4),1)
HAVE_WAIT := 1
endif
endif
# Alternative: Graceful degradation
ifdef HAVE_WAIT
# Use .WAIT for modern Make
all: compile .WAIT link
else
# Fall back to dependencies for older Make
link: compile
all: link
endifPractical Version Check Pattern:
# At the top of your Makefile
MIN_MAKE_VERSION := 4.4
CURRENT_MAKE_VERSION := $(MAKE_VERSION)
# Check and warn if using older Make
ifeq ($(shell printf '%s\n' "$(MIN_MAKE_VERSION)" "$(CURRENT_MAKE_VERSION)" | sort -V | head -n1),$(MIN_MAKE_VERSION))
# Make version is sufficient
else
$(warning GNU Make $(MIN_MAKE_VERSION)+ recommended. You have $(CURRENT_MAKE_VERSION))
$(warning Some parallel control features may not work)
endif| Feature | Use Case | Make Version |
|---|---|---|
Dependencies (b: a) | Actual dependency relationship | All |
.WAIT | Ordering without dependency | 4.4+ |
.NOTPARALLEL: (global) | Disable all parallel | All |
.NOTPARALLEL: target | Serialize specific target's prereqs | 4.4+ |
Example Comparison:
# Using dependencies (works in all Make versions)
# Problem: Creates false dependency relationship
link: compile
package: link
all: package
# Using .WAIT (Make 4.4+)
# Cleaner: Explicit ordering, no false dependencies
all: compile .WAIT link .WAIT package
# Using .NOTPARALLEL with targets (Make 4.4+)
# Best for: Targets that must never run in parallel
.NOTPARALLEL: deploy
deploy: step1 step2 step3# Good parallel structure
SOURCES := src1.c src2.c src3.c src4.c
OBJECTS := $(SOURCES:.c=.o)
# All .o files can build in parallel
program: $(OBJECTS)
$(CC) $^ -o $@
%.o: %.c
$(CC) -c $< -o $@Parallel execution:
make -j4
# Compiles 4 .c files simultaneously
# Then links when all are done# WRONG: Missing header dependencies
main.o: main.c
$(CC) -c $< -o $@
# If common.h changes, main.o won't rebuild!# Generate dependencies during compilation
%.o: %.c
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
# Include generated .d files
-include $(OBJECTS:.o=.d)Generated dependency file (main.d):
main.o: main.c common.h utils.h
common.h:
utils.h:# -MMD: Generate dependency file (.d)
# -MP: Add phony targets for headers
# -MF file: Specify dependency file name
DEPFLAGS = -MMD -MP -MF $(@:.o=.d)
%.o: %.c
$(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@Without -MP:
# Generated main.d:
main.o: main.c utils.h
# If utils.h is deleted:
make: *** No rule to make target 'utils.h'. Stop.With -MP:
# Generated main.d:
main.o: main.c utils.h
utils.h:
# If utils.h is deleted, make continues
# (assumes you also removed #include "utils.h")Make rebuilds targets when prerequisites are newer:
# program rebuilt if any .o is newer
program: $(OBJECTS)
$(CC) $^ -o $@
# main.o rebuilt if main.c or headers are newer
main.o: main.c common.h
$(CC) -c main.c -o main.oInefficient:
# Every source depends on config.h
# Changing config.h rebuilds EVERYTHING
main.o: main.c config.h
utils.o: utils.c config.h
helper.o: helper.c config.h# Only main.c actually uses config.h
main.o: main.c config.h
utils.o: utils.c
helper.o: helper.c%.o: %.c
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
-include $(DEPENDS)
# Automatically tracks which headers each file uses# Mark intermediate files
.INTERMEDIATE: $(OBJECTS)
# Deleted after use
# Keep important intermediate files
.SECONDARY: important.o
# Not deleted
# Never delete these files
.PRECIOUS: %.o %.d
# Protected from deletion# WRONG: Always updates config.h
config.h: config.h.in
sed 's/@VERSION@/$(VERSION)/g' $< > $@
# Updates timestamp even if content unchanged!# RIGHT: Only update if different
config.h: config.h.in
sed 's/@VERSION@/$(VERSION)/g' $< > $@.tmp
cmp -s $@.tmp $@ || mv $@.tmp $@
rm -f $@.tmp# Use ccache for faster recompilation
CC := ccache gcc
CXX := ccache g++
# Or conditionally:
ifeq ($(shell command -v ccache 2>/dev/null),)
CC ?= gcc
else
CC ?= ccache gcc
endifBenefits:
# Distributed compilation across network
CC := distcc gcc
CXX := distcc g++
# Set number of jobs based on available hosts
DISTCC_HOSTS := localhost/2 build1/4 build2/4
JOBS := 10# Keep build artifacts between clean builds
.PHONY: clean distclean
clean:
$(RM) $(TARGET)
# Keep .o and .d files for faster rebuild
distclean: clean
$(RM) -r $(BUILDDIR)
# Complete clean# SLOW: Recursive expansion (evaluated every use)
SOURCES = $(wildcard src/*.c)
OBJECTS = $(SOURCES:.c=.o)
# $(OBJECTS) re-runs wildcard every time!
# FAST: Simple expansion (evaluated once)
SOURCES := $(wildcard src/*.c)
OBJECTS := $(SOURCES:.c=.o)
# Evaluated once when defined# SLOW: Multiple shell calls
FILES = $(shell ls *.c)
COUNT = $(shell ls *.c | wc -l)
# FAST: Single shell call
FILES := $(wildcard *.c)
COUNT := $(words $(FILES))OBJECTS := main.o utils.o helper.o
# FASTER: Static pattern rule (make knows exact files)
$(OBJECTS): %.o: %.c
$(CC) -c $< -o $@
# SLOWER: Pattern rule (make searches for matches)
%.o: %.c
$(CC) -c $< -o $@# SLOW: Complex shell commands in variable assignment
VERSION = $(shell git describe --tags --always --dirty)
# FAST: Use := to evaluate once
VERSION := $(shell git describe --tags --always --dirty)
# FASTER: Cache in file
VERSION := $(file < VERSION.txt)# Top-level Makefile
SUBDIRS := lib1 lib2 app
all:
for dir in $(SUBDIRS); do $(MAKE) -C $$dir; doneProblems:
# Single Makefile
LIB1_SRC := $(wildcard lib1/*.c)
LIB2_SRC := $(wildcard lib2/*.c)
APP_SRC := $(wildcard app/*.c)
ALL_SRC := $(LIB1_SRC) $(LIB2_SRC) $(APP_SRC)
OBJECTS := $(ALL_SRC:.c=.o)
# Single dependency tree
# Accurate parallel buildsReference: "Recursive Make Considered Harmful" by Peter Miller
# Time recipe execution
%.o: %.c
@echo "Compiling $<..."
@time $(CC) $(CFLAGS) -c $< -o $@# Time entire build
time make -j4
# Output:
# real 0m12.345s
# user 0m45.678s
# sys 0m3.456s# Show what make is doing
make -d
# Show only remake decisions
make -d --debug=basic
# Show implicit rule search
make -d --debug=implicit
# Profile make itself
make --profile=profile.log# Add timing to critical paths
$(TARGET): $(OBJECTS)
@echo "==> Linking $(TARGET)"
@time $(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
%.o: %.c
@echo "==> Compiling $<"
@time $(CC) $(CFLAGS) -c $< -o $@# Good: Independent compilation
OBJECTS := a.o b.o c.o d.o
program: $(OBJECTS)
$(CC) $^ -o $@
%.o: %.c
$(CC) -c $< -o $@
# make -j4 compiles 4 files at once# Use automatic dependency generation
CFLAGS += -MMD -MP
-include $(OBJECTS:.o=.d)
# Not manual maintenance# Keep intermediate files by default
clean:
$(RM) $(TARGET)
# Full clean only when needed
distclean: clean
$(RM) $(OBJECTS) $(DEPENDS)# Use := for computed values
SOURCES := $(wildcard src/*.c)
OBJECTS := $(SOURCES:.c=.o)
# Use ?= for user overrides
CC ?= gcc
CFLAGS ?= -O2# Don't rebuild if nothing changed
config.h: config.h.in Makefile
@sed 's/@VERSION@/$(VERSION)/g' $< > $@.tmp
@if ! cmp -s $@ $@.tmp; then \
echo " GEN $@"; \
mv $@.tmp $@; \
else \
rm -f $@.tmp; \
fi# Optimized Makefile for C project
.DELETE_ON_ERROR:
.SUFFIXES:
PROJECT := optimized
VERSION := 1.0.0
# User-overridable (use ?=)
CC ?= gcc
CFLAGS ?= -Wall -Wextra -O2
PREFIX ?= /usr/local
# Computed once (use :=)
SRCDIR := src
BUILDDIR := build
OBJDIR := $(BUILDDIR)/obj
SOURCES := $(wildcard $(SRCDIR)/*.c)
OBJECTS := $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o)
DEPENDS := $(OBJECTS:.o=.d)
TARGET := $(BUILDDIR)/$(PROJECT)
# Check for ccache
ifneq ($(shell command -v ccache 2>/dev/null),)
CC := ccache $(CC)
endif
# Optimization flags for dependencies
DEPFLAGS = -MMD -MP
.PHONY: all clean distclean profile
all: $(TARGET)
# Link (serial)
$(TARGET): $(OBJECTS)
@mkdir -p $(@D)
@echo " LD $@"
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
# Compile (parallel-safe)
$(OBJDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(@D)
@echo " CC $<"
$(CC) $(CPPFLAGS) $(CFLAGS) $(DEPFLAGS) -c $< -o $@
# Include auto-generated dependencies
-include $(DEPENDS)
# Minimal clean (keeps .o for faster rebuild)
clean:
$(RM) $(TARGET)
# Full clean
distclean:
$(RM) -r $(BUILDDIR)
# Profile build
profile:
time $(MAKE) clean
time $(MAKE) -j$(shell nproc) all| Configuration | Build Time | Rebuild Time |
|---|---|---|
| Sequential (make) | 45s | 12s |
| Parallel -j2 | 25s | 7s |
| Parallel -j4 | 15s | 4s |
| Parallel -j8 | 12s | 3s |
| Parallel + ccache (cold) | 14s | 3s |
| Parallel + ccache (warm) | 3s | 1s |
Key takeaways:
# Generate precompiled header
$(OBJDIR)/common.h.gch: $(SRCDIR)/common.h
@mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -x c-header $< -o $@
# Use precompiled header
$(OBJDIR)/%.o: $(SRCDIR)/%.c $(OBJDIR)/common.h.gch
$(CC) $(CPPFLAGS) $(CFLAGS) -include $(OBJDIR)/common.h -c $< -o $@# Enable LTO for release builds
release: CFLAGS += -flto -O3
release: LDFLAGS += -flto -O3
release: $(TARGET)# Combine all sources into one compilation unit
unity.c: $(SOURCES)
@echo "Generating unity build..."
@for src in $(SOURCES); do \
echo "#include \"$$src\"" >> $@; \
done
unity.o: unity.c
$(CC) $(CFLAGS) -c $< -o $@
# Fast single-file compilation
# Trade-off: No parallel compilation# Make's built-in profiling
make --profile=profile.log
# Analyze profile.log
# Time individual targets
make -d 2>&1 | grep -E "Considering|Must remake"
# strace for system call analysis
strace -c make 2>&1 | tail -20
# Remake (make debugger)
remake --debug