Makefile Targets Guide
Overview
This guide covers target definitions, standard GNU targets, .PHONY declarations, dependencies, pattern rules, and best practices for organizing targets in Makefiles.
Target Basics
Target Syntax
target: prerequisites
recipe
recipe
...- target: File to create or action to perform
- prerequisites: Files/targets that must exist or be up-to-date
- recipe: Shell commands to run (must be indented with TAB)
Simple Example
hello: hello.c
gcc hello.c -o helloHow it works:
- Check if
helloexists and is newer thanhello.c - If not, run the recipe
- Recipe creates
hello
.PHONY Targets
What are .PHONY Targets?
Phony targets don't represent actual files. They represent actions:
.PHONY: clean
clean:
rm -f *.o myprogramWithout .PHONY:
- If a file named "clean" exists,
make cleanwon't run - make thinks the target is already up-to-date
With .PHONY:
- make always runs the recipe
- Performance improvement (skips unnecessary file system checks)
Common .PHONY Targets
.PHONY: all clean install uninstall test check help
.PHONY: build dist distclean format lint docsMultiple .PHONY Declarations
# Option 1: All at once (recommended)
.PHONY: all clean install test help
# Option 2: Separate declarations
.PHONY: all
.PHONY: clean
.PHONY: installStandard GNU Targets
GNU Coding Standards define standard targets that users expect:
Essential Targets
all (Default Target)
## Build all targets
.PHONY: all
all: $(TARGET)Requirements:
- Should be the first target (default)
- Should compile the entire program
- Should NOT install, clean, or run tests
- Should create the primary output (executable, library, etc.)
install
## Install built files to PREFIX
.PHONY: install
install: all
$(INSTALL) -d $(DESTDIR)$(BINDIR)
$(INSTALL_PROGRAM) $(TARGET) $(DESTDIR)$(BINDIR)/
$(INSTALL) -d $(DESTDIR)$(LIBDIR)
$(INSTALL_DATA) lib$(PROJECT).a $(DESTDIR)$(LIBDIR)/
$(INSTALL) -d $(DESTDIR)$(INCLUDEDIR)
$(INSTALL_DATA) $(PROJECT).h $(DESTDIR)$(INCLUDEDIR)/
$(INSTALL) -d $(DESTDIR)$(MAN1DIR)
$(INSTALL_DATA) docs/$(PROJECT).1 $(DESTDIR)$(MAN1DIR)/Requirements:
- Should depend on
allto build first - Should respect
DESTDIRandPREFIX - Should create necessary directories
- Should set appropriate permissions
- Should be idempotent (safe to run multiple times)
uninstall
## Remove installed files
.PHONY: uninstall
uninstall:
$(RM) $(DESTDIR)$(BINDIR)/$(TARGET)
$(RM) $(DESTDIR)$(LIBDIR)/lib$(PROJECT).a
$(RM) $(DESTDIR)$(INCLUDEDIR)/$(PROJECT).h
$(RM) $(DESTDIR)$(MAN1DIR)/$(PROJECT).1clean
## Remove built files (keep configuration)
.PHONY: clean
clean:
$(RM) $(OBJECTS) $(TARGET)
$(RM) -r $(BUILDDIR)
$(RM) *.o *.a *.soRequirements:
- Remove object files, executables, libraries
- Keep configuration files and Makefile
- Should allow rebuilding without reconfiguring
distclean
## Remove all generated files (including configuration)
.PHONY: distclean
distclean: clean
$(RM) config.h config.log config.status
$(RM) Makefile
$(RM) -r autom4te.cache/Requirements:
- Should depend on
clean - Remove configure-generated files
- Leave only source files
- After distclean, only
./configureshould work
Testing Targets
test
## Run tests
.PHONY: test
test: $(TARGET)
@echo "Running tests..."
./run_tests.sh
$(TARGET) --self-test
@echo "All tests passed!"check
## Alias for test (GNU convention)
.PHONY: check
check: testDistribution Targets
dist
## Create distribution tarball
.PHONY: dist
dist:
@mkdir -p dist
tar -czf dist/$(PROJECT)-$(VERSION).tar.gz \
--transform 's,^,$(PROJECT)-$(VERSION)/,' \
--exclude='.git*' \
--exclude='*.o' \
--exclude='*.a' \
--exclude='$(BUILDDIR)' \
.distcheck
## Create and verify distribution tarball
.PHONY: distcheck
distcheck: dist
@echo "Verifying distribution..."
@mkdir -p $(BUILDDIR)/distcheck
tar -xzf dist/$(PROJECT)-$(VERSION).tar.gz -C $(BUILDDIR)/distcheck
cd $(BUILDDIR)/distcheck/$(PROJECT)-$(VERSION) && ./configure && make && make check
@echo "Distribution verified successfully!"
$(RM) -r $(BUILDDIR)/distcheckDocumentation Targets
help
## Show available targets and usage
.PHONY: help
help:
@echo "$(PROJECT) - version $(VERSION)"
@echo ""
@echo "Available targets:"
@sed -n 's/^## //p' $(MAKEFILE_LIST) | column -t -s ':' | sed 's/^/ /'
@echo ""
@echo "Variables:"
@echo " PREFIX=$(PREFIX)"
@echo " CC=$(CC)"
@echo " CFLAGS=$(CFLAGS)"
@echo ""
@echo "Examples:"
@echo " make # Build the project"
@echo " make install # Install to PREFIX"
@echo " make PREFIX=/opt/local # Install to custom location"
@echo " make clean # Remove built files"Self-documenting pattern:
## Build the application
.PHONY: build
build: $(TARGET)
## Run all tests
.PHONY: test
test:
./run_tests.shThe ## comments are parsed by the help target.
Target Dependencies
Simple Dependencies
# program depends on main.o and utils.o
program: main.o utils.o
$(CC) $^ -o $@
# main.o depends on main.c
main.o: main.c
$(CC) -c $< -o $@Multiple Targets
# Multiple targets with same recipe
main.o utils.o helper.o: common.h
$(CC) -c $*.c -o $@Order-Only Prerequisites
# Normal prerequisites: update target if prerequisite changes
# Order-only prerequisites (|): only check existence, not timestamp
$(OBJDIR)/%.o: %.c | $(OBJDIR)
$(CC) -c $< -o $@
# $(OBJDIR) must exist, but changes to it don't trigger rebuild
$(OBJDIR):
mkdir -p $@Use cases:
- Directory creation (directory timestamp changes don't matter)
- Lock files
- State files
Circular Dependencies
# WRONG: Circular dependency
a: b
b: a
# Error: Circular a <- b dependency dropped
# RIGHT: Break the cycle
a: c
b: c
c:
touch cPattern Rules
Basic Pattern Rules
# Compile .c to .o
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Compile .c to .s (assembly)
%.s: %.c
$(CC) $(CFLAGS) -S $< -o $@
# Create .c from .y (yacc)
%.c: %.y
$(YACC) $(YFLAGS) -o $@ $<Pattern Rules with Directories
# Match files in specific directories
$(OBJDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(@D)
$(CC) $(CFLAGS) -c $< -o $@
# Multiple directory patterns
build/obj/%.o: src/%.c
$(CC) -c $< -o $@
build/obj/%.o: lib/%.c
$(CC) -c $< -o $@Static Pattern Rules
More efficient than pattern rules for specific files:
OBJECTS := main.o utils.o helper.o
# Static pattern: $(targets): target-pattern: prereq-pattern
$(OBJECTS): %.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Equivalent to:
# main.o: main.c
# utils.o: utils.c
# helper.o: helper.cAdvantages:
- More explicit than pattern rules
- Faster (make knows exact targets)
- Easier to debug
Multiple Pattern Rules
# First matching rule is used
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
%.o: %.s
$(AS) $(ASFLAGS) -c $< -o $@
%.o: %.asm
$(NASM) $(NASMFLAGS) -f elf64 $< -o $@Target-Specific Variables
# Set variable only for specific target and its prerequisites
debug: CFLAGS += -g -O0 -DDEBUG
debug: $(TARGET)
release: CFLAGS += -O3 -DNDEBUG
release: $(TARGET)
# Pattern-specific variables
test_%: CFLAGS += -DTESTING
test_%: LDLIBS += -lcheckAdvanced Target Patterns
Double-Colon Rules
# Double-colon allows multiple recipes for same target
install::
@echo "Installing binaries..."
$(INSTALL) $(TARGET) $(BINDIR)/
install::
@echo "Installing libraries..."
$(INSTALL) lib$(PROJECT).a $(LIBDIR)/
# Each recipe runs independentlyUse cases:
- Modular Makefiles with independent install steps
- Plugin systems
- Rarely needed
Intermediate Files
# Mark files as intermediate (deleted after use)
.INTERMEDIATE: $(OBJECTS)
# Keep specific intermediate files
.SECONDARY: important.o
# Never delete these intermediate files
.PRECIOUS: %.oAutomatic Prerequisites
# Generate dependencies automatically
DEPENDS := $(OBJECTS:.o=.d)
$(OBJDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
# Include generated dependencies
-include $(DEPENDS)Complete Examples
Example 1: Simple C Project
.DELETE_ON_ERROR:
CC ?= gcc
CFLAGS ?= -Wall -Wextra -O2
PREFIX ?= /usr/local
TARGET := hello
SOURCES := hello.c
OBJECTS := $(SOURCES:.c=.o)
.PHONY: all clean install uninstall help
## Build the program (default)
all: $(TARGET)
## Compile and link
$(TARGET): $(OBJECTS)
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
%.o: %.c
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
## Install to PREFIX
install: $(TARGET)
install -d $(DESTDIR)$(PREFIX)/bin
install -m 755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/
## Remove installed files
uninstall:
$(RM) $(DESTDIR)$(PREFIX)/bin/$(TARGET)
## Remove built files
clean:
$(RM) $(OBJECTS) $(TARGET)
## Show this help
help:
@echo "Available targets:"
@sed -n 's/^## //p' $(MAKEFILE_LIST)Example 2: Multi-Directory Project
.DELETE_ON_ERROR:
PROJECT := myapp
CC ?= gcc
CFLAGS ?= -Wall -Wextra -O2
PREFIX ?= /usr/local
SRCDIR := src
BUILDDIR := build
OBJDIR := $(BUILDDIR)/obj
SOURCES := $(wildcard $(SRCDIR)/*.c)
OBJECTS := $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o)
DEPENDS := $(OBJECTS:.o=.d)
TARGET := $(BUILDDIR)/$(PROJECT)
.PHONY: all clean install test help
## Build everything (default)
all: $(TARGET)
## Link executable
$(TARGET): $(OBJECTS)
@mkdir -p $(@D)
@echo " LD $@"
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
## Compile source files
$(OBJDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(@D)
@echo " CC $<"
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
-include $(DEPENDS)
## Install to PREFIX
install: $(TARGET)
install -d $(DESTDIR)$(PREFIX)/bin
install -m 755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/$(PROJECT)
## Run tests
test: $(TARGET)
@echo "Running tests..."
@$(TARGET) --test
## Remove build artifacts
clean:
$(RM) -r $(BUILDDIR)
## Show available targets
help:
@sed -n 's/^## //p' $(MAKEFILE_LIST)Example 3: Library Project
.DELETE_ON_ERROR:
PROJECT := mylib
VERSION := 1.0.0
CC ?= gcc
AR ?= ar
RANLIB ?= ranlib
PREFIX ?= /usr/local
SRCDIR := src
BUILDDIR := build
OBJDIR := $(BUILDDIR)/obj
SOURCES := $(wildcard $(SRCDIR)/*.c)
OBJECTS := $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o)
HEADERS := $(wildcard $(SRCDIR)/*.h)
STATIC_LIB := $(BUILDDIR)/lib$(PROJECT).a
SHARED_LIB := $(BUILDDIR)/lib$(PROJECT).so.$(VERSION)
.PHONY: all static shared clean install install-static install-shared
## Build both static and shared libraries
all: static shared
## Build static library
static: $(STATIC_LIB)
## Build shared library
shared: $(SHARED_LIB)
## Create static library
$(STATIC_LIB): $(OBJECTS)
@mkdir -p $(@D)
@echo " AR $@"
$(AR) rcs $@ $^
$(RANLIB) $@
## Create shared library
$(SHARED_LIB): $(OBJECTS)
@mkdir -p $(@D)
@echo " LD $@"
$(CC) -shared -Wl,-soname,lib$(PROJECT).so.1 $^ -o $@
ln -sf lib$(PROJECT).so.$(VERSION) $(BUILDDIR)/lib$(PROJECT).so.1
ln -sf lib$(PROJECT).so.1 $(BUILDDIR)/lib$(PROJECT).so
## Compile with -fPIC for shared library
$(OBJDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(@D)
@echo " CC $<"
$(CC) $(CPPFLAGS) $(CFLAGS) -fPIC -c $< -o $@
## Install both libraries
install: install-static install-shared
## Install static library
install-static: $(STATIC_LIB)
install -d $(DESTDIR)$(PREFIX)/lib
install -m 644 $(STATIC_LIB) $(DESTDIR)$(PREFIX)/lib/
install -d $(DESTDIR)$(PREFIX)/include/$(PROJECT)
install -m 644 $(HEADERS) $(DESTDIR)$(PREFIX)/include/$(PROJECT)/
## Install shared library
install-shared: $(SHARED_LIB)
install -d $(DESTDIR)$(PREFIX)/lib
install -m 755 $(SHARED_LIB) $(DESTDIR)$(PREFIX)/lib/
ln -sf lib$(PROJECT).so.$(VERSION) $(DESTDIR)$(PREFIX)/lib/lib$(PROJECT).so.1
ln -sf lib$(PROJECT).so.1 $(DESTDIR)$(PREFIX)/lib/lib$(PROJECT).so
ldconfig
## Clean build artifacts
clean:
$(RM) -r $(BUILDDIR)Best Practices
- Use .PHONY for non-file targets
- Implement standard GNU targets (all, install, clean, test)
- Make 'all' the default target (first target)
- Use automatic variables ($@, $<, $^) for concise rules
- Create directories automatically (@mkdir -p $(@D))
- Document targets with ## comments
- Use static pattern rules for better performance
- Include .DELETE_ON_ERROR to prevent corrupted builds
- Use order-only prerequisites for directory creation
- Generate dependencies automatically (-MMD -MP)