CS 70

Working with Makefiles

Manually running the compiler and linker at the command-line gets quite tedious. It’s also quite easy to make mistakes (e.g., recompile and not relink, or vice versa; or recompile the wrong files). Fortunately, we can use a tool called make to automate much of this work.

To tell the make command what to do, we create a configuration file for our project with the name Makefile. A Makefile lets you specify rules of the form “if any of these ‹prerequisites› change, then ‹target› needs to be updated by running one or more ‹commands›.” For example, here is a Makefile that will compile Homework 3’s segfaultGenerator code:

generateSegfault: generateSegfault.o helloSayer.o
    clang++ -o generateSegfault -g -std=c++17 -pedantic -Wall -Wextra 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

Each section of the Makefile has the form

‹target› : prerequisites    ‹commands›

Make is smart in two ways:

  1. It can detect when some or all of your code has not changed and skip commands related to that code.
  2. It operates recursively. If helloSayer.cpp changes and we try to make generateSegfault, then helloSayer.o will be regenerated. After that, the rule for building generateSegfault will run.

There are a couple of tricks to getting your Makefile working:

  • It’s impossible to see in our example code above, but every ‹command› 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.
  • Of course, you also need to get the prerequisites right. In general, a .o file will depend on the corresponding .cppfile and any user-written header files that .cpp file includes (and any header files they include and so on…). Similarly, a runnable program like shuffle will depend on the .o files to be linked.

Phony Targets

In addition to the names of files or programs that we want to keep up-to-date, we can define “phony” targets in our Makefiles. It is idiomatic to include the following “phony” targets:

  • clean, which gets rid of all generated files (e.g., .o files and compiled executables)
  • all, which 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 running make will bring all executables up to date.

We can add these phony targets to our example Makefile from above:

all: generateSegfault

generateSegfault: generateSegfault.o helloSayer.o
    clang++ -o generateSegfault -g -std=c++17 -pedantic -Wall -Wextra 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

Eliminating Redundancy

One problem with the Makefile above 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 the Makefile, then any appearance of $(VARIABLE) is automatically replaced by some text.

We can update our Makefile from above to include variables/macros:

CXX = clang++
CXXFLAGS = -g -std=c++17 -pedantic -Wall -Wextra

generateSegfault: generateSegfault.o helloSayer.o
    $(CXX) -o generateSegfault $(CXXFLAGS) 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 for us:

  • $@: The target
  • $<: The first prerequisite
  • $^: All of the prerequisites

We can update our Makefile from above to make use of these special macros:

CXX = clang++
CXXFLAGS = -g -std=c++17 -pedantic -Wall -Wextra

generateSegfault: generateSegfault.o helloSayer.o
    $(CXX) -o $@ $(CXXFLAGS) $^

generateSegfault.o: generateSegfault.cpp helloSayer.hpp
        $(CXX) -c $(CXXFLAGS) $<

helloSayer.o: helloSayer.cpp helloSayer.hpp
    $(CXX) -c $(CXXFLAGS) $<

clean:
    rm -rf *.o generateSegfault

You can read about more special macros here.

(When logged in, completion status appears here.)