Skip to examples

A visual reference · GNU Make

GNU Make,
one change at a time

From a first rule to reliable builds, LaTeX documents and publishing workflows. Complete source. A small change. Its actual result.

The Org file is the canonical source. Read the HTML guide beside your work, or download its PDF and source for offline reading.

Last changes: 2026-10-11. Unified tour–chapter–example IDs, numbered chapter anchors and updated direct links, downloads and comparisons. Manual references now use descriptive links. Reader pages now omit publishing instructions and highlight PDF downloads.

Download PDFRendered tour · read offline
90worked examples
14topic families
1canonical Org source

Reading the tour

Three ways to see a change

Makefile

Read the exact Makefile used by the example, including targets, prerequisites and recipes.

Build result

Compare captured output and exit status with the files and commands declared by the rule.

Complete workflow

Expand the runnable reproducer or download its script to recreate all local inputs.

The primary panel shows the exact Makefile used by the runnable example. Expand Complete runnable example for setup files, invocation and checks, or download its .mk and .sh files. The PDF keeps the Makefile and output together; its downloads and attached Org source provide the full reproducer. The LaTeX section covers documents, bibliography tools, generated figures, draft/final builds, cleanup and a source package that rebuilds successfully.

Read from the beginning or jump to a stable example ID. Each example includes its complete input and captured result. Copy link preserves the address of the guide you are reading, including its current domain and path.

How to use this tour

Learn GNU Make through 90 complete examples in 14 topics. Read the Makefile, then compare the captured output. Every example has a MAK identifier, a direct URL anchor and a Copy link button in the HTML guide. Tabs introduce ordinary recipes; the visible-prefix example shows an alternative.

Requirements: GNU Make 4.3 or newer, Bash, and common Unix tools. The final three workflow examples also use AWK and gnuplot. The LaTeX section requires latexmk, pdfLaTeX, BibTeX, Biber with biblatex, and pdftotext. On macOS, /usr/bin/make is often GNU Make 3.81; install a current GNU Make with brew install make and use gmake. The runnable examples prefer gmake when it exists and otherwise use make.

Each example writes to its own newly created temporary directory. The directory is removed on exit. To retain its artifacts when running a downloaded script, use KEEP_WORK=1 bash MAK-13-004.sh. These are original examples, not extracts from the GNU manual. Content is CC0; the tour structure is CC BY 4.0.

The primary panel shows the exact Makefile used by the runnable example. Expand Complete runnable example for setup files, invocation and follow-up checks, or download its .sh file. The PDF keeps the Makefile and output together; its download links and attached Org source provide the complete reproducer. Parallel recipe output ordering may vary; MAK-11-003 accepts both complete block orders rather than pretending the scheduler has a fixed order.

Concepts to keep in mind

Make first reads and expands its configuration, then walks the dependency graph. A real file target is stale when missing or older than a normal prerequisite. A phony goal names an action. Order-only prerequisites establish ordering without making timestamp changes propagate. A recipe usually uses one shell per logical line. Make variables and shell variables have different expansion rules. Declare real dependencies before enabling parallel execution.

Read the GNU Make manual for the complete reference.

Selected manual sections:

01 - Read and run a rule

MAK-01-001

A target and its recipe

A rule names a target before the colon. The following tab-indented recipe runs when that target is requested. Run: make hello.

Makefile

hello:
	printf "%s\n" "Hello from Make"

Actual output

Captured run · exit 0

printf "%s\n" "Hello from Make"
Hello from Make
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
hello:
	printf "%s\n" "Hello from Make"
MAKEFILE
m hello

Download: MAK-01-001.mk · MAK-01-001.sh

MAK-01-002

Silence recipe echo with @

Add @ to hide the recipe command while retaining its output. Compare with MAK-01-001. Run: make hello.

Makefile

hello:
	@printf "%s\n" "Hello from Make"

Actual output

Captured run · exit 0

Hello from Make
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
hello:
	@printf "%s\n" "Hello from Make"
MAKEFILE
m hello

Download: MAK-01-002.mk · MAK-01-002.sh

MAK-01-003

Choose the default goal

.DEFAULT_GOAL makes the default explicit even when a helper rule comes first. Run: make.

Makefile

.DEFAULT_GOAL := all
helper:
	@printf "helper\n"
all:
	@printf "default all\n"

Actual output

Captured run · exit 0

default all
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
.DEFAULT_GOAL := all
helper:
	@printf "helper\n"
all:
	@printf "default all\n"
MAKEFILE
m

Download: MAK-01-003.mk · MAK-01-003.sh

MAK-01-004

Request a named target

Only the requested target and its prerequisites are considered. The default goal is irrelevant to make check.

Makefile

all:
	@printf "build\n"
check:
	@printf "check\n"

Actual output

Captured run · exit 0

check
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
all:
	@printf "build\n"
check:
	@printf "check\n"
MAKEFILE
m check

Download: MAK-01-004.mk · MAK-01-004.sh

MAK-01-005

Request two goals

Specify both goals on the command line. This serial invocation prints their results in the requested order.

Makefile

one:
	@printf "one\n"
two:
	@printf "two\n"

Actual output

Captured run · exit 0

one
two
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
one:
	@printf "one\n"
two:
	@printf "two\n"
MAKEFILE
m one two

Download: MAK-01-005.mk · MAK-01-005.sh

MAK-01-006

Use another makefile name

-f selects a makefile explicitly. The runnable script renames the displayed file to rules.mk. Run: make -f rules.mk hello.

Makefile

hello:
	@printf "rules.mk selected\n"

Actual output

Captured run · exit 0

rules.mk selected
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
hello:
	@printf "rules.mk selected\n"
MAKEFILE
mv Makefile rules.mk
m -f rules.mk hello

Download: MAK-01-006.mk · MAK-01-006.sh

02 - Files and incremental builds

MAK-02-001

Create a missing output

The missing report.txt must be built from input.txt. The setup file contains a single record, 7. Run: make report.txt, then cat report.txt.

Makefile

report.txt: input.txt
	@printf "building report\n"
	@cp input.txt report.txt

Actual output

Captured run · exit 0

building report
7
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > input.txt <<'INPUT_7_1'
7
INPUT_7_1
cat > Makefile <<'MAKEFILE'
report.txt: input.txt
	@printf "building report\n"
	@cp input.txt report.txt
MAKEFILE
m -s report.txt
cat report.txt

Download: MAK-02-001.mk · MAK-02-001.sh

MAK-02-002

Skip an up-to-date output

The second invocation has nothing to rebuild. A label separates the two runs; make -s keeps the no-op quiet.

Makefile

report.txt: input.txt
	@printf "building report\n"
	@cp input.txt report.txt

Actual output

Captured run · exit 0

first run
building report
second run
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > input.txt <<'INPUT_8_1'
7
INPUT_8_1
cat > Makefile <<'MAKEFILE'
report.txt: input.txt
	@printf "building report\n"
	@cp input.txt report.txt
MAKEFILE
printf "first run\n"
m -s report.txt
printf "second run\n"
m -s report.txt

Download: MAK-02-002.mk · MAK-02-002.sh

MAK-02-003

Rebuild after a newer prerequisite

Changing the input only triggers a rebuild when its timestamp is newer. The script assigns explicit past timestamps to avoid sleeps and filesystem-resolution races.

Makefile

report.txt: input.txt
	@printf "building report\n"
	@cp input.txt report.txt

Actual output

Captured run · exit 0

building report
building report
9
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > input.txt <<'INPUT_9_1'
7
INPUT_9_1
cat > Makefile <<'MAKEFILE'
report.txt: input.txt
	@printf "building report\n"
	@cp input.txt report.txt
MAKEFILE
m -s report.txt
printf "9\n" > input.txt
touch -t 202001010101 report.txt
touch -t 202101010101 input.txt
m -s report.txt
cat report.txt

Download: MAK-02-003.mk · MAK-02-003.sh

MAK-02-004

Content changes are not enough

Make compares timestamps, not file contents. The script replaces the input but dates it earlier than the report; the report keeps its old value. Content hashing needs an explicit design.

Makefile

report.txt: input.txt
	@cp input.txt report.txt

Actual output

Captured run · exit 0

7
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > input.txt <<'INPUT_10_1'
7
INPUT_10_1
cat > Makefile <<'MAKEFILE'
report.txt: input.txt
	@cp input.txt report.txt
MAKEFILE
m -s report.txt
printf "9\n" > input.txt
touch -t 202001010101 input.txt
m -s report.txt
cat report.txt

Download: MAK-02-004.mk · MAK-02-004.sh

MAK-02-005

Build a dependency chain

Make completes the intermediate table before its dependent report. The order comes from the graph, not the order of recipe text.

Makefile

report.txt: table.txt
	@printf "report\n"
	@cp table.txt report.txt
table.txt: input.txt
	@printf "table\n"
	@cp input.txt table.txt

Actual output

Captured run · exit 0

table
report
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > input.txt <<'INPUT_11_1'
7
INPUT_11_1
cat > Makefile <<'MAKEFILE'
report.txt: table.txt
	@printf "report\n"
	@cp table.txt report.txt
table.txt: input.txt
	@printf "table\n"
	@cp input.txt table.txt
MAKEFILE
m -s report.txt

Download: MAK-02-005.mk · MAK-02-005.sh

MAK-02-006

Share one prerequisite

Two outputs depend on the same prepared file. It is made once per invocation. Run: make all.

Makefile

all: left.txt right.txt
left.txt right.txt: prepared.txt
	@cp prepared.txt $@
prepared.txt:
	@printf "prepare once\n"
	@printf "ready\n" > $@

Actual output

Captured run · exit 0

prepare once
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
all: left.txt right.txt
left.txt right.txt: prepared.txt
	@cp prepared.txt $@
prepared.txt:
	@printf "prepare once\n"
	@printf "ready\n" > $@
MAKEFILE
m -s all

Download: MAK-02-006.mk · MAK-02-006.sh

03 - Phony goals and directories

MAK-03-001

A filename can hide a command goal

A file named check makes an ordinary target appear current. The script creates that file and demonstrates the skipped recipe. Compare with MAK-03-002.

Makefile

check:
	@printf "checked\n"

Actual output

Captured run · exit 0

recipe was skipped
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
check:
	@printf "checked\n"
MAKEFILE
touch check
m -s check
printf "recipe was skipped\n"

Download: MAK-03-001.mk · MAK-03-001.sh

MAK-03-002

Declare a command goal phony

.PHONY says that check names an action rather than a real output. It runs even when a file named check exists.

Makefile

.PHONY: check
check:
	@printf "checked\n"

Actual output

Captured run · exit 0

checked
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
.PHONY: check
check:
	@printf "checked\n"
MAKEFILE
touch check
m -s check

Download: MAK-03-002.mk · MAK-03-002.sh

MAK-03-003

Provide an aggregate all goal

An aggregate phony goal joins several real outputs without creating a file named all. Repeating make all does not rebuild current files.

Makefile

.PHONY: all
all: a.txt b.txt
a.txt b.txt:
	@printf "%s\n" "$@"
	@printf "ready\n" > $@

Actual output

Captured run · exit 0

a.txt
b.txt
again
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
.PHONY: all
all: a.txt b.txt
a.txt b.txt:
	@printf "%s\n" "$@"
	@printf "ready\n" > $@
MAKEFILE
m -s all
printf "again\n"
m -s all

Download: MAK-03-003.mk · MAK-03-003.sh

MAK-03-004

Create an output directory first

The vertical bar introduces an order-only prerequisite. Build the directory before report.txt without using its timestamp to decide whether the report is stale.

Makefile

build/report.txt: input.txt | build
	@cp input.txt $@
	@printf "report created\n"
build:
	@mkdir -p $@

Actual output

Captured run · exit 0

report created
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > input.txt <<'INPUT_16_1'
7
INPUT_16_1
cat > Makefile <<'MAKEFILE'
build/report.txt: input.txt | build
	@cp input.txt $@
	@printf "report created\n"
build:
	@mkdir -p $@
MAKEFILE
m -s build/report.txt

Download: MAK-03-004.mk · MAK-03-004.sh

MAK-03-005

Directory changes do not rebuild outputs

Adding an unrelated file changes the directory timestamp. Because build is order-only, the existing report remains current.

Makefile

build/report.txt: input.txt | build
	@cp input.txt $@
	@printf "report created\n"
build:
	@mkdir -p $@

Actual output

Captured run · exit 0

report created
again
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > input.txt <<'INPUT_17_1'
7
INPUT_17_1
cat > Makefile <<'MAKEFILE'
build/report.txt: input.txt | build
	@cp input.txt $@
	@printf "report created\n"
build:
	@mkdir -p $@
MAKEFILE
m -s build/report.txt
printf "unrelated\n" > build/other.txt
printf "again\n"
m -s build/report.txt

Download: MAK-03-005.mk · MAK-03-005.sh

MAK-03-006

A local clean goal

A clean action removes only the named generated output in the fresh demonstration directory. Phony clean never collides with a file of the same name.

Makefile

.PHONY: all clean
all: result.txt
result.txt:
	@printf "generated\n" > $@
clean:
	@rm -f result.txt

Actual output

Captured run · exit 0

generated output removed
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
.PHONY: all clean
all: result.txt
result.txt:
	@printf "generated\n" > $@
clean:
	@rm -f result.txt
MAKEFILE
m -s all
m -s clean
test ! -e result.txt
printf "generated output removed\n"

Download: MAK-03-006.mk · MAK-03-006.sh

04 - Automatic variables

MAK-04-001

Name the current target with $@

$@ expands to the target being built. One rule can report or create the selected output name.

Makefile

alpha.txt beta.txt:
	@printf "%s\n" "$@"
	@printf "ready\n" > $@

Actual output

Captured run · exit 0

beta.txt
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
alpha.txt beta.txt:
	@printf "%s\n" "$@"
	@printf "ready\n" > $@
MAKEFILE
m -s beta.txt

Download: MAK-04-001.mk · MAK-04-001.sh

MAK-04-002

Read the first prerequisite with $<

$< is the first prerequisite. Copying $< to $@ keeps this recipe independent of the actual names.

Makefile

copy.txt: source.txt
	@cp $< $@

Actual output

Captured run · exit 0

source record
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > source.txt <<'INPUT_20_1'
source record
INPUT_20_1
cat > Makefile <<'MAKEFILE'
copy.txt: source.txt
	@cp $< $@
MAKEFILE
m -s copy.txt
cat copy.txt

Download: MAK-04-002.mk · MAK-04-002.sh

MAK-04-003

List unique prerequisites with $^

$^ contains prerequisite names with duplicates removed. A duplicate dependency still appears only once.

Makefile

all: alpha.txt beta.txt alpha.txt
	@printf "%s\n" "$^"

Actual output

Captured run · exit 0

alpha.txt beta.txt
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > alpha.txt <<'INPUT_21_1'
a
INPUT_21_1
cat > beta.txt <<'INPUT_21_2'
b
INPUT_21_2
cat > Makefile <<'MAKEFILE'
all: alpha.txt beta.txt alpha.txt
	@printf "%s\n" "$^"
MAKEFILE
m -s all

Download: MAK-04-003.mk · MAK-04-003.sh

MAK-04-004

Keep duplicates with $+

$+ retains duplicates and their order. This is useful when repeated arguments matter, such as some linker command lines.

Makefile

all: alpha.txt beta.txt alpha.txt
	@printf "%s\n" "$+"

Actual output

Captured run · exit 0

alpha.txt beta.txt alpha.txt
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > alpha.txt <<'INPUT_22_1'
a
INPUT_22_1
cat > beta.txt <<'INPUT_22_2'
b
INPUT_22_2
cat > Makefile <<'MAKEFILE'
all: alpha.txt beta.txt alpha.txt
	@printf "%s\n" "$+"
MAKEFILE
m -s all

Download: MAK-04-004.mk · MAK-04-004.sh

MAK-04-005

List newer prerequisites with $?

$? reports the prerequisites newer than the target. The script assigns fixed past timestamps so only changed.txt qualifies.

Makefile

report.txt: old.txt changed.txt
	@printf "newer: %s\n" "$?"
	@touch $@

Actual output

Captured run · exit 0

newer: changed.txt
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > old.txt <<'INPUT_23_1'
old
INPUT_23_1
cat > changed.txt <<'INPUT_23_2'
new
INPUT_23_2
cat > report.txt <<'INPUT_23_3'
report
INPUT_23_3
touch -t 202001010101 old.txt
touch -t 202101010101 report.txt
touch -t 202201010101 changed.txt
cat > Makefile <<'MAKEFILE'
report.txt: old.txt changed.txt
	@printf "newer: %s\n" "$?"
	@touch $@
MAKEFILE
m -s report.txt

Download: MAK-04-005.mk · MAK-04-005.sh

MAK-04-006

Separate normal and order-only inputs

$^ excludes order-only prerequisites; $| contains them. The build directory supplies ordering rather than content.

Makefile

build/report.txt: input.txt | build
	@printf "normal=%s order-only=%s\n" "$^" "$|"
	@cp $< $@
build:
	@mkdir -p $@

Actual output

Captured run · exit 0

normal=input.txt order-only=build
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > input.txt <<'INPUT_24_1'
7
INPUT_24_1
cat > Makefile <<'MAKEFILE'
build/report.txt: input.txt | build
	@printf "normal=%s order-only=%s\n" "$^" "$|"
	@cp $< $@
build:
	@mkdir -p $@
MAKEFILE
m -s build/report.txt

Download: MAK-04-006.mk · MAK-04-006.sh

05 - Variables and expansion

MAK-05-001

Define a reusable variable

Use $(NAME) to insert a Make variable. Single quotes in the recipe protect the expanded value as one shell argument.

Makefile

GREETING = Hello
all:
	@printf "%s\n" "$(GREETING), reader"

Actual output

Captured run · exit 0

Hello, reader
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
GREETING = Hello
all:
	@printf "%s\n" "$(GREETING), reader"
MAKEFILE
m -s all

Download: MAK-05-001.mk · MAK-05-001.sh

MAK-05-002

Recursive assignment sees later values

= stores an expression that is expanded when used. Changing NAME after defining GREETING changes the eventual greeting.

Makefile

NAME = Ada
GREETING = Hello $(NAME)
NAME = Grace
all:
	@printf "%s\n" "$(GREETING)"

Actual output

Captured run · exit 0

Hello Grace
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
NAME = Ada
GREETING = Hello $(NAME)
NAME = Grace
all:
	@printf "%s\n" "$(GREETING)"
MAKEFILE
m -s all

Download: MAK-05-002.mk · MAK-05-002.sh

MAK-05-003

Simple assignment captures the current value

:= expands its right-hand side immediately. GREETING therefore remembers Ada. Compare with MAK-05-002.

Makefile

NAME = Ada
GREETING := Hello $(NAME)
NAME = Grace
all:
	@printf "%s\n" "$(GREETING)"

Actual output

Captured run · exit 0

Hello Ada
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
NAME = Ada
GREETING := Hello $(NAME)
NAME = Grace
all:
	@printf "%s\n" "$(GREETING)"
MAKEFILE
m -s all

Download: MAK-05-003.mk · MAK-05-003.sh

MAK-05-004

Provide a default with ?=

?= assigns a value only when the variable is undefined. A command-line value takes precedence over this default.

Makefile

MODE ?= release
all:
	@printf "%s\n" "$(MODE)"

Actual output

Captured run · exit 0

release
debug
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
MODE ?= release
all:
	@printf "%s\n" "$(MODE)"
MAKEFILE
m -s all
m -s all MODE=debug

Download: MAK-05-004.mk · MAK-05-004.sh

MAK-05-005

Append with +=

+= adds words to an existing variable. This is convenient for assembling option lists or target sets.

Makefile

WORDS := small
WORDS += complete
WORDS += examples
all:
	@printf "%s\n" "$(WORDS)"

Actual output

Captured run · exit 0

small complete examples
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
WORDS := small
WORDS += complete
WORDS += examples
all:
	@printf "%s\n" "$(WORDS)"
MAKEFILE
m -s all

Download: MAK-05-005.mk · MAK-05-005.sh

MAK-05-006

Override from the command line

A command-line assignment beats an ordinary assignment in the Makefile. Run: make all NAME=Grace.

Makefile

NAME = Ada
all:
	@printf "Hello %s\n" "$(NAME)"

Actual output

Captured run · exit 0

Hello Grace
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
NAME = Ada
all:
	@printf "Hello %s\n" "$(NAME)"
MAKEFILE
m -s all NAME=Grace

Download: MAK-05-006.mk · MAK-05-006.sh

06 - Variable scope and conditionals

MAK-06-001

Inspect the origin of a value

$(origin NAME) explains where NAME came from. This helps distinguish a Makefile value from a command-line override.

Makefile

NAME = Ada
all:
	@printf "%s: %s\n" "$(origin NAME)" "$(NAME)"

Actual output

Captured run · exit 0

file: Ada
command line: Grace
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
NAME = Ada
all:
	@printf "%s: %s\n" "$(origin NAME)" "$(NAME)"
MAKEFILE
m -s all
m -s all NAME=Grace

Download: MAK-06-001.mk · MAK-06-001.sh

MAK-06-002

An empty value is still defined

A command-line empty string counts as defined, so ?= does not replace it. This differs from a shell fallback that tests for nonempty text.

Makefile

NAME ?= Ada
all:
	@printf "[%s]\n" "$(NAME)"

Actual output

Captured run · exit 0

[]
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
NAME ?= Ada
all:
	@printf "[%s]\n" "$(NAME)"
MAKEFILE
m -s all NAME=

Download: MAK-06-002.mk · MAK-06-002.sh

MAK-06-003

Export a value to the recipe shell

Exported Make variables become shell environment variables. Double dollars pass the dollar sign through Make to the shell.

Makefile

export LABEL = published
all:
	@printf "%s\n" "$$LABEL"

Actual output

Captured run · exit 0

published
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
export LABEL = published
all:
	@printf "%s\n" "$$LABEL"
MAKEFILE
m -s all

Download: MAK-06-003.mk · MAK-06-003.sh

MAK-06-004

Use a target-specific value

A target-specific assignment applies when that target is built and is inherited by its prerequisites unless overridden.

Makefile

debug: MODE = debug
release: MODE = release
debug release:
	@printf "%s: %s\n" "$@" "$(MODE)"

Actual output

Captured run · exit 0

debug: debug
release: release
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
debug: MODE = debug
release: MODE = release
debug release:
	@printf "%s: %s\n" "$@" "$(MODE)"
MAKEFILE
m -s debug release

Download: MAK-06-004.mk · MAK-06-004.sh

MAK-06-005

Choose rules at parse time with ifeq

ifeq is processed while Make reads the file. It chooses the recipe text before any target is executed.

Makefile

MODE ?= release
ifeq ($(MODE),debug)
MESSAGE := extra diagnostics
else
MESSAGE := compact output
endif
all:
	@printf "%s\n" "$(MESSAGE)"

Actual output

Captured run · exit 0

compact output
extra diagnostics
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
MODE ?= release
ifeq ($(MODE),debug)
MESSAGE := extra diagnostics
else
MESSAGE := compact output
endif
all:
	@printf "%s\n" "$(MESSAGE)"
MAKEFILE
m -s all
m -s all MODE=debug

Download: MAK-06-005.mk · MAK-06-005.sh

MAK-06-006

Deliberately override a command-line value

The override directive gives this assignment precedence over a command-line assignment. Use it only for values the Makefile really must control.

Makefile

override MODE := required
all:
	@printf "%s\n" "$(MODE)"

Actual output

Captured run · exit 0

required
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
override MODE := required
all:
	@printf "%s\n" "$(MODE)"
MAKEFILE
m -s all MODE=optional

Download: MAK-06-006.mk · MAK-06-006.sh

07 - Pattern rules and file discovery

MAK-07-001

Capture the stem with $*

% matches a stem shared by the target and prerequisite. $* contains that stem while the recipe runs.

Makefile

%.out: %.in
	@printf "stem=%s\n" "$*"
	@cp $< $@

Actual output

Captured run · exit 0

stem=sample
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > sample.in <<'INPUT_37_1'
record
INPUT_37_1
cat > Makefile <<'MAKEFILE'
%.out: %.in
	@printf "stem=%s\n" "$*"
	@cp $< $@
MAKEFILE
m -s sample.out

Download: MAK-07-001.mk · MAK-07-001.sh

MAK-07-002

Reuse one transformation rule

Both files follow the same pattern rule. tr uppercases their complete input. $< and $@ identify the current pair.

Makefile

all: alpha.upper beta.upper
%.upper: %.txt
	@tr "[:lower:]" "[:upper:]" < $< > $@

Actual output

Captured run · exit 0

ADA
GRACE
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > alpha.txt <<'INPUT_38_1'
ada
INPUT_38_1
cat > beta.txt <<'INPUT_38_2'
grace
INPUT_38_2
cat > Makefile <<'MAKEFILE'
all: alpha.upper beta.upper
%.upper: %.txt
	@tr "[:lower:]" "[:upper:]" < $< > $@
MAKEFILE
m -s all
cat alpha.upper beta.upper

Download: MAK-07-002.mk · MAK-07-002.sh

MAK-07-003

Limit a static pattern to selected targets

A static pattern rule applies to the explicit target list before the second colon. It is more selective than a general implicit rule.

Makefile

OUTPUTS := alpha.out beta.out
all: $(OUTPUTS)
$(OUTPUTS): %.out: %.in
	@cp $< $@

Actual output

Captured run · exit 0

one
two
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > alpha.in <<'INPUT_39_1'
one
INPUT_39_1
cat > beta.in <<'INPUT_39_2'
two
INPUT_39_2
cat > Makefile <<'MAKEFILE'
OUTPUTS := alpha.out beta.out
all: $(OUTPUTS)
$(OUTPUTS): %.out: %.in
	@cp $< $@
MAKEFILE
m -s all
cat alpha.out beta.out

Download: MAK-07-003.mk · MAK-07-003.sh

MAK-07-004

An explicit recipe wins

An explicit recipe for special.out takes precedence over the generic pattern recipe for that target.

Makefile

%.out: %.in
	@cp $< $@
special.out: special.in
	@printf "special handling\n" > $@

Actual output

Captured run · exit 0

special handling
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > special.in <<'INPUT_40_1'
ordinary input
INPUT_40_1
cat > Makefile <<'MAKEFILE'
%.out: %.in
	@cp $< $@
special.out: special.in
	@printf "special handling\n" > $@
MAKEFILE
m -s special.out
cat special.out

Download: MAK-07-004.mk · MAK-07-004.sh

MAK-07-005

Search for inputs with vpath

vpath supplies a search directory for matching prerequisites. $< resolves to the discovered path, so the recipe reads the correct file.

Makefile

vpath %.txt data
copy.txt: input.txt
	@printf "input=%s\n" "$<"
	@cp $< $@

Actual output

Captured run · exit 0

input=data/input.txt
found record
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
mkdir -p data
cat > data/input.txt <<'INPUT_41_1'
found record
INPUT_41_1
cat > Makefile <<'MAKEFILE'
vpath %.txt data
copy.txt: input.txt
	@printf "input=%s\n" "$<"
	@cp $< $@
MAKEFILE
m -s copy.txt
cat copy.txt

Download: MAK-07-005.mk · MAK-07-005.sh

MAK-07-006

Set a pattern-specific value

A pattern-specific assignment supplies a value to all matching targets. Other target patterns can use a different value.

Makefile

%.debug: MODE = debug
%.release: MODE = release
app.debug app.release:
	@printf "%s: %s\n" "$@" "$(MODE)"

Actual output

Captured run · exit 0

app.debug: debug
app.release: release
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
%.debug: MODE = debug
%.release: MODE = release
app.debug app.release:
	@printf "%s: %s\n" "$@" "$(MODE)"
MAKEFILE
m -s app.debug app.release

Download: MAK-07-006.mk · MAK-07-006.sh

08 - Functions for lists and paths

MAK-08-001

Discover existing files with wildcard

$(wildcard ...) returns matching existing filenames. The script creates alpha.txt and beta.txt before Make reads the expression.

Makefile

FILES := $(wildcard *.txt)
all:
	@printf "%s\n" "$(FILES)"

Actual output

Captured run · exit 0

alpha.txt beta.txt
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > alpha.txt <<'INPUT_43_1'
a
INPUT_43_1
cat > beta.txt <<'INPUT_43_2'
b
INPUT_43_2
cat > Makefile <<'MAKEFILE'
FILES := $(wildcard *.txt)
all:
	@printf "%s\n" "$(FILES)"
MAKEFILE
m -s all

Download: MAK-08-001.mk · MAK-08-001.sh

MAK-08-002

Transform filenames with patsubst

patsubst substitutes matching word patterns. Here source names become output names without executing a shell.

Makefile

SOURCES := alpha.txt beta.txt
OUTPUTS := $(patsubst %.txt,build/%.out,$(SOURCES))
all:
	@printf "%s\n" "$(OUTPUTS)"

Actual output

Captured run · exit 0

build/alpha.out build/beta.out
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
SOURCES := alpha.txt beta.txt
OUTPUTS := $(patsubst %.txt,build/%.out,$(SOURCES))
all:
	@printf "%s\n" "$(OUTPUTS)"
MAKEFILE
m -s all

Download: MAK-08-002.mk · MAK-08-002.sh

MAK-08-003

Filter a list by suffix

filter keeps matching words; filter-out keeps the remainder. Both operate on whitespace-separated lists.

Makefile

FILES := notes.org data.tsv plot.gp
all:
	@printf "data: %s\n" "$(filter %.tsv,$(FILES))"
	@printf "other: %s\n" "$(filter-out %.tsv,$(FILES))"

Actual output

Captured run · exit 0

data: data.tsv
other: notes.org plot.gp
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
FILES := notes.org data.tsv plot.gp
all:
	@printf "data: %s\n" "$(filter %.tsv,$(FILES))"
	@printf "other: %s\n" "$(filter-out %.tsv,$(FILES))"
MAKEFILE
m -s all

Download: MAK-08-003.mk · MAK-08-003.sh

MAK-08-004

Sort and remove duplicates

sort orders words and removes duplicates. It is suitable for a unique dependency list, but not for arguments where repetitions matter.

Makefile

FILES := beta.txt alpha.txt beta.txt
all:
	@printf "%s\n" "$(sort $(FILES))"

Actual output

Captured run · exit 0

alpha.txt beta.txt
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
FILES := beta.txt alpha.txt beta.txt
all:
	@printf "%s\n" "$(sort $(FILES))"
MAKEFILE
m -s all

Download: MAK-08-004.mk · MAK-08-004.sh

MAK-08-005

Build a list with foreach

foreach evaluates the expression once for each list item. addprefix supplies the corresponding directory prefixes.

Makefile

NAMES := alpha beta
OUTPUTS := $(foreach name,$(NAMES),$(addprefix build/,$(name)).out)
all:
	@printf "%s\n" "$(OUTPUTS)"

Actual output

Captured run · exit 0

build/alpha.out build/beta.out
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
NAMES := alpha beta
OUTPUTS := $(foreach name,$(NAMES),$(addprefix build/,$(name)).out)
all:
	@printf "%s\n" "$(OUTPUTS)"
MAKEFILE
m -s all

Download: MAK-08-005.mk · MAK-08-005.sh

MAK-08-006

Capture a shell result once

$(shell ...) captures command output as a Make value. := evaluates this local command once as the file is read; repeated variable references reuse the value.

Makefile

VALUE := $(shell printf "computed")
all:
	@printf "%s / %s\n" "$(VALUE)" "$(VALUE)"

Actual output

Captured run · exit 0

computed / computed
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
VALUE := $(shell printf "computed")
all:
	@printf "%s / %s\n" "$(VALUE)" "$(VALUE)"
MAKEFILE
m -s all

Download: MAK-08-006.mk · MAK-08-006.sh

09 - The recipe shell

MAK-09-001

Escape a shell variable with $$

Make consumes one dollar sign. $$name reaches the shell as $name, while $(name) would refer to a Make variable.

Makefile

all:
	@name=Ada; printf "%s\n" "$$name"

Actual output

Captured run · exit 0

Ada
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
all:
	@name=Ada; printf "%s\n" "$$name"
MAKEFILE
m -s all

Download: MAK-09-001.mk · MAK-09-001.sh

MAK-09-002

Separate recipe lines use separate shells

A shell variable set on the first recipe line is absent on the next. ${value-unset} gives the shell a visible default.

Makefile

all:
	@value=kept
	@printf "[%s]\n" "$${value-unset}"

Actual output

Captured run · exit 0

[unset]
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
all:
	@value=kept
	@printf "[%s]\n" "$${value-unset}"
MAKEFILE
m -s all

Download: MAK-09-002.mk · MAK-09-002.sh

MAK-09-003

Join commands that share state

Join commands with && to keep a directory change and its dependent command in the same shell. Each example starts in its own temporary directory.

Makefile

all:
	@mkdir -p sub
	@cd sub && printf "inside sub\n" > result.txt
	@cat sub/result.txt

Actual output

Captured run · exit 0

inside sub
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
all:
	@mkdir -p sub
	@cd sub && printf "inside sub\n" > result.txt
	@cat sub/result.txt
MAKEFILE
m -s all

Download: MAK-09-003.mk · MAK-09-003.sh

MAK-09-004

Use one shell for a complete recipe

.ONESHELL passes all recipe lines to one shell. Explicit -eu flags make an unset variable or a failed command stop the recipe.

Makefile

.ONESHELL:
SHELL := /bin/sh
.SHELLFLAGS := -eu -c
all:
	@value=kept
	printf "%s\n" "$$value"

Actual output

Captured run · exit 0

kept
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
.ONESHELL:
SHELL := /bin/sh
.SHELLFLAGS := -eu -c
all:
	@value=kept
	printf "%s\n" "$$value"
MAKEFILE
m -s all

Download: MAK-09-004.mk · MAK-09-004.sh

MAK-09-005

Choose a visible recipe prefix

.RECIPEPREFIX changes the recipe introducer from a tab to the first character of its value. The > prefix makes indentation visible.

Makefile

.RECIPEPREFIX := >
all:
>@printf "visible prefix\n"

Actual output

Captured run · exit 0

visible prefix
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
.RECIPEPREFIX := >
all:
>@printf "visible prefix\n"
MAKEFILE
m -s all

Download: MAK-09-005.mk · MAK-09-005.sh

MAK-09-006

Write a shell loop

The backslash continues the logical recipe line. $$item belongs to the shell loop, while Make processes the surrounding recipe first.

Makefile

all:
	@for item in alpha beta; do \
		printf "%s\n" "$$item"; \
	done

Actual output

Captured run · exit 0

alpha
beta
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
all:
	@for item in alpha beta; do \
		printf "%s\n" "$$item"; \
	done
MAKEFILE
m -s all

Download: MAK-09-006.mk · MAK-09-006.sh

10 - Includes and recursive Make

MAK-10-001

Include a small configuration file

include reads another makefile at this point. options.mk contains MODE = debug; the main file uses that setting.

Makefile

include options.mk
all:
	@printf "%s\n" "$(MODE)"

Actual output

Captured run · exit 0

debug
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > options.mk <<'INPUT_55_1'
MODE = debug
INPUT_55_1
cat > Makefile <<'MAKEFILE'
include options.mk
all:
	@printf "%s\n" "$(MODE)"
MAKEFILE
m -s all

Download: MAK-10-001.mk · MAK-10-001.sh

MAK-10-002

Allow an optional include

-include permits a missing optional file. The default remains available when local.mk has not been created.

Makefile

-include local.mk
MODE ?= release
all:
	@printf "%s\n" "$(MODE)"

Actual output

Captured run · exit 0

release
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
-include local.mk
MODE ?= release
all:
	@printf "%s\n" "$(MODE)"
MAKEFILE
m -s all

Download: MAK-10-002.mk · MAK-10-002.sh

MAK-10-003

Generate an included makefile

Make can build a missing included file, then restart reading its configuration. The generated file defines MODE. .DEFAULT_GOAL prevents the helper rule becoming the default.

Makefile

.DEFAULT_GOAL := all
include options.mk
options.mk:
	@printf "writing options\n"
	@printf "MODE = generated\n" > $@
all:
	@printf "%s\n" "$(MODE)"

Actual output

Captured run · exit 0

writing options
generated
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
.DEFAULT_GOAL := all
include options.mk
options.mk:
	@printf "writing options\n"
	@printf "MODE = generated\n" > $@
all:
	@printf "%s\n" "$(MODE)"
MAKEFILE
m -s all

Download: MAK-10-003.mk · MAK-10-003.sh

MAK-10-004

Read several files with -f

Repeated -f options read makefiles in order. extra.mk appends a word to the list defined in the displayed base.mk.

Makefile

WORDS := base
all:
	@printf "%s\n" "$(WORDS)"

Actual output

Captured run · exit 0

base extra
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > extra.mk <<'INPUT_58_1'
WORDS += extra
INPUT_58_1
cat > Makefile <<'MAKEFILE'
WORDS := base
all:
	@printf "%s\n" "$(WORDS)"
MAKEFILE
mv Makefile base.mk
m -s -f base.mk -f extra.mk all

Download: MAK-10-004.mk · MAK-10-004.sh

MAK-10-005

Delegate with $(MAKE)

$(MAKE) selects the same executable and marks a recursive invocation for Make. -C enters the child directory; the helper suppresses directory-announcement lines.

Makefile

.PHONY: all
all:
	@$(MAKE) -s -C child all

Actual output

Captured run · exit 0

child built
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
mkdir -p child
cat > child/Makefile <<'INPUT_59_1'
all:
	@printf "child built\n"
INPUT_59_1
cat > Makefile <<'MAKEFILE'
.PHONY: all
all:
	@$(MAKE) -s -C child all
MAKEFILE
m -s all

Download: MAK-10-005.mk · MAK-10-005.sh

MAK-10-006

Forward a command-line variable

Command-line assignments propagate to a recursive Make through its flags. The child Makefile uses a default that the parent invocation overrides.

Makefile

.PHONY: all
all:
	@$(MAKE) -s -C child all

Actual output

Captured run · exit 0

child sees Grace
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
mkdir -p child
cat > child/Makefile <<'INPUT_60_1'
NAME ?= Ada
all:
	@printf "child sees %s\n" "$(NAME)"
INPUT_60_1
cat > Makefile <<'MAKEFILE'
.PHONY: all
all:
	@$(MAKE) -s -C child all
MAKEFILE
m -s all NAME=Grace

Download: MAK-10-006.mk · MAK-10-006.sh

11 - Parallel builds

MAK-11-001

Build independent outputs concurrently

-j2 permits two recipes at once. Each writes its own file; the final cat uses a fixed order, independent of job scheduling.

Makefile

all: alpha.txt beta.txt
alpha.txt beta.txt:
	@printf "%s\n" "$@" > $@

Actual output

Captured run · exit 0

alpha.txt
beta.txt
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
all: alpha.txt beta.txt
alpha.txt beta.txt:
	@printf "%s\n" "$@" > $@
MAKEFILE
m -s -j2 all
cat alpha.txt beta.txt

Download: MAK-11-001.mk · MAK-11-001.sh

MAK-11-002

Dependencies enforce order under -j

Both outputs require prepared.txt. Their recipes cannot start before its recipe finishes, even with parallel execution enabled.

Makefile

all: alpha.txt beta.txt
alpha.txt beta.txt: prepared.txt
	@test -s prepared.txt
	@cp prepared.txt $@
prepared.txt:
	@printf "prepared\n" > $@

Actual output

Captured run · exit 0

prepared
prepared
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
all: alpha.txt beta.txt
alpha.txt beta.txt: prepared.txt
	@test -s prepared.txt
	@cp prepared.txt $@
prepared.txt:
	@printf "prepared\n" > $@
MAKEFILE
m -s -j2 all
cat alpha.txt beta.txt

Download: MAK-11-002.mk · MAK-11-002.sh

MAK-11-003

Group output per target

--output-sync=target keeps each recipe output together. The order of the left and right blocks may vary; both complete orders are accepted.

Makefile

all: left right
.PHONY: all left right
left right:
	@printf "%s begin\n" "$@"
	@printf "%s end\n" "$@"

Actual output

Captured run · exit 0

left begin
left end
right begin
right end
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
all: left right
.PHONY: all left right
left right:
	@printf "%s begin\n" "$@"
	@printf "%s end\n" "$@"
MAKEFILE
m -s -j2 --output-sync=target all

Download: MAK-11-003.mk · MAK-11-003.sh

MAK-11-004

Use one job for serial output

-j1 requests one job at a time. With this prerequisite list, the two command goals finish in left-to-right order.

Makefile

.PHONY: all left right
all: left right
left right:
	@printf "%s\n" "$@"

Actual output

Captured run · exit 0

left
right
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
.PHONY: all left right
all: left right
left right:
	@printf "%s\n" "$@"
MAKEFILE
m -s -j1 all

Download: MAK-11-004.mk · MAK-11-004.sh

MAK-11-005

Disable parallel execution for an invocation

An empty .NOTPARALLEL target serializes this Make invocation even when -j2 is supplied. Recursive invocations have their own parallel policy.

Makefile

.NOTPARALLEL:
.PHONY: all left right
all: left right
left right:
	@printf "%s\n" "$@"

Actual output

Captured run · exit 0

left
right
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
.NOTPARALLEL:
.PHONY: all left right
all: left right
left right:
	@printf "%s\n" "$@"
MAKEFILE
m -s -j2 all

Download: MAK-11-005.mk · MAK-11-005.sh

MAK-11-006

Share jobs with recursive Make

Use $(MAKE) for recursion so the child participates in the jobserver. Avoid adding an independent -j value to every child. Each module writes its own output.

Makefile

.PHONY: all alpha beta
all: alpha beta
alpha beta:
	@$(MAKE) -s -C $@ all

Actual output

Captured run · exit 0

alpha
beta
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
mkdir -p alpha
cat > alpha/Makefile <<'INPUT_66_1'
all:
	@printf "alpha\n" > result.txt
INPUT_66_1
mkdir -p beta
cat > beta/Makefile <<'INPUT_66_2'
all:
	@printf "beta\n" > result.txt
INPUT_66_2
cat > Makefile <<'MAKEFILE'
.PHONY: all alpha beta
all: alpha beta
alpha beta:
	@$(MAKE) -s -C $@ all
MAKEFILE
m -s -j2 all
cat alpha/result.txt beta/result.txt

Download: MAK-11-006.mk · MAK-11-006.sh

12 - Inspect and diagnose a build

MAK-12-001

Preview an ordinary recipe with -n

-n prints this ordinary recipe without running it. The follow-up test confirms that result.txt was not created. Recursive $(MAKE), + recipes and makefile remakes have special dry-run behavior.

Makefile

result.txt:
	@printf "generated\n" > result.txt

Actual output

Captured run · exit 0

printf "generated\n" > result.txt
file remains absent
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
result.txt:
	@printf "generated\n" > result.txt
MAKEFILE
m -n result.txt
test ! -e result.txt
printf "file remains absent\n"

Download: MAK-12-001.mk · MAK-12-001.sh

MAK-12-002

Ask whether an output is current

-q runs no ordinary recipe: exit 0 means current, 1 means a rebuild is needed, and 2 means an error. The script distinguishes these statuses rather than treating every failure as stale.

Makefile

result.txt: input.txt
	@cp $< $@

Actual output

Captured run · exit 0

needs rebuild
current
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > input.txt <<'INPUT_68_1'
record
INPUT_68_1
cat > Makefile <<'MAKEFILE'
result.txt: input.txt
	@cp $< $@
MAKEFILE
if m -s -q result.txt; then printf "current\n"; else status=$?; test "$status" = 1; printf "needs rebuild\n"; fi
m -s result.txt
if m -s -q result.txt; then printf "current\n"; else exit 1; fi

Download: MAK-12-002.mk · MAK-12-002.sh

MAK-12-003

Trace why a recipe runs

--trace reports the rule and the prerequisite that prompted rebuilding. The script replaces the numeric line location with LINE for a stable transcript.

Makefile

copy.txt: source.txt
	@cp source.txt copy.txt

Actual output

Captured run · exit 0

Makefile:LINE: update target 'copy.txt' due to: source.txt
cp source.txt copy.txt
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > source.txt <<'INPUT_69_1'
record
INPUT_69_1
cat > Makefile <<'MAKEFILE'
copy.txt: source.txt
	@cp source.txt copy.txt
MAKEFILE
cp source.txt copy.txt
touch -t 202001010101 copy.txt
touch -t 202101010101 source.txt
m --trace copy.txt | sed -E "s/Makefile:[0-9]+:/Makefile:LINE:/g"

Download: MAK-12-003.mk · MAK-12-003.sh

MAK-12-004

Print a parse-time diagnostic

$(info ...) writes a message as the file is read. This precedes the target recipe and helps inspect an expanded variable.

Makefile

MODE := debug
$(info reading MODE=$(MODE))
all:
	@printf "running target\n"

Actual output

Captured run · exit 0

reading MODE=debug
running target
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
MODE := debug
$(info reading MODE=$(MODE))
all:
	@printf "running target\n"
MAKEFILE
m -s all

Download: MAK-12-004.mk · MAK-12-004.sh

MAK-12-005

Reject an invalid configuration

This is an intentional error. $(error ...) stops reading the file before the recipe can run; Make returns status 2. The captured standard error explains the missing setting.

Makefile

ifeq ($(strip $(INPUT)),)
$(error INPUT is required)
endif
all:
	@printf "would run\n"

Actual output

Captured run · exit 2

(no stdout)

Standard error

Makefile:2: *** INPUT is required.  Stop.
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
ifeq ($(strip $(INPUT)),)
$(error INPUT is required)
endif
all:
	@printf "would run\n"
MAKEFILE
m -s all

Download: MAK-12-005.mk · MAK-12-005.sh

MAK-12-006

Diagnose spaces where a tab is required

This is an intentional syntax error: the recipe begins with four spaces. Make reports missing separator and exits 2. Compare with MAK-01-001 or the visible prefix in MAK-09-005.

Makefile

all:
    printf "bad indentation\n"

Actual output

Captured run · exit 2

(no stdout)

Standard error

Makefile:2: *** missing separator.  Stop.
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
all:
    printf "bad indentation\n"
MAKEFILE
m -s all

Download: MAK-12-006.mk · MAK-12-006.sh

13 - Reliable outputs and a complete workflow

MAK-13-001

Delete a target when its recipe fails

.DELETE_ON_ERROR removes an output modified by a failed recipe. The script verifies that the incomplete file is gone; this expected failure is caught explicitly.

Makefile

.DELETE_ON_ERROR:
report.txt:
	@printf "incomplete\n" > $@
	@false

Actual output

Captured run · exit 0

partial output removed
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > Makefile <<'MAKEFILE'
.DELETE_ON_ERROR:
report.txt:
	@printf "incomplete\n" > $@
	@false
MAKEFILE
if m -s report.txt 2> failure.txt; then exit 1; else status=$?; test "$status" = 2; fi
test ! -e report.txt
printf "partial output removed\n"

Download: MAK-13-001.mk · MAK-13-001.sh

MAK-13-002

Publish an output with an atomic rename

Write to a temporary sibling first, then rename it only after success. A simulated failure before mv preserves the previously complete report.

Makefile

.PHONY: all broken
all: report.txt
report.txt: input.txt
	@cp $< $@.tmp && mv $@.tmp $@
broken:
	@printf "partial\n" > report.txt.tmp; false

Actual output

Captured run · exit 0

complete record
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > input.txt <<'INPUT_74_1'
complete record
INPUT_74_1
cat > Makefile <<'MAKEFILE'
.PHONY: all broken
all: report.txt
report.txt: input.txt
	@cp $< $@.tmp && mv $@.tmp $@
broken:
	@printf "partial\n" > report.txt.tmp; false
MAKEFILE
m -s all
if m -s broken 2> failure.txt; then exit 1; else test "$?" = 2; fi
cat report.txt

Download: MAK-13-002.mk · MAK-13-002.sh

MAK-13-003

Declare outputs produced together

&: is a grouped-target rule, available in GNU Make 4.3 and later. One invocation creates both files. Asking for both with -j2 must still run the recipe only once.

Makefile

all: summary.txt count.txt
summary.txt count.txt &: input.txt
	@printf "one grouped recipe\n"
	@cp input.txt summary.txt
	@wc -l < input.txt | tr -d " " > count.txt

Actual output

Captured run · exit 0

one grouped recipe
Ada
Grace
2
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > input.txt <<'INPUT_75_1'
Ada
Grace
INPUT_75_1
cat > Makefile <<'MAKEFILE'
all: summary.txt count.txt
summary.txt count.txt &: input.txt
	@printf "one grouped recipe\n"
	@cp input.txt summary.txt
	@wc -l < input.txt | tr -d " " > count.txt
MAKEFILE
m -s -j2 all
cat summary.txt count.txt

Download: MAK-13-003.mk · MAK-13-003.sh

MAK-13-004

Connect data, AWK, gnuplot and HTML

A complete dependency graph aggregates three input records, plots the totals and writes a linked HTML report. data.tsv contains books 4, pens 2 and books 3. The full runnable example includes plot.gp. Run: make report.html.

Makefile

.DEFAULT_GOAL := report.html
totals.tsv: data.tsv
	@printf "aggregate data\n"
	@awk '{sum[$$1]+=$$2} END {for (key in sum) print key, sum[key]}' $< | sort > $@
chart.svg: totals.tsv plot.gp
	@printf "draw chart\n"
	@gnuplot plot.gp
report.html: chart.svg
	@printf "publish report\n"
	@printf '<!doctype html><title>Make report</title><h1>Totals</h1><img src="chart.svg" alt="Totals by category">\n' > $@

Actual output

Captured run · exit 0

aggregate data
draw chart
publish report
books 7
pens 2
HTML and SVG ready
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > data.tsv <<'INPUT_76_1'
books 4
pens 2
books 3
INPUT_76_1
cat > plot.gp <<'INPUT_76_2'
set terminal svg size 640,360
set output 'chart.svg'
set style fill solid 0.7
set boxwidth 0.6
plot 'totals.tsv' using 2:xticlabels(1) with boxes title 'Totals'
INPUT_76_2
cat > Makefile <<'MAKEFILE'
.DEFAULT_GOAL := report.html
totals.tsv: data.tsv
	@printf "aggregate data\n"
	@awk '{sum[$$1]+=$$2} END {for (key in sum) print key, sum[key]}' $< | sort > $@
chart.svg: totals.tsv plot.gp
	@printf "draw chart\n"
	@gnuplot plot.gp
report.html: chart.svg
	@printf "publish report\n"
	@printf '<!doctype html><title>Make report</title><h1>Totals</h1><img src="chart.svg" alt="Totals by category">\n' > $@
MAKEFILE
m -s report.html
cat totals.tsv
test -s chart.svg
test -s report.html
printf "HTML and SVG ready\n"

Download: MAK-13-004.mk · MAK-13-004.sh

MAK-13-005

Rebuild only a changed downstream branch

Changing plot.gp requires a new chart and HTML report, while totals.tsv remains current. Fixed past timestamps make the dependency test repeatable without sleeping.

Makefile

.DEFAULT_GOAL := report.html
totals.tsv: data.tsv
	@printf "aggregate data\n"
	@awk '{sum[$$1]+=$$2} END {for (key in sum) print key, sum[key]}' $< | sort > $@
chart.svg: totals.tsv plot.gp
	@printf "draw chart\n"
	@gnuplot plot.gp
report.html: chart.svg
	@printf "publish report\n"
	@printf '<!doctype html><title>Make report</title><h1>Totals</h1><img src="chart.svg" alt="Totals by category">\n' > $@

Actual output

Captured run · exit 0

aggregate data
draw chart
publish report
change only plotting settings
draw chart
publish report
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > data.tsv <<'INPUT_77_1'
books 4
pens 2
books 3
INPUT_77_1
cat > plot.gp <<'INPUT_77_2'
set terminal svg size 640,360
set output 'chart.svg'
set style fill solid 0.7
set boxwidth 0.6
plot 'totals.tsv' using 2:xticlabels(1) with boxes title 'Totals'
INPUT_77_2
cat > Makefile <<'MAKEFILE'
.DEFAULT_GOAL := report.html
totals.tsv: data.tsv
	@printf "aggregate data\n"
	@awk '{sum[$$1]+=$$2} END {for (key in sum) print key, sum[key]}' $< | sort > $@
chart.svg: totals.tsv plot.gp
	@printf "draw chart\n"
	@gnuplot plot.gp
report.html: chart.svg
	@printf "publish report\n"
	@printf '<!doctype html><title>Make report</title><h1>Totals</h1><img src="chart.svg" alt="Totals by category">\n' > $@
MAKEFILE
m -s report.html
sed "s/size 640,360/size 800,450/" plot.gp > plot.gp.tmp
mv plot.gp.tmp plot.gp
touch -t 202001010101 totals.tsv chart.svg report.html data.tsv
touch -t 202101010101 plot.gp
printf "change only plotting settings\n"
m -s report.html
grep -q 'viewBox="0 0 800 450"' chart.svg

Download: MAK-13-005.mk · MAK-13-005.sh

MAK-13-006

Stage and package a local website

Stage only public HTML and SVG files, then archive that directory. This example performs no network deployment. Keep actual SSH uploads as a separate, explicit action.

Makefile

.DEFAULT_GOAL := report.html
totals.tsv: data.tsv
	@printf "aggregate data\n"
	@awk '{sum[$$1]+=$$2} END {for (key in sum) print key, sum[key]}' $< | sort > $@
chart.svg: totals.tsv plot.gp
	@printf "draw chart\n"
	@gnuplot plot.gp
report.html: chart.svg
	@printf "publish report\n"
	@printf '<!doctype html><title>Make report</title><h1>Totals</h1><img src="chart.svg" alt="Totals by category">\n' > $@

.PHONY: stage package
stage: report.html
	@mkdir -p public
	@cp report.html public/index.html
	@cp chart.svg public/chart.svg
package: stage
	@tar -czf website.tar.gz -C public .

Actual output

Captured run · exit 0

aggregate data
draw chart
publish report
public/chart.svg
public/index.html
website.tar.gz ready
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > data.tsv <<'INPUT_78_1'
books 4
pens 2
books 3
INPUT_78_1
cat > plot.gp <<'INPUT_78_2'
set terminal svg size 640,360
set output 'chart.svg'
set style fill solid 0.7
set boxwidth 0.6
plot 'totals.tsv' using 2:xticlabels(1) with boxes title 'Totals'
INPUT_78_2
cat > Makefile <<'MAKEFILE'
.DEFAULT_GOAL := report.html
totals.tsv: data.tsv
	@printf "aggregate data\n"
	@awk '{sum[$$1]+=$$2} END {for (key in sum) print key, sum[key]}' $< | sort > $@
chart.svg: totals.tsv plot.gp
	@printf "draw chart\n"
	@gnuplot plot.gp
report.html: chart.svg
	@printf "publish report\n"
	@printf '<!doctype html><title>Make report</title><h1>Totals</h1><img src="chart.svg" alt="Totals by category">\n' > $@

.PHONY: stage package
stage: report.html
	@mkdir -p public
	@cp report.html public/index.html
	@cp chart.svg public/chart.svg
package: stage
	@tar -czf website.tar.gz -C public .
MAKEFILE
m -s package
test -s website.tar.gz
find public -type f | sort
printf "website.tar.gz ready\n"

Download: MAK-13-006.mk · MAK-13-006.sh

14 - LaTeX documents, references and generated figures

MAK-14-001

Build a PDF with latexmk

Make coordinates the project; latexmk runs the TeX passes needed for a stable PDF. The real target is build/main.pdf, with build as an order-only prerequisite. The recipe keeps auxiliary files and compiler diagnostics in build/. .DELETE_ON_ERROR removes a file target modified by a failed recipe. If compilation fails, it prints the saved log and returns failure. The driver checks the actual PDF text. Run: make all. The -norc flag makes this demonstration independent of personal latexmk settings.

Makefile

LATEXMK ?= latexmk
FLAGS := -norc -pdf -no-shell-escape
FLAGS += -interaction=nonstopmode -halt-on-error
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
.PHONY: all
all: build/main.pdf
build/main.pdf: main.tex | build
	@printf "compile main.tex\n"
	@$(LATEXMK) $(FLAGS) -outdir=build $< > build/latexmk.log 2>&1 || \
	  { cat build/latexmk.log >&2; exit 1; }
build:
	@mkdir -p $@

Actual output

Captured run · exit 0

compile main.tex
PDF text verified; second build is current
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > main.tex <<'INPUT_79_1'
\documentclass{article}
\begin{document}
Hello from a Make-built document.
\end{document}
INPUT_79_1
cat > Makefile <<'MAKEFILE'
LATEXMK ?= latexmk
FLAGS := -norc -pdf -no-shell-escape
FLAGS += -interaction=nonstopmode -halt-on-error
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
.PHONY: all
all: build/main.pdf
build/main.pdf: main.tex | build
	@printf "compile main.tex\n"
	@$(LATEXMK) $(FLAGS) -outdir=build $< > build/latexmk.log 2>&1 || \
	  { cat build/latexmk.log >&2; exit 1; }
build:
	@mkdir -p $@
MAKEFILE
m -s all
test -s build/main.pdf
pdftotext build/main.pdf text.txt
grep -q 'Hello from a Make-built document.' text.txt
m -q all
printf "PDF text verified; second build is current\n"

Download: MAK-14-001.mk · MAK-14-001.sh

MAK-14-002

Rebuild after editing an included chapter

A file target needs every relevant input in its prerequisites, including included chapters and custom .cls/.sty files. This small project lists sections/*.tex explicitly with wildcard. The driver changes the chapter text, assigns controlled past timestamps and verifies that the new text reaches the PDF. Wildcard covers existing files in this directory; it does not discover arbitrary nested inputs. See MAK-14-003 for delegation to latexmk.

Makefile

LATEXMK ?= latexmk
FLAGS := -norc -pdf -no-shell-escape
FLAGS += -interaction=nonstopmode -halt-on-error
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
CHAPTERS := $(wildcard sections/*.tex)
.PHONY: all
all: build/main.pdf
build/main.pdf: main.tex $(CHAPTERS) | build
	@printf "compile document and chapters\n"
	@$(LATEXMK) $(FLAGS) -outdir=build main.tex > build/latexmk.log 2>&1 || \
	  { cat build/latexmk.log >&2; exit 1; }
build:
	@mkdir -p $@

Actual output

Captured run · exit 0

compile document and chapters
compile document and chapters
edited chapter appears in the PDF
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > main.tex <<'INPUT_80_1'
\documentclass{article}
\begin{document}
\input{sections/intro}
\end{document}
INPUT_80_1
mkdir -p sections
cat > sections/intro.tex <<'INPUT_80_2'
Original chapter text.
INPUT_80_2
cat > Makefile <<'MAKEFILE'
LATEXMK ?= latexmk
FLAGS := -norc -pdf -no-shell-escape
FLAGS += -interaction=nonstopmode -halt-on-error
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
CHAPTERS := $(wildcard sections/*.tex)
.PHONY: all
all: build/main.pdf
build/main.pdf: main.tex $(CHAPTERS) | build
	@printf "compile document and chapters\n"
	@$(LATEXMK) $(FLAGS) -outdir=build main.tex > build/latexmk.log 2>&1 || \
	  { cat build/latexmk.log >&2; exit 1; }
build:
	@mkdir -p $@
MAKEFILE
m -s all
m -q all
printf "Revised chapter text.\n" > sections/intro.tex
touch -t 202001010101 main.tex build/main.pdf
touch -t 202101010101 sections/intro.tex
m -s all
pdftotext build/main.pdf text.txt
grep -q 'Revised chapter text.' text.txt
if grep -q 'Original chapter text.' text.txt; then exit 1; fi
printf "edited chapter appears in the PDF\n"

Download: MAK-14-002.mk · MAK-14-002.sh

MAK-14-003

Let latexmk discover inputs; preview a watch command

The pdf goal is phony, so Make always asks latexmk to inspect the document. Latexmk tracks inputs and reruns TeX only when needed; the driver verifies an unchanged TeX log on the second invocation, then changes an included chapter. This avoids maintaining a second list of nested TeX inputs. Declare generated figures or tables as pdf prerequisites so Make creates them first. The watch goal uses -pvc for continuous preview. Only make -n watch is exercised here: run make watch interactively and stop it with Ctrl-C. A latexmk watcher does not itself invoke unrelated Make rules when their source data changes.

Makefile

LATEXMK ?= latexmk
FLAGS := -norc -pdf -no-shell-escape
FLAGS += -interaction=nonstopmode -halt-on-error
.DELETE_ON_ERROR:
.DEFAULT_GOAL := pdf
.PHONY: pdf watch
pdf: | build
	@$(LATEXMK) $(FLAGS) -outdir=build main.tex > build/latexmk.log 2>&1 || \
	  { cat build/latexmk.log >&2; exit 1; }
watch: | build
	@$(LATEXMK) $(FLAGS) -outdir=build -pvc main.tex
build:
	@mkdir -p $@

Actual output

Captured run · exit 0

unchanged input: TeX engine skipped
changed input: PDF updated
watch command checked without starting a watcher
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > main.tex <<'INPUT_81_1'
\documentclass{article}
\begin{document}
\input{sections/intro}
\end{document}
INPUT_81_1
mkdir -p sections
cat > sections/intro.tex <<'INPUT_81_2'
First discovered chapter.
INPUT_81_2
cat > Makefile <<'MAKEFILE'
LATEXMK ?= latexmk
FLAGS := -norc -pdf -no-shell-escape
FLAGS += -interaction=nonstopmode -halt-on-error
.DELETE_ON_ERROR:
.DEFAULT_GOAL := pdf
.PHONY: pdf watch
pdf: | build
	@$(LATEXMK) $(FLAGS) -outdir=build main.tex > build/latexmk.log 2>&1 || \
	  { cat build/latexmk.log >&2; exit 1; }
watch: | build
	@$(LATEXMK) $(FLAGS) -outdir=build -pvc main.tex
build:
	@mkdir -p $@
MAKEFILE
m -s pdf
cp build/main.log first.log
m -s pdf
cmp first.log build/main.log
printf "unchanged input: TeX engine skipped\n"
printf "Changed discovered chapter.\n" > sections/intro.tex
m -s pdf
pdftotext build/main.pdf text.txt
grep -q 'Changed discovered chapter.' text.txt
printf "changed input: PDF updated\n"
m -n watch > watch-command.txt
grep -q -- '-pvc main.tex' watch-command.txt
printf "watch command checked without starting a watcher\n"

Download: MAK-14-003.mk · MAK-14-003.sh

MAK-14-004

Resolve a classic BibTeX bibliography

List the .bib database as a prerequisite and let latexmk choose the BibTeX and TeX passes. The sample cites a fictional book defined in references.bib. The driver checks the .bbl, the final PDF text and the absence of unresolved citations in the final TeX log. When using a local .bst file, add it to the prerequisites too. Run: make all.

Makefile

LATEXMK ?= latexmk
FLAGS := -norc -pdf -no-shell-escape
FLAGS += -interaction=nonstopmode -halt-on-error
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
.PHONY: all
all: build/main.pdf
build/main.pdf: main.tex references.bib | build
	@$(LATEXMK) $(FLAGS) -outdir=build main.tex > build/latexmk.log 2>&1 || \
	  { cat build/latexmk.log >&2; exit 1; }
build:
	@mkdir -p $@

Actual output

Captured run · exit 0

BibTeX bibliography resolved and PDF verified
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > main.tex <<'INPUT_82_1'
\documentclass{article}
\begin{document}
This fictional example cites a notebook~\cite{bossert2026}.
\bibliographystyle{plain}
\bibliography{references}
\end{document}
INPUT_82_1
cat > references.bib <<'INPUT_82_2'
@book{bossert2026,
  author = {Lukas C. Bossert},
  title = {A Small Build Notebook},
  year = {2026},
  publisher = {Tour Examples}
}
INPUT_82_2
cat > Makefile <<'MAKEFILE'
LATEXMK ?= latexmk
FLAGS := -norc -pdf -no-shell-escape
FLAGS += -interaction=nonstopmode -halt-on-error
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
.PHONY: all
all: build/main.pdf
build/main.pdf: main.tex references.bib | build
	@$(LATEXMK) $(FLAGS) -outdir=build main.tex > build/latexmk.log 2>&1 || \
	  { cat build/latexmk.log >&2; exit 1; }
build:
	@mkdir -p $@
MAKEFILE
m -s all
test -s build/main.bbl
grep -q 'bossert2026' build/main.bbl
pdftotext build/main.pdf text.txt
grep -q 'Small Build Notebook' text.txt
if grep -Eq 'undefined citations|Citation .* undefined' build/main.log; then exit 1; fi
m -q all
printf "BibTeX bibliography resolved and PDF verified\n"

Download: MAK-14-004.mk · MAK-14-004.sh

MAK-14-005

Use biblatex and Biber, then update a citation

With biblatex configured for backend=biber, latexmk detects and runs Biber automatically. Do not also run BibTeX on the same auxiliary data. The driver builds a real citation, changes the database title and verifies the revised reference in the PDF. This example needs compatible biblatex and Biber installations, normally supplied together by a TeX distribution. The bibliography entry is fictional.

Makefile

LATEXMK ?= latexmk
FLAGS := -norc -pdf -no-shell-escape
FLAGS += -interaction=nonstopmode -halt-on-error
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
.PHONY: all
all: build/main.pdf
build/main.pdf: main.tex references.bib | build
	@$(LATEXMK) $(FLAGS) -outdir=build main.tex > build/latexmk.log 2>&1 || \
	  { cat build/latexmk.log >&2; exit 1; }
build:
	@mkdir -p $@

Actual output

Captured run · exit 0

Biber citation resolved
changed bibliography appears in the PDF
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > main.tex <<'INPUT_83_1'
\documentclass{article}
\usepackage[backend=biber,style=numeric]{biblatex}
\addbibresource{references.bib}
\begin{document}
This fictional example cites a notebook~\cite{bossert2026}.
\printbibliography
\end{document}
INPUT_83_1
cat > references.bib <<'INPUT_83_2'
@book{bossert2026,
  author = {Lukas C. Bossert},
  title = {A Small Build Notebook},
  year = {2026},
  publisher = {Tour Examples}
}
INPUT_83_2
cat > Makefile <<'MAKEFILE'
LATEXMK ?= latexmk
FLAGS := -norc -pdf -no-shell-escape
FLAGS += -interaction=nonstopmode -halt-on-error
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
.PHONY: all
all: build/main.pdf
build/main.pdf: main.tex references.bib | build
	@$(LATEXMK) $(FLAGS) -outdir=build main.tex > build/latexmk.log 2>&1 || \
	  { cat build/latexmk.log >&2; exit 1; }
build:
	@mkdir -p $@
MAKEFILE
m -s all
test -s build/main.bcf
test -s build/main.bbl
grep -q 'bossert2026' build/main.bbl
pdftotext build/main.pdf first.txt
grep -q 'Small Build Notebook' first.txt
printf "Biber citation resolved\n"
sed 's/Small Build Notebook/Revised Build Notebook/' references.bib > references.bib.tmp
mv references.bib.tmp references.bib
touch -t 202001010101 main.tex build/main.pdf
touch -t 202101010101 references.bib
m -s all
pdftotext build/main.pdf revised.txt
grep -q 'Revised Build Notebook' revised.txt
if grep -Eq 'undefined citations|Please .*run Biber' build/main.log; then exit 1; fi
printf "changed bibliography appears in the PDF\n"

Download: MAK-14-005.mk · MAK-14-005.sh

MAK-14-006

Build a gnuplot figure before the document

Make owns the graph data.tsv -> figures/chart.pdf -> build/main.pdf. gnuplot uses the pdfcairo terminal; graphicx includes that generated PDF without TeX shell escape. Both the data and plotting script are prerequisites of the figure. The driver changes one value and verifies that the figure and document really rebuild. Keep generated figures as file targets so unchanged ones can be reused. Run: make all.

Makefile

LATEXMK ?= latexmk
FLAGS := -norc -pdf -no-shell-escape
FLAGS += -interaction=nonstopmode -halt-on-error
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
.PHONY: all
all: build/main.pdf
figures/chart.pdf: data.tsv plot.gp | figures
	@printf "generate PDF figure\n"
	@gnuplot plot.gp
build/main.pdf: main.tex figures/chart.pdf | build
	@printf "compile document with figure\n"
	@$(LATEXMK) $(FLAGS) -outdir=build main.tex > build/latexmk.log 2>&1 || \
	  { cat build/latexmk.log >&2; exit 1; }
build figures:
	@mkdir -p $@

Actual output

Captured run · exit 0

generate PDF figure
compile document with figure
generate PDF figure
compile document with figure
updated figure and document verified
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > main.tex <<'INPUT_84_1'
\documentclass{article}
\usepackage{graphicx}
\begin{document}
\section*{Measured values}
\includegraphics[width=0.8\linewidth]{figures/chart.pdf}
\end{document}
INPUT_84_1
cat > data.tsv <<'INPUT_84_2'
1 2
2 4
3 3
INPUT_84_2
cat > plot.gp <<'INPUT_84_3'
set terminal pdfcairo size 5in,3in
set output 'figures/chart.pdf'
set xlabel 'Sample'
set ylabel 'Value'
set yrange [0:6]
plot 'data.tsv' using 1:2 with linespoints title 'Measurements'
INPUT_84_3
cat > Makefile <<'MAKEFILE'
LATEXMK ?= latexmk
FLAGS := -norc -pdf -no-shell-escape
FLAGS += -interaction=nonstopmode -halt-on-error
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
.PHONY: all
all: build/main.pdf
figures/chart.pdf: data.tsv plot.gp | figures
	@printf "generate PDF figure\n"
	@gnuplot plot.gp
build/main.pdf: main.tex figures/chart.pdf | build
	@printf "compile document with figure\n"
	@$(LATEXMK) $(FLAGS) -outdir=build main.tex > build/latexmk.log 2>&1 || \
	  { cat build/latexmk.log >&2; exit 1; }
build figures:
	@mkdir -p $@
MAKEFILE
m -s all
m -q all
cp figures/chart.pdf first-chart.pdf
printf "1 2\n2 5\n3 3\n" > data.tsv
touch -t 202001010101 plot.gp main.tex figures/chart.pdf build/main.pdf
touch -t 202101010101 data.tsv
m -s all
if cmp -s first-chart.pdf figures/chart.pdf; then exit 1; fi
pdftotext build/main.pdf text.txt
grep -q 'Measured values' text.txt
grep -q 'Measurements' text.txt
printf "updated figure and document verified\n"

Download: MAK-14-006.mk · MAK-14-006.sh

MAK-14-007

Generate a LaTeX table from data with AWK

A generated table is a normal prerequisite of the PDF. The AWK recipe emits rows for a tabular environment, writes a temporary sibling and renames it on success. $$1 and $$2 survive Make expansion as AWK field references. This fixture accepts only plain alphabetic labels and integer values; arbitrary text needs LaTeX escaping. The driver changes books from 4 to 7 and checks both the table source and PDF text.

Makefile

LATEXMK ?= latexmk
FLAGS := -norc -pdf -no-shell-escape
FLAGS += -interaction=nonstopmode -halt-on-error
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
.PHONY: all
all: build/main.pdf
tables/rows.tex: data.tsv | tables
	@printf "generate table rows\n"
	@awk '{printf "%s & %d \\\\\n", $$1, $$2}' $< > $@.tmp && mv $@.tmp $@
build/main.pdf: main.tex tables/rows.tex | build
	@printf "compile document with table\n"
	@$(LATEXMK) $(FLAGS) -outdir=build main.tex > build/latexmk.log 2>&1 || \
	  { cat build/latexmk.log >&2; exit 1; }
build tables:
	@mkdir -p $@

Actual output

Captured run · exit 0

generate table rows
compile document with table
generate table rows
compile document with table
edited data appears in the table and PDF
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > main.tex <<'INPUT_85_1'
\documentclass{article}
\begin{document}
\section*{Stock counts}
\begin{tabular}{lr}
Item & Count \\
\hline
\input{tables/rows}
\end{tabular}
\end{document}
INPUT_85_1
cat > data.tsv <<'INPUT_85_2'
books 4
pens 2
INPUT_85_2
cat > Makefile <<'MAKEFILE'
LATEXMK ?= latexmk
FLAGS := -norc -pdf -no-shell-escape
FLAGS += -interaction=nonstopmode -halt-on-error
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
.PHONY: all
all: build/main.pdf
tables/rows.tex: data.tsv | tables
	@printf "generate table rows\n"
	@awk '{printf "%s & %d \\\\\n", $$1, $$2}' $< > $@.tmp && mv $@.tmp $@
build/main.pdf: main.tex tables/rows.tex | build
	@printf "compile document with table\n"
	@$(LATEXMK) $(FLAGS) -outdir=build main.tex > build/latexmk.log 2>&1 || \
	  { cat build/latexmk.log >&2; exit 1; }
build tables:
	@mkdir -p $@
MAKEFILE
m -s all
m -q all
printf "books 7\npens 2\n" > data.tsv
touch -t 202001010101 main.tex tables/rows.tex build/main.pdf
touch -t 202101010101 data.tsv
m -s all
grep -Fq 'books & 7 \\' tables/rows.tex
pdftotext -layout build/main.pdf text.txt
grep -Eq 'books[[:space:]]+7' text.txt
printf "edited data appears in the table and PDF\n"

Download: MAK-14-007.mk · MAK-14-007.sh

MAK-14-008

Keep draft and final builds in separate directories

draft.tex defines DraftVersion and both wrappers input main.tex. The pattern rule uses the stem ($*) to choose a wrapper, a separate output directory ($(@D)) for each variant, and jobname=main for a consistent PDF filename. Auxiliary state is never shared between draft and final. The driver inspects both PDFs, then confirms that make -q all is current. Run: make draft, make final or make all.

Makefile

LATEXMK ?= latexmk
FLAGS := -norc -pdf -no-shell-escape
FLAGS += -interaction=nonstopmode -halt-on-error
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
.PHONY: all draft final
all: draft final
draft: build/draft/main.pdf
final: build/final/main.pdf
build/%/main.pdf: %.tex main.tex
	@mkdir -p $(@D)
	@$(LATEXMK) $(FLAGS) -outdir=$(@D) -jobname=main $*.tex > $(@D)/latexmk.log 2>&1 || \
	  { cat $(@D)/latexmk.log >&2; exit 1; }

Actual output

Captured run · exit 0

draft and final PDFs have independent auxiliary state
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > draft.tex <<'INPUT_86_1'
\def\DraftVersion{1}
\input{main}
INPUT_86_1
cat > final.tex <<'INPUT_86_2'
\input{main}
INPUT_86_2
cat > main.tex <<'INPUT_86_3'
\documentclass{article}
\begin{document}
\ifdefined\DraftVersion
Draft version: comments are visible.
\else
Final version: ready for readers.
\fi
\end{document}
INPUT_86_3
cat > Makefile <<'MAKEFILE'
LATEXMK ?= latexmk
FLAGS := -norc -pdf -no-shell-escape
FLAGS += -interaction=nonstopmode -halt-on-error
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
.PHONY: all draft final
all: draft final
draft: build/draft/main.pdf
final: build/final/main.pdf
build/%/main.pdf: %.tex main.tex
	@mkdir -p $(@D)
	@$(LATEXMK) $(FLAGS) -outdir=$(@D) -jobname=main $*.tex > $(@D)/latexmk.log 2>&1 || \
	  { cat $(@D)/latexmk.log >&2; exit 1; }
MAKEFILE
m -s all
pdftotext build/draft/main.pdf draft.txt
pdftotext build/final/main.pdf final.txt
grep -q 'Draft version: comments are visible.' draft.txt
grep -q 'Final version: ready for readers.' final.txt
test -s build/draft/main.aux
test -s build/final/main.aux
m -q all
printf "draft and final PDFs have independent auxiliary state\n"

Download: MAK-14-008.mk · MAK-14-008.sh

MAK-14-009

Track project compiler settings and resolve references

Put project settings in a versioned latexmkrc and load it explicitly with -norc -r. List the settings file as a prerequisite so editing it schedules another build. The Perl %O and %S placeholders are latexmk command substitutions. This document contains a section reference; latexmk performs the passes needed to resolve it. The driver checks the final PDF and rejects unresolved-reference warnings.

Makefile

LATEXMK ?= latexmk
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
.PHONY: all
all: build/main.pdf
build/main.pdf: main.tex latexmkrc | build
	@$(LATEXMK) -norc -r latexmkrc -pdf -outdir=build main.tex > build/latexmk.log 2>&1 || \
	  { cat build/latexmk.log >&2; exit 1; }
build:
	@mkdir -p $@

Actual output

Captured run · exit 0

project settings loaded; section reference resolved
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > main.tex <<'INPUT_87_1'
\documentclass{article}
\begin{document}
\section{Introduction}\label{sec:intro}
See section~\ref{sec:intro}.
\end{document}
INPUT_87_1
cat > latexmkrc <<'INPUT_87_2'
$pdflatex = 'pdflatex -no-shell-escape -interaction=nonstopmode -halt-on-error %O %S';
$max_repeat = 5;
INPUT_87_2
cat > Makefile <<'MAKEFILE'
LATEXMK ?= latexmk
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
.PHONY: all
all: build/main.pdf
build/main.pdf: main.tex latexmkrc | build
	@$(LATEXMK) -norc -r latexmkrc -pdf -outdir=build main.tex > build/latexmk.log 2>&1 || \
	  { cat build/latexmk.log >&2; exit 1; }
build:
	@mkdir -p $@
MAKEFILE
m -s all
pdftotext build/main.pdf text.txt
grep -q 'See section 1.' text.txt
if grep -Eq 'undefined references|Reference .* undefined' build/main.log; then exit 1; fi
m -q all
printf "project settings loaded; section reference resolved\n"

Download: MAK-14-009.mk · MAK-14-009.sh

MAK-14-010

Build independent documents in parallel

Each document gets its own directory and log, so make -j2 can build both without colliding auxiliary filenames. The list functions produce build/article/main.pdf and build/notes/main.pdf; the pattern rule chooses article.tex or notes.tex using $*. Shared generated assets must be declared as prerequisites before adding them. The driver verifies both PDFs and reports a fixed result after both jobs finish.

Makefile

LATEXMK ?= latexmk
FLAGS := -norc -pdf -no-shell-escape
FLAGS += -interaction=nonstopmode -halt-on-error
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
DOCS := article notes
PDFS := $(addsuffix /main.pdf,$(addprefix build/,$(DOCS)))
.PHONY: all
all: $(PDFS)
build/%/main.pdf: %.tex
	@mkdir -p $(@D)
	@$(LATEXMK) $(FLAGS) -outdir=$(@D) -jobname=main $< > $(@D)/latexmk.log 2>&1 || \
	  { cat $(@D)/latexmk.log >&2; exit 1; }

Actual output

Captured run · exit 0

two independent PDFs built with -j2
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > article.tex <<'INPUT_88_1'
\documentclass{article}
\begin{document}
An independent article.
\end{document}
INPUT_88_1
cat > notes.tex <<'INPUT_88_2'
\documentclass{article}
\begin{document}
Independent working notes.
\end{document}
INPUT_88_2
cat > Makefile <<'MAKEFILE'
LATEXMK ?= latexmk
FLAGS := -norc -pdf -no-shell-escape
FLAGS += -interaction=nonstopmode -halt-on-error
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
DOCS := article notes
PDFS := $(addsuffix /main.pdf,$(addprefix build/,$(DOCS)))
.PHONY: all
all: $(PDFS)
build/%/main.pdf: %.tex
	@mkdir -p $(@D)
	@$(LATEXMK) $(FLAGS) -outdir=$(@D) -jobname=main $< > $(@D)/latexmk.log 2>&1 || \
	  { cat $(@D)/latexmk.log >&2; exit 1; }
MAKEFILE
m -s -j2 all
pdftotext build/article/main.pdf article.txt
pdftotext build/notes/main.pdf notes.txt
grep -q 'An independent article.' article.txt
grep -q 'Independent working notes.' notes.txt
test -s build/article/main.aux
test -s build/notes/main.aux
m -q all
printf "two independent PDFs built with -j2\n"

Download: MAK-14-010.mk · MAK-14-010.sh

MAK-14-011

Clean auxiliaries while keeping the PDF

latexmk -c removes regenerable auxiliary files while keeping the final PDF. This project puts all generated document files in build/, so distclean can remove that owned directory. Neither goal deletes main.tex or source figures. The driver checks that clean keeps the PDF, distclean removes build/ and a subsequent build works. Do not place authoritative source files in the disposable build directory.

Makefile

LATEXMK ?= latexmk
FLAGS := -norc -pdf -no-shell-escape
FLAGS += -interaction=nonstopmode -halt-on-error
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
.PHONY: all clean distclean
all: build/main.pdf
build/main.pdf: main.tex | build
	@$(LATEXMK) $(FLAGS) -outdir=build main.tex > build/latexmk.log 2>&1 || \
	  { cat build/latexmk.log >&2; exit 1; }
build:
	@mkdir -p $@
clean:
	@$(LATEXMK) -norc -c -outdir=build main.tex > cleanup.log 2>&1 || \
	  { cat cleanup.log >&2; exit 1; }
distclean: clean
	@rm -rf -- build

Actual output

Captured run · exit 0

clean: PDF and source retained
distclean: generated directory removed, source retained
document rebuilt after cleanup
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > main.tex <<'INPUT_89_1'
\documentclass{article}
\begin{document}
Hello from a Make-built document.
\end{document}
INPUT_89_1
cat > Makefile <<'MAKEFILE'
LATEXMK ?= latexmk
FLAGS := -norc -pdf -no-shell-escape
FLAGS += -interaction=nonstopmode -halt-on-error
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
.PHONY: all clean distclean
all: build/main.pdf
build/main.pdf: main.tex | build
	@$(LATEXMK) $(FLAGS) -outdir=build main.tex > build/latexmk.log 2>&1 || \
	  { cat build/latexmk.log >&2; exit 1; }
build:
	@mkdir -p $@
clean:
	@$(LATEXMK) -norc -c -outdir=build main.tex > cleanup.log 2>&1 || \
	  { cat cleanup.log >&2; exit 1; }
distclean: clean
	@rm -rf -- build
MAKEFILE
m -s all
test -s build/main.aux
m -s clean
test -s build/main.pdf
test ! -e build/main.aux
test -s main.tex
printf "clean: PDF and source retained\n"
m -s distclean
test ! -e build
test -s main.tex
printf "distclean: generated directory removed, source retained\n"
m -s all
test -s build/main.pdf
printf "document rebuilt after cleanup\n"

Download: MAK-14-011.mk · MAK-14-011.sh

MAK-14-012

Package sources and verify a fresh rebuild

Archive the Makefile, document, included chapter and compiler settings explicitly. The package goal first verifies the local PDF build; auxiliary caches and local output directories are excluded from the source archive. The driver unpacks into another fresh directory and builds there with make -C, checking the resulting PDF text. Add real bibliography, class/style and figure inputs to the archive list when needed. This is a source package for collaborators with the documented tools installed, not a claim of byte-identical PDFs across TeX distributions.

Makefile

LATEXMK ?= latexmk
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
SOURCES := Makefile main.tex sections/intro.tex latexmkrc
.PHONY: all package
all: build/main.pdf
build/main.pdf: main.tex sections/intro.tex latexmkrc | build
	@$(LATEXMK) -norc -r latexmkrc -pdf -outdir=build main.tex > build/latexmk.log 2>&1 || \
	  { cat build/latexmk.log >&2; exit 1; }
build dist:
	@mkdir -p $@
package: dist/paper-source.tar.gz
dist/paper-source.tar.gz: $(SOURCES) build/main.pdf | dist
	@tar -czf $@ $(SOURCES)

Actual output

Captured run · exit 0

Makefile
main.tex
sections/intro.tex
latexmkrc
unpacked sources build a verified PDF
Complete runnable example
#!/usr/bin/env bash
set -euo pipefail
unset MAKEFLAGS MFLAGS GNUMAKEFLAGS
make_bin=$(command -v gmake || command -v make)
m() { "$make_bin" --no-print-directory -rR "$@"; }
work_dir=$(mktemp -d)
if [ "${KEEP_WORK:-0}" = 1 ]; then
  trap 'printf "Artifacts: %s\n" "$work_dir"' EXIT
else
  trap 'rm -rf -- "$work_dir"' EXIT
fi
cd "$work_dir"
export TMPDIR="$work_dir"
cat > main.tex <<'INPUT_90_1'
\documentclass{article}
\begin{document}
\input{sections/intro}
\end{document}
INPUT_90_1
mkdir -p sections
cat > sections/intro.tex <<'INPUT_90_2'
A document rebuilt from its source package.
INPUT_90_2
cat > latexmkrc <<'INPUT_90_3'
$pdflatex = 'pdflatex -no-shell-escape -interaction=nonstopmode -halt-on-error %O %S';
$max_repeat = 5;
INPUT_90_3
cat > Makefile <<'MAKEFILE'
LATEXMK ?= latexmk
.DELETE_ON_ERROR:
.DEFAULT_GOAL := all
SOURCES := Makefile main.tex sections/intro.tex latexmkrc
.PHONY: all package
all: build/main.pdf
build/main.pdf: main.tex sections/intro.tex latexmkrc | build
	@$(LATEXMK) -norc -r latexmkrc -pdf -outdir=build main.tex > build/latexmk.log 2>&1 || \
	  { cat build/latexmk.log >&2; exit 1; }
build dist:
	@mkdir -p $@
package: dist/paper-source.tar.gz
dist/paper-source.tar.gz: $(SOURCES) build/main.pdf | dist
	@tar -czf $@ $(SOURCES)
MAKEFILE
m -s package
tar -tzf dist/paper-source.tar.gz
mkdir unpack
tar -xzf dist/paper-source.tar.gz -C unpack
test ! -e unpack/build
m -s -C unpack all
pdftotext unpack/build/main.pdf text.txt
grep -q 'A document rebuilt from its source package.' text.txt
printf "unpacked sources build a verified PDF\n"

Download: MAK-14-012.mk · MAK-14-012.sh

Example and command index

Content: CC0 1.0. Structure and publishing framework: CC BY 4.0 by Lukas C. Bossert. Repository source · Publishing framework source · License scope and third-party notices.