Although make
is a bless for any programmers, writing the Makefile
is sometimes painful. How convenient would that be to have a truly general Makefile
that would work for all your (C/C++
) projects ? Don't dream anymore, here it is and with plenty explanations.
A Makefile
basically is a series of instructions for the system to execute. An instruction is written as follows
inst_name: prior_inst
command_line
Please note that the tabulation has to be a tab space (
\t
). Not multiple spaces.
When the instruction inst_name
is called two things happen :
- The instruction(s)
prior_inst
is(are) called. - The command line
command_line
is executed.
The term prerequisites is more adapted than prior instructions as these could be filenames instead of instructions.
For example, you could write the instructions to compile your source files into object files as
file1_to_o:
g++ -std=c++14 -O3 -Wall -Wextra -c -o file1.o file1.cpp
file2_to_o:
g++ -std=c++14 -O3 -Wall -Wextra -c -o file2.o file2.cpp
Most of the time, when an instruction produces a file, the instruction is named after it.
file1.o:
g++ -std=c++14 -O3 -Wall -Wextra -c -o file1.o file1.cpp
file2.o:
g++ -std=c++14 -O3 -Wall -Wextra -c -o file2.o file2.cpp
Also, when an instruction needs some files, it is good behavior to write them as prior instructions such that, if they have to be produced they will. Furthermore, if they already are produced and haven't changed since the last time the instruction was called, the command line won't be executed, which might save a bunch of computations.
file1.o: file1.cpp
g++ -std=c++14 -O3 -Wall -Wextra -c -o file1.o file1.cpp
file2.o: file2.cpp
g++ -std=c++14 -O3 -Wall -Wextra -c -o file2.o file2.cpp
Nevertheless, if you have a medium-to-large sized project, writing all command lines by hand might take a very long time...
Macros are a way to avoid writing the same text, such as compilation flags, multiple times. They also allow to modify quickly compilation parameters and to prevent inconsistensies.
The symbol =
is used to instantiate a macro and the symbol $
is used to recall it.
CXX = g++
CXXFLAGS = -sdt=c++14 -O3 -Wall -Wextra
file1.o: file1.cpp
$(CXX) $(CXXFLAGS) -c -o file1.o file1.cpp
file2.o: file2.cpp
$(CXX) $(CXXFLAGS) -c -o file2.o file2.cpp
It is also possible to access the instruction name within the command line via $@
, the first prior instruction via $<
and all prior instruction via $^
.
CXX = g++
CXXFLAGS = -sdt=c++14 -O3 -Wall -Wextra
file1.o: file1.cpp
$(CXX) $(CXXFLAGS) -c -o $@ $<
file2.o: file2.cpp
$(CXX) $(CXXFLAGS) -c -o $@ $<
Patterns are used to write generalized instructions, i.e. instructions that stand for several basic instructions.
To produce a pattern from a string, you replace the part of it that is specific by the symbol %
.
CXX = g++
CXXFLAGS = -sdt=c++14 -O3 -Wall -Wextra
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c -o $@ $<
Within the same instruction, all
%
have the same value.
It is also possible to limit a generalized instruction to a specific set of names.
CXX = g++
CXXFLAGS = -sdt=c++14 -O3 -Wall -Wextra
file1.o file2.o: %.o: %.cpp
$(CXX) $(CXXFLAGS) -c -o $@ $<
There is also functions that produce and modify macros. Here are a few
-
The function
wildcard
creates a macro containing all files matching a command line pattern.SRCS = $(wildcard *.cpp) # SRCS = file1.cpp file2.cpp
-
The function
patsubst
substitutes each sub-string of a string by another according to a pattern.OBJS = $(patsubst %.cpp, %.o, $(SRCS)) # OBJS = file1.o file2.o
-
The function
filter-out
removes each sub-string of a stringA
from a stringB
.F2 = $(filter-out file3.cpp file1.cpp, $(SRCS)) # F2 = file2.cpp
A sub-string doesn't have space (
) characters.
.PHONY: all clean
clean:
rm -rf bin/
Labelling an instruction as .PHONY
prevents make
to call it, unless it is explicitly asked by the user.
~:make clean
For instance, if another instruction requests clean
as prior instruction, the .PHONY
instruction clean
won't be executed. Instead, make
will look for changes in the file clean
, if it exists.
For more information, see the make manual.
It is possible to include instructions from another file using include
. For example, dependancy files (.d
) contains proper instructions to build object files (.o
).
include file1.d file2.d
If those files don't exist,
make
will throw an error. To prevent it, use-include
instead.
Here is an example of a very versatile C/C++
Makefile
.
ALL = program1 program2 program3
SRCDIR = src/
BINDIR = bin/
EXT = cpp
CXX = g++
CXXFLAGS = -std=c++14 -O3 -Wall -Wextra
ALL
is the list of all executable files to be produced, i.e. the basename of all source files containing amain
function.
SRCDIR
is the diretory where source files are located.
BINDIR
is the diretory where object and dependency files are/will be located.
EXT
is the source file extension.
SRCS = $(wildcard $(SRCDIR)*.$(EXT))
OBJS = $(patsubst $(SRCDIR)%.$(EXT), $(BINDIR)%.o, $(SRCS))
DEPS = $(OBJS:.o=.d)
XOBJS = $(filter-out $(patsubst %, $(BINDIR)%.o, $(ALL)), $(OBJS))
XOBJS
is the list of object files without those that correspond toALL
files.
all: $(ALL)
$(ALL): %: $(BINDIR)%.o $(XOBJS)
$(CXX) $(CXXFLAGS) -o $@ $^
The instruction
all
induces the production of all executable files.
$(BINDIR)%.d: $(SRCDIR)%.$(EXT)
mkdir -p $(BINDIR)
$(CXX) $(CXXFLAGS) $< -MM -MT $(patsubst $(SRCDIR)%.$(EXT), $(BINDIR)%.o, $<) -MF $@
The usage of
mkdir
is mandatory in order to prevent errors.
The second line produces dependency files, yet I barely understand how.
-include $(DEPS)
$(BINDIR)%.o: $(SRCDIR)%.$(EXT)
$(CXX) $(CXXFLAGS) -c -o $@ $<
.PHONY: all clean dist-clean
clean:
rm -rf $(BINDIR)
dist-clean: clean
rm -rf $(ALL)