make
is a tool used on all the CS 24 programming projects to build executables and run tests.
The make
program reads the Makefile
in the current directory to understand how files can be built from other files.
Knowing how a Makefile
works will help you understand how your code is compiled and how the tests work, which will let you investigate test failures and change compiler settings.
We will focus on GNU Make, but there are numerous build programs based on the same concepts.
Understanding a Makefile
We will walk through the Makefile
for project01 (JVM), which will demonstrate several of make
’s features.
By the end, you should feel comfortable reading the Makefile
s for most of the CS 24 projects.
Sources, targets, and build commands
At its core, a Makefile
consists of “rules” for all files that can be built.
Unfortunately, Makefile
s are written in a unique sort of programming language, which takes some practice to read.
Each rule has three parts:
- The name of the file to build (this is called the “target”)
- The files that the target is built from (these are called the “sources”).
These can be files written manually, or other targets in the
Makefile
. - The command needed to build the target from the sources (this is called the “recipe”). (Technically, the recipe consists of zero or more commands to run sequentially.) Commands have to be indented with a tab character.
For example, here is a build rule from the JVM Makefile
:
jvm: jvm.o read_class.o
$(CC) $(CFLAGS) $^ -o $@
The pieces that start with $
are variables, which we’ll discuss later.
After substituting in the values of these variables, this rule becomes:
jvm: jvm.o read_class.o
clang-with-asan -Wall -Wextra -Werror -fno-sanitize=integer jvm.o read_class.o -o jvm
Here, jvm
is the target file: this rule explains how to build the jvm
executable.
jvm.o
and read_class.o
are the source files: they are needed to build jvm
.
Finally, clang-with-asan ... -o jvm
is the command that builds jvm
: it links the .o
files together into a final executable.
When you run make
to build a file, it automatically builds all of the necessary intermediate files.
For example, make jvm
will build jvm
, which will recursively build jvm.o
and read_class.o
first.
If you’re interested, you can read more about the theory behind this on Wikipedia.
If you run make
again after editing some files, it will use the rules to rebuild only the files that might change.
For example, if only jvm.c
is edited, then running make jvm
will rebuild jvm.o
(since it is built from jvm.c
) and jvm
(since it depends on jvm.o
), but not read_class.o
.
This can save a lot of time when recompiling a large codebase after making small changes.
Pattern rules
When there are many files that can be built the same way, it becomes tedious to write out a build rule for each one.
make
allows you to write “pattern rules”, which use %
to match part of the source and target names.
Here’s an example in the JVM Makefile
:
tests/%.class: tests/%.java
javac $^
This rule says that any .class
file in the tests
directory can be built from a corresponding .java
file using javac
.
So Arithmetic.class
is built from Arithmetic.java
by running javac Arithmetic.java
, Jumps.class
is built from Jumps.java
, etc.
The %
can match any part of a filename, but it must match the same string in the target and the sources.
Variables
User variables
Variables can be set in Makefile
s and used in recipes, targets, and sources.
They are often used to set configuration options.
For example, the JVM Makefile
defines the standard variables CC
(the C compiler to use) and CFLAGS
(the flags to pass to the C compiler):
CC = clang-with-asan
CFLAGS = -Wall -Wextra -Werror -fno-sanitize=integer
Every place where $(CC)
occurs later in the Makefile
, it is substituted with clang-with-asan
, and similarly for $(CFLAGS)
.
Automatic variables
Makefile
recipes can also access several “automatic variables”, whose values come from the source and target filenames.
They are used frequently to avoid repeating the sources and targets, or to get the matched filename in a pattern rule.
Unfortunately, the names of these variables are hard to remember.
There is a full list in the make
documentation, but the most common ones are:
-
$@
: the target filename -
$^
: the space-separated source filenames -
$<
: the first source filename
Now you should be able to read most of the rules in the JVM Makefile
!
For example, the rule to run the test programs using your JVM:
tests/%-actual.txt: tests/%.class jvm
./jvm $< > $@
This is a pattern rule, where %
represents the name of the test (e.g. Primes
).
It says that the output depends on both the test’s compiled .class
file and the jvm
execuable.
That way, if either the test or the jvm
source code changes, the test will be re-run.
Substituting the automatic variables, you can see that the recipe (for the Primes
test, for example) is:
1
./jvm tests/Primes.class > tests/Primes-actual.txt
(This stores everything printed by ./jvm tests/Primes.class
in tests/Primes-actual.txt
.)
Builtin functions
make
provides many builtin functions for advanced functionality beyond the needs of CS 24.
There is a full list, but the most important one to know is the one to replace filename endings.
This looks like $(VARIABLE:old_suffix=new_suffix)
.
For example, in the JVM Makefile
:
test2: $(TESTS_2:=-result)
This says to replace the empty suffix with -result
(i.e. add -result
to the end) for each file in TESTS_2
.
Since the TESTS_2
variable has the value OnePlusTwo PrintOnePlusTwo
, this rule expands to:
test2: OnePlusTwo-result PrintOnePlusTwo-result
Pseudo targets
You may have noticed that there are several rules in the JVM Makefile
whose recipes don’t actually create their target file.
(Specifically: test
, test1
through test8
, %-result
, and clean
.)
These are called “pseudo targets”.
This is allowed by make
, and just means that the recipe will always run if you ask make
to build these targets.
For example, make clean
will always run the rm -f ...
command and make TEST_NAME-result
will always print out the result of the test TEST_NAME
, even if the test doesn’t need to be re-run.
One use case for pseudo targets is to represent collections of files to build.
In this case, the rule has a list of sources to build, but no additional commands to build the pseudo target.
For example, the test8
pseudo target (or just test
) represents running all the tests.
Since test8
has all of the test results as sources, running make test8
will report all the test results:
TESTS_8 = $(TESTS_7) Arithmetic CoinSums DigitPermutations FunctionCall \
Goldbach IntegerTypes Jumps PalindromeProduct Primes Recursion
test8: $(TESTS_8:=-result)
You can also run make
with no arguments, which will build the first target declared in the Makefile
.
The CS 24 Makefile
s intentionally have test
or all
as their first targets, so you can often just run make
.