Module 04: Rule Semantics, Precedence, and Edge Cases¶
Modules 01 to 03 teach graph truth, parallel safety, and deterministic operation. Module 04 is where you slow down and learn how GNU Make actually decides.
This module exists for the moments when a build surprises you and the old explanations are too vague to help:
- a target rebuilds and nobody can say why
- a variable value changes somewhere between the shell and the recipe
- an included file changes Make's behavior in a way that feels invisible
- a clever rule works once, then flakes under
-jor incremental rebuilds
The goal is not to memorize trivia. The goal is to leave with a durable mental model of Make semantics so you can tell the difference between a legitimate advanced feature and a bug disguised as one.
Why this day matters¶
Without a semantic model, advanced Make usage turns into folklore. Learners end up with phrases like "sometimes Make gets weird" instead of being able to say which rule, precedence boundary, or include behavior caused the result.
The same text can be evaluated at different times:
flowchart LR
startup["startup inputs\nenvironment, command line"] --> read["read and parse\nassignments, conditionals, includes"]
read --> remake["remake included makefiles"]
remake --> restart{"restart needed?"}
restart -->|yes| read
restart -->|no| select["select goals and rules"]
select --> expand["expand prerequisites and recipes"]
expand --> shell["execute shell or recursive Make"]
shell --> child["child Make starts a new semantic cycle"]
Most “Make surprises” are boundary mistakes:
- expecting a parse-time conditional to see a recipe-time automatic variable
- treating an environment value as if it were a command-line override
- assuming a generated include is read only once
- trusting
-nas side-effect-free even when parsing or recursive recipes do work - assuming rule selection and prerequisite expansion happen in one undifferentiated step
This module teaches you to locate the boundary before changing syntax.
Entry diagnostic¶
For one surprising target in a Make repository, record:
- the requested goal and command-line assignments
- the effective variable’s value, origin, flavor, scope, and export status
- every included makefile and
MAKE_RESTARTS - the selected explicit or implicit rule
- the prerequisite set before and after any secondary expansion
- the exact recipe Make expands and the command the shell receives
- any recursive Make command and the variables it forwards
Do not repair the target yet. The missing row in this record usually identifies the semantic boundary that needs work.
What this module is for¶
By the end of Module 04, you should be able to explain five things with confidence:
- which command-line flags reveal a problem instead of hiding it
- how variable precedence, expansion, and export interact
- how to gate capabilities without scattering hidden inputs
- why included makefiles can trigger restart behavior and how to keep that safe
- which rule features preserve correctness and which ones quietly damage it
The live demo for this module¶
Use one small variable-introspection target and one generated-include example as the anchors for the day:
flowchart LR
value["Print origin, flavor, and value"] --> compare["Override from file, env, and CLI"]
compare --> include["Generate and include one fragment"]
include --> restart["Observe stable or unstable restarts"]
restart --> explain["Name the semantic rule that caused each result"]
If you can do that calmly, the rest of the module becomes far easier to absorb.
Ask Make where a value came from¶
Do not debug variable precedence by staring at assignments. Create this Makefile:
MODE ?= local
.PHONY: show
show:
@printf 'MODE=%s origin=%s flavor=%s\n' \
"$(MODE)" "$(origin MODE)" "$(flavor MODE)"
Predict and run:
The output teaches more than a precedence list:
?=accepts an environment value because the variable is already defined- a command-line value outranks both
origintells you which boundary supplied the valueflavortells you when the stored expression expands
When a value surprises you later, reduce the question to this form before changing
operators or adding override. First establish origin and flavor; then decide whether
the current ownership contract is correct.
Plan for the day¶
| Focus | Activity | Evidence you should produce |
|---|---|---|
| expansion | compare =, :=, ?=, and += in one file |
raw, expanded, flavor, and origin values |
| precedence | invoke defaults, environment, command line, and recursive Make | a provenance matrix with forwarding evidence |
| conditionals | compare parse-time choice with recipe-time observation | capability decision and output-equivalence proof |
| includes | generate and include one stable fragment | restart count, parse-side-effect count, converged rerun |
| rule choice | compare explicit, implicit, static-pattern, and secondary expansion | database and debug evidence for the selected rule |
| diagnosis | follow a compound failure across semantic boundaries | an event ledger and bounded repair |
| practice | complete all ten exercises | accepted and rejected observations, not feature definitions |
This module is dense. Run each small experiment before reading the compound worked example; otherwise several semantic rules arrive at once.
The question to keep asking¶
Every time the build surprises you, ask:
Which semantic boundary owns this surprise: startup, read time, makefile remake, rule selection, expansion, recipe execution, or recursive invocation?
That question keeps the incident small enough to debug.
Study route¶
flowchart TD
start["Overview"] --> core1["CLI Semantics and Debug Control"]
core1 --> core2["Variable Precedence and Expansion"]
core2 --> core3["Conditionals and Capability Gates"]
core3 --> core4["Includes, Remake Semantics, and Search Paths"]
core4 --> core5["Rule Selection, Multi-Output, and Special Targets"]
core5 --> example["Worked Example: Diagnosing Semantic Failures"]
example --> practice["Exercises"]
practice --> answers["Exercise Answers"]
answers --> glossary["Glossary"]
Read the module in that order the first time. Later, come back to the page that matches the kind of failure or design decision you are facing.
The ten files in this module¶
- Overview (
index.md) - CLI Semantics and Debug Control
- Variable Precedence and Expansion
- Conditionals and Capability Gates
- Includes, Remake Semantics, and Search Paths
- Rule Selection, Multi-Output, and Special Targets
- Worked Example: Diagnosing Semantic Failures
- Exercises
- Exercise Answers
- Glossary
How to use the file set¶
| If you need to... | Start here |
|---|---|
| choose the right Make flag for an incident | CLI Semantics and Debug Control |
| prove where a variable value came from | Variable Precedence and Expansion |
| centralize capability checks without folklore | Conditionals and Capability Gates |
| understand include layering and restart behavior | Includes, Remake Semantics, and Search Paths |
| model multi-output rules and sharp special targets safely | Rule Selection, Multi-Output, and Special Targets |
| see the whole module in one realistic review path | Worked Example: Diagnosing Semantic Failures |
| test your own understanding | Exercises |
| compare your answers against a reference | Exercise Answers |
| stabilize vocabulary while reading | Glossary |
The running question¶
Carry this question through every page:
When Make behaves in a surprising way, which exact semantic rule explains the behavior?
Good Module 04 answers usually mention one or more of these:
- the difference between preview and execution
- a variable origin or expansion decision
- a capability check that was spread across too many places
- an include that changed evaluation order or restart behavior
- an advanced rule form that was used without a matching correctness contract
What a strong Module 04 day looks like¶
By the end of the day, you should have:
- shown the value, origin, flavor, scope, export status, and forwarding route of a surprising variable
- observed one stable generated-include restart and one unstable one
- proved that parse-time diagnostics do not create undeclared work during a restart
- diagnosed one rule-selection ambiguity with Make-native evidence
- traced one secondary-expansion prerequisite from escaped text to concrete edge
- replaced one vague platform branch with a clearer capability gate
- explained one incident by naming the semantic stage that caused it
Commands to keep close¶
These commands form the evidence loop for Module 04:
gmake -n <target>
gmake --trace <target>
gmake -pRrq <target> 2>/dev/null
gmake -q <target>
gmake -rR <target>
gmake --debug=implicit <target>
You do not need every one on every incident. You do need the habit of choosing them on purpose.
Learning outcomes¶
By the end of this module, you should be able to:
- use Make's CLI as a diagnostic instrument instead of a bag of random switches
- prove variable origin, flavor, and timing instead of guessing
- explain how
MAKEFLAGS,MAKEOVERRIDES, export state, and explicit forwarding affect a child Make - design conditionals that express capabilities without smuggling in hidden state
- reason about generated includes, include order, and search paths as part of architecture
- keep parse-time work restart-safe and distinguish include intent from accepted runtime evidence
- choose rule forms and special targets that preserve convergence and parallel safety
Exit standard¶
Do not move on until all of these are true:
- you can explain a rebuild using
--tracerather than story-telling - you can show why a variable won by naming its origin and expansion mode
- you can predict which value reaches a recursive Make and why
- you can centralize one capability gate instead of repeating shell probes everywhere
- you can explain when an included makefile causes Make to restart
- you can prove whether a dry run executed a recursive recipe or hidden parse-time writer
- you can explain one secondary-expansion edge using the first and second expansion views
- you can repair one unsafe advanced-rule pattern without resorting to
.NOTPARALLEL - you completed ten exercises and can prove variable origin without inspecting CI folklore
When those feel ordinary, Module 04 has done its job.