Makefile Development Instructions

November 15, 2025 ยท View on GitHub

Instructions for writing clean, maintainable, and portable GNU Make Makefiles. These instructions are based on the GNU Make manual.

General Principles

  • Write clear and maintainable makefiles that follow GNU Make conventions
  • Use descriptive target names that clearly indicate their purpose
  • Keep the default goal (first target) as the most common build operation
  • Prioritize readability over brevity when writing rules and recipes
  • Add comments to explain complex rules, variables, or non-obvious behavior

Naming Conventions

  • Name your makefile Makefile (recommended for visibility) or makefile
  • Use GNUmakefile only for GNU Make-specific features incompatible with other make implementations
  • Use standard variable names: objects, OBJECTS, objs, OBJS, obj, or OBJ for object file lists
  • Use uppercase for built-in variable names (e.g., CC, CFLAGS, LDFLAGS)
  • Use descriptive target names that reflect their action (e.g., clean, install, test)

File Structure

  • Place the default goal (primary build target) as the first rule in the makefile
  • Group related targets together logically
  • Define variables at the top of the makefile before rules
  • Use .PHONY to declare targets that don't represent files
  • Structure makefiles with: variables, then rules, then phony targets
# Variables
CC = gcc
CFLAGS = -Wall -g
objects = main.o utils.o

# Default goal
all: program

# Rules
program: $(objects)
	$(CC) -o program $(objects)

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# Phony targets
.PHONY: clean all
clean:
	rm -f program $(objects)

Variables and Substitution

  • Use variables to avoid duplication and improve maintainability
  • Define variables with := (simple expansion) for immediate evaluation, = for recursive expansion
  • Use ?= to set default values that can be overridden
  • Use += to append to existing variables
  • Reference variables with $(VARIABLE) not $VARIABLE (unless single character)
  • Use automatic variables ($@, $<, $^, $?, $*) in recipes to make rules more generic
# Simple expansion (evaluates immediately)
CC := gcc

# Recursive expansion (evaluates when used)
CFLAGS = -Wall $(EXTRA_FLAGS)

# Conditional assignment
PREFIX ?= /usr/local

# Append to variable
CFLAGS += -g

Rules and Prerequisites

  • Separate targets, prerequisites, and recipes clearly
  • Use implicit rules for standard compilations (e.g., .c to .o)
  • List prerequisites in logical order (normal prerequisites before order-only)
  • Use order-only prerequisites (after |) for directories and dependencies that shouldn't trigger rebuilds
  • Include all actual dependencies to ensure correct rebuilds
  • Avoid circular dependencies between targets
  • Remember that order-only prerequisites are omitted from automatic variables like $^, so reference them explicitly if needed

The example below shows a pattern rule that compiles objects into an obj/ directory. The directory itself is listed as an order-only prerequisite so it is created before compiling but does not force recompilation when its timestamp changes.

# Normal prerequisites
program: main.o utils.o
	$(CC) -o $@ $^

# Order-only prerequisites (directory creation)
obj/%.o: %.c | obj
	$(CC) $(CFLAGS) -c $< -o $@

obj:
	mkdir -p obj

Recipes and Commands

  • Start every recipe line with a tab character (not spaces) unless .RECIPEPREFIX is changed
  • Use @ prefix to suppress command echoing when appropriate
  • Use - prefix to ignore errors for specific commands (use sparingly)
  • Combine related commands with && or ; on the same line when they must execute together
  • Keep recipes readable; break long commands across multiple lines with backslash continuation
  • Use shell conditionals and loops within recipes when needed
# Silent command
clean:
	@echo "Cleaning up..."
	@rm -f $(objects)

# Ignore errors
.PHONY: clean-all
clean-all:
	-rm -rf build/
	-rm -rf dist/

# Multi-line recipe with proper continuation
install: program
	install -d $(PREFIX)/bin && \
		install -m 755 program $(PREFIX)/bin

Phony Targets

  • Always declare phony targets with .PHONY to avoid conflicts with files of the same name
  • Use phony targets for actions like clean, install, test, all
  • Place phony target declarations near their rule definitions or at the end of the makefile
.PHONY: all clean test install

all: program

clean:
	rm -f program $(objects)

test: program
	./run-tests.sh

install: program
	install -m 755 program $(PREFIX)/bin

Pattern Rules and Implicit Rules

  • Use pattern rules (%.o: %.c) for generic transformations
  • Leverage built-in implicit rules when appropriate (GNU Make knows how to compile .c to .o)
  • Override implicit rule variables (like CC, CFLAGS) rather than rewriting the rules
  • Define custom pattern rules only when built-in rules are insufficient
# Use built-in implicit rules by setting variables
CC = gcc
CFLAGS = -Wall -O2

# Custom pattern rule for special cases
%.pdf: %.md
	pandoc $< -o $@

Splitting Long Lines

  • Use backslash-newline (\) to split long lines for readability
  • Be aware that backslash-newline is converted to a single space in non-recipe contexts
  • In recipes, backslash-newline preserves the line continuation for the shell
  • Avoid trailing whitespace after backslashes

Splitting Without Adding Whitespace

If you need to split a line without adding whitespace, you can use a special technique: insert $ (dollar-space) followed by a backslash-newline. The $ refers to a variable with a single-space name, which doesn't exist and expands to nothing, effectively joining the lines without inserting a space.

# Concatenate strings without adding whitespace
# The following creates the value "oneword"
var := one$ \
       word

# This is equivalent to:
# var := oneword
# Variable definition split across lines
sources = main.c \
          utils.c \
          parser.c \
          handler.c

# Recipe with long command
build: $(objects)
	$(CC) -o program $(objects) \
	      $(LDFLAGS) \
	      -lm -lpthread

Including Other Makefiles

  • Use include directive to share common definitions across makefiles
  • Use -include (or sinclude) to include optional makefiles without errors
  • Place include directives after variable definitions that may affect included files
  • Use include for shared variables, pattern rules, or common targets
# Include common settings
include config.mk

# Include optional local configuration
-include local.mk

Conditional Directives

  • Use conditional directives (ifeq, ifneq, ifdef, ifndef) for platform or configuration-specific rules
  • Place conditionals at the makefile level, not within recipes (use shell conditionals in recipes)
  • Keep conditionals simple and well-documented
# Platform-specific settings
ifeq ($(OS),Windows_NT)
    EXE_EXT = .exe
else
    EXE_EXT =
endif

program: main.o
	$(CC) -o program$(EXE_EXT) main.o

Automatic Prerequisites

  • Generate header dependencies automatically rather than maintaining them manually
  • Use compiler flags like -MMD and -MP to generate .d files with dependencies
  • Include generated dependency files with -include $(deps) to avoid errors if they don't exist
objects = main.o utils.o
deps = $(objects:.o=.d)

# Include dependency files
-include $(deps)

# Compile with automatic dependency generation
%.o: %.c
	$(CC) $(CFLAGS) -MMD -MP -c $< -o $@

Error Handling and Debugging

  • Use $(error text) or $(warning text) functions for build-time diagnostics
  • Test makefiles with make -n (dry run) to see commands without executing
  • Use make -p to print the database of rules and variables for debugging
  • Validate required variables and tools at the beginning of the makefile
# Check for required tools
ifeq ($(shell which gcc),)
    $(error "gcc is not installed or not in PATH")
endif

# Validate required variables
ifndef VERSION
    $(error VERSION is not defined)
endif

Clean Targets

  • Always provide a clean target to remove generated files
  • Declare clean as phony to avoid conflicts with a file named "clean"
  • Use - prefix with rm commands to ignore errors if files don't exist
  • Consider separate clean (removes objects) and distclean (removes all generated files) targets
.PHONY: clean distclean

clean:
	-rm -f $(objects)
	-rm -f $(deps)

distclean: clean
	-rm -f program config.mk

Portability Considerations

  • Avoid GNU Make-specific features if portability to other make implementations is required
  • Use standard shell commands (prefer POSIX shell constructs)
  • Test with make -B to force rebuild all targets
  • Document any platform-specific requirements or GNU Make extensions used

Performance Optimization

  • Use := for variables that don't need recursive expansion (faster)
  • Avoid unnecessary use of $(shell ...) which creates subprocesses
  • Order prerequisites efficiently (most frequently changing files last)
  • Use parallel builds (make -j) safely by ensuring targets don't conflict

Documentation and Comments

  • Add a header comment explaining the makefile's purpose
  • Document non-obvious variable settings and their effects
  • Include usage examples or targets in comments
  • Add inline comments for complex rules or platform-specific workarounds
# Makefile for building the example application
#
# Usage:
#   make          - Build the program
#   make clean    - Remove generated files
#   make install  - Install to $(PREFIX)
#
# Variables:
#   CC       - C compiler (default: gcc)
#   PREFIX   - Installation prefix (default: /usr/local)

# Compiler and flags
CC ?= gcc
CFLAGS = -Wall -Wextra -O2

# Installation directory
PREFIX ?= /usr/local

Special Targets

  • Use .PHONY for non-file targets
  • Use .PRECIOUS to preserve intermediate files
  • Use .INTERMEDIATE to mark files as intermediate (automatically deleted)
  • Use .SECONDARY to prevent deletion of intermediate files
  • Use .DELETE_ON_ERROR to remove targets if recipe fails
  • Use .SILENT to suppress echoing for all recipes (use sparingly)
# Don't delete intermediate files
.SECONDARY:

# Delete targets if recipe fails
.DELETE_ON_ERROR:

# Preserve specific files
.PRECIOUS: %.o

Common Patterns

Standard Project Structure

CC = gcc
CFLAGS = -Wall -O2
objects = main.o utils.o parser.o

.PHONY: all clean install

all: program

program: $(objects)
	$(CC) -o $@ $^

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

clean:
	-rm -f program $(objects)

install: program
	install -d $(PREFIX)/bin
	install -m 755 program $(PREFIX)/bin

Managing Multiple Programs

programs = prog1 prog2 prog3

.PHONY: all clean

all: $(programs)

prog1: prog1.o common.o
	$(CC) -o $@ $^

prog2: prog2.o common.o
	$(CC) -o $@ $^

prog3: prog3.o
	$(CC) -o $@ $^

clean:
	-rm -f $(programs) *.o

Anti-Patterns to Avoid

  • Don't start recipe lines with spaces instead of tabs
  • Avoid hardcoding file lists when they can be generated with wildcards or functions
  • Don't use $(shell ls ...) to get file lists (use $(wildcard ...) instead)
  • Avoid complex shell scripts in recipes (move to separate script files)
  • Don't forget to declare phony targets as .PHONY
  • Avoid circular dependencies between targets
  • Don't use recursive make ($(MAKE) -C subdir) unless absolutely necessary