Working with Makefiles
Building C++ programs requires you to compile each .cpp
file into object (.o
) files, then link those object files into a final executable, possibly including linking in other libraries your program uses. Manually running the compiler and linker at the command line gets quite tedious, especially as many steps require you to compile multiple files or link multiple libraries and object files. It’s also quite easy to make mistakes (e.g., recompile and not relink, or vice versa; or recompile the wrong files).
This problem has been with us since the earliest days of C in the 1970s. In 1976, after commiserating with his colleagues about the frustrations of remembering all the necessary steps or trying to maintain program-specific shell scripts, Stuart Feldman at Bell Labs wrote a C program called make
.
make
allows you to maintain a (relatively) simple file (the “makefile”) that lists all the dependencies and the necessary commands for compiling and linking each component. make
also provides features that allow you to generalize rules; so that, for example, one line in the makefile can be used to compile any .cpp
file.
Finally, make
looks at the timestamps on files to see if they've been modified and runs the appropriate commands to recompile and link any files that have changed. make
checks through the dependencies recursively, so that even if a particular file hasn't changed, if one or more of its dependencies (or dependencies' dependencies) have changed, make
will run all the commands necessary to ensure that all the parts are up to date.
make
vs. cs70-make
In addition to the regular version of make
on the server, we provide the cs70-make
program. cs70-make
does the same things that regular make
does, but also gives you information about why it's doing something. For example, cs70-make
might give you output like
- Making generateSegfault
- Found rule for generateSegfault, prereqs: generateSegfault.o helloSayer.o
- Making generateSegfault.o
- Found rule for generateSegfault.o, prereqs: generateSegfault.cpp helloSayer.hpp
- Making generateSegfault.cpp
+ Okay, no rule found, but file exists
- Making generateSegfault.hpp
+ Okay, no rule found, but file exists
+ Target generateSegfault.o doesn't need to be rebuilt (no newer prereqs)
- Making helloSayer.o
- Found rule for helloSayer.o, prereqs: helloSayer.cpp helloSayer.hpp
- Making helloSayer.cpp
+ Okay, no rule found, but file exists
- Making helloSayer.hpp (skipped, already made!)
- Target helloSayer.o must be rebuilt because these prereqs are newer: helloSayer.cpp
- Building: helloSayer.o
- Running: clang++ -c -g -std=c++17 -pedantic -Wall -Wextra helloSayer.cpp
+ Made helloSayer.o!
- Target generateSegfault must be rebuilt because these prereqs are newer: helloSayer.o
- Building: generateSegfault
- Running: clang++ -o generateSegfault generateSegfault.o helloSayer.o
+ Made generateSegfault!
whereas make
would just show
clang++ -o generateSegfault -g -std=c++17 -pedantic -Wall -Wextra generateSegfault.o helloSayer.o
along with whatever output the compiler produced when compiling and linking. (If you don't want all the chatter because you know your makefile is working, cs70-make
can also provide more minimal output if you specify the --hush
(or -H
) option.)
cs70-make
is also more likely to give an error when you've made a mistake in your Makefile
. Sometimes regular make
assumes that you're an expert and you did something weird on purpose.
Note that cs70-make
supports suffix rules, but not pattern rules or some other advanced features present in some versions of mak
. Your makefiles in CS 70 must work with cs70-make
, so you'll need to make sure that cs70-make
works with your Makefile
and not use features that cause problems.
You can find out more about cs70-make
by running
cs70-make --help
or
man cs70-make
Writing Makefiles
For almost any project you work on, the default name for your makefile will be Makefile
(note the capital “M”). make
(and cs70-make
) will expect to find Makefile
if run without the -f
option specifying a different file.
Each section of the Makefile has the form
target :prerequisites commands
where the
are indented by a single TAB character (not spaces!). You can read these rules as “if any of these ‹prerequisites› change, then ‹target› needs to be updated by running one or more ‹commands›.”
It’s impossible to see in our example, but every
line in the Makefile
must start with a literal “tab” character! If you try to use eight spaces instead of one tab character, your makefile will not work at all. Thus, makefiles are exempt from the usual CS 70 rule on indenting with spaces rather than tabs.
Targets and “Phony” Targets
Each rule set defines a “target” that can be used as a prerequisite in other rules (or even run from the command line). Targets are typically object files to build, executables to be linked, and similar things that result in the creation of a file.
“Phony” targets allow you to specify actions that don't produce a file directly. The two idiomatic phony targets that appear in almost any makefile are
all
- Has all relevant programs as prerequisites, but runs no commands itself. Having this target ensures all its prerequisites are up to date (as usual), but then doesn’t do anything else. It is idiomatic for
all
to be the first target in a makefile, so that just runningmake
(ormake all
) will bring all executables up to date. clean
- Deletes all generated files (e.g.,
.o
files and compiled executables). Runningmake clean
removes generated files.
Prerequisites
Each rule depends on one or more rules, and the results of those rules are checked by make
recursively.
As we're building C++ programs, we know that we have to compile our .cpp
files and then link the resulting .o
files into an executable. Because header (.hpp
) files are included in the compilation process, we also need to make sure that any changes to a header file result in our recompiling the files that include
it.
So our Makefile
might look like
all: generateSegfault generateSegfault: generateSegfault.o helloSayer.o clang++ -o generateSegfault generateSegfault.o helloSayer.o generateSegfault.o: generateSegfault.cpp helloSayer.hpp clang++ -c -g -std=c++17 -pedantic -Wall -Wextra generateSegfault.cpp helloSayer.o: helloSayer.cpp helloSayer.hpp clang++ -c -g -std=c++17 -pedantic -Wall -Wextra helloSayer.cpp clean: rm -rf *.o generateSegfault
Finding Prerequisites
Running
clang++ -std=c++17 -MM *.cpp
will give you a list of .cpp
files and the .cpp
and .hpp
files they depend on.
Eliminating Redundancy
One problem with our example Makefile
is its redundancy. If we wanted to use the g++
compiler instead of clang++
, or if we wanted to turn on even more compiler warnings, we’d have to make lots of changes in lots of places, which is tedious and error-prone.
We can reduce that redundancy by using variables (which make
calls macros). Specifically, if we have the line
VARIABLE=some text
in our makefile, then any appearance of $(VARIABLE)
is automatically replaced by some text
.
So we can update our makefile example to replace some redundant elements:
CXX = clang++ CXXFLAGS = -g -std=c++17 -pedantic -Wall -Wextra generateSegfault: generateSegfault.o helloSayer.o $(CXX) -o generateSegfault generateSegfault.o helloSayer.o generateSegfault.o: generateSegfault.cpp helloSayer.hpp $(CXX) -c $(CXXFLAGS) generateSegfault.cpp helloSayer.o: helloSayer.cpp helloSayer.hpp $(CXX) -c $(CXXFLAGS) helloSayer.cpp clean: rm -rf *.o generateSegfault
Special Macros
In addition to defining our own macros, make
provides some predefined macros based on the target and prerequisites. The following three are especially useful
$@
→ The target
$<
→ The first prerequisite
$^
→ All of the prerequisites
As a mnemonic, consider
- The
@
sign looks a bit like a target - The
<
points to the left towards the first prerequisite - The
^
points up to all the prerequisites listed above
We can update our makefile example to use these special macros:
CXX = clang++ CXXFLAGS = -g -std=c++17 -pedantic -Wall -Wextra generateSegfault: generateSegfault.o helloSayer.o $(CXX) -o $@ $^ generateSegfault.o: generateSegfault.cpp helloSayer.hpp $(CXX) -c $(CXXFLAGS) $< helloSayer.o: helloSayer.cpp helloSayer.hpp $(CXX) -c $(CXXFLAGS) $< clean: rm -rf *.o generateSegfault
Suffix Rules
Suffix rules let us make things even more generic. They say “here's how to transform a .xxx
file into a .yyy
file”. For example, we can say “here's how to transform a .cpp
file into a .o
file”:
.cpp.o: $(CXX) $(CXXFLAGS) -c $<
This special kind of rule says “if no one gave you any build commands for building a particular .o
file from a .cpp
file, use these commands”.
Thus you can just define a single suffix rule for .cpp
to .o
and then you only need to write the dependencies for each .o
file, as you can see:
CXX = clang++ CPPFLAGS = CXXFLAGS = -g -std=c++17 -pedantic -Wall -Wextra LDFLAGS = .cpp.o: $(CXX) $(CXXFLAGS) $(CPPFLAGS) -c $< generateSegfault: generateSegfault.o helloSayer.o $(CXX) -o $@ $^ $(LDFLAGS) generateSegfault.o: generateSegfault.cpp helloSayer.hpp helloSayer.o: helloSayer.cpp helloSayer.hpp clean: rm -rf *.o generateSegfault
One random thing: The suffix rule we just gave is actually already built into
make
andcs70-make
(using exactly those variables), so you don't even need to write it in your makefile. But it's good to know how to write one, in case you ever need to write a different kind of suffix rule.Technically, that kind of rule is known as a double-suffix rule, because it specifies both the source and target suffixes (i.e.,
.cpp
and.o
). There are also single-suffix rules that specify just the source suffix and assume the target doesn't have a suffix. That variant almost always appears as just.o:
, and is typically used to specify how to make an executable from.o
files. But since we're almost always just making one executable, it's not nearly as useful as the double-suffix rule for.cpp
to.o
.
Built-in Rules
Like most versions of make
, cs70-make
has a number of built-in rules, including a version of the .cpp.o:
rule we just saw. You can see a list of all the built-in rules by running
cs70-make -p
and you can find a much longer set of the built-in rules for GNU make
by running
make -p -f /dev/null
More make
We've gone over the basics and a bit about some useful features, but make
can go even further.
The Linux systems we're using use GNU make, which supports many more features. You can read more about it in its manual, much of which will also apply to other make
s (e.g., BSD make
on BSD systems).
Other Build Systems
There are other build systems besides make
, although make
(and, in particular, GNU make
) is probably the most commonly used one, especially for free/open source software, and especially on Unix/Unix-like systems.
Other languages also have their own build systems. But make
is the inspiration for them all.
(When logged in, completion status appears here.)