Using Makefiles

Programming Workshop 2 (CSCI 1061U)

Faisal Qureshi

Faculty of Science, UOIT

http://vclab.science.uoit.ca


Things are getting out of hand

Lets consider a simple program that comprises three files.

We now know how to create an executable prog.

Method 1

> g++ -c main.cpp
> g++ -c util.cpp
> g++ main.o util.o -o prog

For this simple case, we can reduce typing somewhat as shown below

Method 2

> g++ main.cpp util.cpp -o prog

Consider the situation where we have a lot more files---Linux, for example, consists of roughly 15,000 files---both methods quickly become infeasible:

We need a better scheme to manage program comprising multiple files.

The make utility

Make utility will enable you automatically build executable from your source code. You describe how you want your executable to be built using a Makefile, which make utility uses to build the executable.

Anatomy of a Makefile

Makefiles allow us to specify three things: 1) targets, 2) dependencies, and 3) recipies for building the targets from dependencies. The logic goes as follows. When asked to build a target, the make utility will see if any of the dependency has changed since the last time target was built. If a dependency is newer than the target, the target is built using the recipe. Consider the following Makefile

Makefile

prog: main.cpp util.cpp util.h
    g++ main.cpp util.cpp -o prog

Here prog is the name of the target, main.cpp util.cpp util.h are the dependencies that are required to build this target, and g++ main.cpp util.cpp -o util is the recipe that builds the target from the dependencies.

See what happens when we call make the first time.

> make
g++ main.cpp util.cpp -o prog

Since file prog doesn't exists. The make utility uses the recipe to build the target prog.

If we execute make again

> make
make: `prog' is up to date.

make detected that target prog already exists and that none of the dependencies have been changed since the last time prog was built.

Lets change one of the dependencies and build the target again.

> touch util.h
> make
g++ main.cpp util.cpp -o prog

make detected that file util.h has been modified since the last time target prog was built, so it decided to build the target again.

Using the Makefile

We use make command that will use the above make file to build the executable as follows

> make

By default the make command reads the Makefile sitting in the current folder.

It is also possible to specify a different Makefile using the the -f switch as follows

> make -f MyOwnMakefile

Multiple targets

It is possible for a Makefile to have multiple targets as seen below

Makefile

prog: main.cpp util.cpp util.h
    g++ main.cpp util.cpp -o prog

run: prog
    ./prog  

The above makefile has two targets. The first target builds the prog. The second target executes the program. Notice that the second target requires the prog to already exist.

By default make builds the first target encountered in the makefile. However, it is possible to specify a target explicitly as follows

> make prog

or

> make run

The power of make utility comes from the fact that it is able to build targets recursively. Say we issue builds target run

> make run

Since prog is a dependency of this target, first make will attempt to build prog. If prog doesn't exists, it will be built. If it already exists, it will be used. See what happens when we call make run the first time

> make run
g++ main.cpp util.cpp -o prog
./prog
sum = 83
smallest = 34
largest = 1

And then we call it again

> g++ main.cpp util.cpp -o prog
./prog
sum = 83
smallest = 34
largest = 1

Can you spot the difference?

Makefiles in practice

Makefiles allow us to specify rules to help automate file processing. We can also use variables instead of hardcoding values. Lets write a more general purpose makefile that you can easily adapt for your own program. A line that begins with # indicates a comment. Together these features allows flexibility, improves readibality, avoids repetition, and simplifies adding new source files.

Makefile

# Lets specify compiler
CC = g++
CFLAGS = -c

# Source files.  We keep header files separate from the cpp files.
# Recall that we will compile cpp files, but we do not compile
# the header files.
HEADER = util.h
CPP = main.cpp util.cpp

# Program name
PROGNAME = prog

# Object files
OBJ = $(CPP:.cpp=.o)

all: $(PROGNAME)

$(PROGNAME): $(OBJ)
    $(CC) -o $(PROGNAME) $(OBJ)

%.o: %.cpp $(HEADER)
    $(CC) $(CFLAGS) -o $@ $<

# Target clean is phony, since it doesn't
# create a file called clean.  Notice that other
# targets create files.
.PHONY: clean

# We will use target clean to delete both the object files
# and the program.
clean:
    rm $(PROGNAME) $(OBJ)

Recursive Makefile example

Consider the following situation. Folder lib-util contains source to build the library that is needed by two programs. Folders alpha and beta contain the source for the two programs, respectively. The folder structures looks as follows:

> tree
.
├── Makefile
├── alpha
│   ├── Makefile
│   └── main.cpp
├── beta
│   ├── Makefile
│   ├── main.cpp
│   ├── test1
│   ├── test1.out
│   ├── test2
│   └── test2.out
└── util-lib
    ├── Makefile
    ├── util.cpp
    └── util.h

3 directories, 13 files

Notice that we have created three makefiles.

Top-level Makefile

alpha: util-lib
    make -C alpha

beta: util-lib
    make -C beta

util-lib:
    make -C util-lib

.PHONY: alpha
.PHONY: beta
.PHONY: util-lib
.PHONY: clean

clean:
    make -C alpha clean
    make -C beta clean
    make -C util-lib clean

This makefile has three targets. Target alpha depends upon the library in lib-util folder. Target beta also depends upon the library in lib-util folder. Target util-lib has no dependencies. The recipe for building the targets involve calling make command with -C switch. The -C switch sets the root folder for make command. This means that make -C alpha is similar to calling make command in subfolder alpha. What you see here is the recursive use of makefiles. The makefiles sitting in the three subfolders are responsible for bulding their own targets.

Makefile in alpha

CC = g++
CFLAGS = -c

PROGNAME = alpha
CPP = main.cpp

HEADER = util.h
HEADER_INCLUDE_FOLDER = ../util-lib

LIB = util
LIB_FOLDER = ../util-lib


OBJ = $(CPP:.cpp=.o)

$(PROGNAME): $(OBJ) $(LIB_FOLDER)/libutil.a
    $(CC) -o $(PROGNAME) $(OBJ) -L $(LIB_FOLDER) -l $(LIB)

%.o: %.cpp
    $(CC) $(CFLAGS) -I $(HEADER_INCLUDE_FOLDER) -o $@ $<

$(LIB):
    make -C ../util-lib

.PHONY: clean
.PHONY: util

clean:
    rm $(PROGNAME)
    rm $(OBJ)

Makefile in beta

CC = g++
CFLAGS = -c

PROGNAME = beta
CPP = main.cpp

HEADER = util.h
HEADER_INCLUDE_FOLDER = ../util-lib

LIB = util
LIB_FOLDER = ../util-lib


OBJ = $(CPP:.cpp=.o)

$(PROGNAME): $(OBJ) ../util-lib/libutil.a
    $(CC) -o $(PROGNAME) $(OBJ) -L $(LIB_FOLDER) -l $(LIB)

%.o: %.cpp
    $(CC) $(CFLAGS) -I $(HEADER_INCLUDE_FOLDER) -o $@ $<

$(LIB):
    make -C ../util-lib

.PHONY: clean
.PHONY: $(LIB)
.PHONY: test1
.PHONY: test2
.PHONY: tests

clean:
    rm $(PROGNAME)
    rm $(OBJ)

test1: $(PROGNAME)
    $(PROGNAME) < test1 > foo
    diff test1.out foo

test2: $(PROGNAME)
    $(PROGNAME) < test2 > foo
    diff test2.out foo

tests: test1 test2

Makefile in util-lib

# Lets specify compiler
CC = g++
CFLAGS = -c
AR = ar
ARFLAGS = -rvs

# Source files.  We keep header files separate from the cpp files.
# Recall that we will compile cpp files, but we do not compile
# the header files.
HEADER = util.h
CPP = util.cpp

# Program name
LIBNAME = libutil.a

# Object files
OBJ = $(CPP:.cpp=.o)

all: $(LIBNAME)

$(LIBNAME): $(OBJ)
    $(AR) $(ARFLAGS) $(LIBNAME) $(OBJ)

%.o: %.cpp $(HEADER)
    $(CC) $(CFLAGS) -o $@ $<

# Target clean is phony, since it doesn't
# create a file called clean.  Notice that other
# targets --- all and message --- create file
# message.
.PHONY: clean

# We will use target clean to delete both the object files
# and the program.
clean:
    rm $(LIBNAME) $(OBJ)

See what happens when we call make command.

> make
make -C util-lib
g++ -c -o util.o util.cpp
ar -rvs libutil.a util.o
ar: creating archive libutil.a
a - util.o
make -C alpha
g++ -c -I ../util-lib -o main.o main.cpp
g++ -o alpha main.o -L ../util-lib -l util

Notice that when building target alpha, target util-lib is also build. Target alpha depends upon util-lib.

Now lets see what happens when we build target beta.

make -C util-lib
make[1]: Nothing to be done for `all'.
make -C beta
g++ -c -I ../util-lib -o main.o main.cpp
g++ -o beta main.o -L ../util-lib -l util

Notice that in this case the util library wasn't recreated.

References