Using the make utility to build software (Primer)

As the number of source files and libraries required to build a software increase in number, the build process gets more complicated and the time required to build the final executable becomes more and more. Here, the classic make utility available in Linux and other Unix-like environments comes to the rescue as it automatically builds the software based on rules given in a makefile and optimizes on the build process by compiling only the files that have changed since the last build.

As the name suggests, make is a program to make (executable) programs. make is a classic Unix utility, first authored by Stuart Feldman in 1977. The present day GNU make that we use under Linux is authored by Richard Stallman, Roland McGrath and Paul D. Smith.

Makefile

make works on the basis of contents of a specially formatted text file, named Makefile or makefile in the current working directory where make has been invoked. It is preferable to name the makefile with an uppercase M as this makes the Makefile stand out in a directory listing. Once you have Makefile in your current directory, you can run make by simply typing,

make [target]

at the shell prompt. As will be explained later, a Makefile contains information about targets. So make target builds the specified target. If no target is given in the command, make builds the first target given in Makefile.

Makefile is a text file, with the format,

<target>: <pre-requisites>
         <command>
         <command>
         .....

A typical Makefile entry is for building a <target>. A <target> is normally an executable file or an object file. The <pre-requisites> are the files the <target> depends on. If any of the <pre-requisites> files has changed since the last build of <target>, the <target> has to be built again. make optimizes its work and builds the <target> only if at least one of the <pre-requisites> files has changed since the last time <target> was built.

After <pre-requisites>, the subsequent lines give the command(s) to build the target. Here, we have the most important requirement. All command lines in a Makefile must start with a tab character. Failure to put the tab character at beginning of command lines has been a source of hard-to-solve problems in using make for quite sometime.

Lines starting with # in Makefile are comments.

A simple Makefile

Using the concepts described above, let's look at a simple Makefile,

#
# Makefile
#

scheduler_app: scheduler_app.o scheduler.o linklist.o
        gcc scheduler_app.o scheduler.o linklist.o -o scheduler_app

scheduler_app.o: scheduler.h scheduler_app.c
        gcc -c scheduler_app.c

scheduler.o: scheduler.h scheduler.c
        gcc -c scheduler.c

linklist.o: linklist.h linklist.c
        gcc -c linklist.c

The above Makefile is for building an executable file scheduler_app. scheduler_app is a target that depends on scheduler_app.o, scheduler.o and linklist.o, which in turn are targets themselves dependent on their respective .c files. Although not visible, there is a tab character at the beginning of all command lines containing the gcc command.

PREREQUISITES

There are two types of prerequisites, normal prerequisites and order-only prerequisites. Normal prerequisites, impose two constraints on the target. It gives the order of execution of other targets for the given target. That is, the prerequisite targets should be built in the order listed for the given target. Second, if any of the prerequisite target is out of date, that is, its own prerequisites are newer than itself, then it should be built before building the original target. Order-only prerequisites only impose the first constraint. That is, prerequisites targets should be built in a particular order. It is not necessary to check whether a prerequisite target is out of date and should be re-built. Order-only prerequisites are listed after normal prerequisites and the two are separated by the pipe symbol, |,

targets: normal-prerequisites | order-only prerequisites

For example, if we wish to create scheduler_app in scheduler-app directory, we can make a target scheduler-app-dir as shown below and make it an order-only prerequisite for scheduler-app/scheduler_app.

scheduler-app-dir:
      mkdir -p scheduler-app

scheduler-app/scheduler_app: $(OBJECTS) | scheduler-app-dir
      gcc $(OBJECTS) -o scheduler-app/scheduler_app

PHONY TARGETS

Normally, a target is a file built by a program. However, sometimes, you need to do some housekeeping work. For this, phony targets are used (Phony targets have other uses also). A phony target is a name given to some command(s). For example, if you wish to remove all the .o files, you can have a phony target remove_objs, as shown below. If you wish to remove all .o files as well as the executable, scheduler_app so as to have a clean slate, you can add a clean phony target to the Makefile,

.PHONY: remove_objs clean

remove_objs:
        rm *.o
clean: remove_objs
        rm scheduler_app

Here, remove_objs is a prerequisite for clean. For phony targets, a prerequisite works like a subroutine. So if you give the command make clean, first all the .o files are removed, and then scheduler_app is removed.

Another use of phony targets is the case where there is a single Makefile for multiple executables, say app1, app2 and app3. You can have a phony target all,

all: app1 app2 app3

app1: <pre-requisites>
         <command>

app2: <pre-requisites>
         <command>

app3: <pre-requisites>
         <command>

make invoked without any arguments builds the first target. In this case, when make is invoked without any arguments, it amounts to make all, which in turn, builds all the three programs, app1, app2 and app3.

VARIABLES

You can define variables in a Makefile. Variables are just like shell variables. Each variable contains a text string. The value of a variable VAR is $(VAR). For example, in the first Makefile, we could have defined,

OBJECTS = scheduler_app.o scheduler.o linklist.o

The Makefile becomes much neater once we introduce OBJECTS variable and remove_objs and clean phony targets.

#
# Makefile
#
OBJECTS = scheduler_app.o scheduler.o linklist.o

scheduler_app: $(OBJECTS)
        gcc $(OBJECTS) -o scheduler_app

scheduler_app.o: scheduler.h scheduler_app.c
        gcc -c scheduler_app.c

scheduler.o: scheduler.h scheduler.c
        gcc -c scheduler.c

linklist.o: linklist.h linklist.c
        gcc -c linklist.c

.PHONY: remove_objs clean
remove_objs:
        rm *.o
clean: remove_objs
        rm scheduler_app

MISCELLANEOUS

You can terminate the pre-requisites with a ; and continue writing the command on the same line. Also, if you write a @ character at the beginning of a command, it is not echoed on the output terminal as it is executed by make. For example.

OBJECTS = scheduler_app.o scheduler.o linklist.o
linklist.o: linklist.c ; gcc -c linklist.c
scheduler_app: $(OBJECTS) ; @echo "Making scheduler-app"
      gcc $(OBJECTS) -o scheduler_app
scheduler_app.o: scheduler_app.c ; gcc -c scheduler_app.c
scheduler.o: scheduler.c ; gcc -c scheduler.c

Turning the command echo off with @ is meaningful for echo command. Although you can write the command on the same line by putting a ; after pre-requisites; it is more readable and hence preferable to write the command on a new line starting with the tab character.

REFERENCE

SEE ALSO

Using the make utility to build software (Advanced)