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:

  1. A target, the name of a file that will be created or updated by the rule.
  2. 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.
  3. 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:

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:

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 4
echo 5 \
    echo 6
echo 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 #includes are updated, the makefile rule needs to be updated correspondingly.

Although it is common to manually synchronize makefile rules and the #includes 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:

4 Some additional references

On make in particular:

On build systems:


  1. See also GNU make manual, Two Flavors of Variables.

    For example,

    BAR = c 
    FOO = $(BAR) b
    BAR = a

    will make $(FOO) evaluate to a b even though BAR.

    In contrast:

    BAR = c
    FOO := $(BAR) b
    BAR = a

    will make $(FOO) evaluate to c 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). Since DATE2’s assignment uses :=, the date command will be run just once for it, making the value of $(DATE2) constant. In contrast, $(DATE1) will rerun the date command each time it is used, potentially resulting in different values over time.↩︎