#[1]R4nd0m's Blog Atom

  [2]R4nd0m's

[3]R4nd0m's

  Software Engineer

    * [4]links
    * [5]whoami
    * [6]CV

    *
    *
    *
    *
    *

  [7]Home [8]Archives [9]Categories [10]Tags [11]Atom

                      The quest to the perfect Makefile

  Posted on Wed 17 October 2018 in [12]Programming

  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)

$(BUILD_DIR)/%.o: %.cpp
-       mkdir -p $(dir $@)
-       $(CXX) $(CXXFLAGS) -c $< -o $@
+       $(SILENCE)mkdir -p $(dir $@)
+       $(SILENCE)$(CXX) $(CXXFLAGS) -c $< -o $@

clean:
-       rm -rf $(BUILD_DIR)
+       $(SILENCE)rm -rf $(BUILD_DIR)
.PHONY: clean

  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)

$(BUILD_DIR)/%.o: %.cpp
+       $(SHOW_CXX) $@
      $(SILENCE)mkdir -p $(dir $@)
      $(SILENCE)$(CXX) $(CXXFLAGS) -c $< -o $@

clean:
+       $(SHOW_CLEAN) $(BUILD_DIR)
      $(SILENCE)rm -rf $(BUILD_DIR)
.PHONY: clean

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

$ find build/ | grep \\.d\$
build/src/main.d
build/src/FibonacciNumbersRecursed.d
build/src/FibonacciNumbersDynamic.d
build/src/IFibonacciNumbers.d

$ cat build/src/main.d
build/src/main.o: src/main.cpp src/IFibonacciNumbers.hpp

  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

$ touch src/FibonacciNumbersDynamic.hpp
$ make
[ g++ ]        build/src/IFibonacciNumbers.o
[ g++ ]        build/src/FibonacciNumbersDynamic.o
[ g++ ]        build/fibonacci

  Much better!

Can enable debug or release compilation mode

  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

ifneq ($(DEBUG),)
 COMMON_CFLAGS     += -g
 BUILD_DIR         := $(BUILD_DIR)/debug
else
 COMMON_CFLAGS     += -DNDEBUG -O3
 BUILD_DIR         := $(BUILD_DIR)/release
endif

CFLAGS              += $(COMMON_CFLAGS)
CXXFLAGS            += $(COMMON_CFLAGS) -std=c++14

  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

$ make V=1 DEBUG=1
g++ -Wall -Werror -Wextra -MMD -g -std=c++14 -c src/IFibonacciNumbers.cpp -o bui
ld/debug/src/IFibonacciNumbers.o

Builds the unit tests when requested

  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: