blog: Make Pattern Rules

This commit is contained in:
Eryn Wells 2024-12-05 17:26:55 -08:00
parent f6955df4f9
commit cc98666b2d
2 changed files with 194 additions and 0 deletions

View file

@ -0,0 +1,67 @@
# A Makefile that explores pattern and grouped rules. This Makefile expects the
# GNU version of Make.
# Relevant docs:
# https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html
# https://www.gnu.org/software/make/manual/html_node/Pattern-Match.html
# https://www.gnu.org/software/make/manual/html_node/Pattern-Intro.html
# https://www.gnu.org/software/make/manual/html_node/Multiple-Targets.html
NAMES=zip zap
.PHONY: all clean
all: $(NAMES:%=%.x) $(NAMES:%=%.y)
# Here's a simple pattern rule that generates a .txt file from a name. In this
# rule % represents the name (the 'stem' in Makefile lingo) in the targets and
# prerequisites, and $* represents the stem in the recipe.
%.txt:
@echo "=> Making $*.txt"
python -c 'print("$*!".title())' > "$@"
# Here are a couple explicit rules that generate a single file given some input
# files. Note the input files (prerequisites) will be generated with the pattern
# rule above.
zip.a : florp.txt
@echo "=> Making $@ with $<"
cat "$<" > "$@"
zap.a : bloop.txt
@echo "=> Making $@ with $<"
cat "$<" > "$@"
# This is a Grouped rule, marked with &:. It indicates that the two target files
# are produced together in one invocation of the rule, rather than two separate
# files produced by the same rule.
zip.x zip.y &: zip.a
@echo "=> Making XY files from $< with explicit rule"
echo "[X]" > zip.x
cat "$<" >> zip.x
echo "[Y]" > zip.y
cat "$<" >> zip.y
# Pattern rules with multiple targets are always Grouped, so you don't need the
# &: separator.
%.x %.y : %.a
@echo "=> Making XY files from $< with pattern rule"
echo "[X]" > $*.x
cat "$<" >> $*.x
echo "[Y]" > $*.y
cat "$<" >> $*.y
clean:
rm -f bloop.txt florp.txt
rm -f $(NAMES:%=%.a)
rm -f $(NAMES:%=%.x)
rm -f $(NAMES:%=%.y)

View file

@ -0,0 +1,127 @@
---
title: "Makefile Pattern Rules"
date: 2024-12-04T14:26:57-08:00
publishDate: 2024-12-05
resources:
- name: makefile
src: Makefile
tags:
- TIL
- Make
- Software
---
I recently found myself hacking on a Makefile (the GNU kind, not the [BSD
kind][bsdmake]) that made heavy use of pattern rules and grouped rules. These
are concepts I haven't spent a lot of time with so I wrote myself a small test
Makefile to explore them in a little more detail.
## Pattern Rules
[Pattern rules][patterns] drive most of the usage of `make` as a tool for
building software. `make` includes a bunch of pattern rules for many of the
kinds of source files you're likely to encounter. For example, it has implicit
rules for building `.o` files out of `.c` files.
Here's a simple pattern rule that generates text files:
```make
%.txt:
@echo "=> Making $*.txt"
python -c 'print("$*!".title())' > $@
```
The `%` is tells `make` that this is a patten rule. It's a placeholder for a
string of non-whitespace characters, which `make` calls a _stem_. You can
reference the stem in the body of the rule with the [automatic variable][auto]
`$*`.
This particular rule creates a text file with a particular stem by echoing a
string to a file with this little Python snippet.
Here's a slightly more complex pattern rule:
```make
%.x : %.a
@echo "=> Making X file from $< with pattern rule"
echo "[X]" > $*.x
cat "$<" >> $*.x
```
The major difference with this rule is that it has a prerequisite (the part
after the colon) that also has a `%`. So, this rule defines how to build a `.x`
file from a `.a` file with the same stem.
Later on, if I write a rule like this:
```make
zip.a : florp.txt
@echo "=> Making $@ with $<"
cat $< > $@
```
`make` will understand that it first needs to generate `florp.txt` with the
pattern rule for `%.txt`. Then it can execute this rule to build `zip.a`.
## Grouped Rules
Grouped rules are another feature of `make` that lets you specify [more than one
output][multiples] for a given rule. If you're writing C family languages, this
is useful for generating a header and source file pair, and making sure that
they get updated together.
```make
zip.x zip.y &: zip.a
@echo "=> Making XY files from $< with explicit rule"
echo "[X]" > zip.x
cat "$<" >> zip.x
echo "[Y]" > zip.y
cat "$<" >> zip.y
```
Generally when you write a rule with more than one output, `make` understands
that each of the ouput files is built separately with the same rule. However, a
rule with a `&:` separator indicates that the outputs are built from a single
invocation of the rule. `make` will rebuild the rule if any of the outputs is
out-of-date.
## Altogether Now
You can combine pattern rules and group rules into a single rule too. This rule
creates a pattern for building a pair of `.x` and `.y` files from a `.a` file
with a particular stem.
```make
%.x %.y : %.a
@echo "=> Making XY files from $< with pattern rule"
echo "[X]" > $*.x
cat "$<" >> $*.x
echo "[Y]" > $*.y
cat "$<" >> $*.y
```
These rules are always treated as a group. It doesn't matter if you use `:` or
`&:`.
## Rule Precedence
The way `make` decides which rule to use to produce a file are a little subtle,
especially when you combine pattern rules and explicit rules. In general an
explicit rule should take precedence over a pattern. In my Makefile, the rule
that explicitly builds `zip.x` and `zip.y` will win over the pattern for `%.x`
and `%.y`. When multiple pattern rules match a target, the rules are [more
complex][matching].
---
Download the full [Makefile]({{< page/resource-ref makefile >}}).
[bsdmake]: {{< ref "blog/2024/bsd-make" >}}
[auto]: https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html
[patterns]: https://www.gnu.org/software/make/manual/html_node/Pattern-Intro.html
[multiples]: https://www.gnu.org/software/make/manual/html_node/Multiple-Targets.html
[matching]: https://www.gnu.org/software/make/manual/html_node/Pattern-Match.html