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.
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:
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
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
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 $
makeprogram 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
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
.PHONY (see Section 4.8 in the
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 $
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.
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
$ make cc intAndString.c -o intAndString $
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
$ 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
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)
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
gcc, so I can change the value of
# 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
# Build intAndString program CC = gcc CFLAGS = -Wall -O0 -masm=intel -g intAndString: .PHONY: clean clean: rm intAndString
$ 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
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
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
$ 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
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
make to end with the
clean recipe. The
-f option tells
rm to ignore nonexistent files, thus preventing the error condition from ending
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.
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).