CLI Semantics and Debug Control¶
Page Maps¶
graph LR
family["Reproducible Research"]
program["Deep Dive Make"]
section["Rule Semantics Precedence Edge Cases"]
page["CLI Semantics and Debug Control"]
capstone["Capstone evidence"]
family --> program --> section --> page
page -.applies in.-> capstone
flowchart LR
orient["Orient on the page map"] --> read["Read the main claim and examples"]
read --> inspect["Inspect the related code, proof, or capstone surface"]
inspect --> verify["Run or review the verification path"]
verify --> apply["Apply the idea back to the module and capstone"]
When a Make build behaves strangely, many engineers reach for flags in the wrong order.
They add -B, sprinkle -j1, or run clean until the symptom disappears.
That feels productive because the build changes. It is usually the opposite of evidence.
The first goal of this page is to replace that reflex with a better one:
choose the flag that reveals the graph fact you need, not the flag that makes the pain go away for one run.
That is why Module 04 starts with the CLI. Before you can reason about variables, includes, or rule edge cases, you need a disciplined way to ask Make what it thinks.
The three kinds of CLI switches¶
Most of the Make CLI fits into three practical groups:
- switches that reveal information
- switches that simulate a condition
- switches that change behavior so much that they can hide the underlying bug
Keeping those groups separate prevents a lot of wasted time.
| Kind | Examples | What they are for |
|---|---|---|
| reveal | --trace, -p, -q, -n |
showing why Make made a decision |
| simulate | -W file, -B |
asking "what if this input were stale?" |
| alter build behavior | -j, -rR, -C, -f |
changing scheduling, built-ins, or entrypoint |
The mistake is not using the third group. The mistake is using it before you understand what question you are actually asking.
The small set of flags that matter constantly¶
--trace: the rebuild explanation tool¶
If you only keep one Module 04 habit from this page, keep this one:
--trace is your fastest route to a plain-language answer to "why did this run?" It
prints the target, the rule location, and the prerequisite relationship that made the
recipe eligible.
That matters because most Make incidents are not mysterious shell failures. They are causality failures. Something ran because the graph said it should.
-n: preview without recipe execution¶
This is useful when you want to preview what Make intends to run. It is not a frozen
simulation of reality. Make still parses the files, expands variables, and may still
evaluate things like $(shell ...) and $(file ...).
So -n is a preview of recipe execution, not a promise that nothing meaningful happened
during evaluation.
That distinction becomes important later in the module when includes or shell assignments show up at parse time.
There is another exception: recipe lines marked with + or recognized as recursive Make
invocations may execute even under -n, -t, or -q. GNU Make preserves this behavior
so a parent can pass the selected mode to its child.
Treat a dry run as a semantic mode with documented exceptions, not a sandbox.
-p: the evaluated world¶
-p prints the database Make is actually using after parsing and evaluation. It is noisy,
but the noise is useful when you have a variable or rule-selection dispute:
- which value did a variable end up with
- which built-in rule still exists
- which implicit behavior is present even though nobody wrote it explicitly
If --trace explains one decision, -p explains the world that made that decision
possible.
-p does not suppress the ordinary build by itself. Pair it with query mode when you want
the database without running an eligible recipe:
This still reads and expands makefiles. Parse-time effects and makefile remakes remain possible.
-q: convergence as an exit code¶
Query mode is simple and easy to misuse:
- exit
0: the target is up to date - exit
1: the target would rebuild - exit
2: Make encountered an actual error
Many teams accidentally treat exit 1 as a build crash. It is not a crash. It is the
signal that the current graph says work remains.
That makes -q valuable for selftests, CI checks, and "did the second run converge?"
style assertions.
Capture the status before another shell command overwrites it:
-W file: simulate staleness honestly¶
-W tells Make to pretend one file is newer than it really is. This is one of the best
ways to test whether a dependency edge is honest because it changes the staleness model
without forcing you to edit files or corrupt timestamps by hand.
It is a diagnostic tool, not a repair.
If -W include/config.h app reveals that app does not rebuild, the answer is not "keep
running with -W." The answer is "the graph is missing an input edge."
-B: useful, but suspicious¶
-B forces everything to be treated as out of date. That can be useful when you want a
quick full rebuild or want to see whether an incremental bug disappears under a total
rebuild.
But if -B "fixes" the build, do not celebrate. Treat that as a clue that incremental
truth is broken.
Know which boundary each switch changes¶
| Switch | Boundary changed or observed | Claim it cannot prove alone |
|---|---|---|
--trace |
selected target and triggering prerequisite | why an undeclared input changed |
-n |
suppresses most recipe execution | absence of parse-time or recursive side effects |
-q |
reports whether governed work remains | that output bytes match semantic inputs |
-p |
prints evaluated database | that the selected recipe ran correctly |
-W file |
simulates one file as newly changed | actual output correctness after the recipe |
-B |
makes targets unconditionally stale | incremental graph truth |
-rR |
removes built-in rules and variables | that repository-authored implicit rules are unambiguous |
-f file |
selects an entry makefile | equivalence with the repository’s public entrypoint |
-C dir |
changes Make’s working directory before reading | equivalence of relative paths and included files |
-jN |
changes scheduling pressure | correctness under independent Make processes |
Choose the smallest switch that changes the boundary you intend to test. Record every switch in the evidence packet; otherwise a later reviewer cannot reproduce the semantic world you observed.
Prove the dry-run exceptions¶
Use an isolated fixture whose only purpose is to expose evaluation:
$(file >>artifacts/semantics/parse-events.log,parent parsed)
.PHONY: observe-child
observe-child:
+$(MAKE) --no-print-directory -f child.mk observe
And child.mk:
$(file >>artifacts/semantics/parse-events.log,child parsed)
.PHONY: observe
observe:
@printf '%s\n' 'child recipe executed'
Run:
Expected observations:
- the parent parse record is written
- the recursive line executes despite
-n - the child parses and writes its record
- the child recipe is printed under inherited dry-run mode rather than executed
Now remove the + and spell the child command without $(MAKE). GNU Make no longer
recognizes it as recursive, so -n only prints the line and the child parse record is
absent.
This is why a dry-run test needs filesystem and subprocess evidence, not just terminal output.
flowchart LR
dry["gmake -n parent"] --> parent["parent parse effects"]
parent --> recursive{"recognized recursive line?"}
recursive -->|yes| child["child starts and parses"]
recursive -->|no| printed["line printed only"]
child --> child_recipe["child recipe remains dry-run"]
A better incident loop¶
When you say "Make is being weird," the usual issue is not weirdness. The issue is that the investigation has no order. Use this loop instead:
- Record the entrypoint, directory, goals, assignments, and inherited
MAKEFLAGS. - Prove causality with
gmake --trace <target>. - Inspect the evaluated state with
gmake -pRrq <target>. - Preview eligible recipes with
gmake -n, while auditing parse and recursive effects. - Simulate one suspected change with
gmake -W file --trace <target>. - Only then decide whether a clean rebuild, serial run, or built-in rule audit answers a remaining question.
That order forces you to gather evidence before changing the conditions too aggressively.
An isolated staleness probe¶
Use this Makefile:
.PHONY: clean
report.txt: data.txt template.txt
@printf 'report from %s and %s\n' data.txt template.txt > $@
data.txt:
@printf 'data\n' > $@
template.txt:
@printf 'template\n' > $@
clean:
rm -f report.txt data.txt template.txt
Now run:
gmake clean
gmake report.txt
gmake --trace report.txt
gmake -q report.txt; printf 'status=%s\n' "$?"
gmake -W template.txt --trace report.txt
gmake -q report.txt; printf 'status=%s\n' "$?"
What this teaches:
- after the first build,
-qshould return0 -W template.txtshould makereport.txteligible again--traceshows the exact prerequisite relationship that explains the rebuild
This is a tiny example, but the reasoning scales to real builds.
Failure signatures worth recognizing¶
"It only behaves when I run clean first"¶
That usually means the incremental graph is wrong. clean is not the evidence. clean
simply hides the distinction between a correct incremental build and a brute-force full
rebuild.
"It works under -j1"¶
That is not a resolution. It tells you parallel scheduling exposed a real bug, often a missing edge or a multi-writer output.
"It looked fine under -n"¶
That can still happen if the problem depends on actual recipe execution, timestamp
publication, or concurrent writers. It can also hide in the opposite direction: parse-time
functions and recognized recursive recipes may still act. -n is useful, but it is not
the same thing as a side-effect-free simulation or a successful build.
"-B makes the issue disappear"¶
That usually points at stale-state logic, not a healthy build.
The beginner trap: using flags as superstition¶
Bad Make debugging often sounds like this:
- "I always run
make clean all." - "Try
-B." - "Try
-j1." - "Maybe the cache is weird."
None of those statements explains anything.
Good Make debugging sounds like this:
- "
--traceshowsapprebuilt becauseconfig.mkwas remade." - "
-qreturned1, so the second run did not converge." - "
-W include/api.hproduced no rebuild, which proves the edge is missing."
That is the level of explanation this module wants.
What to practice from this page¶
Take one small target in the capstone or your own project and answer all four questions:
- Which single flag would you run first to explain a rebuild?
- Which flag would tell you whether the target is up to date without executing the recipe?
- Which flag would simulate one stale prerequisite honestly?
- Which flag would be dangerous to use too early because it might hide the incremental bug?
- Which parse-time and recursive effects could still occur under the chosen mode?
- Which inherited switches reach a recursive Make through
MAKEFLAGS?
If you can answer those without hand-waving, the CLI has stopped being a bag of tricks and become an instrument.
End-of-page checkpoint¶
Before leaving this lesson, make sure you can explain:
- why
--traceis the default starting point for rebuild investigations - why
-qexit code1means "would rebuild," not "broken build" - why
-Wis a cleaner probe than touching files by hand - why
-Bcan be useful while still being a warning sign - why
-npreviews recipe execution but does not erase parse-time effects - why recognized recursive recipe lines are an explicit dry-run exception
- why
-pneeds a non-executing companion mode for database-only inspection - why every diagnostic record must include entrypoint, directory, assignments, and flags