Using make
Using GNU make
to Build Programs
Program development is an iterative process of editing and rebuilding. You can save a lot of time by automating the building process with the GNU make
program. I recommend downloading the make manual and consulting it as you read this post.
Using the make
program is straightforward. You first create a makefile, which consists of a set of rules for building your program. In the simplest case, just entering the make command will execute the rules needed to build your program. The make
program is very flexible. We’ll look only at some of the basic features here, which should be sufficient for the programming in my books.
I recommend placing the primary source files for each project in its own directory along with the makefile for your project. Starting in Chapter 14, we’ll write some functions that we’ll use in several projects. They should be placed in a separate directory that is accessible from each primary project directory. We’ll see how to access files in this separate directory from a project makefile later in this post.
We’ll start by looking at some conventions that make
follows regarding the naming of a makefile.
Naming a Makefile
If you enter the make command without specifying the name of a makefile, make
looks for a makefile named, in this order, GNUmakefile,makefile, or Makefile. If it finds a makefile with one of these names, make
follows the rules in that makefile and ignores any others with subsequent names on this list. I generally use Makefile.
If you want to use a different name for your makefile, you can explicitly specify the name with the -f
option. For example, the command make -f myMakefile will follow the rules in the myMakefile file.
The rules in a makefile are typically written such that if you change any of the source code files in the program, make
will execute the appropriate rules needed to rebuild the program.
Writing a Makefile
A makefile rule has this general format:
target: prerequisites
recipe
The target is usually the name of the file that will be generated by executing the recipe, a series of commands that produce target when executed. The prerequisites is a list of the files needed to produce the target. If any of the prerequisites is newer than target, make
will execute the recipe to bring target up to date. But before executing the recipe, make
first checks to see if any of the prerequisites is itself a target. If so, make
first updates that target. We can also define rules that have a target but no prerequisites, which causes make
to always execute the rule’s recipe, but we need to be a bit careful about this.
Let’s look at a makefile that could be used to build the intAndString
program in Listing 2-1 from Chapter 2.
# Build intAndString program
intAndString: intAndString.c
gcc intAndString.c -o intAndString
IMPORTANT
Each command in a recipe must be indented with a TAB character. If you have your text editor program set to use spaces when you press the TAB key, you need to figure out how to enter a TAB character at the beginning of each command in the recipe.
All text on a line following the # character is a comment.
If we don’t specify a target name in the make command, the make
program will start with the first rule. The first rule in this makefile will use gcc
to compile the code in the intAndString.c file and produce the intAndString
program.
Even in the very simple case, make
simplifies your life. Whenever you change the source code in intAndString.c, you simply type make. The make
program compares the time that any of the prerequisite files were last changed with the time the target file was last changed. If the target file is out of date or missing, make
executes the commands in the recipe. In this example, if the intAndString.c file is newer, or intAndString doesn’t exist, make
executes the recipe:
$ make
gcc intAndString.c -o intAndString
$
IMPORTANT
The
make
program shows us what the rules are doing. Be sure to read this output to ensure it’s doing what you want.
We can also write rules that do other things for us. For example, the only files I need to back up (you do back up your files, don’t you?) are the source files and the makefile for my project. Let’s add a rule for deleting the file that doesn’t need to be backed up:
# Build intAndString program
intAndString: intAndString.c
gcc intAndString.c -o intAndString
.PHONY: clean
clean:
rm intAndString
The rule, clean
, has no prerequisites. It simply executes its recipe, which deletes the file that is no longer needed (because we can easily build the program from the source file). Since there are no prerequisites to clean
, if we happen to have a file named clean in the directory, the recipe would never get executed because make
would think that the target is already up-to-date. We can avoid this problem by declaring our target as a prerequisite to a special built-in make
target, .PHONY
(see Section 4.8 in the make
manual).
You tell make
to go directly to a rule by giving the rule’s target as an argument to the make command. For example:
$ make clean
rm intAndString
$
Again, make
shows us what it’s doing.
As you can probably guess, makefiles for programs with many files can become quite large, and the rules can be repetitive. In the next section, we’ll look at a way to help reduce the amount of typing you need to do, which helps us to avoid typos.
Implicit Rules
Much of what we do to build a program from source files is very common. The make
program includes many implicit rules to handle common cases. If make
can deduce what we want to do from the filenames in our project, and if we don’t provide an explicit rule, make
will use what it thinks is the appropriate implicit rule. For example, if I write my makefile as:
# Build intAndString program
intAndString:
.PHONY: clean
clean:
rm intAndString
make
will look for a source file named intAndString.* in the directory where the makefile is located. If it finds, say, intAndString.c, it will assume that the file is a C source file and that we want to compile it to produce a program named intAndString
”
$ make
cc intAndString.c -o intAndString
$
If, instead, make
finds a file named intAndString.cpp it will assume that the file is a C++ source file and that we want to compile it to produce a program named intAndString
”
$ make
g++ intAndString.cpp -o intAndString
$
Implicit rules use built-in variables to specify their actions. Most built-in variables use uppercase letters in their names. In our examples here, if make
found intAndString.c it assumes that this file is a prerequisite to building a program named intAndString
and then uses the cc
compiler to build it. Similarly, it uses the g++
compiler if instead it finds intAndString.cpp. You can find the correspondence to the filename extensions in Section 10.2 in the make
manual.
You probably noticed that make
used the cc
compiler. The cc
command is linked to the default compiler on your computer. Under Ubuntu 20.04, that’s the gcc
compiler, but it may be different in other programming environments. We can see which compiler it’s linked to with
the following command:
$ cc -v
<--snip-->
gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1\~20.04)
But make
has many built-in variables that it uses for its implicit rules. Almost all the built-in rules use uppercase letters. The C compiler is specified in the CC
variable. I want to make sure that make
uses gcc
, so I can change the value of CC
:
# Build intAndString program
CC = gcc
intAndString:
.PHONY: clean
clean:
rm intAndString
Now I get:
$ make
gcc intAndString.c -o intAndString
$
We can also specify some options to the C compiler with the built-in CFLAGS
variable:
# Build intAndString program
CC = gcc
CFLAGS = -Wall -O0 -masm=intel -g
intAndString:
.PHONY: clean
clean:
rm intAndString
which gives:
$ make
gcc -Wall -O0 -masm=intel -g intAndString.c -o intAndString
$
You can find a list of the built-in variables in Section 10.3 in the make
manual.
Programs with Multiple Source Code Files
Most programs have many source code files. With a properly designed makefile, only those files that are changed will be recompiled. Let’s first look at a C program with three source code files:
# Build sum9Ints program
CC = gcc
CFLAGS = -Wall -O0 -masm=intel -g
sum9Ints: addNine.h addNine.c
.PHONY: clean
clean:
rm sum9Ints
The main
function is in sum9Ints.c, which make
finds when it looks for sum9Ints.*. The subfunction is in addNine.c which has an accompanying header file, addNine.h. Invoking this make file gives:
$ make
gcc -Wall -O0 -masm=intel -g sum9Ints.c addNine.h addNine.c -o sum9Ints
$
Although this works, every time one of the three files is changed, both sum9Ints.c and addNine.c will be recompiled. In addition, addNine.h is not needed in the compilation command. The effect is negligible in this very small program, but real-world programs have many source code files, and recompiling all of them can take a long time.
Instead of focusing on the source code files, let’s turn our attention to the object code files.
# Build sum9Ints program
CC = gcc
CFLAGS = -Wall -O0 -masm=intel -g
sum9Ints: sum9Ints.o addNine.o
sum9Ints.o: addNine.h
addNine.o: addNine.h
.PHONY: clean
clean:
rm sum9Ints.o addNine.o sum9Ints
which gives
$ make
gcc -Wall -O0 -masm=intel -g -c -o sum9Ints.o sum9Ints.c
gcc -Wall -O0 -masm=intel -g -c -o addNine.o addNine.c
gcc sum9Ints.o addNine.o -o sum9Ints
$
To avoid having to retype the object filenames, I like to define my own variable and set it equal to the object filenames needed to build the program.
Defining Your Own Variables
I use lowercase for my variables to help distinguish them from the built-in variables. Here’s my entire makefile for the sum9Ints program.
# Build sum9Ints program
$(objects)
CC = gcc
CFLAGS = -Wall -O0 -masm=intel -g
genasm = -S -Wall -masm=intel -fno-asynchronous-unwind-tables \
-fcf-protection=none
sum9Ints: $(objects)
sum9Ints.o: addNine.h
addNine.o: addNine.h
sum9Ints.s: sum9Ints.c addNine.h
gcc $(genasm) -o temp $<
expand -t 8 temp > $@
rm temp
addNine.s: addNine.c addNine.h
gcc $(genasm) -o temp $<
expand -t 8 temp > $@
rm temp
.PHONY: clean allclean
clean:
rm -f $(objects)
allclean: clean
rm sum9Ints
The syntax for substituting the value of a variable is $(
variable_name)
.
I’ve added recipes for telling gcc
to generate the assembly language equivalent of the C source code. The gcc
compiler uses tabs for spacing the assembly language code it produces. That doesn’t work well when I copy-and-paste into a Word document, so I use the expand
command to convert tabs to the appropriate number of spaces.
I’ve also used two very useful built-in variables in these assembly-language generation recipes. The <
refers to the first argument on the list of prerequisites, and the @
refers to the target file name. These single-letter variables don’t need to be enclosed in parentheses.
I may wish to delete the object files but not the executable program. So I’ve defined clean
to delete the object files and allclean
to also delete the program. Specifying clean
as a prerequisite to allclean
causes the clean
recipe to be executed first. If I’ve already used clean
to delete the object files, its recipe would fail when we use allclean
, causing make
to end with the clean
recipe. The -f
option tells rm
to ignore nonexistent files, thus preventing the error condition from ending make
.
Mixing C and Assembly
One of the reasons I’m using gcc
in my books is that it very nicely integrates C and assembly source code. Our focus on the object code files works well here. Here’s the makfile I use for building a program that has one C source file and one assembly source file.
# Build threeFactorial program
objects = threeFactorial.o factorial.o
CC = gcc
CFLAGS = -Wall -O0 -masm=intel -g
AS = as
ASFLAGS = --gstabs
genasm = -S -Wall -masm=intel -fno-asynchronous-unwind-tables \
-fcf-protection=none
threeFactorial: $(objects)
threeFactorial.o: factorial.h
threeFactorial.s: threeFactorial.c factorial.h
gcc $(genasm) -o temp $<
expand -t 8 temp > $@
rm temp
.PHONY: clean allclean
clean:
rm -f $(objects)
allclean: clean
rm threefactorial
When I build the program with this makefile, we can see that it compiles the C source file and assembles the assembly source file, and then links the two object files:
$ make
gcc -Wall -O0 -masm=intel -g -c -o threeFactorial.o threeFactorial.c
as -o factorial.o factorial.s
gcc threeFactorial.o factorial.o -o threeFactorial
$
Searching Other Directories
So far, I’ve only discussed the case where all the files used in our program are in the same directory with the makefile. Of course, you could use files in other directories by creating symbolic links to them. But make
includes a special variable, VPATH
, that we can use to specify other directories to search.
You are asked in the book to write I/O functions that are used in Your Turn exercises in subsequent chapters. I place the source code files for these functions in a directory named common
and use VPATH
to access them from my working directory. Here’s an example for a program that is written entirely in assembly language:
# Build rulerAdd program
objects = rulerAdd.o getLength.o displayLength.o getUInt.o \
decToUInt.o putUInt.o intToUDec.o writeStr.o readLn.o
myio = ../common
VPATH = $(myio)
ASFLAGS = --gstabs
AS = as
CC = gcc
rulerAdd: $(objects)
.PHONY: clean
clean:
rm $(objects)
allclean: clean
rm rulerAdd
I’ve defined my own variable, myio
, to be the relative path to my common
directory. Then I just set VPATH
to equal this path.
The VPATH
variable applies only to make
searches. We still need to tell the compiler where the files are located if they’re not in the current directory. Here’s the make file I use to build the convertDec
program in Chapter 16.
# Build convertDec program
objects = convertDec.o decToUInt.o writeStr.o readLn.o
myio = ../../../common
VPATH = $(myio)
CFLAGS = -O0 -Wall -masm=intel -g -I$(myio)
CC = gcc
AS = as
ASMFLAGS = --gstabs
genasm = -O0 -Wall -masm=intel -fno-asynchronous-unwind-tables \
-fcf-protection=none -I$(myio)
convertDec: $(objects)
convertDec.o: decToUInt.h writeStr.h readLn.h
decToUInt.o: decToUInt.h
convertDec.s: convertDec.c decToUInt.h writeStr.h readLn.h
gcc -S $(genasm) -o temp $<
expand -t 8 temp > $@
rm temp
decToUInt.s: decToUInt.c decToUInt.h
gcc -S $(genasm) -o temp $<
expand -t 8 temp > $@
rm temp
.PHONY: clean allclean
clean:
rm -f $(objects)
allclean: clean
rm convertDec
Notice that I use the -I
option to tell the compiler where to find the header files required by the compilation of the C files.
This brief discussion should help you to get started with using make. After you learn the program’s basic usage, if you want to become an expert, I recommend John Graham-Cumming’s book, The GNU Make Book (No Starch Press, 2015).