This page is a light-weight introduction to the tool make
and to Makefile
s.
Makefiles are quite powerful and, while sometimes considered antiquated compared to tools like cmake, gradle, and scons, it remains the dominant build automation tool in the GNU and Linux ecosystems. You’re welcome to read a little more or read a lot more than this one example summary if you wish.
There are many build management tools. They exist for many reasons, but a few of the main ones are
Compiling code takes time. We’d rather only do that when we need to (i.e. when a source file changes).
Source files often have dependencies, such that changing one file means a handful of files need to be recompiled. We’d rather have an automated way of figuring our what needs to be recompiled.
Compilation commands can become complicated, with dozens of flags controlling optimizations, include paths, libraries to link against, and so on. We’d rather specify this just once and re-use it thereafter.
Broadly speaking, there are three approaches to managing build processes.
Ad-hoc build processes. You either type the commands you want directly in the command line or save a general-purpose script that has all the commands written out manually inside it.
This is fine for tiny, throw-away programs but quickly becomes unwieldy as programs grow in size and complexity.
Language-specific build tools. These incorporate a deep understanding of the language build used to automate a great deal of the build process, such as tracing all #include
s in C or C++ or being aware of how Java converts lambdas into anonymous classes.
These can be very effective and powerful, but also quite limiting. Language updates might require build tool updates, making the build process somewhat fragile. Switching languages can require switching tools, increasing cognitive load. IDEs like VS Code tend to supply dozens of language-specific build tools, all abstracted behind a common interface, which can make them more usable but can also make changing IDEs more complicated.
Generic language-agnostic build tools. These tend to provide a mini language that makes common build patterns easy to write, while still requiring the developer to provide the language-specific details themselves.
This is the type of tool that make
is.
make
BasicsThis section outlines the most basic, common approach to creating a Makefile. Everything in this section has exceptions or alternatives, but this section does not mention them.
Capitalization matters. There are several implementations of make
and it runs on many file systems; some treat upper- and lower-case letters as interchangeable, others do not. To be safe, always assume that everything make
does is case-sensitive, and never have two file names that differ only in capitalization.
When you run make
, it looks for a file named Makefile
in the current working directory.
make
works best if file names lack spaces. Don’t name a file my file.c
, nor a folder some folder
; use something like my_file.c
or some-folder
instead.
Many advanced Makefile
features depend on file extensions. While the C compiler doesn’t care whether your file names end in .c
and .h
or not, make runs more smoothly if they do.
Makefiles allow you to define variables. These are traditionally given all-upper-case names. They are defined by having =
between the variable name and its meaning (the spaces are important).
The following line in a Makefile
defines a variable named CFLAGS
:
CFLAGS = -g -Wall -Werror -pedantic-errors -std=c17
The value of the variable would be written in C as "-g -Wall -Werror -pedantic-errors -std=c17"
.
Variables are not automatically expanded; instead, to get their value, you have to place them inside parentheses preceded by a dollar sign.
Continuing the previous example,
cc CFLAGS -c dll.c
would literally run the shell command cc CFLAGS -c dll.c
.
$(CFLAGS) -c dll.c cc
would instead expand the variable CFLAGS
to its definition and run the shell command cc -g -Wall -Werror -pedantic-errors -std=c17 -c dll.c
.
Most programming languages have some process whereby one file is created from one or more other files by invoking some compilation command. This is so common and universal, it is the main feature that make
supports.
There are three parts of a file-creation rule:
The file name to be created.
If any other rule needs this file, this rule will be used to create it.
The dependencies of the file.
These all need to exist (either be created by another rule or be present on disk) for the rule to work.
If the file being created exists but is older than any of its dependencies (as reported by the file system’s tracking of file modification times), the rule will be used to re-create the file.
The commands to run to create the file.
In the Makefile
, these three parts are presented as follows:
"\t"
, not space characters U+0020 " "
.Both the dependencies and the commands are optional, though at least one of the two must be included.
The following is a file-creation rule
dll.o: dll.c dll.h
$(CFLAGS) -c dll.c cc
It explains how to create the file dll.o
.
The dependencies of dll.o
are dll.c
and dll.h
, so if either of those is newer than dll.o
the rule will be used to re-create dll.o
.
There is just one command used to create dll.o
; assuming the same CFLAGS
variable as in previous examples, that command is cc -g -Wall -Werror -pedantic-errors -std=c17 -c dll.c
.
If you run make
, it will try to build the first filename that has a file creation rule.
If you run make test
, it will try to build a file named test
, if there’s a rule for that.
Often we want to have a few rules that don’t make any files. These are provided by using a special line in the Makefile identifying those rules as phony
: they don’t create a file, so there’s no checking to compare the age of the file to the age of its dependencies. This is indicated by a rule-like line starting .PHONY:
following by the rules that are not file names.
The following shows three common phony rules:
.PHONY: all clean test
all: dll_c dll_cpp
clean:
rm -f dll_cpp dll_c dll.o
test:
python3 tests.py
The first line indicates that all
, clean
, and test
are all phony rules, not creating files.
The all
rule has only dependencies; this means that make all
will make both dll_c
and dll_cpp
.
The clean
and test
rules have only commands. clean
removes some files (typically, clean
is used to remove all files that other rules could create); and test
runs a program that does not create any files (typically, test
is used to run any unit tests supplied with the code).
It is common for commands to refer to the file name and dependencies of a rule. Repeating those filenames in both places leads to potential for mistakes where they don’t match, so make
has special variables that can be used in commands to avoid having to type them twice.
$@
the name of the file being created.
$^
the entire list of dependencies.
$<
the first dependency in the list of dependencies.
Compilation commands often have one source file and several header files in their dependencies. The source files should be part of the compilation command, but the header files should not. This might result in a rule like the following:
dll_cpp: usedll.cpp dll.hpp
$< -o $@ c++
which expands out to the same thing as
dll_cpp: usedll.cpp dll.hpp
c++ usedll.cpp -o dll_cpp
Linking commands often have several compiled object and/or source files as their dependencies, all of which should be linked together. This might result in a rule like the following:
dll_c: dll.o usedll.c
$(CFLAGS) $^ -o $@ cc
which expands out to the same thing as
dll_c: dll.o usedll.c
$(CFLAGS) dll.o usedll.c -o dll_c cc
When projects expand to include many files, it is common that large numbers of them all use very similar rules in the Makefile
. To avoid having to write out all the rules individually, Makefiles allow us to create a general pattern of rules. If a %
appears in a file name to be created, it can match any part of a filename and can also be used in the dependencies to match the same string.
The following rule
workloads/%.o: workloads/%.c
$(CC) -c $< -o $@
means to make any file
. This can make workloads/
something.o
, the dependency is workloads/
the same thing.c
and the command is $(CC) -c $< -o $@
workloads/backstep.o
from workloads/backstep.c
, and workloads/linked_list.o
from workloads/linked_list.c
, and so on for as many files as match that pattern.
These patterned rules typically work best when all the files they could create are used as the dependency of some other rule. To facilitate getting many files all in the dependency list, make
has various functions that are often used when defining variables. Function invocations look like variable instantiation, but inside the parenthesis is the function name, a space, and then the function’s arguments separated by commas. The two functions I see the most often are wildcard
and patsubst
:
wildcard
This takes a single argument, a filename with *
s in it, which is expanded similarly to how ls
operates.
The function invocation $(wildcard workloads/*.c)
becomes all of the .c
files in the workloads/
folder, separated by spaces.
patsubst
This takes three arguments: a pattern to match, a pattern to replace it with, and a space-separated list of strings to make the replacements in.
The function invocation $(patsubst %.c,%.o,dll.c usedll.c)
becomes dll.o usedll.o
.
These are often used together to create a set of file names to create based on a set of source files that exist.
Consider the following parts of a single Makefile
, in particular the one used for MP4.
CASES := $(patsubst %.c,%.so,$(wildcard workloads/*.c))
The snippet above causes the variable $(CASES)
to expand to many filenames workloads/
something.so
, one for every workloads/
something.c
that exists on the disk.
workloads/%.so: workloads/%.o
$(CC) -shared -fPIC $^ -o $@
The snippet above explains how to make one of those workloads/
something.so
files, with workloads/
something.o
as a dependency.
workloads/%.o: workloads/%.c
$(CC) -c $< -o $@
The snippet above explains how to make one of those workloads/
something.o
files, with workloads/
something.c
as a dependency.
.PHONY: all test clean build
all: tester mytest.so $(CASES)
The snippet above says that all
is a phony rule that makes the files tester
and mytest.so
, and also makes a workloads/
something.so
from each workloads/
something.c
. Coupled with the earlier rules, each .so
file first makes a .o
file.