Complete roguelike tutorial using C++ and libtcod - extra 4: makefiles
Complete roguelike tutorial using C++ and libtcod -originally written by Jice Text in this tutorial was released under the Creative Commons Attribution-ShareAlike 3.0 Unported and the GNU Free Documentation License (unversioned, with no invariant sections, front-cover texts, or back-cover texts) on 2015-09-21. |
---|
|
This article will show you how to use a makefile to speed up compilation. If you're using an IDE, you probably don't need this, except if you want to port your application to an OS where your IDE is not available. As with gdb, even if there are better alternatives, it's always useful to know how to quickly setup a makefile for your project.
There are several ways a makefile will speed up compilation compared to recompiling everything every time with a single g++ command line :
- we will generate an intermediate .o file for each .cpp source file, then link all .o files together to get the executable. When a single cpp file is modified, we only need to recompile its .o and link the new executable.
- using the same idea, we can generate an intermediate file for a header. This is called a precompiled header and uses the .gch extension. As long as you don't modify a header, the compiler will be able to use this precompiled header instead of re-parsing them.
- finally, make can use multi-threading to compile several .cpp files at the same time.
Note that with the current size of the project, you won't see much difference in compilation times, but the gain will get bigger and bigger as your project grows.
A 11 lines makefile
So we will create a "makefile" file in our project root directory. We start by determining the list of sources files for the project :
SOURCES=$(wildcard src/*.cpp)
SOURCES will contain a space separated list of filenames : src/Actor.cpp src/Ai.cpp ... From this variable, we deduce the list of .o files by replacing .cpp with .o :
OBJS=$(SOURCES:.cpp=.o)
Now we're going to tell make how to generate the tuto file. The problem is that the flags for the linker depend on the operating system. So we're using the uname -s shell command to detect if we're on Linux or Windows :
ifeq ($(shell sh -c 'uname -s'),Linux)
LIBFLAGS=-L. -ltcod_debug -ltcodxx_debug -Wl,-rpath=.
else
LIBFLAGS=-Llib -ltcod-mingw-debug -static-libgcc -static-libstdc++
endif
Now we can write our first compilation rule :
tuto : $(OBJS)
g++ $(OBJS) -o tuto -Wall $(LIBFLAGS) -g
It's important to use your final .exe as the first rule in the makefile because it's what make will build when you run it without parameter. We're telling make that the target named 'tuto' depends on all .o files. Or rather : if a .o file is modified, we need to execute the commands associated with the tuto target. This command is the linking phase of the compilation, using g++ to link all .o files together. It's important for make to work that the command line starts with a tabulation.
Ok now make knows how to generate tuto (or tuto.exe on Windows), but not how to get the .o files. Let's tell him.
src/%.o : src/%.cpp
g++ $< -c -o $@ -Iinclude -Wall -g
This is a generic rule, telling that any src/xxx.o file is depending on src/xxx.cpp. When src/xxx.cpp is modified, we call the command below :
g++ $< -c -o $@ -Iinclude -Wall -g
We're using two make automatic variables here :
$@ represent the current target (src/xxx.o)
$< represent the current target's dependencies (src/xxx.cpp)
So the executed command will be :
g++ src/xxx.cpp -c -o src/xxx.o -Iinclude -Wall -g
The -c flag tells g++ to generate the object file, but not try to link it into an executable.
That's it. This is a tiny makefile but it does the job, compiling the project on both Windows/Mingw and Linux :
SOURCES=$(wildcard src/*.cpp)
OBJS=$(SOURCES:.cpp=.o)
ifeq ($(shell sh -c 'uname -s'),Linux)
LIBFLAGS=-L. -ltcod_debug -ltcodxx_debug -Wl,-rpath=.
else
LIBFLAGS=-Llib -ltcod-mingw-debug -static-libgcc -static-libstdc++ -mwindows
endif
tuto : $(OBJS)
g++ $(OBJS) -o tuto -Wall $(LIBFLAGS) -g
src/%.o : src/%.cpp
g++ $< -c -o $@ -Iinclude -Wall -g
Now type 'make' in your terminal and check that it actually builds the game. Now open one of the cpp files and save it, then run make again. Only this cpp file is compiled. Even better, delete all src/*.o files and type
make -j4
to compile the project using 4 threads.
Including precompiled headers
Ok but there's an issue. If you modify a header and run make, nothing happens. Well believe it or not, I generally stick with this makefile. Handling header dependencies like all IDE do is not that great. If you change a comment in some header, every file including it should not be recompiled. Even when you change a method's prototype, there's no point recompiling every file including the header. Only the ones actually calling this function. In fact there's only one case where you should recompile every file including a header : when you change the data in a class, either the fields order or add/remove a field. So I generally add a clean target to recompile everything (see below) and use that when I know some data structure has changed. If you know what you're doing, it's really the most efficient way to work. But if you're only beginning, the safest way is to actually handle headers dependencies.
We're going to do that by adding a dependency to the cpp files. But first, let's tell make how to generate precompiled headers.
src/main.hpp.gch : src/*.hpp
g++ src/main.hpp -Iinclude -Wall
Fortunately, all our cpp files only depend on a single header : main.hpp. As soon as one of the header is modified, we're going to regenerate the precompiled main.hpp.gch file. Now all we need to do is add the file to the cpp dependencies :
src/%.o : src/%.cpp src/main.hpp.gch
Now try to save one of the headers and run make again : it recompiles everything.
Handling debug and release versions
You know that you should have debugging information in your program as long as you're in development phase. But when the time of release comes, you should remove them and even tell the compiler to optimize the code.
So let's remove the hardcoded '-g' flag and replace it with a variable :
tuto : $(OBJS)
g++ $(OBJS) -o tuto -Wall $(LIBFLAGS) $(CFLAGS)
src/%.o : src/%.cpp
g++ $< -c -o $@ -Iinclude -Wall $(CFLAGS)
What we want is CFLAGS to be -g when we run 'make debug' and -O2 -s (optimize for speed and size) when we run 'make release'. Fortunately, the goal specified in the command line is available in the MAKECMDGOALS variable :
ifneq (,$(findstring release,$(MAKECMDGOALS)))
CFLAGS=-O2 -s
else
CFLAGS=-g
endif
The first line is a complex way to tell "if MAKECMDGOALS contains release". Now we just have to add our debug and release targets :
debug : tuto
release : tuto
There's no specific command associated with these targets, but they depend on the tuto file, so typing make debug will build tuto using the debug flags. Now we're able to switch from debug version to release version but there's an issue. If you type
make debug
then
make release
Nothing gets compiled for the second command. That's because no dependency was modified. The right way to handle that would be to store debug and release .o files in a separate directory. But this will bring some unnecessary complexity to the makefile. To be able to recompile everything even when no dependency was modified, we simply add a new 'clean' target that deletes every compiled file :
clean :
rm -f src/main.hpp.gch $(OBJS)
This way, you can run 'make debug' as long as you're in development phase. Then 'make clean release' to recompile the release version from scratch. Then 'make clean debug' to go back to debug version.
The complete makefile :
SOURCES=$(wildcard src/*.cpp)
OBJS=$(SOURCES:.cpp=.o)
# compiler options : add debug information in debug mode
# optimize speed and size in release mode
ifneq (,$(findstring debug,$(MAKECMDGOALS)))
CFLAGS=-g
else
CFLAGS=-O2 -s
endif
# linker options : OS dependant
ifeq ($(shell sh -c 'uname -s'),Linux)
LIBFLAGS=-L. -ltcod_debug -ltcodxx_debug -Wl,-rpath=.
else
LIBFLAGS=-Llib -ltcod-mingw-debug -static-libgcc -static-libstdc++ -mwindows
endif
debug : tuto
release : tuto
tuto : $(OBJS)
g++ $(OBJS) -o tuto -Wall $(LIBFLAGS) $(CFLAGS)
src/main.hpp.gch : src/*.hpp
g++ src/main.hpp -Iinclude -Wall
src/%.o : src/%.cpp src/main.hpp.gch
g++ $< -c -o $@ -Iinclude -Wall $(CFLAGS)
clean :
rm -f src/main.hpp.gch $(OBJS)