The makefile can be confusing at times because a lot of the steps are left out for simplicity sakes, and implied by make
using default behaviour.
It might make a bit more sense if you manually replace the variables (OBJS and HDRS) with their "expanded" values, e.g.:
# automatically generated list of object files
# OBJS = $(SRCS:.c=.o)
OBJS = speller.o dictionary.o
# default target // here comes the confusing part!
# $(EXE): $(OBJS) $(HDRS) Makefile
# $(CC) $(CFLAGS) -o $@ $(OBJS) $(LIBS)
speller: speller.o dictionary.o dictionary.h Makefile
$(CC) $(CFLAGS) -o $@ speller.o dictionary.o
# dependencies
# $(OBJS): $(HDRS) Makefile
speller.o dictionary.o: dictionary.h Makefile
$(CC) $(CFLAGS)
There are actually two different kinds of rules here:
- When to compile something (e.g.
speller: speller.o dictionary.o dictionary.h Makefile
).
- How to compile the thing (e.g.
$(CC) $(CFLAGS) -o $@ speller.o dictionary.o
).
Your intuition is correct, in that make
recompiles the .c
files when the .o
files are out of date. It's basically checking the timestamps, and if the .c
file is newer than the corresponding .o
file then make
recompiles.
The reason there is no direct correlation here is because make
has a default behaviour to try to look for the .c
file with the same name as the output file.
Quick detour, recall earlier in the course where make
was executed without the existence of a makefile, e.g:
make mario
Here you're asking make
for an executable. make
goes and figures out that it needs to find a file called mario.c
file. Actually what's happening behind the scenes, is that make
sees that it needs to create an executable, and to do that it needs a binary .o
file. When make
sees that a .o
file is needed, it knows to look for a .c
source file. The .c
file is compiled to a .o
, which is then linked to create the final executable.
One way is to think of it as a chain of dependencies, that make
fills in by itself, unless you tell it otherwise:
mario -> mario.o -> mario.c
Back to the makefile. When you run make
from the command line, it will first look in the makefile for something to do. In this case it finds speller
and tries to compile it. With the default behaviour, make
would just look at speller.c
before deciding if anything needs to be done. If we're only modifying dictionary.c
then make
would not see anything has changed, and would not do anything.
The extra file names (speller.o
, dictionary.o
, etc) are telling make
to look at those files as well, to see if anything has changed:
speller: speller.o dictionary.o dictionary.h Makefile
So if any of these files are newer than the current executable, then make
knows to recompile using the line below that condition:
$(CC) $(CFLAGS) -o $@ speller.o dictionary.o
Without this line, make
will just follow the default behaviour and compile speller.o
only with the default options.
The "dependencies" line is similar:
speller.o dictionary.o: dictionary.h Makefile
This is telling make
that speller.o
and dictionary.o
should be recompiled if dictionary.h
or Makefile
has changes.
The reason the Makefile
itself is included in the list, is to ensure that make
checks the Makefile
for changes. This is to cater for the scenario where the Makefile
changes, but not any of the source or binary files.
The next line below that doesn't specify any compiler commands, so make
will use the default behaviour to compile those files. The command it creates would look something like this:
$(CC) $(CFLAGS)
Since that is the command we want anyway it can be left out. CC
and CFLAGS
are both "default" variables in make
. In other words, just by defining the CFLAGS
variable set in the file, make
will pass those arguments to the compiler.
For more information, here's a nice introduction to how make works.