I have been using [13]cmake for a while because I think writing good
Makefile is a very tricky task. Unfortunately, cmake is not always
available and writing a Makefile is the only solution. I decided to
spend some time on writing a small Makefile structure that I could use
for new projects and would share my journey to this quest with you.
The material of this article is also available on [14]gitlab
Requirements
Here is what I expect from a well written Makefile:
* Takes well care of parallel builds
* Makes easy to add a new source files
* Makes cross compilation easy
* Has fancy and debug output
* Rebuilds only and everything that is required on changes
* Can enable debug or release compilation mode
* Builds the unit tests when requested
* Finds depedencies to other libraries automatically
* Generates sources automatically
That may sound a lot but it should be the bare minimal to make it
usable in complex projects.
The project
To illustrate the usage of this Makefile, I will implement an
overengineered Fibonnaci numbers computation. I won't spend too much
time on the details here but this will give us some source files to
compile.
The base structure
Let's start writing the basic structure of our project with a entry
point and some interfaces. The tree looks like this:
├── Makefile
└── src
├── FibonacciNumbersRecursed.cpp
├── FibonacciNumbersRecursed.hpp
├── IFibonacciNumbers.cpp
├── IFibonacciNumbers.hpp
└── main.cpp
We have an entry point in main.cpp, an interface to compute fibonacci
numbers and a first implementation that does nothing for now. Let's now
write a basic Makefile.
The first Makefile
First we define some useful variables that can be reused later:
PROJECT = fibonacci
BUILD_DIR ?= build
The binary and the list of source files will also be saved in a
variable:
APP_BIN = $(BUILD_DIR)/$(PROJECT)
APP_SOURCES = src/IFibonacciNumbers.cpp \
src/FibonacciNumbersRecursed.cpp \
src/main.cpp
Here we create a list of compiled .o files from the list of sources,
that can be used as a prerequist list.
APP_OBJS = $(patsubst %.cpp,$(BUILD_DIR)/%.o,$(APP_SOURCES))
Define here all the compiler flags:
COMMON_CFLAGS = -Wall -Werror -Wextra
CFLAGS += $(COMMON_CFLAGS)
CXXFLAGS += $(COMMON_CFLAGS) -std=c++14
Now we have all the variables we need, we can start writing a default
target that is by convention all. It will simply build the main binary:
all: $(APP_BIN)
PHONY: all
Now how to link the binary:
$(APP_BIN): $(APP_OBJS)
$(CXX) -o $@ $(APP_OBJS)
How each source will be built:
$(BUILD_DIR)/%.o: %.cpp
mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) -c $< -o $@
Finally a small clean target:
clean:
rm -rf $(BUILD_DIR)
PHONY: clean
First make commands
Let's give a first attempt to make to build our application:
$ make
mkdir -p build/src/
g++ -Wall -Werror -Wextra -std=c++14 -c src/IFibonacciNumbers.cpp -o build/src/I
FibonacciNumbers.o
mkdir -p build/src/
g++ -Wall -Werror -Wextra -std=c++14 -c src/FibonacciNumbersRecursed.cpp -o buil
d/src/FibonacciNumbersRecursed.o
mkdir -p build/src/
g++ -Wall -Werror -Wextra -std=c++14 -c src/main.cpp -o build/src/main.o
g++ -o build/fibonacci build/src/IFibonacciNumbers.o build/src/FibonacciNumbersR
ecursed.o build/src/main.o
$ ./build/fibonacci
Computing using implementation recursive
Fibonacci number of 3 is ... 3
The application is not implemented but at least it generates a working
executable. Let's see what goals we have achieved until now.
Takes well care of parallel builds
This is important to build the project faster by making use of all
CPUs. We can check if make behaves correctly with the -j option. After
cleaning and rebuilding the project with different jobs, it always
succeeds. We can also see that the sources files are built in parallel:
$ make
$ make clean
$ make -j2
$ make clean
$ make -j3
$ ...
Makes easy to add a new source file
When adding a new source file, the Makefile should have as least
changes as possible. To demonstrate this, we simply add a new Fibonacci
number implementation and update the source list:
APP_BIN = $(BUILD_DIR)/$(PROJECT)
APP_SOURCES = src/IFibonacciNumbers.cpp \
src/FibonacciNumbersRecursed.cpp \
+ src/FibonacciNumbersDynamic.cpp \
src/main.cpp
APP_OBJS = $(patsubst %.cpp,$(BUILD_DIR)/%.o,$(APP_SOURCES))
The application is updated in order to be able to choose the
implementation at runtime. We can now check that it works as expected:
$ make
# ...
$ ./build/fibonacci -t 0
Computing using implementation recursive
Fibonacci number of 3 is ... 3
$ ./build/fibonacci -t 1
Computing using implementation dynamic
Fibonacci number of 3 is ... 3
Some poeple would prefere having a function that automatically searches
for all the source files with something like this:
SOURCES = $(shell find src -name '*.cpp')
However, we lose a bit of control on which source file we want to
include in the build in case we support different platforms which don't
use the same sources files. This is why I'm fine with adding a line in
the Makefile when I add a new source file in the project for more
control on what is being built.
Makes cross compilation easy
When using multiple hardware, it should be easy to build for another
architecture with the minimal effort. In our case, cross compiling can
be done by defining another build directory and compiler on the command
line:
$ make BUILD_DIR=build_arm CXX=arm-linux-g++
# ...
Has fancy and debug output
We don't want make to print the executed commands that is very verbose,
this can be done by adding the @ character at the beginning of the
command. When something goes wrong, we need to see what is done. This
should be enabled with an environment variable.
First we define an environement variable to enable the verbose mode:
ifneq ($(V),)
SILENCE =
else
SILENCE = @
endif
Update all the recipes consequently:
$(APP_BIN): $(APP_OBJS)
- $(CXX) -o $@ $(APP_OBJS)
+ $(SILENCE)$(CXX) -o $@ $(APP_OBJS)
Now we can enable the verbose mode by setting the V environment
variable to any value:
make V=1
# ...
Unfortunately, in silent mode, we don't see anything:
$ make
$
Some feedback is always good in order to know what happens behind the
hood. However, we can print something nicer than the build command and
use for that some printf that can be reused:
SHOW_COMMAND := @printf "%-15s%s\n"
SHOW_CXX := $(SHOW_COMMAND) "[ $(CXX) ]"
SHOW_CLEAN := $(SHOW_COMMAND) "[ CLEAN ]"
Update the recipe to print the status of the build:
$(APP_BIN): $(APP_OBJS)
+ $(SHOW_CXX) $@
$(SILENCE)$(CXX) -o $@ $(APP_OBJS)
Now the bulid is more verbose but still very comfortable to watch:
$ make
[ g++ ] build/src/IFibonacciNumbers.o
[ g++ ] build/src/FibonacciNumbersRecursed.o
[ g++ ] build/src/FibonacciNumbersDynamic.o
[ g++ ] build/src/main.o
[ g++ ] build/fibonacci
$ make clean
[ CLEAN ] build
Nice!
Rebuilds only and everything that is required on changes
This is very important for big projects, we don't want to recompile
everything if we changed only one source file. When using unit tests,
we want to have a feedback aboout our changes as fast as possible, to
achieve this we need to recompile only the files that changed.
As a first test, we will check what happens if we update a source file:
$ touch src/IFibonacciNumbers.cpp
$ make
[ g++ ] build/src/IFibonacciNumbers.o
[ g++ ] build/fibonacci
The file gets compiled and the binary linked again, this seems correct.
However, having a nice depedency of headers is more difficult to
maintain as demonstrated here:
$ touch src/IFibonacciNumbers.hpp
$ make
make: Nothing to be done for 'all'.
We would expect main.cpp and IFibonacciNumbers.cpp to be compiled again
as those include the modified header. We could manually list the header
depedency for each source file like this:
main.o: main.cpp IFibonacciNumbers.hpp
IFibonacciNumbers.o: IFibonacciNumbers.cpp IFibonacciNumbers.hpp
# ...
But this is a nightmare to maintain and is source of mistakes when the
project evolve! Fortunately, the compiler is able to generate this list
of depedencies automatically with the -MMD argument that will generate
a file with the .d extension. First we add this flags:
-COMMON_CFLAGS = -Wall -Werror -Wextra
+COMMON_CFLAGS = -Wall -Werror -Wextra -MMD
Out of curiosity, let's have a look at the list of generated files:
$ make
# ...
A rule is automatically generated with the list of headers, exactly
what we need! We must now include those rules in our Makfile by
defining a list of generated depedency files, that is the list of
objects with the .d extension:
DEPS = $(APP_OBJS:.o=.d)
And include them right after the default target:
all: $(APP_BIN)
PHONY: all
-include $(DEPS)
Now verify that it works as expected when modifying different header:
$ touch src/IFibonacciNumbers.hpp
$ make
[ g++ ] build/src/IFibonacciNumbers.o
[ g++ ] build/src/FibonacciNumbersRecursed.o
[ g++ ] build/src/FibonacciNumbersDynamic.o
[ g++ ] build/src/main.o
[ g++ ] build/fibonacci
We often need a debug version during the development but the
application should be shipped in release mode. To switch from a mode to
another, we will use an environement variable and change the flags
consequently. We also change the build directory to not mix objects
compiled with different flags:
COMMON_CFLAGS = -Wall -Werror -Wextra -MMD
To verify that the flags are passed correctly we run make in verbose
mode:
$ make V=1
g++ -Wall -Werror -Wextra -MMD -DNDEBUG -O3 -std=c++14 -c src/IFibonacciNumbers.
cpp -o build/release/src/IFibonacciNumbers.o
During the development, it makes a lot of sense to build and execute
the unit tests. For an end user, this is probably not required and
should be skippable.
I like the [15]CPPUTest test framework and will use it here. If you use
another testing framework, the idea is the same, you would need to
adapt the flags consequently. First we write some tests source files
that contains an entry point and a test that fails: