Make 01: Fundamentals – Targets, Prerequisites, and Recipes
Summary: Learn what GNU Make is, how it works, and how to write your first Makefile. By the end you will understand targets, prerequisites, recipes, variables, automatic variables, and
.PHONYtargets – enough to read and write basic Makefiles for any project.
| Key | Value |
|---|---|
| OS | Ubuntu 24.04 LTS |
| Make version | GNU Make 4.3 |
| Working directory | ~/projects/makefile-tutorial |
0. Prerequisites
- A Linux system (this tutorial uses Ubuntu 24.04 LTS)
- A terminal and a text editor
Install GNU Make if it is not already present.
sudo apt update && sudo apt install -y make
Verify the installation.
make --version
GNU Make 4.3
Built for x86_64-pc-linux-gnu
Code language: CSS (css)
1. What Is Make?
Make is a build automation tool. It reads a file called Makefile and executes shell commands based on rules you define. Each rule says: “to build this target, check if these prerequisites have changed, and if so, run these recipe commands.”
The core idea is timestamps. If a target file is older than its prerequisites – or does not exist – Make runs the recipe to rebuild it. If the target is already up to date, Make skips it. This avoids repeating work that has already been done.
Make was originally designed for compiling C programs, but it works with any language or task that produces files from other files.
2. Your First Makefile
Create a working directory.
mkdir -p ~/projects/makefile-tutorial
cd ~/projects/makefile-tutorial
Code language: JavaScript (javascript)
Create a file called Makefile with this content.
hello:
echo "Hello, Make!"
Code language: PHP (php)
Warning: The indented line must use a tab character, not spaces. Make requires tabs for recipe lines. If you use spaces, you will see the error
*** missing separator. Stop.
Run it.
make
echo "Hello, Make!"
Hello, Make!
Code language: PHP (php)
Make prints each command before executing it. The first line is Make showing you what it will run. The second line is the actual output.
To suppress the command echo, prefix the recipe line with @.
hello:
@echo "Hello, Make!"
Code language: CSS (css)
Now make only prints the output.
Hello, Make!
3. Anatomy of a Rule
Every Makefile rule has three parts.
target: prerequisites
recipe
| Part | Meaning |
|---|---|
| Target | The file to build, or a label to run |
| Prerequisites | Files the target depends on – if any are newer than the target, the recipe runs |
| Recipe | Shell commands that build the target – each line must start with a tab |
A rule can have all three parts, or just a target and a recipe.
Here is a rule with all three parts.
report.txt: data.txt
wc -l data.txt > report.txt
Code language: CSS (css)
This says: “to build report.txt, check that data.txt exists and is newer. If so, count its lines and write the count to report.txt.”
Create data.txt and test it.
echo -e "line one\nline two\nline three" > data.txt
make report.txt
Code language: CSS (css)
wc -l data.txt > report.txt
Code language: CSS (css)
Check the result.
cat report.txt
Code language: CSS (css)
3 data.txt
Code language: CSS (css)
Run make report.txt again without changing data.txt.
make report.txt
Code language: CSS (css)
make: 'report.txt' is up to date.
Code language: HTTP (http)
Make skipped the recipe because report.txt already exists and is newer than data.txt. This is the timestamp logic at work.
4. Multiple Rules and the Default Target
A Makefile can contain many rules. When you type make with no arguments, Make runs the first target in the file. By convention this target is called all.
all: greeting.txt farewell.txt
greeting.txt:
echo "Hello" > greeting.txt
farewell.txt:
echo "Goodbye" > farewell.txt
clean:
rm -f greeting.txt farewell.txt
Code language: CSS (css)
Run make and it builds all, which depends on both text files.
make
echo "Hello" > greeting.txt
echo "Goodbye" > farewell.txt
Code language: PHP (php)
The all target has no recipe of its own – it only lists prerequisites. Make builds both files because all depends on them.
Run make clean to remove the generated files.
make clean
rm -f greeting.txt farewell.txt
Code language: CSS (css)
5. Variables
Variables reduce repetition. Define them at the top of the Makefile and reference them with $(NAME).
OUTPUT_DIR = build
FILES = $(OUTPUT_DIR)/greeting.txt $(OUTPUT_DIR)/farewell.txt
all: $(FILES)
$(OUTPUT_DIR)/greeting.txt:
mkdir -p $(OUTPUT_DIR)
echo "Hello" > $(OUTPUT_DIR)/greeting.txt
$(OUTPUT_DIR)/farewell.txt:
mkdir -p $(OUTPUT_DIR)
echo "Goodbye" > $(OUTPUT_DIR)/farewell.txt
clean:
rm -rf $(OUTPUT_DIR)
Code language: JavaScript (javascript)
There are four ways to assign a variable.
| Syntax | Meaning |
|---|---|
VAR = value | Recursively expanded – re-evaluated every time $(VAR) is used |
VAR := value | Simply expanded – evaluated once at assignment time |
VAR ?= value | Conditional – set only if VAR is not already defined |
VAR += value | Append – adds to the existing value of VAR |
The difference between = and := matters when variables reference other variables.
A = $(B)
B = hello
# $(A) expands to "hello" because = is lazy - it waits until A is used
Code language: PHP (php)
A := $(B)
B = hello
# $(A) expands to "" because := evaluated A immediately, before B was defined
Code language: PHP (php)
Tip: Use
:=when the value comes from a shell command or computation. Use=for simple string values.
6. Automatic Variables
Make provides special variables inside recipes that refer to parts of the current rule.
| Variable | Meaning |
|---|---|
$@ | The target name |
$< | The first prerequisite |
$^ | All prerequisites, space-separated |
$? | Only the prerequisites that are newer than the target |
These save you from repeating filenames. Here is an example.
combined.txt: part1.txt part2.txt
cat $^ > $@
$^ expands to part1.txt part2.txt (all prerequisites) and $@ expands to combined.txt (the target). This is identical to writing the following.
combined.txt: part1.txt part2.txt
cat part1.txt part2.txt > combined.txt
Code language: CSS (css)
Test it.
echo "Part one" > part1.txt
echo "Part two" > part2.txt
make combined.txt
Code language: PHP (php)
cat part1.txt part2.txt > combined.txt
Code language: CSS (css)
cat combined.txt
Code language: CSS (css)
Part one
Part two
7. .PHONY Targets
Some targets are not files. They are labels for actions like clean, all, or test. If a file with the same name happens to exist in your directory, Make will think the target is already up to date and skip the recipe.
Declare non-file targets as .PHONY to prevent this.
.PHONY: all clean
all: greeting.txt farewell.txt
greeting.txt:
echo "Hello" > greeting.txt
farewell.txt:
echo "Goodbye" > farewell.txt
clean:
rm -f greeting.txt farewell.txt
Code language: CSS (css)
Now make clean always runs, even if a file called clean exists in the directory.
Tip: Always mark action targets like
all,clean,test, andinstallas.PHONY.
8. Comments and Recipe Prefixes
Comments start with #.
# Build both output files
all: greeting.txt farewell.txt # this is an inline comment
Code language: PHP (php)
Recipe lines support two special prefixes.
| Prefix | Effect |
|---|---|
@ | Suppress command echo – Make does not print the command before running it |
- | Ignore errors – Make continues even if the command fails |
clean:
@echo "Cleaning up..."
-rm -f *.txt
@echo "Done."
Code language: PHP (php)
The - before rm means Make will not stop if there are no .txt files to delete.
9. Putting It All Together
Here is a complete Makefile that combines everything from this tutorial.
# Variables
SRC_DIR = src
BUILD_DIR = build
FILES = $(BUILD_DIR)/report.txt $(BUILD_DIR)/summary.txt
# Default target
.PHONY: clean
all: $(FILES)
# Rule: build report from raw data
$(BUILD_DIR)/report.txt: $(SRC_DIR)/data.txt
mkdir -p $(BUILD_DIR)
wc -l $< > $@
# Rule: build summary from report
$(BUILD_DIR)/summary.txt: $(BUILD_DIR)/report.txt
echo "Summary generated on $$(date)" > $@
cat $< >> $@
clean:
rm -rf $(BUILD_DIR)
Code language: PHP (php)
Set up the source file and run it.
mkdir -p src
echo -e "alpha\nbeta\ngamma\ndelta" > src/data.txt
make
Code language: PHP (php)
mkdir -p build
wc -l src/data.txt > build/report.txt
echo "Summary generated on $(date)" > build/summary.txt
cat build/report.txt >> build/summary.txt
Code language: PHP (php)
Note:
$$(date)uses a double dollar sign to pass a literal$to the shell. A single$(date)would be interpreted as a Make variable, which is probably empty.
Run make again without changing anything.
make
make: Nothing to be done for 'all'.
Code language: HTTP (http)
Make detected that every target is already up to date and skipped all the recipes.
Summary
You learned the core concepts of GNU Make.
- A Makefile contains rules that tell Make how to build targets from prerequisites
- Make uses file timestamps to decide what needs rebuilding and skips everything else
- Variables (
$(VAR)) reduce repetition across rules - Automatic variables (
$@,$<,$^) reference parts of the current rule without repeating filenames - .PHONY declares targets that are actions, not files
- Recipe lines must use tab characters, not spaces
@suppresses command echo and-ignores errors
The next tutorial covers Make’s most common use case – compiling C and C++ projects, where the timestamp-based rebuild logic truly shines.
GNU Make — All Parts
- 1 Make 01: Fundamentals – Targets, Prerequisites, and Recipes You are here
- 2 Make 02: C and C++ – Compiling Real Projects
- 3 Make 03: Used as a Task Runner – Shell Scripts, Python, and Beyond
- 4 Make 04: Advanced – Libraries, Linking, and Mixed Builds
