A Practical Guide to Make
In this article, you will learn how to use the Make build system to compile software projects. This article is targeted at those who have experience programming, but are not deeply familiar with the shell or commandline build systems. However, this article does assume you have basic familiarity with the UNIX commandline.
I decided to write this article because, in my experience observing my peers, and in 2 semesters of TAing an intro to UNIX class, Make seems to perpetually be a sticking point for new users. This article is in particular targeted at my peers and students who have often completed one or more semesters of a computing degree, and are competent programmers, but have not had the benefit of UNIX experience.
Background - What is Make?
The true purpose and utility of Make are often neglected in courses and articles which discuss it. Make is often regarded and used as simply a tool for chaining shell commands together. While Make can indeed do this, using make as a glorified shell script parser leaves many of it’s most powerful and useful features neglected.
Make is as much a tool for executing commands and scripts as it is a dependency resolution system. In Make, instructions on how to build a piece of software are split up into targets, each of which may have zero or more dependencies. A given target cannot be executed until all of it’s dependencies have, and multiple targets can share overlapping dependencies.
Consider the following:
- To execute A, I must execute B, C, and F
- To execute B, I must execute C
- To execute C, I must execute F and D
- To execute D, I must execute F
If you then wished to execute “A”, figuring out the most efficient order in which to execute the tasks B-F without repeating or executing a task without it’s dependencies, is a quite difficult problem, the complexity of which grows multiplicatively with the number of targets and dependencies.
Make Syntax
Make uses a very simple syntax. Targets are strings which start in column zero
and are terminated by a :
, and followed by a list of dependencies. The
command to be executed within a target are indented below the target name
by a single tab character (8 spaces will not work).
Consider the example from the previous section encoded as a Makefile:
A: B C F
echo "executing A"
B: C
echo "executing B"
C: F D
echo "executing C"
D: F
echo "executing D"
F:
echo "executing F"
Executing this make file with the command make A
yields the following output
echo "executing F"
executing F
echo "executing D"
executing D
echo "executing C"
executing C
echo "executing B"
executing B
echo "executing A"
executing A
Thus, we can see that F->D->C->B->A was the most efficient order in which to execute the tasks such that each task’s dependencies were executed before the task itself.
Notice that the commands being executed are displayed alongside their output,
this can be turned off by preceding the command with an @
symbol, for
example:
A: B C F
@echo "executing A"
B: C
@echo "executing B"
C: F D
@echo "executing C"
D: F
@echo "executing D"
F:
@echo "executing F"
yields the output:
executing F
executing D
executing C
executing B
executing A
A Simple C Program
Consider the following files comprising a simple C program:
main.c
#include "main.h"
int main() {
hello();
printf("Hello from main.c!\\n");
}
main.h
#ifndef MAIN_H
#include<stdio.h>
#include "module.h"
#define MAIN_H
#endif
module.c
#include "module.h"
void hello() {
printf("Hello from module.c!\\n");
}
module.h
#ifndef MODULE_H
#include<stdio.h>
void hello();
#define MODULE_H
#endif
The corresponding Makefile
might look like:
helloprog: main.o module.o
gcc -o helloprog main.o module.o
main.o: module.o main.c main.h
gcc -o main.o -c main.c main.h
module.o: module.c module.h
gcc -o module.o -c module.c module.h
clean:
-rm module.o
-rm main.o
-rm helloprog
-rm module.h.gch
-rm main.h.gch
Lets dissect line-by-line…
helloprog: main.o module.o
- the helloprog
target requires that main.o
and module.o
be built before it can run.
gcc -o helloprog main.o module.o
- this gcc
command produces a program
named helloprog
by linking the object files main.o module.o
. For those
unfamiliar with C, .o
files are analogous to .class
files in Java.
main.o: module.o main.c main.h
- the target main.o
requires that
module.o
be built first, and also depends on the contents of the files
main.c
and main.h
. This is one of the clever features of make - if
main.o
is already present and main.c
and main.h
have not changed since
the last time the target was built, the target can simply be skipped - more on
this later.
gcc -c main.c main.h
- generate the object files for main
module.o: module.c module.h
- the target module.o
depends on the files
module.c
and module.h
. Notice that it depends on no other Make target.
clean:
- it is a convention that a Makefile
should provide a target named
clean
which removes any build artifacts such as object files, binaries,
debug symbols, and so on. Notice that the lines in this target are preceded by
-
, this instructs make to proceed even if one of the command fails. Without
the -
character, if say module.o
did not exist, but main.o
did,
the command rm module.o
would fail, causing make clean
to exit without
removing main.o
.
Using Variables
Standard Variables
In GNU make, variables are assigned in one of four ways. The simplest is with
the :=
operator, and take the form VARNAME := value
. The other three
methods are beyond the scope of the tutorial, but more information can be found
here.
Variables can be assigned the output of shell commands, for example
THEDATE := $(shell date)
would set THEDATE
to the output of the
shell command date
.
Variables are accessed using the syntax $(VARNAME)
. Note that variables can
be accessed from anywhere in the Makefile, but only defined outside of targets.
Consider the example below…
Makefile:
VARNAME:=a string
THEDATE:=$(shell date)
mytarget:
@echo "the date is $(THEDATE)"
@echo "VARNAME is: $(VARNAME)"
Output:
the date is Sun May 28 18:18:00 EDT 2017
VARNAME is: a string
Special Variables
NOTE What I refer to as “special variables” are actually referre to as Automatic Variables in the Make documentation.
Automatic variables allow you to quickly write very powerful Makefiles, and write rules so they may be easily copy-pasted or otherwise re-used. For the sake of brevity, I shall not reproduce the full list of automatic variables here, but rather provide a link to the pertinent page in the Make documentation, plus the following example…
Makefile:
myrule: otherrule coolrule
@echo "target name is: $@"
@echo "first prerequisite: $<"
@echo "all prerequisites: $^"
otherrule:
@echo "hello from otherrule"
coolrule:
@echo "hello from coolrule"
Output:
hello from otherrule
hello from coolrule
target name is: myrule
first prerequisite: otherrule
all prerequisites: otherrule coolrule
Note that there are other special variables, but these are the ones I find to be the most useful.
Armed with this information, let’s rewrite our simple C program’s Makefile to be a little cleaner…
Makefile:
helloprog: main.o module.o
gcc -o $@ $^
main.o: main.c main.h
gcc -c $^
module.o: module.c module.h
gcc -c $^
clean:
-rm module.o
-rm main.o
-rm helloprog
-rm module.h.gch
-rm main.h.gch
Notice that all three of the compilation-related rules are considerably shorter and easier to type. This is especially handy when writing a target which has many dependencies which later change - any changes can be made in one place.
Pattern Rules
One of the most powerful, but also most arcane features of Make is pattern
rules.
Pattern rules allow you to generalize the command to build a particular type of
file beyond one single instance of that file. A pattern rule’s target and
dependencies can be defined in terms of a single base part of file name. As an
example, a pattern rule to build a C .o
file might associate %.o
(the
target) with the dependencies %.h
and %.c
.
Consider a trivial example:
%.baz: %.txt
cp $< $@
And a shell session with this Makefile:
[cad@kronos][11:09][~/Desktop/tmp]
(zsh) $ make bar.baz
cp bar.txt bar.baz
[cad@kronos][11:09][~/Desktop/tmp]
(zsh) $ make foo.baz
cp foo.txt foo.baz
[cad@kronos][11:09][~/Desktop/tmp]
(zsh) $ ls
bar.baz bar.txt foo.baz foo.txt Makefile
As you can see, %
is a sort of context-sensitive variable we can use to
associate a single common action with different possible input and output
files. As an example, in most C programs, every C source code file will be
compiled into an object file in the same way, it is thus redundant to specify a
separate rule for each and every C file individually.
Now, adding in what we have learned about pattern rules, let’s re-write our ideomatic C Makefile one again to use them:
helloprog: main.o module.o
gcc -o $@ $^
%.o: %.c %.h
gcc -c $^
clean:
-rm module.o
-rm main.o
-rm helloprog
-rm module.h.gch
-rm main.h.gch
In this very simple example, we haven’t saved all that much space, we just got
rid of one single rule. However, keep in mind that pattern rules will scale to
arbitrarily many files. With the Makefile above, we could have the helloprog
target depend on dozens of object files, but still have all the object files
generated by a single pattern-rule.
As an aside, you should also familiarize yourself with Make’s Implicit Rules which can sometimes be used to simplify your Makefile even further; Implicit rules are built-in pattern rules that know how to build common file types such as C object files, and equivalents for other languages. Personally, I do not make use of implicit rules, because I feel that they make the build process too opaque and intractable to developers not deeply familiar with Make. Nevertheless, implicit rules are a longstanding and mature feature of Make which any C developer should be familiar with.
Best Practices
-
Be careful about which commands should fail the build when they fail, and which you can safely ignore failures of using the
-
character. -
Always try to name rules according to the file they produce as output (when applicable) - this allows make to cleverly avoid rebuilding files whose sources have not changed on disk since the last build.
-
When a rule depends on the contents of one or more files, such as compiling a program from source, always list the files which the rule takes as input as it’s dependencies. This is for the same reason as above - Make will cleverly avoid rebuilding object files if the object file exists, and it’s corresponding source file on disk has not changed.
-
Do not run
make clean
excessively, for the same reason as the previous points; Make is smart and will re-use object files (and similar) if they already exist and their dependencies have not changed. -
Typically, when working with compiled languages, you should create variables for your compiler and the flags for the same. For example, in C, it is convention to store your compiler in
CC
and your compiler flags in CFLAGS. For example, you might setCC
togcc
andCFLAGS
to-Wall -Werror
. Then in your rules, you can use these variables rather than writing out your compiler and all it’s options - this will save you lots of times if you decide you want to add another compiler flag or anything of that sort.
Common Gotchas and Pitfalls
-
Make targets must be indented by the tab character - this is NOT the same thing as some number of spaces.
-
Makefiles must be named
makefile
orMakefile
, or make will not see them. -
If you run the command
make
without a specifying a target, it simply executes the first target in the Makefile. -
On case-sensitive systems such as Linux,
makefile
andMakefile
are separate files (unlike Mac and Windows) - creating both in the same directory will cause unexpected consequences.
The Future of Build Systems
In the modern age, much of software development is moving away from Make. It is becoming increasingly common for new languages to implement first-party build and dependency management tools. Even in cases where language developers do not do so, community projects often spring up to create language-specific build tools. Many of these build tools offer much more advanced features than Make does, as they can be tailored to their specific language, rather than acting as a general-case solution. Nevertheless, Make is still widely used for C and C++ projects large and small, and it is unlikely that Make will be going away any time soon in that space. Make also has the benefit of being a general-case solution, which makes it useful for tasks beyond compiling C and C++ code (case in point, this website is generated by a Makefile).
Some popular build systems for Java include
All three of these tools are fairly similar, and solve more or less the same problems: managing dependencies and compiling projects. I have personally only used Maven, and even then not much, so I am unqualified to make an educated recommendation between the three. Anecdotally, Maven seems to be the most popular for large-scale enterprise applications.
For Rust, the first-party build and dependency management system is Cargo.
For C and C++, CMake has become popular in recent years. CMake is a tool for generating and executing Makefiles in an automated fashion, but does not handle dependency installation and management as Cargo, Gradle, Maven and Ant do.
There are may other language-specific build tools aside from these - you should investigate the ones available for your language of choice… it is frequently better to use a purpose-build solution rather than a general purpose one.
Finally, in the general-purpose build system space, Make has a new competitor: Sake. Sake offers many of the same features and benefits of Make, with a few key differences:
- Sake uses the standardized YAML syntax, rather than inventing it’s own.
- Sake build files are self-documenting.
- Sake is fully cross-platform, and runs equally well as a native Windows application as a native Linux application.
- Sake uses file hashes, rather than filesystem timestamps to determine if a dependency or target is out of date. This can make it more reliable on some systems.
Further Reading
Edit History
- 2018-02-22 - removed “Using Make with Java” section, it was just an anecdote and did not provide a useful example, nor useful information.
- 2018-03-02 - Added Pattern Rules section.
- 2018-03-02 - Added “The Future of Build Systems” section.
- 2018-03-06 - Added “Further Reading” Section.
- 2019-11-25 - Update section headers to new style.
- 2020-01-18 - Fix some minor formatting errors.
- 2020-02-25 - Use
gcc -o
to set output file names explicitly (thanks James!). - 2023-03-14 - Formatting tweaks while migrating to Hugo.