Introduction to Make

Implicit Rules

Overview

Teaching: 15 min
Exercises: 15 min
Questions
  • Are there standard ways of doing things with Makefiles?

Objectives
  • Allow make to find a customary method for updating a target file.

At this point, we have the following Makefile:

include config.mk

TXT_FILES=$(wildcard books/*.txt)
DAT_FILES=$(patsubst books/%.txt, %.dat, $(TXT_FILES))

# Generate summary table.
results.txt : $(DAT_FILES) $(ZIPF_SRC)
	$(ZIPF_EXE) $(DAT_FILES) > $@

# Count words.
.PHONY : dats
dats : $(DAT_FILES)

%.dat : books/%.txt $(COUNT_EXE)
	./$(COUNT_EXE) $< > $*.dat

$(COUNT_EXE) : $(COUNT_SRC)
	c++ --std=c++11 -o $(COUNT_EXE) $(COUNT_SRC)

.PHONY : clean
clean :
	rm -f $(DAT_FILES)
	rm -f results.txt

.PHONY : variables
variables:
	@echo TXT_FILES: $(TXT_FILES)
	@echo DAT_FILES: $(DAT_FILES)

Our wordcount program is generated from two source files, wordcount.cpp and main.cpp. You may have noticed that whenever one of these files changes, both are recompiled to generate the wordcount program. It would be much better to only recompile the source file that changed. Fortunately it is possible to compile each source file into an object file before combining it into the final program. If we were to do that manually, it would look something like this:

wordcount.o : wordcount.cpp
	c++ --std=c++11 -c -o wordcount.o wordcount.cpp

main.o : main.cpp
	c++ --std=c++11 -c -o main.o main.cpp
	
wordcount : wordcount.o main.o
	c++ -o wordcount wordcount.o main.o

Try replacing the following lines with the lines above and see what happens.

$(COUNT_EXE) : $(COUNT_SRC)
	c++ --std=c++11 -o $(COUNT_EXE) $(COUNT_SRC)
$ touch wordcount.cpp
$ make
c++ --std=c++11   -c -o wordcount.o wordcount.cpp
c++ -o wordcount wordcount.o main.o
./wordcount books/abyss.txt > abyss.dat
./wordcount books/isles.txt > isles.dat
./wordcount books/last.txt > last.dat
./wordcount books/sierra.txt > sierra.dat
python zipf_test.py  abyss.dat  isles.dat  last.dat  sierra.dat > results.txt

You should notice the wordcount program is still recompiled whenever a source file is changed, but now only the modified source file (in this case wordcount.cpp) is recompiled.

However, the changes we made have re-introduced a lot of redundancy. We could remove some of this by using variables again, such as using COUNT_OBJ to represent the object files:

COUNT_OBJ=wordcount.o main.o
COUNT_EXE=wordcount

wordcount.o : wordcount.cpp
	c++ --std=c++11 -c -o wordcount.o wordcount.cpp

main.o : main.cpp
	c++ --std=c++11 -c -o main.o main.cpp
	
$(COUNT_EXE) : $(COUNT_OBJ)
	c++ -o $(COUNT_EXE) $(COUNT_OBJ)

We could continue to do this for the other source and object files, but fortunately there is an easier way. Make provides some standard ways of creating certain types of targets. These implicit rules tell Make to use customary methods for creating a target.

Often all that needs to be done to allow Make to find a customary method is to refrain from specifying commands yourself. Either write a rule with no command lines, or don’t write a rule at all. Then Make will figure out which implicit rule to use based on which kind of source file exists or can be made.

We can do this by removing the commands for compiling the object files:

COUNT_OBJ=wordcount.o main.o
COUNT_EXE=wordcount

wordcount.o : wordcount.cpp

main.o : main.cpp
	
$(COUNT_EXE) : $(COUNT_OBJ)
	c++ -o $(COUNT_EXE) $(COUNT_OBJ)
$ touch wordcount.cpp
$ make
c++    -c -o wordcount.o wordcount.cpp
wordcount.cpp:47:8: warning: 'auto' type specifier is a C++11 extension
      [-Wc++11-extensions]
  for (auto& word : words) {
       ^
wordcount.cpp:47:19: warning: range-based for loop is a C++11 extension
      [-Wc++11-extensions]
  for (auto& word : words) {
                  ^
wordcount.cpp:58:8: warning: 'auto' type specifier is a C++11 extension
      [-Wc++11-extensions]
  for (auto& kv : get_freq) {
       ^
wordcount.cpp:58:17: warning: range-based for loop is a C++11 extension
      [-Wc++11-extensions]
  for (auto& kv : get_freq) {
                ^
4 warnings generated.
c++ -o wordcount wordcount.o main.o
./wordcount books/abyss.txt > abyss.dat
./wordcount books/isles.txt > isles.dat
./wordcount books/last.txt > last.dat
./wordcount books/sierra.txt > sierra.dat
python zipf_test.py  abyss.dat  isles.dat  last.dat  sierra.dat > results.txt

Note however that the commands being executed are different from the orginal commands, they don’t have the --std=c++11 option. This may cause some compilers to generate warnings if they default to using an earlier C++ standard.

We can solve this because implict rules also have implicit variables defined with them. In the case of the rule to generate an object file from a C++ source file, there is a variable called CXXFLAGS which are extra flags to be given to the compiler. By setting this variable, we can modify the command that is executed by the implict rule.

We can even go one step further and remove the rules altogether as make will still know that you want to use the implicit rule by specifying the object files as part of the last compile rule.

Assuming we added COUNT_OBJ, COUNT_EXE, and CXXFLAGS to config.mk, our complete Makefile with these changes would be:

include config.mk

TXT_FILES=$(wildcard books/*.txt)
DAT_FILES=$(patsubst books/%.txt, %.dat, $(TXT_FILES))

# Generate summary table.
results.txt : $(DAT_FILES) $(ZIPF_SRC)
	$(ZIPF_EXE) $(DAT_FILES) > $@

# Count words.
.PHONY : dats
dats : $(DAT_FILES)

%.dat : books/%.txt $(COUNT_EXE)
	./$(COUNT_EXE) $< > $*.dat

$(COUNT_EXE) : $(COUNT_OBJ)
	c++ -o $(COUNT_EXE) $(COUNT_OBJ)

.PHONY : clean
clean :
	rm -f $(DAT_FILES)
	rm -f results.txt

.PHONY : variables
variables:
	@echo TXT_FILES: $(TXT_FILES)
	@echo DAT_FILES: $(DAT_FILES)

Remember, the config.mk file contains:

# Count words program.
COUNT_SRC=wordcount.cpp main.cpp
COUNT_OBJ=wordcount.o main.o
COUNT_EXE=wordcount
CXXFLAGS=--std=c++11

# Test Zipf's rule
ZIPF_SRC=zipf_test.py
ZIPF_EXE=python $(ZIPF_SRC)

Linking a single object file

In addition to knowing how to build an object file from a source file, Make also knows how to build a program from multiple object files. Remove the remaining compile command (leaving the rule) and check that the program is still built correctly.

Solution

This Makefile contains a solution to this challenge.

Key Points