cs24-21fa make

Introduction to Computing Systems (Fall 2021)

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 Makefiles 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, Makefiles are written in a unique sort of programming language, which takes some practice to read.

Each rule has three parts:

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 Makefiles 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:

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:

./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 Makefiles intentionally have test or all as their first targets, so you can often just run make.