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.