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_linePlease 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_instis(are) called. - The command line
command_lineis 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.cppMost 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.cppAlso, 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.cppNevertheless, 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.cppIt 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
wildcardcreates a macro containing all files matching a command line pattern.SRCS = $(wildcard *.cpp) # SRCS = file1.cpp file2.cpp
-
The function
patsubstsubstitutes 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-outremoves each sub-string of a stringAfrom 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 cleanFor 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.dIf those files don't exist,
makewill throw an error. To prevent it, use-includeinstead.
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
ALLis the list of all executable files to be produced, i.e. the basename of all source files containing amainfunction.
SRCDIRis the diretory where source files are located.
BINDIRis the diretory where object and dependency files are/will be located.
EXTis 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))
XOBJSis the list of object files without those that correspond toALLfiles.
all: $(ALL)
$(ALL): %: $(BINDIR)%.o $(XOBJS)
$(CXX) $(CXXFLAGS) -o $@ $^The instruction
allinduces 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
mkdiris 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)