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:
- functions
like
$(filter ...)and$(sort ...); - conditionals;
- variable expansion with pattern substitution;
- integration with the shell using the
$(shell ...)function; - more advanced rules and variables;
- and more.
Check the full documentation for GNU Make online.