Skip to content

Makefiles

Franz Miltz edited this page Aug 7, 2021 · 1 revision

Makefiles

This page explains the structure of our makefiles as well as the general Makefile syntax. It is formatted as FAQs so please be encouraged to add/suggest more questions.

Basic usage

What are the basic configuration options and what do they do? (MAIN, VERBOSE,...)

TARGET  := hyped          # define the binary name to compile into
MAIN    := run/main.cpp   # define application entry point
CROSS   := 0              # set to 1 to use a cross-compiler, to compile on a laptop and run on BBB
NOLINT  := 0              # set to 1 to prevent linting the code
VERBOSE := 0              # set to 1 to print all commands Makefile runs
RELEASE := 0

These are found at the top of Makefile. Users are free to modify these variables to generate their specific setup.

As explained below, you don't need to edit the Makefile to change the options. Just add the assignment to your make command. For example make CROSS=1 to cross-compile the default main.

What are the most important targets?

make        # compile the main stuff
make lint   # checks code style of the src/ folder
make test   # compiles and runs the collection of unit tests

How/when do I use the -j option?

Run make -j to run multiple Makefile targets in parallel. For example, when compiling the code base, individual .cpp files can be compiled independently. This speeds up the overall compilation process, provided the hardware you are running on actually has parallel execution units/cores.

It is recommended to run make -j4 to limit the number of parallel jobs to 4. Otherwise, too many parallel jobs might fight for the CPUs, resulting in sub-par compilation time.

DO NOT use -j on BBB. Unfortunately, BBB is a single core machine with limited memory. Running parallel jobs does not yield compilation speedup. Furthermore, if too many jobs are run, build might just crash.

Common modifications

Most users should only need to modify the few variables at the top of Makefile.

To modify any of these variables, it is enough to override them when running make. For example, make MAIN=run/demo1.cpp CROSS=1 will compile the code base with a demo1.cpp entry point and will use a cross compiler (if installed).

Some useful modifications can be also found in utils/build/build.mk. Here, you can change optimisation options, compiler, or linking flags if you require some extra libraries.

Another makefile script, utils/build/libs.mk defines libraries to be used by the main source code. The script defines installation targets for the libraries, and registers them with the rest of the build infrastructure by appending the DEPENDENCIES variable.

Our structure

Where are all the makefiles? What are the different makefiles for?

There is a single Makefile that serves as a entry point for building stuff. It includes several supporting makefile scripts. These live in the utils/build/ folder. These are:

  • config.mk - generic configurations used by everyone
  • build.mk - how to build the main source files
  • libs.mk - what libraries/dependencies are used by the main source files, and how to install them
  • test.mk - let the QA team do their things (keep out)

Makefile syntax

This section gives a quick overview of some aspects of makefiles. More useful details, tutorials, documentation about Makefile can be found HERE.

What is the basic structure of a Makefile?

Makefile is a collection of recipes (series of steps; also called rules) to generate targets (files). Each recipe looks something like this:

targets : prerequisites
  command
  command
  command
  ...

A recipe for a target is run whenever the target file does not exist or whenever any of the target's prerequisites changes. Prerequisites can be other makefile targets or files. File prerequisites are regarded as changed when the timestamp is changed from what the makefile remembers since last time it checked. This implies, whenever the file is modified.

Note, every command is run as a new shell script. Shell variables do not carry over between multiple commands, even within the same recipe.

Indentation, tabs/spaces

By default, Makefiles use tabs for indentation of commands in recipes. This is hard to change portably, so we are stuck with it.

Variables: how do we declare and use them?

Makefile variables are a bit different from shell/environment variables. Do not use them interchangeably.

Variables do not have a naming scheme/case. The convention is to use UPPER_CASE. Defining variables is as simple as VAR = <value>. Using a variable can be done by TEMP = $(DEFINED_VARIABLE). If variables are not defined, they return empty value when used.

Makefile can specify default values for variables. However, these are overwritten by any runtime definition, e.g. make CROSS=1 will use CROSS=1 even if the makefile defines CROSS=0.

What's the difference/relationship between =, := and +=?

= is an expansion assignment, that get copy pasted to place where the variable is used, think #define macros in c++

:= is a value assignment, that stores the value on the right into the variable exactly at the place you write it

+= is an append value assignment, so TEMP += new_val is equivalent to TEMP := $(TEMP) new_val

What does it mean when there is nothing on the right hand side of =?

TEMP= is actually just assigning empty nothing, so this is semantically unnecessary. However, it might be a good thing to indicate that such a variable might be defined and used later on by some recipes.

What is $@, $* and $<? Are there any similar ones?

$@ is an automatic variable, target name.

$< is an automatic variable, name of the first prerequisite

$* is an automatic variable, name of the "stem" of implicit/pattern rules

There are a few (3) others.

What's up with the percentage sign? E.g. in %.o.

% is a placeholder that can match the same value at different places. This is a good opportunity for a demo. This is how we compile the main binary (after some variable expansion, only approximately):

# to generate the hyped binary, we depend on compiling the individual object files
hyped : bin/run/main.o  bin/navigation/navigation.o bin/propulsion/can/can_sender.o bin/propulsion/can/fake_can_endpoint.o bin/propulsion/can/fake_can_sender.o ...
  echo Linking into into $@   # expands into "echo Linking into hyped"
  g++ -o $@ $^                # expands into "g++ -o hyped <all objects/prerequisites>"

# to generate the individual object files, we use pattern rules
bin/%.o : src/%.cpp
  echo Compiling $<           # expands into "echo Compiling <source file>"
  g++ -c -o $@ $<             # expands into "g++ -c -o <object file> <source file>"

How does include work? Does it just "copy" over the contents of the included file or does it do something smarter?

I always see it just like #include in c++. Just copy-paste the makefile script in.

What does define <...> endef do?

Similarly to bash, we can create functions in Makefiles. The best example can be found in Makefile:53:

define echo_var
  @echo $(1) = $($1)
endef

Here, echo_var is a function that takes one argument and does stuff with it. We can call the function by $(call echo_var,UNAME).

What's .PHONY?

Makefile targets are usually files that the makefile generates as a result of compilation. This means running make clean will check if a file clean exists, and will run the recipe if the file is not there. This means, if we create a file clean, then make clean will actually do nothing. .PHONY: clean is used to still run the recipe. It tells the makefile that this target is not a file but should be run always.

What are shell, call, foreach, findstring, info, warning, patsubst and dir?

shell - run commands using the shell environment

call - this can be used if you define a makefile functions, to call them

foreach - makefile iteration overs values in a makefile variable

findstring - check if a value contains a substring, returns the substring or empty is not found

info - just print something, like echo in shell

warning / error - print something, error also fails running the make command

patsubst - path substitution, handy for changing src/path/to/file to bin/debug/path/to/file

dir - get directory of a file

More details, as well as other interesting functions, if/else, text processing can be found HERE

This is to let make know what needs to be recompiled. When you modify a header file, you really should recompile all object files that depend on the header file. This means not only .cpp files including the header file, but .cpp file including other headers that can eventually include the modified header file. Since the dependency tree is hard to track (for makefile), and is language (c++) specific, DEPFLAGS are passed to the compiler to generate those .d files. Those .d are formatted such, that they create dependency targets when included into Makefile. We include all of those in Makefile, line 92: -include $(OBJS:.o=.d).

The -M<...> options are magic I do not entirely understand, but it works :)

Clone this wiki locally