⏎ back home

My GNU Make Study Guide

Published: 11/24

Article Contents:

I have a love-hate relationship with make.

On one hand, it is one of my favorite tools. It is installed almost everywhere. For simple projects it is easy to setup, and when configured well, it is easy to run and extend. On the other hand, writing makefiles for anything other than trivial builds can be tough. The language and syntax of makefiles has many sharp edges buried in the details. Every time I want to write a non-trivial makefile, I have to reacquaint myself with a grab bag of make specific stuff to get going again.

I created this study guide in an attempt to really solidify my understanding of the core features you can use in makefiles. To make this study guide, I did two things:

  1. I read a few papers that generally describe build systems. I then tried to describe make framed in the terms of those papers. That is what is covered in the Make as a Build System section.
  2. I read through almost every section of the official make documentation and tried to note down the features of the tool and the language that'd I'd come across or used before. I then tried to explain each of those features in my own words in a section of this document.

Disclaimer: This post is more of a collection of notes than a cohesive piece of writing. I have not taken the time to massage this into writing that flows, and frankly, this is neither the topic nor the post that I want to make that investment in. I am publishing this in spite of its rough condition because it might still have some value for others and because not everything needs to be perfect 💎

Make as a Build System

🏭 Make is a Build Automation System (a.k.a. a Build System).

🕰️ Build Systems use Dependency Graphs to Build Your Project

dependency graph example

🧑‍⚖️ Dependency Graphs are Encoded in Rules of Makefiles

target: prerequisite1 prerequisite2 ...
  recipe-line-1
  recipe-line-2
  ...

Simple Make Syntax

📜 Simple Rules

target: prerequisite1 prerequisite2 ...
  recipe-line-1
  recipe-line-2
  ...
main: main.o helper.o
  ld -o main main.o helper.o

main.o: main.c helper.h
  gcc -c -o main.o main.c

helper.o: helper.c
  gcc -c -o helper.o helper.c
# Print out the directory contents before running make
> tree ./
./
├── Makefile
├── helper.c
├── helper.h
└── main.c

# Build main target
> make main
gcc -c -o main.o main.c
gcc -c -o helper.o helper.c
ld -o main main.o helper.o

# Try to build the main target again, but nothing happens
# because everything is up to date
> make main
make: `main' is up to date.

# Print the directory content again so we can see the files
# make generated.
> tree ./
./
├── Makefile
├── helper.c
├── helper.h
├── helper.o
├── main
├── main.c
└── main.o

✏️ Simple Variable Assignment

variable_name=variable_value

# Whitespace before and after '=' is ignored, so this is equivalent
variable_name = variable_value

# As is this
variable_name        =        variable_value
# `name` gets the string value "beau carlborg"
name = beau carlborg

# `location` gets the value "oakland" (leading whitespace is ignored)
location =            oakland

# `occupation` gets the string value "software    engineer" (internal whitespace preserved)
occupation = software    engineer

# `age` gets string value "42" (notice it is a string, not a number)
age = 42

# Common practice is to not use spaces around the `=` in an assignment
CC=gcc

📖 Basic Variable Reference:

$(variable_name)

# or alternatively
${variable_name}

# Or, for single character variable names only
$v
# This may look like a valid variable reference for the variable `foo`
$foo

# But, in fact, make treats this reference as if it were
$(f)oo
TARGET_NAME=main
PREREQUISITE_NAMES=main.o helper.h helper.o
LINKER=ld

$(TARGET_NAME): $(PREREQUISITE_NAMES)
  $(LINKER) -o $(TARGET_NAME) $(PREREQUISITE_NAMES)

🤖 Basic Automatic Variables

main: main.o helper.o
  ld -o $@ $^

main.o: main.c helper.h
  gcc -c -o $@ $<

helper.o: helper.c
  gcc -c -o $@ $^

🎰 Basic Function Invocation

$(function_name argument1,argument2,argument3)

${function_name argument1,argument2,argument3}

💹 Common Functions

# If your current directory has these files
# ./
# ├── Makefile
# ├── test1.txt
# ├── test2.txt
# ├── test3.txt
# └── testA.txt

# `VAR1` would get value "test1.txt test2.txt test3.txt testA.txt"
VAR1=$(wildcard *.txt)

# `VAR2` would get value "test1.txt test3.txt"
VAR2=$(wildcard test[13].txt)

# `VAR3` would get value "test1.txt test2.txt test3.txt testA.txt"
VAR3=$(wildcard test?.txt)
# `contents` will equal the data in the file `foo`
contents=$(shell cat foo)
# `VAR1` would get value "hola world"
VAR1=$(subst hello,hola,hello world)

# `VAR2` would get value "CompA.tsx CompB.tsx CompC.tsx"
VAR2=$(subst .jsx,.tsx,CompA.jsx CompB.jsx CompC.jsx)
# `VAR1` would get value "new_test1.txt new_test2.txt test3.pdf"
VAR1=$(patsubst %.txt,new_%.txt,test1.txt test2.txt test3.pdf)
# `VAR1` would get value "foo/ foo/bar/ ./"
VAR1=$(dir foo/ foo/bar/baz.txt ./blamo)
# `VAR1` would get value "text baz.txt blamo"
VAR1=$(notdir foo/text foo/bar/baz.txt blamo)

Syntax for More General Rules

🤡 Phony Targets

# `all` is a common phony target that typically has all of the major
# artifacts of a project as its dependencies
all: executable test-bench documentation

# `clean` is a common phony target whose recipe will remove all
# intermediate files created by other build rules
clean:
  rm -rf ./bin/*

# In previous projects that involved uploading binaries to external
# devices like FPGAs or EEPROMs, I've used an `upload` phony target.
upload: executable
  loadFileToExternalDevice --device=fpga executable
.PHONY: clean
clean:
  rm -rf ./bin/*

🎨 Wildcard Rules

# Using a wildcard rule to make a list of prerequisites that includes
# each chapter text file in the current directory
book.txt: chapter*.txt
  cat $^ > $@

🏵️ Pattern Rules

# This pattern rule tells make that the dependency for any file ending
# in `.o` is a file with the same base name ending in `.c`
%.o: %.c
  gcc -c -o $@ $^

🫥 Built-in Rules

👬 Multiple Targets per Rule

output1 output2 output3: prereq1 prereq2
  compile -o $@ $^

# The above rule is equivalent to these three rules
output1: prereq1 prereq2
  compile -o $@ $^

output2: prereq1 prereq2
  compile -o $@ $^

output3: prereq1 prereq2
  compile -o $@ $^
foo bar biz &: baz boz
  echo $^ > foo
  echo $^ > bar
  echo $^ > biz

🦏 Order-only Prerequisites

# The `bin/` directory is an order-only prerequisite here. As long as it
# exists, no updates to it will cause the rule to re-run
executable: main.o | bin/
  ld -o executable main.o

main.o: main.c
  gcc -c -o main.o main.c

bin/:
  mkdir bin

Other Useful Syntax

🧞 Variable Substitution References

VAR_A:=foo.c bar.c baz.c

# VAR_B receives value foo.o bar.o baz.o
VAR_B:=$(A:.c=.o)
VAR_A:=foo.c bar.c baz.c

# VAR_B receives value foo_new.c bar_new.c baz_new.c
VAR_B:=$(A:%.c=%_new.c)

# This code using the patsubst function is equivalent
VAR_B:=$(patsubst %.c,%_new.c,$(VAR_A))

references:

👨‍🍳 Make Syntax for Recipes

# In this example, when the rule is run, the echo command will be
# executed, causing "building the book!" to be echoed, but make will
# not print `echo "building the book!"` to stdout
book.txt: chapter*.txt
  @echo "building the book!"
  cat $^ > $@

🧟‍♂️ Different Types of Variable Assignment

random=$(shell echo $$RANDOM)

all:
  @echo $(random)
  @echo $(random)

# Running `make all` against this `Makefile` will output two different
# random numbers because make executes the function from the declaration
# every time the variable is referenced.
random:=$(shell echo $$RANDOM)

all:
  @echo $(random)
  @echo $(random)

# Running `make all` against this `Makefile` will output the same number twice
# because make executes the function during the declaration and assigns the
# output to the variable.

Other Bits and Bops

🛶 Make Executes Each Recipe Line in its Own Subshell

target1: prereq1
  cd subdir
  pwd          # pwd will not be subdir/ when pwd evaluates, because
               # the previous line was executed in a separate shell instance

🏚️ Creating a Prerequisite That Is Always Out of Date

# The `FORCE` target has no dependencies and no rule,
# and thus can never be created and will always be out of date
FORCE:

# Every time make has `log.txt` as a goal or a prerequisite of another
# rule, it will consider `log.txt` out of date.
log.txt: FORCE
  echo "MAKE BLAMO" > foobar.txt

# This is the same as the above, but using a pattern rule instead.

%.txt: FORCE
  echo "MAKE BLAMO" > foobar.txt

👩‍👩‍👧‍👦 Using Multiple Makefiles

include some_makefile.mk some_other_makefile.mk
executable:
  cd src/ && $(make)

tests:
  cd tests && $(make)

docs:
  cd documentation && $(make)
my_exported_var:=foobar
export my_exported_var

subsystem:
  cd subdir && $(MAKE)

# Now when `make` is executed in `subdir`, the variable `my_exported_var` will
# be defined during the execution of the `subdir` `Makefile`