GNU make: A tutorial
I’m using GNU Make in one of my projects and I’m always struggling to get everything right. This is because the nature of the project, because of Make’s cryptic way of doing things and because of a lack of good tutorials. I said tutorials because most of the time I don’t care about my build system, my project being at the beginning, and I just want to apply a recipe and just get my project compiled.
It looks that I’m out of luck with Make in this regard because all the tutorials are either basic or advanced discussing how to use Make in large projects for example. Let’s see why the basic tutorials are useless and which parts should be covered in order to remedy their shortcomings. The basic tutorials tell you that Make, when invoked, looks for a Makefile file in the current directory. This Makefile contains rules that tell Make what commands to issue. The rules in the Makefile have the following form:
target: dependencies
[tab] system command
You have a special target called all that can be used as a default target. When you invoke make, if you don’t specify the target, this target and the corresponding rules will be executed.
Knowing this the tutorial shows how to compile a file (or two):
all: test test: test.o sort.o gcc test.o sort.o -o test test.o: test.c gcc -c test.c sort.o: sort.c gcc -c sort.c
The all target is dependent on test and the test target is dependent on test.o and test.o target is dependent on test.c and test.c isn’t a target, so the command associated with this rule is executed producing test.o object, and going back we see that test is also dependent on sort.o and sort.o is dependent on sort.c, so sort.c is compiled and then the command for test is executed now that all its dependencies are satisfied and we end up with the compiled and linked program named test.
What I’m trying to say is that all the dependencies are organized into a tree of dependencies and the rules executed from leafs to root like in the figure below.
Now you can write a Makefile to compile some source files but usually you want to use a build system to compile a small project and you want to use it to simplify your life. What you learned so far doesn’t simplify shit because you can do the same, involving the same amount of work, with bash scripts. Let me elaborate!
Let’s say that you will add 10 more files to the project. For each file you add 3 lines (2 lines of code and a empty line) of repetitive commands and you modify the test target to contain the new dependencies. That means 30 lines more of text adding to a total of 40 lines. I bet you can do better using a Bash script! Imagine what happens when you delete a file or when you rename a file. Every time you modify something you have to edit the Makefile. This is the reason I think many basic Make tutorials are not so good. Because they teach you some basic stuff about the system but those things are not enough to use the system in such a way that simplifies things for you.
Letting make Deduce the Recipes
Let’s see what info is missing that we can use to have a simple, more generic Makefile that adapts more easily to simple modifications. The first important bit of info that will help us a lot can be found in the GNU Make manual and it concerns a special feature of make that lets make deduce predefined recipes for you. For example make knows that a .o file is produced from a .c or .cpp file. So if you don’t specify an explicit recipe for a target like this, make will execute a predefined one for you if it finds a corresponding .c or .cpp file for the intended target.
all: test test: test.o sort.o gcc test.o sort.o -o test
Using this info we can modify the Makefile to look like above. When a new file is added we just add it in two places. And even for 30 more files we can keep the build file at only 4 lines. But still can we do better? Of course!
Special make variables
First, what do better in our example means? When we add a file, modify a file or delete a file we must modify the same information in two places. We can avoid this by placing the list of files in a variable but it pays to learn about several special variables that can come in handy at times even if for this example they are not the only answer.
- $^ – will expand to the list of prerequisites (dependencies)
- $< – will expand to the first prerequisite (dependency)
- $@ – will expand to the target
The explanations above are very simple and you can use the variables like this for simple examples but watch out for unexpected behaviour and if something unusual happens, go read the manual. Also in the manual you’ll find several others useful special make variables. Using these we can transform our Makefile:
SOURCES=test.o sort.o all: test test: $(SOURCES) gcc $^ -o $@
So now if something in our project changes we have to make the change to a single line and not worry about the rest.
Configuring the compiler
Previously when we discussed the automatic recipes I’ve left out an important bit of info. When the compiler is invoked you’ll probably see a simple command like cc -c -o file.o file.c but usually we want to pass configuration options to both gcc or g++. We can do that by putting those configure options in the environment variables CFLAGS (for gcc), CXXFLAGS (for g++) or CPPFLAGS (for the preprocessor) or setting them in our Makefile like in the example below:
SOURCES=test.o sort.o CFLAGS=-O3 CXXFLAGS=-O3 all: test test: $(SOURCES) gcc $^ -o $@
These variables can be configured when you issue the make command:
CFLAGS=-O3 make
Conclusions
This is, in my opinion, everything one needs to start out with make and use it to save time. And for simple projects we just move one Makefile around modify the SOURCES line and we’re compiling things. But by using a final tip we can have a generic Makefile that compiles all .c or .cpp files in the current directory. This is achieved by using two make functions: wildcard and patsubst.
SOURCES=$(patsubst %.c,%.o,$(wildcard *.c)) CFLAGS=-O3 CXXFLAGS=-O3 all: test test: $(SOURCES) gcc $^ -o $@
Wildcard makes a list of all .c files in the current directory and then patsubst replaces the extension from .c to .o.
You now have a generic file that can be used as a starting point for any small project. For bigger projects I strongly recommend reading the GNU make manual. It’s up to you to add a clean target or any other targets that your project may need but you’ll find this info elsewhere.
