Skip to content

Instantly share code, notes, and snippets.

@francois-rozet
Last active May 31, 2023 06:54
Show Gist options
  • Save francois-rozet/c8efb19f66fed666263641d4e40f8863 to your computer and use it in GitHub Desktop.
Save francois-rozet/c8efb19f66fed666263641d4e40f8863 to your computer and use it in GitHub Desktop.
Step-by-step Makefile tutorial
# Macros
ALL = program1 program2 program3
SRCDIR = src/
BINDIR = bin/
EXT = cpp
CXX = g++
CXXFLAGS = -std=c++14 -O3 -Wall -Wextra
# Files
SRCS = $(wildcard $(SRCDIR)*.$(EXT))
OBJS = $(patsubst $(SRCDIR)%.$(EXT), $(BINDIR)%.o, $(SRCS))
DEPS = $(OBJS:.o=.d)
XOBJS = $(filter-out $(patsubst %, $(BINDIR)%.o, $(ALL)), $(OBJS))
# Executable files
all: $(ALL)
$(ALL): %: $(BINDIR)%.o $(XOBJS)
$(CXX) $(CXXFLAGS) -o $@ $^
# Dependency files
$(BINDIR)%.d: $(SRCDIR)%.$(EXT)
mkdir -p $(BINDIR)
$(CXX) $(CXXFLAGS) $< -MM -MT $(patsubst $(SRCDIR)%.$(EXT), $(BINDIR)%.o, $<) -MF $@
# Object files
-include $(DEPS)
$(BINDIR)%.o: $(SRCDIR)%.$(EXT)
$(CXX) $(CXXFLAGS) -c -o $@ $<
# Phony
.PHONY: all clean dist-clean
clean:
rm -rf $(BINDIR)
dist-clean: clean
rm -rf $(ALL)

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.

Basics

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 :

  1. The instruction(s) prior_inst is(are) called.
  2. 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

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

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 $@ $<

Functions

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 string A from a string B.

     F2 = $(filter-out file3.cpp file1.cpp, $(SRCS)) # F2 = file2.cpp

A sub-string doesn't have space ( ) characters.

Phony

.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.

Include

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.

Example

Here is an example of a very versatile C/C++ Makefile.

Macros

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 a main 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.

Files selection

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 to ALL files.

Instructions

Executable files
all: $(ALL)

$(ALL): %: $(BINDIR)%.o $(XOBJS)
	$(CXX) $(CXXFLAGS) -o $@ $^

The instruction all induces the production of all executable files.

Dependency 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.

Object files
-include $(DEPS)

$(BINDIR)%.o: $(SRCDIR)%.$(EXT)
	$(CXX) $(CXXFLAGS) -c -o $@ $<

Phony

.PHONY: all clean dist-clean

clean:
	rm -rf $(BINDIR)

dist-clean: clean
	rm -rf $(ALL)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment