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
allto 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.,
.ofiles and compiled executables). Runningmake cleanremoves 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
makeandcs70-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.,
.cppand.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.ofiles. But since we're almost always just making one executable, it's not nearly as useful as the double-suffix rule for.cppto.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
A Template Makefile
The makefile below is a good starting point for later assignments in CS 70 where you need to create a makefile for a C++ program. It uses all the features we've discussed so far. To use it,
- Replace
awesome-programandfoobar-testin theTARGETSvariable with the names of the executables you want to build. - Add rules for building the necessary object files from your source files in the “Targets to build object files” section.
- The command
clang++ -MM *.cppcan help you find the necessary prerequisites for each object file. - You don't need a rule to turn
.cppfiles into.ofiles, because the suffix rule for that is already defined in the built-in rules that come withmakeandcs70-make.
- The command
- Add rules for building each executable from its object files in the “Targets to build executables” section, making sure to include any necessary libraries in the linking step.
- Be sure to update the comments to reflect your changes!
The Makefile
#
# Makefile for a CS 70 Assignment
#
## Makefile variables
#
# Using variables simplifies changing compiler options and adding new
# executables, allowing you to override them on the command line if needed.
#
# - CXX: the C++ compiler to use
# - OPTFLAGS: optimization flags to pass to the compiler
# - GENFLAGS: general flags to pass to the compiler at all stages
# - DBGFLAGS: debugging flags to pass to the compiler
# - CPPFLAGS: flags to pass to the C preprocessor
# - CXXFLAGS: flags to pass to the compiler
# - LDFLAGS: flags to pass to the linker
# - TARGETS: names of all the targets in the executables section below
CXX = clang++ $(GENFLAGS)
OPTFLAGS =
GENFLAGS =
DBGFLAGS = -g
CPPFLAGS =
CXXFLAGS = $(DBGFLAGS) -std=c++20 -pedantic -Wall -Wextra $(OPTFLAGS)
LDFLAGS =
TARGETS = awesome-program foobar-test
## Phony Targets (These are not actually files to build)
all: $(TARGETS)
clean:
rm -f *.o $(TARGETS)
# ^--- important, that's a tab, NOT spaces! true for all build commands
# Magic variables explained:
# $@ is the target (left side of the colon)
# $< is the first prerequisite (right side of the colon)
# $^ is all of the prerequisites (right side of the colon)
## Targets to build object files
foobar.o: foobar.cpp foobar.hpp foo.hpp bar.hpp
foo.o: foo.cpp foo.hpp
bar.o: bar.cpp bar.hpp
foobar-test.o: foobar-test.cpp foobar.hpp foo.hpp bar.hpp
awesome-program.o: awesome-program.cpp foobar.hpp foo.hpp bar.hpp
## Targets to build executables
#
# Note:
# - foobar-test needs to be linked with the testinglogger library
# - awesome-program needs to be linked with the randuint32 library
#
# Note that we don't put the libraries in LDFLAGS because not all
# executables need the same libraries -- if everything needed the same
# libraries, we'd list them in LDFLAGS instead.
foobar-test: foobar-test.o foobar.o foo.o bar.o
$(CXX) $(LDFLAGS) -o $@ $^ -ltestinglogger
awesome-program: awesome-program.o foobar.o foo.o bar.o
$(CXX) $(LDFLAGS) -o $@ $^ -lranduint32
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 makes (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.)