As a result, the use of Make significantly reduces the time required to recompile a program, especially for very large projects.
We’re now going to pretend to be game developers writing a game in C++11, which we creatively name “BallGame”. Let’s also pretend that we know what we’re doing and use the OpenGL Mathematics (GLM) and SDL2 libraries in our game. With that said, the project directory might look something like that:
. ├── Makefile ├── src │ ├── ball.cpp │ ├── ball.hpp │ ├── game.cpp │ ├── game.hpp │ ├── main.cpp │ ├── paddle.cpp │ └── paddle.hpp ├── lib │ └── libSDL2.a └── include ├── GLM │ └── *.hpp └── SDL2 └── *.h
srcdirectory contains all the source files (.cpp) and headers files (.hpp).
libdirectory contains the archive files (.a)—also known as libraries—to be statically linked into the final binary.
includedirectory contains the libraries’ header files that will be included in our source code.
While SDL2 comes with both an archive file
libSDL2.a as well as the needed header files found in
SDL2, GLM is a header only mathematics library (used for graphics software, if you must know) which only provides header files found in
The project needs to be compiled! Let’s write a makefile to do it for us.
Before we delve into makefiles, it’s important to understand the basic syntax.
# Hi, I'm a comment! BINARY = BallGame # Dynamically bound assignment to BINARY variable. BINARY := BallGame # Statically bound assignment to BINARY variable. # To use/expand the variable, we write $(BINARY).
This StackOverflow answer perfectly illustrates the difference between dynamically bound (value is expanded when the variable is used) and statically bound (value expanded when the variable is declared) assignments in makefiles. In our case, both types of assignments have the same effect.
Comments and variables are optional but give more clarity to the code and help with maintenance later on.
In addition to comments and variables, makefiles consist of rules. Each rule is made up of three building blocks: targets, dependencies and commands.
target: dependencies command command ...
A target is the name of a generated file such as an object file (.o) or executable. However, targets can also be the name of an action to carry out—such as
install—which copies generated executable files into some appropriate locations for convenient access. The target name
install is not predefined, but a very well-established convention. Same applies to the commonly used target
make install will execute the commands associated with the target
A dependency can be a source file, header file, object file or executable. When the contents of a dependency change, the commands get executed and the corresponding target gets recompiled.
A command is an action that Make carries out. Usually a command serves to create a file with the same name as the target if any of the dependencies change. Commands are executed by
$(SHELL) which expands to “/bin/sh” by default, meaning that any valid Bourne shell (sh) command can be executed.
Make creates a dependency graph out of all targets, which it topologically sorts to build the dependencies in a valid and efficient order.
One last thing to take into consideration is indentation. Each command is preceded by a tab. In makefiles, it’s necessary to use tabs, not spaces! Otherwise you’ll be seeing this friendly error a lot:
Makefile:19: *** missing separator. Stop.
Forgetting to write a tab or typing 4 (or 8) spaces instead of a tab is a very common mistake. Be careful especially if you use a text editor like Vim that’s set up to automatically convert tabs into spaces. With Vim, I just tend to press “u” in normal mode as soon as the file opens to undo the conversion. Not the most practical solution, but it’ll do.
Now that we’ve gone through the basics, it’s time to write our makefile.
Let’s start by assigning some values to variables.
BINARY := BallGame SRCS := $(wildcard src/*.cpp) # ball.cpp game.cpp main.cpp paddle.cpp OBJS := $(SRCS:.cpp=.o) # ball.o game.o main.o paddle.o
BallGame is simply the name of the binary to generate.
A wildcard function allows certain characters to be given a special meaning; the asterisk (*) for example is given the meaning “all”. The variable in the second assignment will therefore expand to a space-delimited list of all source files found in the
The third assignment expands the
SRCS variable and replaces all occurrences of “.cpp” with “.o”, resulting in a space-delimited list of all the corresponding object files.
Next, we assign the flag
DEBUG := -g
-g is a GCC flag that enables the use of extra debugging information to make debugging work better in GDB. However, it can make other debuggers crash or refuse to read the program. We’re adding the flag to a variable so that we can use it in multiple places and remove it whenever we use a different compiler or don’t want to attach extra debugging information to the binary file. We only have one flag but could always add more debugging flags.
We need to provide the compiler with a path to the library header files included in the source code as well as a path to the libraries (archive files) to be linked to the binary. As previously stated, library header files and archive files can be found inside the
lib directories respectively.
INCLUDE := -Iinclude LIB := -Llib -lSDL2 CPPFLAGS += $(INCLUDE) CXXFLAGS += $(DEBUG) -Wall -std=c++11 LDFLAGS += $(LIB)
-I is used to specify the path of the directory containing library header files.
-Iinclude informs the compiler that the library header files can be found in the
-L is used to specify the path of the directory containing the libraries.
-Llib informs the compiler that archive files can be found in the
-l is the name of the library you want to link to the binary, written without the beginning “lib” and the ending “.a” extension. For the archive file libSDL2.a, we pass
One remark about variables: you may define some variables in the makefile such as
BINARY, but there also exists predefined variables found in the large makefile database. For example, the following variables have special significance:
CFLAGSholds flags that will be passed to the C compiler (e.g. version of the C language, C compiler warning settings, or other options specific to the C compiler).
CPPFLAGSholds flags that will be passed to the C/C++ preprocessor (e.g. add paths to the header file search mechanism).
CXXFLAGSholds flags that will be passed to the C++ compiler (e.g. version of the C++ language, C++ compiler warning settings, or other options specific to the C++ compiler).
LDFLAGSholds flags that will be passed to the linker (e.g. libraries against which to link and additional search paths to be used for locating and resolving library dependencies).
-Wall option flag enables all compiler warning messages. The
-std=c++11 option flag indicates to the compiler the C++ version, in this case C++11.
Let’s start with this rule:
.PHONY: all clean
.PHONY indicates that the targets
clean are phony targets. Specifying a target as a phony target saves Make from having to look for a file of that name.
If no command line arguments are passed to make, it builds the first target that appears in the makefile which is traditionally a symbolic phony target named
all. The name
all is not predefined, it’s just used by convention to inform that Make will build all that is needed for a complete build. By default, I want Make to build everything which is why
all: $(BINARY) is always the first rule.
If command line arguments are passed, the specified target will be built. For example, running
make clean will execute commands to clean the project directory.
$(BINARY): $(OBJS) $(LINK.cc) $(OBJS) -o $(BINARY) $(LDFLAGS)
This is where all the magic happens. Perhaps one of the most useful and confusing feature of Make is its ability to automatically deduce intermediate targets.
You see, Make is smart enough to deduce that a file ending with the extension “.o” depends on a file with the same basename and the extension “.cpp”. As a result, we do not need to create a rule with object files as targets and explicitly listing corresponding source files as dependencies.
Simply listing all the object files as dependencies to the target binary “BallName” is sufficient for Make to build the object files using the system’s default C compiler or C++ compiler.
Furthermore, it knows how to create the object files from the source files. Let’s see what’s inside “LINK.cc”:
$ make -p | grep "LINK.cc =" LINK.cc = $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)
In our case, if we expand the variables we get
LINK.cc = g++ -g -Wall -std=c++11 -Iinclude -Llib -lSDL2.
Now, it would be great if the makefile can clean up after itself.
clean: @- $(RM) $(BINARY) @- $(RM) $(OBJS) @- $(RM) src/.depend
This rule is responsible for removing all the generated files. A quick explanation of the symbols used:
@simply suppresses that.
-tells Make to keep going if the command fails for some reason.
$(RM)simply expands to
make clean, the binary, object files and dependency file (.depend) are removed. Pretty much everything that isn’t source code is removed. Convenient, isn’t it?
Now consider this scenario: you edit a source file (.cpp) and expect it to get recompiled and linked. It works. Then you edit a header file (.h) and expect all source files which include it to get recompiled, but instead you get:
make: Nothing to be done for 'all'.
What happened? It turns out that Make doesn’t track header file dependencies. Honestly, I find it odd that a build system doesn’t have trivial solution to track header file dependencies.
There are several methods for dealing with header file dependencies. One way is to use the tool makedepend but we will be using another method that doesn’t require installing an additional tool. Makefiles cannot generate header file dependencies but the GNU compiler certainly can. Our problem simply becomes a matter of informing the makefile of those dependencies.
.depend: $(SRCS) @- $(RM) .depend @- $(CXX) $(CPPFLAGS) $(CXXFLAGS) -MM $^ | sed -r 's|^([^ ])|src/\1|' > .depend;
A lot is happening here. The target
.depend is a hidden file (due to the dot) that contains all the header file dependencies. If any of the source files is modified, the file gets reconstructed. The first time the makefile runs, it will not find a
.depend file and will proceed to create it. If the file exists, it will check if any of the source files (.cpp) listed in
$(SRCS) have been modified, will proceed to remove the old
.depend and will create a new one reflecting the changes.
The sed command
sed -r 's|^([^ ])|src/\1|' basically prefixes “src/” to each object file name because we want object files to be in the “src/” directory.
Once the header file dependencies have been generated, we have to make sure to inform the makefile by including the dependencies:
include directive tells Make to suspend reading the current makefile and to read the contents of
.depend before proceeding. And what does
.depend contain? You guessed it, the header file dependencies. Here is an example of a
.depend file generated by the makefile of a pong game:
src/ball.o: src/ball.cpp src/ball.hpp src/pong.hpp src/paddle.hpp src/main.o: src/main.cpp src/pong.hpp src/paddle.o: src/paddle.cpp src/paddle.hpp src/pong.hpp src/ball.hpp src/pong.o: src/pong.cpp src/pong.hpp src/ball.hpp src/paddle.hpp \ src/utilities.hpp src/utilities.o: src/utilities.cpp src/utilities.hpp
Notice the header files listed along with the source files? Now every time the contents of a header file change, the corresponding object files will get recompiled.
Once the makefile finishes executing, the executable
BallGame will have been compiled. To run it, simply type
Note: Tup deals with header file dependencies more elegantly.
Putting it all together, we get:
BINARY := BallGame SRCS := $(wildcard src/*.cpp) OBJS := $(SRCS:.cpp=.o) DEBUG := -g INCLUDE := -Iinclude LIB := -Llib -lSDL2 CPPFLAGS += $(INCLUDE) CXXFLAGS += $(DEBUG) -Wall -std=c++11 LDFLAGS += $(LIB) .PHONY: all clean all: $(BINARY) $(BINARY): $(OBJS) $(LINK.cc) $(OBJS) -o $(BINARY) $(LDFLAGS) .depend: $(SRCS) @- $(RM) .depend @- $(CXX) $(CPPFLAGS) $(CXXFLAGS) -MM $^ | sed -r 's|^([^ ])|src/\1|' > .depend; -include .depend clean: @- $(RM) $(BINARY) @- $(RM) $(OBJS) @- $(RM) .depend
That’s it! Now you can go ahead and adapt this makefile to your own project.
This proposed process for writing a makefile isn’t perfect. I still haven’t figured out how to have object files in a separate
obj directory and I’m sure there’s a way to avoid using
sed. For two real-world examples where I use makefiles, see my Tetris and Pong clones. With that said, it’s time for me to use something else than makefiles. For bigger projects, I’d better use CMake which I have yet to try. For smaller ones, I’ll stick with Tup. If you have any remarks or suggestions, let me know by leaving a comment below.