Table of Contents

Makefile Advanced

Recursive Make handles multi-directory projects. Each subdirectory has its own Makefile; the top-level Makefile calls them.

# Top-level Makefile
SUBDIRS = src lib test
 
.PHONY: all clean $(SUBDIRS)
 
all: $(SUBDIRS)
 
$(SUBDIRS):
	$(MAKE) -C $@
 
clean:
	for dir in $(SUBDIRS); do \
		$(MAKE) -C $$dir clean; \
	done

$(MAKE) -C $@ runs Make in the subdirectory. Each subdirectory builds independently.

Include files:

include common.mk
include config.mk

Include other Makefiles to share rules and variables. Useful for project-wide settings.

Defining and calling functions:

define build_obj
	gcc $(CFLAGS) -c -o $(1) $(2)
endef
 
main.o: main.c
	$(call build_obj, main.o, main.c)

define ... endef defines a function. $(call ...) invokes it with arguments. Powerful for reusable recipes.

Variable scope: Variables are global. Use functions for local scope:

define compile_files
    CC = gcc
    $(COMPILE_OBJS)
endef

Variables inside functions are still global (no true local scope in Make).

Order-only prerequisites: Some files should be prerequisites for creation, not modification:

build/%.o: src/%.c | build
	gcc -c $< -o $@
 
build:
	mkdir -p build

The | separates normal prerequisites from order-only. Changes to build/ don't trigger rebuilds of .o files—only its existence matters.

Secondary expansion:

.SECONDEXPANSION:
 
app: $$^
	gcc -o app $^

.SECONDEXPANSION allows two-pass variable expansion. Rare; use for advanced metaprogramming.

Reading from files:

VERSION = $(file < VERSION.txt)
 
app: main.o
	gcc -o app main.o -DVERSION=\"$(VERSION)\"

$(file < filename) reads the file's contents.

Writing to files:

$(file > build.log, Building...)
$(file >> build.log, Done)

$(file > ...) writes, $(file >> ...) appends.

Debugging Makefiles: Use @echo to print variables:

debug:
	@echo "SRCS: $(SRCS)"
	@echo "OBJS: $(OBJS)"
	@echo "CFLAGS: $(CFLAGS)"

See Makefile Debugging for more techniques.