How to automate the compilation

A simple way to automate the compilation steps is to write a Makefile.

Here is a simple Makefile for the pi_time example:

pi_time: pi_time.cpp
	g++ pi_time.cpp -std=c++20 -O2 -Wall -Wextra -o pi_time

Save it with the name Makefile in the same directory as pi_time.cpp, and run make:

$ make
g++ pi_time.cpp -std=c++20 -O2 -Wall -Wextra -o pi_time

What is a Makefile

A Makefile is a text file with a list of rules which describe how to build one ore more target files (in this case, pi_time) from their dependencies or prerequisites (in this case, pi_time.cpp) by executing a recipe made of various commands (g++ ...):

targets: dependencies
	command
	command
	...

Two things to note: first, the lines with the commands must start with a <tab> character, not spaces! And each line is independent from the others, so things like cd or setting a shell variable are lost between lines.

Advantages of make

The main reasons for writing and using a Makefile are to simplify the compilation, and to build only what is needed.

By writing down the compilation rules, you don’t need to remember all the compiler commands and flags: just write them down once, and run make any time you want to rebuild the code. This makes it easier to always use the same compilation steps, but also to set up some alternatives, as we will see below.

Another advantage is that make will build a TARGET only if it does not exist, or if it is older than any of its DEPENDENCIES. If you re-run make you should see a message like

$ make
make: 'pi_time' is up to date.

Prerequisites

When you run make, it will check if the target exists or not. If if does, it will compare its timestamp with that of its prerequisites, and rebuild the target only if any of the prerequisites is newer that it is.

If we make any changes to the sources, or just “touch” them to update their timestamp, make will happily rebuild the target when we ask it:

$ touch pi_time.cpp
$ make
g++ pi_time.cpp -std=c++20 -O2 -Wall -Wextra -o pi_time

Variables

Once a Makefile gorws in complexity - for example if you have multiple source files to build and link together - it becomes helpful to use variables to store commands and flags.

For example:

CXX := g++
CXXFLAGS := -std=c++20 -Wall -march=native

pi_time: pi_time.cpp Makefile
	$(CXX) pi_time.cpp $(CXXFLAGS) -o pi_time

Note that We added the Makefile itself to the list of prerequisites, so make will rebuild the target when the rules are updated.

Variables are written in uppercase by convention, but almost any word will work.

To set a variable use the := operator (= also works, but with a different behaviour):

CXXFLAGS := -std=c++20 -Wall -march=native

To expand a variable, write it between $( and )

... $(CXXFLAGS) ...

Make also provides some “automatic variables”:

CXX := g++
CXXFLAGS := -std=c++20 -Wall -march=native

pi_time: pi_time.cpp Makefile
	$(CXX) $< $(CXXFLAGS) -o $@

$@ expands to the current TARGET, in this case pi_time.

$< expands to the first dependency, in this case pi_time.cpp.

Multiple targets

Now that we have variables, we can write a Makefile to build multiple targets using similar commands.

For example, we can add

deb/pi_time: pi_time.cpp Makefile
	mkdir -p deb
	$(CXX) $< $(CXXFLAGS) -Og -g -o $@

to build a debug version of the example.

But if we run make, it will build only the regular pi_time… why ?

By default, make will build only the first target. A common approach is to add a first “phony” (or “fake”) target that lists as dependnecies all the targets that we want to build:

.PHONY: all

all: release debug

release: pi_time

debug: deb/pi_time

...

Now make will build both release and debug targets by default, or we can ask explicitly to build only one of them:

$ make debug
mkdir -p deb
g++ pi_time.cpp -std=c++20 -Wall -march=native -Og -g -o deb/pi_time

Cleaning up

Another common target is clean, used to tell make to delete the files it may have built:

.PHONY: clean

clean:
	rm -f pi_time deb/pi_time

Now we can run make clean to delete the binaries we have built:

$ make clean
rm -f pi_time deb/pi_time

Since we have declared clean as a “phony” target, make will run its recipe every time we invoke it:

$ make clean
rm -f pi_time deb/pi_time
$ make clean
rm -f pi_time deb/pi_time

Parallel builds

When make knows all the dependencies among the different targets, it can also compile more of them in parallel to speed up the build process.

You can run multiple compilation jobs in parallell passing the -j N option to make:

$ make clean
rm -f pi_time deb/pi_time
$ make -j 2
# Make will start both rules in parallel ...
g++ pi_time.cpp -std=c++20 -Wall -march=native -o pi_time
mkdir -p deb
g++ pi_time.cpp -std=c++20 -Wall -march=native -Og -g -o deb/pi_time
# ... and wil wait for both of them to complete

… and more

Simple rules and variables let you automate many common compilation tasks, but for more complex projects you may need to deal with file discovery, manipulate paths and strings, run external commands, and so on.

make has many more features to help with complex projects:

Check the full documentation for GNU Make online.