Changelog:
- 17 Jan 2024: move text previously in glossary here to compilation; add text about automatically finding dependencies
- 18 Jan 2024: add section on suffix rules
- 18 Jan 2024: correct typo in := versus = example code; format that discussion as endnote as intended
- 24 Jan 2024: mention that variables can be overriden on command line
1 The make
tool
make
is a tool that reads a file (called a makefile,
named Makefile
by default) and decides what commands it
need to run to update your project. To avoid redundant commands when
only a small part of the project has been updated, make
checks file modifications times when it determines which commands to
generate. A well-constructed makefile can simplify building and running
code on many platforms.
1.1 Rules
The key component of a makefile is a rule, consisting of three parts:
- A target, the name of a file that will be created or updated by the rule.
- A list of dependencies (or
prerequisites
): if the target is missing or is older than at least one dependency, the system commands will be run. - A list of system commands (or the
recipe
): shell commands that should result in (re)making the target
The target must begin a line (i.e., must not be indented) and end in a colon. Anything after that colon is a dependency. Indented lines that follow (which must be indented with a single tab character, not spaces) are system commands.
The following rule will build hello.o
if either
hello.c
or hello.h
is newer than
hello.o
. Otherwise, it will do nothing.
hello.o: hello.c hello.h
clang -c hello.c
When you run make
it reads Makefile
and
executes the first rule it finds. If you want to run a
different rule, you can give its target as an argument to
make
.
Consider the following Makefile
:
hello.o: hello.c hello.h
clang -c hello.c
bye.o: bye.c bye.h
clang -c bye.c
Running make
will ensure hello.o
is up to
date. Running make bye.o
will ensure bye.o
is
up to date.
If a dependency of an executed rule is the target of another rule, that other rule will be executed first.
Consider the following Makefile
:
runme: hello.o bye.o main.c main.h
clang main.c hello.o bye.o -o runme
hello.o: hello.c hello.h
clang -c hello.c
bye.o: bye.c bye.h
clang -c bye.c
Running make
will ensure runme
is up to
date; since runme
’s dependencies include
hello.o
and bye.o
, both of those rules will
also be executed.
1.2 Macros
Makefiles routinely use variables (which they call macros) to separate out the list of files from the rules that make them, as well as to allow easy swapping out of different compilers, compilation flags, etc.
Variables are defined with NAME = meaning
or NAME := meaning
syntax. (The syntaxes with :
evaluate meaning
immediately; the syntax without evaluates it each time it is used.1) Traditionally, variable names are
in all-caps. Variables are used by placing them in parentheses, preceded
by a dollar sign: $(NAME)
.
Some variables names that are very common to find in Makefiles include:
Name | example | notes |
---|---|---|
CC | clang | The C compiler to use |
CFLAGS | -O2 -g | Compile-time flags for the C compiler |
LDFLAGS | -static | Link-time flags for the compiler (typically placed before list of object files on command line) |
LIBS or LDLIBS | -lm | Link-time flags representing libraries for the compiler (typically placed after list of object files on command line) |
CXX | clang++ | The C++ compiler to use |
CXXFLAGS | -O2 -g | Compile-time flags for the C++ compiler |
Even if you do not need linker flags or libraries, it’s still common
to define a blank LDFLAGS :=
or LIBS :=
and
use $(LDFLAGS)
or $(LIBS)
in all linking
locations so that if you later realize you need to link to an external
library (like the math library, -lm
), you can easily do
so.
In addition to being edited in the Makefile, variables can be
temporarily overriden on the command line. For example, running
make target CFLAGS="-Wall -g"
will run
make target
, but with CFLAGS
set to
-Wall -g
instead of whatever setting is in the
Makefile.
1.3 Targets that aren’t files
Two of these we will find useful:
You can have a (set of) targets that do not represent files, commonly
including all
and clean
. These are specified
by having the line .PHONY: all clean
, and then using
all
and clean
like regular targets. You
should always have your first rule be named all
and have it do the main
task (i.e., building the program or
library you are providing). You should always have a rule named
clean
that removes all files that are built by
make
.
1.4 Pattern rules
You will often want a few pattern rules
using a few
automatic variables
. There are a lot of things you
could learn about these, but a simple version will often
suffice:
- Use
%.ending
as a target.%
is a wild-card and can represent any name. - Use
%.other_ending
as a dependency.%
has the same meaning it did in the target. - Use
$<
in the system commands as the name of the first dependency. - Use
$@
in the system commands as the name of the target.
Many makefiles will include a command like
%.o: %.c %.h config.h
$(CC) -c $(CFLAGS) $< -o $@
In other words,
Part | Meaning |
---|---|
%.o |
To make any .o file |
: %.c |
from a C file of the same name |
%.h config.h |
(with its .h file and the file
config.h as extra dependencies) |
$(CC) |
use our C compiler |
$(CFLAGS) |
with our compile-time flags |
$< |
to build that .c file |
-o $@ |
and name the result the name of our target. |
1.5 Suffix rules
Pattern rules are as shown above are flexible and relatively
intuitive, but do not work in all versions of make
. (In
particular, they are an addition of GNU make, which is almost always the
version of make
you will find on a Linux or OS X system,
and the version of make
we will assume in this course.)
They were added to supplement a less flexible and (in my opinion) less
intuitive (but much older and more widely supported) mechanism known as
suffix rules.
A suffix rule represents the case files with one extension are
produced from files with another extension. To make a suffix rule that
produces .ending
files from .other_ending
files, then:
- Use
.other_ending.ending
should be thetarget
of the rule; this will represent (something).other_ending being a dependency and (something).ending being a target. - Use
$<
in the system commands for the dependency file ((something).other_ending) - Use
$@
in the system commands for the target file ((something).ending)
In order to ensure that a .other_ending.ending
rule has
the wild-card property, one needs to inform make
that
.other_ending
and .ending
are file extensions
using the special .SUFFIXES
target:
.SUFFIXES: .ending .other_ending
A suffix rule for compiling .o files from .c files with the same effect as the above pattern rule example may look like:
.c.o: config.h
$(CC) -c $(CFLAGS) $< -o $@
2 A bit about
bash
Recall that each line of bash
begins with a program and
is followed by any number of arguments. bash
also has
various control constructs and special syntax that can be used where
programs are expected, like for
and x=
, which
we will not cover in this class. It can also redirect input and output
using |
, >
, <
,
>>
, 2>
, and a few others.
Because new-lines are important to bash
, but sometimes
line breaks help reading, you can end a bash
line with
\
to say I’m not really done with this line.
Thus
echo 1 echo 2 \
\
echo \
3
echo 4echo 5 \
echo 6echo 7
is equivalent to
echo 1 echo 2 echo 3 echo 4
echo 5 echo 6
echo 7
You can also combine lines; a ;
is (almost) the same as
a new-line to bash
.
2.1 Identifying dependencies
In the case of .o files depending on their .c files and executables depending on their .o files and similar, a programmer can easily determine the appropriate dependencies for a makefile rule by reading the command the rule would run.
But dependencies of .o files on .h files are less apparent. If
foo.c
includes bar.h
, then the rule for
producing foo.o
should depend on bar.h
(and
foo.c
), since changes to bar.h
might change
what goes into foo.o
. This would mean that when
foo.c
’s #include
s are updated, the makefile
rule needs to be updated correspondingly.
Although it is common to manually synchronize makefile rules and the
#include
s in source files, often projects that use
makefiles automate this task. There are a variety of mechanisms that you
might see projects use to do this (examples: 1,
2). (Alternately,
avoiding worrying about such issues might be a reason projects will use
a build system where the programmers do not use make
or do
not use make
directly.)
3 Multi-file project design
In simple use of Makefiles, a project, which may produce multiple executable program and/or libraries, will have a single Makefile.
Generally, this Makefile will have targets and corresponding rules:
for each object file, to compile each source file (for example,
foo.c
) into that object file (foo.o
) whenever it or anything it #includes changes. Commonly, rather than writing an individual rule for each .o file, pattern rules will be used for this task.for each executable program and/or library, to link all the object files that form a parto of it if any of the object files have changed
for common tasks (like
all
for building everything andclean
for removing built files) that just depend on other targets or run certain commands, so users can run things likemake clean
to do those tasks
4 Some additional references
On make in particular:
- The manual for
GNU make (the version of
make
on the department machines and most Linux systems). - The POSIX specification for make.
On build systems:
- Some selected other build systems for C/C++ (when make isn’t enough, maybe):
See also GNU make manual,
Two Flavors of Variables
.For example,
BAR = c FOO = $(BAR) b BAR = a
will make
$(FOO)
evaluate toa b
even thoughBAR
.In contrast:
BAR = c FOO := $(BAR) b BAR = a
will make
$(FOO)
evaluate toc b
.In addition to variable references, the right-hand side of assignments can contain
function
calls, for example:DATE1 = $(shell date) DATE2 := $(shell date)
will run the
date
command and use its output as the value of$(DATE1)
or$(DATE2)
. SinceDATE2
’s assignment uses:=
, thedate
command will be run just once for it, making the value of$(DATE2)
constant. In contrast,$(DATE1)
will rerun thedate
command each time it is used, potentially resulting in different values over time.↩︎