Last modified 14 years ago Last modified on 21/05/07 15:29:22

Fawkes Build Process

This document gives an overview of the Fawkes build process. The build process is the task of compiling the whole Fawkes software tree. It describes what happens if you type make in the Fawkes directory and a few of the decisions that were made during the development of the system. Also some of the shortcomings and things where you have to spend special attention are mentioned.

Note that currently several of the mentioned URLs are unreachable. I have contacted the author and waiting for a reply. I'm going to mirror these if the pages do not come back. You can use the Google cache for now to view the pages.

Assumptions and goals

Here are a few assumptions made and goals that we wanted to accomplish that will be explained in more detail below.

  • Everything depends on libraries in src/libs
  • Sub-directories in src/ do not have inter-dependencies besides all depending on libs and interfaces
  • GCC is used on a Linux system
  • Stay close to recommended make procedures
  • Separate objects and dependency files from source
  • Binaries reside in BINDIR, libs in LIBDIR and plugins in PLUGINDIR and these three directories are pairwise disparate

The overall build process

If you type make the main Makefile will cause make to recurse into the src directory from where it recurses through the subdirectories. First the libs have to be built. This is a very central point and it has to happen before any other sub-directory. The assumption is here that everything depends on the base libraries (which is probably true, anyway). The libs directory is also special by means of error handling. Any build error in this directories is considered to be a fatal error and the build is stopped immediately. The interfaces directory behaves similar. It could rest in libs as well but since it is of more central meaning to regular plugin developers the decision was made to place it directly in src. For the other sub-directories it is assumed that they are somewhat independent from each other and can be built in parallel and it is not a problem if one of these directories fails. Nevertheless the build process will bail out here, too, if there is for instance a compilation error due to a syntax error or and unhandled dependency.

So after the libs have been built, the interfaces are built. The other directories are the processed in parallel if available on the compiling machine (i.e. on a dual core machine like our new robots). Sub-directories are recursed and built.

Building of a concrete target in more detail

Concrete targets are binaries, libraries and plugins. It means that you compile a bunch of files and then link them to one binary or shared library. These targets are somewhat special. As you can see in Makefiles like the mainapp Makefile they include the file from the build system. This is were some of the magic happens.

One decision was to build all objects to .objs, similarly to what we had in the old system. Rule 3 of Reference( says: "Life is simplest if the targets are built in the current working directory.". More elaborated this means that we do not want the implicit rules to build the objects to another directory different from the current directory. This means that we first have to change the directory to .objs and then get the source files from the original location. This was mainly done to do the right thing and maybe to allow for multiple targets in one make. In the retrospective we may have decided differently since this is what causes most of the problems that we faced.

So we used Reference( for multi-arch build and modified it slightly to fit our system. What we do is that we change into the directory by including in Then we call the old Makefile in the .objs directory again. This time recognizes that it is already in the .objs directory and just includes the rules. This will start the compilation process depending on the targets set via the variables in the Makefile (see .objs). BASEDIR is automatically re-set to the correct value so you don't need to bother. Also all other variables like SRCDIR and LIBDIR are set appropriately. But you cannot rely on the fact that you are in the source directory, you have to use the SRCDIR variable.

The SRCDIR variable if also of special interest for another case: in dependency checks. Here we want to spit out a message once with a warning or an error. For this we define something like a warning_libNAME target that spits out a warning message. Now, if we would just write it into the Makefile the target would be called twice. One time where we expect it and once in the case where make is executed in the parent directory of .objs (not in the .objs dir). To avoid this you have to guard this with

ifneq ($(SRCDIR),)

This causes the code only to be included in the run in .objs.

The make rules

The rules have been designed to be of fast and simple and to have as few as possible. This was one of the major shortcomings of the old system. You had tons of code and still you were limited in what you could do. Moving to a build-system that better honors the well-known procedures in make makes it a lot easier to maintain the system and to get into it for newcomers.

The basic idea is the following: We have a little bit of code which deals with the output and indentation for good readability, then we have code to deal with recursion into sub-directories. It will allow for parallel building of multiple sub-directories if no ordering constraints forbid this (code taken from Reference( Then come the real rules that build the world.

The idea is to use pattern targets for the compilation of files and linking of binaries. The basic patterns used are

%.o: %.cpp
$(BINDIR)/%: $$(OBJS_$$*)
$(LIBDIR)/ $$(OBJS_$$*)
$(PLUGINDIR)/ $$(OBJS_$$*)

The first target is used to compile C++ files (see below). The other targets are used to build binaries, libraries and plugins (see below).

C++ compilation and dependency generation

C++ compilation and dependency generation is done at the same time. Following an idea from Reference( we decided not to have an extra dependency step as we had it with the old system. When thinking about it you can easily convince yourself that it makes sense. If you did not yet compile a file you do not need the dependencies anyway, since the rebuilt is triggered already by the missing object file. Rebuilding the dependency before compilation if a file has new or changed dependencies is also not necessary. By changing the file you already trigger a rebuild. The only time when you need the deps is if the dependencies themselves get changed but your files stays unchanged. This means that some of the dependencies that are lying around with the source indicate a reason for a rebuild. For this it is enough to generate the dependencies when compiling a file.

The compilation generally assumes that we use GCC with the wealth of all of its flags in a GNU make on a Linux system. The build system has not been optimized for super multi-platform support (although probably not too hard to implement).

The compilation honors CFLAGS and CFLAGS_X (where X depends on the build target and may be set per file). It is a straight call to $(CC) (which is gcc) where the only unfamiliar thing is the dependency generation.

Targets for bins, libs & plugins

We distinguish three separate targets: binaries, libraries and plugins. Binaries are linked ELF executables, libraries are ELF shared object, so are plugins (but placed in a different location for a different purpose). The basic assumption is here that binaries reside in BINDIR, libraries in LIBDIR and plugins in PLUGINDIR (which must be pairwise disparate directories!). Then the appropriate linker commands are executed which honor LDFLAGS and LDFLAGS_Y (where Y depends on the target).

For these targets we use secondary expansion (Reference( This is also the main reason (besides realpath) why we need make at least in version 3.81. Secondary expansion allows for very sleek and code-efficient Makefiles. With this we can determine the dependencies from a variable whose name depends on the target. We will explain this with the libs target as an example.

$(LIBDIR)/ $$(OBJS_$$*)

Noteworthy are here the double dollars. They cause the variable to be finally expanded in the secondary expansion. In make $* is set to the stem of a pattern (what the % sign matched for in the left-hand pattern). This means that if we ordered to build $(LIBDIR)/ (by adding this to LIBS_all) this is expanded to:

$(LIBDIR)/ $(OBJS_libtest)

So this is why we set OBJS_libtest in our Makefile to the names of the objects that we are building. This is automatically expanded here to determine the correct pre-requesites for the lib target.