Programming Workshop 2 (CSCI 1061U)
Faculty of Science, Ontario Tech University
Lets consider a simple program that comprises three files.
main.cpp
util.cpp
util.h
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.
make
utilityMake 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.
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.
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
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 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)
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.
man make
command.