Automating builds with Make
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 lib/main.o lib/square.o foo.o 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
Phony targets
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
. Declaring all
as a phony target 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
, ensuring make will ignore any files called all
in our working directory.
Phew! That was a lot of explaining for two short lines. Keep reading though, and hopefully it will all make sense by the end.
Compiling and linking files
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
.
Cleaning up
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 download 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.