Implicit rules in Make
Part of an ongoing series of essays tentatively entitled Don’t embarrass me, Don’t embarrass yourself: Notes on thinking in C and Unix.
As I hope you recall, we are an on ongoing journey to explore how to use Make to make your program construction more efficient. We started by looking at a sample C project and thinking about the relationships between the files in that project. We continued by learning how to write basic rules in Make, using the basic pattern of
TARGET: DEPENDENCIES
INSTRUCTIONS
While the Makefile we created using that approach was clear, it was also repetitious. Next, we learned how to use variables to eliminate some of that repetition [1]. Here’s one of the improved Makefiles we created along the way.
# Makefile
# A simple Makefile for our basic C project
# +-----------+------------------------------------------------------
# | Variables |
# +-----------+
CC = gcc
CFLAGS = -g -Wall
# +---------+--------------------------------------------------------
# | Targets |
# +---------+
# Our application
gcd: gcd.o mathlib-gcd.o
$(CC) -o $@ $^
# Our tests
test: ./test-gcd
$<
test-gcd: test-gcd.o mathlib-gcd.o
$(CC) -o $@ $^
# The components
mathlib-gcd.o: mathlib-gcd.c mathlib.h
$(CC) $(CFLAGS) -c -o $@ $<
gcd.o: gcd.c mathlib.h
$(CC) $(CFLAGS) -c -o $@ $<
test-gcd.o: test-gcd.c mathlib.h
$(CC) $(CFLAGS) -c -o $@ $<
However, as we noted, this Makefile is still a bit repetitious. The designers
of Make [2] wanted to make it easier to avoid that repetition, and introduced
what they call implicit
rules. Implicit rules involve patterns. In
particular, the symbol %
matches an arbitrary string of characters,
and can appear on both the left-hand-side and right-hand-side of a rule.
For example, here’s how I’d write that to create a .o
file, I need
a .c
file with the same prefix.
%.o: %.c
INSTRUCTIONS
Let’s try it out by updating our Makefile once again.
# Makefile
# A simple Makefile for our basic C project
# +-----------+------------------------------------------------------
# | Variables |
# +-----------+
CC = gcc
CFLAGS = -g -Wall
# +----------------+-------------------------------------------------
# | Implicit Rules |
# +----------------+
# Generating .o files
%.o: %.c mathlib.h
$(CC) $(CFLAGS) -c -o $@ $<
# Generating executables
%: %.o mathlib-gcd.o
$(CC) -o $@ $^
# +---------+--------------------------------------------------------
# | Targets |
# +---------+
# Our tests
test: ./test-gcd
$<
This new Makefile is more concise, although those new implicit rules are a bit hard to read. Does this work, even though we don’t mention most things explicitly? Let’s see.
$ touch *.c
$ make test-gcd.o
gcc -g -Wall -c -o test-gcd.o test-gcd.c
$ make gcd
gcc -g -Wall -c -o gcd.o gcd.c
gcc -g -Wall -c -o mathlib-gcd.o mathlib-gcd.c
gcc -o gcd gcd.o mathlib-gcd.o
$ make foo.o
make: *** No rule to make target 'foo.o'. Stop.
$ make test
gcc -o test-gcd test-gcd.o mathlib-gcd.o
test-gcd
Yes, it works pretty well. It also knows what it can and cannot make. We’re doing well.
Now, here’s the really cool thing. The designers of Make realized that
there are a number of common patterns. For example, almost everyone
converts a .c
file to a .o
file with an instruction almost identical
to ours. And so they included all of those patterns in a default
database
. You can read that database with make -p
. Here’s a sampling.
$ make -p
# default
OUTPUT_OPTION = -o $@
...
# default
LINK.o = $(CC) $(LDFLAGS) $(TARGET_ARCH)
...
# default
COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
...
%.o: %.c
# recipe to execute (built-in):
$(COMPILE.c) $(OUTPUT_OPTION) $<
...
%: %.o
# recipe to execute (built-in):
$(LINK.o) $^ $(LOADLIBES) $(LDLIBS) -o $@
So, we can even remove the rules for generating the .o
files from our
Makefile, and it will still work. We should, however, still indicate
that the .o
files depend on mathlib.h
.
# Makefile
# A simple Makefile for our basic C project
# +-----------+------------------------------------------------------
# | Variables |
# +-----------+
CC = clang
CFLAGS = -Wall -g
# +----------------+-------------------------------------------------
# | Implicit Rules |
# +----------------+
# Generating executables
%: %.o mathlib-gcd.o
$(CC) -o $@ $^
# +---------+--------------------------------------------------------
# | Targets |
# +---------+
# Our tests
test: ./test-gcd
$<
# +-------------------------+----------------------------------------
# | Additional Dependencies |
# +-------------------------+
*.o: mathlib.h
As we hoped, this revised file continues to work well.
$ touch *.c
$ make mathlib-gcd.o
clang -Wall -g -c -o mathlib-gcd.o mathlib-gcd.c
$ make test
clang -Wall -g -c -o test-gcd.o test-gcd.c
clang -o test-gcd test-gcd.o mathlib-gcd.o
test-gcd
$ touch mathlib.h
$ make test
clang -Wall -g -c -o test-gcd.o test-gcd.c
clang -Wall -g -c -o mathlib-gcd.o mathlib-gcd.c
clang -o test-gcd test-gcd.o mathlib-gcd.o
test-gcd
Can we get rid of the other implicit rule in our Makefile? Let’s try dropping it altogether and making no other changes.
$ touch *.c
$ make test
clang -Wall -g -c -o test-gcd.o test-gcd.c
clang test-gcd.o -o test-gcd
test-gcd.o: In function `main':
/home/rebelsky/Web/musings/examples/cnix-make-variables/test-gcd.c:24: undefined reference to `gcd'
/home/rebelsky/Web/musings/examples/cnix-make-variables/test-gcd.c:25: undefined reference to `gcd'
/home/rebelsky/Web/musings/examples/cnix-make-variables/test-gcd.c:26: undefined reference to `gcd'
/home/rebelsky/Web/musings/examples/cnix-make-variables/test-gcd.c:29: undefined reference to `gcd'
/home/rebelsky/Web/musings/examples/cnix-make-variables/test-gcd.c:30: undefined reference to `gcd'
test-gcd.o:/home/rebelsky/Web/musings/examples/cnix-make-variables/test-gcd.c:31: more undefined references to `gcd' follow
clang: error: linker command failed with exit code 1 (use -v to see invocation)
<builtin>: recipe for target 'test-gcd' failed
make: *** [test-gcd] Error 1
Whoops! I guess now. Why not? Well, let’s look at the instruction that
make
used to create test-gcd
.
clang test-gcd.o -o test-gcd
What did we use successfully?
clang -o test-gcd test-gcd.o mathlib-gcd.o
What’s different? The order of arguments and the fact that we included
mathlib-gcd.o
and Make didn’t. The order of arguments shouldn’t
matter [3]. But it’s hard to use the gcd
function if we don’t have
the code for it. Can we force Make to include that? Well, let’s look
at the default rule again.
%: %.o
# recipe to execute (built-in):
$(LINK.o) $^ $(LOADLIBES) $(LDLIBS) -o $@
It looks to me like we could define $(LDLIBS)
[4] to use mathlib-gcd.o
as a library [5]. Let’s try.
# Makefile
# A simple Makefile for our basic C project
# +-----------+------------------------------------------------------
# | Variables |
# +-----------+
CC = clang
CFLAGS = -Wall -g
LDLIBS = mathlib-gcd.o
# +---------+--------------------------------------------------------
# | Targets |
# +---------+
# Our tests
test: ./test-gcd
$<
# +-------------------------+----------------------------------------
# | Additional Dependencies |
# +-------------------------+
*.o: mathlib.h
That’s nice and concise. Will it work?
$ touch *.c
$ make test
clang -Wall -g -c -o test-gcd.o test-gcd.c
clang test-gcd.o mathlib-gcd.o -o test-gcd
test-gcd
Is this what we want? Not quite. You’ll note that it did not remake
mathlib-gcd.o
, even though the source had changed. Can you figure out
why?
What should you take as the morals of this section? First, that implicit rules let you write much more concise Makefiles. Second, that you need some experience to read implicit rules, since they are not as clear as more explicit rules [6]. Third, that there are a large number of built-in rules that you can leverage. Finally, that sometimes you still have to write your own implicit and explicit rules, since the built-in implicit rules won’t do quite what you want.
You just have a few more things to learn about Make and then you’ll know enough that, with practice, you can be a competent user of Make, or at least a competent reader of Makefiles.
[1] Some programmers would say we wanted to DRY
out our code. DRY stands
for Don’t Repeat Yourself
.
[2] Or at least of GNU Make.
[3] Fingers crossed.
[4] Or $(LOADLIBES)
.
[5] Okay, we really should turn mathlib-gcd.o
into a separate library.
But we’ll do that later.
[6] Don’t worry! With some practice, you’ll find that the implicit rules are pretty clear.
Version 1.0 released 2017-01-09.
Version 1.0.2 of 2021-04-22.