đź”” Prelude: I often noticed Makefile files in the root directory when browsing open-source projects on GitHub. For a long time, I thought it was some kind of sophisticated “magic tool”. So during my spare time at my internship, I decided to learn some basic syntax… only to discover that it’s actually like a “recipe” for compilation - to get a target file (the dish), you need some dependencies (ingredients) and commands (cooking instructions). Anyway, at least now I can understand it (mostly).

Make

Basic Rules

target: dependencies ..
[tab][Command]

TARGET = main
main: hello.o world.o   # will be run by default as the first target
    gcc $^ -o $(TARGET) # treat these as normal bash commands

hello.o: hello.c
    gcc -c hello.c      # -c for compile only

world.o: world.c
    gcc -c world.c

clean:                  # will not be run unless "make clean"
    rm -f $(wildcard *.o) $(TARGET)

.PHONY: clean

Common CLI Parameters

-v
-n --just-print        # Print commands without executing
-p                     # Output makefile data (lots of info)
-t --touch             # Update file timestamps without changing targets (dry run)
-q --question          # Check if target exists. Do nothing if yes, print error if no
                       # Usually used with -qp
-W --what-if <file>    # Pretend this file was just modified, trigger related rules
-B                     # Rebuild everything
-b -m                  # Used to ignore compatibility between different make versions
-debug=[option]        # Debug mode
                       # option can be: a(all), b(basic), v(verbose)
                       # i(implicit), j(jobs), m(makefile)
                       # -d = -debug=a
-e var=sth             # Override environment variable values
-h                     # Help me!
-f <file>              # Execute specific makefile
-i                     # Ignore errors
-I --include-dir <dir> # Specify make search directory
-k --keep-going        # Continue on errors, except missing dependencies
-s --silent
-j --jobs              # Parallel compilation when dependencies allow
                       # make will try to parallelize as much as possible by default

Special Syntax

$<                  # First item in dependencies
                    # Note: if target is matched via %,
                    # $< represents all matched items
$^                  # All dependencies
$?                  # All dependencies that are newer than target
$+                  # All dependencies without deduplication
$*                  # The part matched by %
$@                  # The target
$%                  # Target member name (only for archive files)
$$                  # Escape $ character
$(wildcard *.c)     # Use wildcards
$(source:.c=.o)     # Change suffix of variable

:=                  # Immediate expansion of $() expressions (no variable hoisting)
?=                  # Assign only if not already defined
+=                  # Append to variable value (with leading space)

%                   # Wildcard in pattern rules

@[command]          # Execute command silently
-[command]          # Ignore errors, continue running
[command];[command] # Similar to pipeline ("|")
                    # If written on separate lines, executed separately

override var := new-value

# $ with uppercase D(Directory) and F(File)
# Further specify the scope of dependency strings

dir/foo.o: ...

# Need parentheses to avoid ambiguity
$(@D)               # dir/foo.o -> dir
$(@F)               # dir/foo.o -> foo.o

Pattern Rules

%.o: %.c    # % means "match", represents "any string"
            # Meaning: to build .o file, its dependency is corresponding .c file
            # No directory search performed

# Study these examples for better understanding

src/eat
e%t
% -> src/a

# then, % is passed to another place...

c%r -> csrc/ar

# but if there is a nested % pattern on the original side...

c%r -> src/car  # % should be "src/a"
                # but "src/" will be removed before matching patterns
                # so in this situation, % is "a"
                # this rule also applies to the first step

Functions

# Basic syntax
$(<function> <arguments>)

# Can also be written as
${<function> <arguments>}   # Perhaps more readable

$(patsubst %.c,%.o,$(source))  # Better demonstrates % syntax behavior
                               # patsubst = pattern substitute
                               # For any .c file, replace with corresponding .o file
                               # Scope is $(source), which should be text

# Common functions

subst <from>,<to>,<text>    # Substitute text
strip <text>                # Remove spaces
findstring <find>,<in>      # Find substring, return <find> if found
filter <pattern...>,<text>  # Can pass multiple patterns
filter-out <pattern...>,<text> # Reverse filter, keeps unmatched items
sort <list>                 # Default ascending order
word <n>,<text>             # Get nth word (a.c b.c c.c -> b.c)
wordlist <s>,<e>,<text>     # Slice
words <text>                # Count words
firstword <text>

dir <names...>              # Extract directory from filename
notdir <names...>           # Remove directory part, keep filename only
suffix <names...>           # Extract suffix
basename <names...>         # Extract name without suffix (prefix)
addsuffix <suffix>,<names...> # Add suffix
addprefix <prefix>,<names...>
join <list1>,<list2>

origin <variable>           # Where did this variable come from?

shell <command>             # Execute command in shell

error <text ...>            # Generate error message, but doesn't abort
warning <text ...>          # Generate warning message

# Iteration
foreach <var>,<list>,<text>

names := a b c d
files := $(foreach n,$(names),$(n).o)  # a.o b.o c.o d.o

Custom Functions with call

call <expression>,<parm1>,<parm2>,...,<parmn>

# Examples are easier to understand
reverse = $(2) $(1)
foo = $(call reverse,a,b)  # $(foo) -> b a

Special Variables

VPATH                            # Set default search path
# Syntax: vpath <pattern> <directories>
# Example: vpath %.h src:../headers:lib  # Directories separated by colon (:)

# Can actively set paths
# Advantage: if file not found, will search in VPATH
VPATH = src
BUILD_DIR = build

$(BUILD_DIR)/%.o: %.c
    gcc -c $< -o $@

Sub-Makefiles

subsystem:
    cd subdir && $(MAKE)

# Equivalent to

subsystem:
    $(MAKE) -C subdir

# Execute make in subsystem

export VAR = value     # Pass VAR to subsystem
unexport VAR_PRIVATE   # Don't pass this variable

# $(SHELL) and $(MAKEFLAGS) are always passed to subsystems

Define Directive

define reusable
dosth..
dosth..
endef

$(reusable)

Conditionals

# Similar to bash syntax
ifeq (var1, var2)
    [command]
else
    [command]
endif

# Available keywords
ifeq ifneq ifdef ifndef

# if as a function
$(if <condition>,<then-part>)
$(if <condition>,<then-part>,<else-part>)

Implicit Rules

# Implicit rules use these predefined variables:

CC = gcc        # C compiler
CXX = g++       # C++ compiler
CFLAGS =        # C compile options
CXXFLAGS =      # C++ compile options
LDFLAGS =       # Linker options

# Only need to define dependencies, compile rules apply automatically
CFLAGS = -Wall -O2

main: main.o utils.o  # Implicit rules automatically compile .c → .o
    $(CC) $^ -o $@    # Linking rules need to be written manually
                      # (implicit rules only handle single file compilation)

# View all implicit rules
make -p | grep "^%"  # Display all pattern rules

Include Directive

include file1.mk file2.mk
-include optional.mk  # Don't error if file doesn't exist

Special Targets

.PHONY: clean test install      # Declare phony targets
.PRECIOUS: %.o                  # Preserve intermediate files
.SECONDARY:                     # All intermediate files are secondary
.DELETE_ON_ERROR:               # Delete target file if command errors

Tips and Tricks

all: $(object)                      # make finds dependencies then executes commands
                                    # doesn't have to compile to 'all'
$(object) : ...
    ...

gcc -MM hello.c                     # Find header dependencies for hello.c

dev: mymodule.so
    @echo "Development build complete"

test: dev                           # Set prerequisite tasks for multi-language development
    $(PYTHON) -c "import mymodule; print(mymodule.hello())"