CS 70

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 makefile option specifying a different file.

Each section of the Makefile has the form

target : prerequisites
	commands

where the commands 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 commands 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 running make (or make all) will bring all executables up to date.
clean
Deletes all generated files (e.g., .o files and compiled executables). Running make 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
  • L-Diskie speaking

    One random thing: The suffix rule we just gave is actually already built into make and cs70-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.

  • Rabbit speaking

    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 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.)