Banner: ZumGuy Publications and Network

ZumGuy Publications and Network

Automating builds with make

Posted by Sean on Saturday, 30th September 2017 12:38
make
is a very useful tool when working on large projects with many dependencies. A C++ project with many header includes, for instance, can quickly get tedious to compile, when at each compilation you must run several commands:
g++ -c main.cpp -o lib/main.o
g++ -c myfile.cpp -o lib/myfile.o
g++ lib/main.o lib/myfile.o -o main
What
make
allows you to do is summarize that whole dependency chain in one file, and then run only the necessary commands automatically. What before took several commands can now be done simply with one short line:
make
Installation on Ubuntu is very straightforward:
sudo apt install make

A sample
make
file


This is what a simple make file might look like:
# This is a comment. Sample Makefile:
MY_FLAGS = -Wall

.PHONY: all
all: main foo

main: lib/main.o lib/square.o
	g++ $^ -o $@

lib/main.o: main.cpp lib/square.h
	g++ -c $< -o $@

lib/square.o: lib/square.cpp lib/square.h
	g++ -c $< -o $@

foo: foo.cpp
	g++ ${MY_FLAGS} foo.cpp -o foo

.PHONY: clean
clean: 
	rm -f main
	rm -f lib/main.o
	rm -f lib/square.o
	rm -f foo.o
	rm -f foo
Saving this file as
Makefile
will allow you to run the
make
command and compile your project in a flash.
make
will assume your make-file is called
Makefile
by default. If you give it a different name, say
myfile.mk
, you can always run it with
make -f myfile.mk
.

Explanation of commands


The first line creates a constant called
MY_FLAGS
, which we can use later. Next we have the first target:
.PHONY: all
all: main foo
The second line in this snippet creates a target: this is a file that has certain dependencies. When make executes, it will check if the timestamp on any of the files after the
:
is newer than on the file before the colon - in this case,
all
(or if
all
does not exist).
all
, however, isn't a real file - it's what is called a "phony" target. In this case, the files
foo
and
main
don't share any dependencies, but we still want to compile both if we run
make
. Creating a phony target called
all
at the beginning of the file ensures that make will compile both (otherwise it would only compile the first). If, however, our directory were to contain a file called
all
, this would not work if the timestamp on that file is newer than on
main
or
foo
. So, we need to declare
all
as a phony target with
.PHONY all
. Phew! That was a lot of explaining for two short lines. Keep reading though, and hopefully it will all make sense by the end.
main: lib/main.o lib/square.o
	g++ $^ -o $@
This line is in charge of making sure the file
main
is recompiled every time either of its dependencies,
lib/main.o lib/square.o
, is updated. If
lib/main.o
or
lib/square.o
is newer than
main
,
make
will first check if these files have dependencies of their own, and then run the command on the second line:
g++ $^ -o $@
.
$^
is a make automatic variable - make will replace this with all of the dependencies for this target. Similarly,
$@
stands for "this target's name". So, in this case,
g++ $^ -o $@
becomes
g++ lib/main.o lib/square.o -o main
, and this command is then executed in the terminal. As you may guess, this command uses the g++ compiler to compile the file
main
.

Next up, we have more dependencies, this time for
main.o
and
square.o
:
lib/main.o: main.cpp lib/square.h
	g++ -c $< -o $@

lib/square.o: lib/square.cpp lib/square.h
	g++ -c $< -o $@
Here we see another
make
automatic variable:
$<
, which stands for "the first dependency of this target". Hence, these lines will run the commands
g++ -c main.cpp -o lib/main.o
and
g++ -c lib/square.cpp -o lib/square.o
respectively, if any of their dependencies have been updated since the last time the target (
lib/main.o
or
lib/square.o
respectively) was compiled.

Next, we have our second executable
foo
:
foo: foo.cpp
	g++ ${MY_FLAGS} foo.cpp -o foo
You will notice this file is independent of the dependency tree of
main
: it could even be a whole different project. It has only one dependency,
foo.cpp
.
${MY_FLAGS}
is a custom variable, and will be replaced with the value it was declared to have (at the beginning of this file). So the command becomes
g++ -Wall foo.cpp -o foo
.

If at any point you want to update only one target, and not all those specified in
all
, you can do so by running
make <targetname>
. For instance, you may want to update
foo
:
make foo
.

Last but not least, we have one more phony target,
clean
. This is in no way compulsory, but certainly is good practice, especially when working with shared projects e.g. with
git
:
.PHONY: clean
clean: 
	rm -f main
	rm -f lib/main.o
	rm -f lib/square.o
	rm -f foo.o
	rm -f foo
This allows you to remove any unwanted files (like executables, *.o files, etc.) from your project directory. This is useful for instance before committing to a shared
git
repository: your colleagues don't want to have to downlad binaries they can compile for themselves, especially give the fact that they might not even work on a different machine. Running
make clean
will now allow you to "clean up" your project before committing.

You must be logged in to post messages.

Quote of the day...


ZumGuy Internet Promotions