Felix Crux

Technology & Miscellanea 

Tags: ,

Makefiles are the granddaddy of build systems. Though falling out of favour relative to more modern systems like SCons and ant, make is still the lingua franca of software builds, particularly in the C and C++ parts of the open source world. Because of this, it is imperative to have at least a basic understanding of makefiles and their use.

There are plenty of tutorials introducing the fundamentals of makefile syntax, and a handful that show off some advanced features. There are very few, however, that actually show how to write a useful makefile, or that introduce makefile conventions and patterns. For me, this meant that writing makefiles became an arduous process of stringing together snippets from various places, and hoping they interoperated harmoniously. Frustratingly, I'd often learn of a new feature months later and rip out half of the file and replace it with a single line. Worst of all, I had no idea if what I was doing was conventional or even passable as a serious makefile.

I therefore want to put out this guide to basic makefile usage and conventions, and in the process, develop a basic makefile template that can be used for most small projects or as a starting point for more elaborate build systems. The resulting makefile will also roughly adhere to the GNU makefile conventions, but only where it makes sense for a small project and where support is not too onerous. For the purposes of the guide, we'll be writing a makefile for a C program, but the ideas are easily applicable to other languages. So if you'll oblige me by firing up your text editors, I'll get started.

Build Variables

At the top of our makefile, we will want to declare the variables used in the build process. Keeping everything in variables allows for easy modification of multiple build rules at once, as well as exporting variables from higher-level makefiles in the case of recursive builds (ones where this makefile is just building a particular component). For portability, we can start out by declaring our SHELL and compiler. These two variables are among Make's many special names, and are used implicitly in certain situations, so it is good practice to specify them. We can do so with the following snippet:


SHELL = /bin/sh
CC    = gcc

Next we'll define variables for the actual compiler flags used for building. My personal system is to break these up into four parts: FLAGS, used for mandatory flags without which the project will not build, RELEASEFLAGS and DEBUGFLAGS, for public release and debugging flags, respectively, and CFLAGS for user-defined C compiler settings. This last one is a standard that some users like to define for themselves, and so it should always use that name. I use it for things that are not essential but that I would always like to have in place when building. For this makefile, I've defined these variables as follows:


FLAGS        = -std=gnu99 -Iinclude
CFLAGS       = -pedantic -Wall -Wextra -march=native -ggdb3
DEBUGFLAGS   = -O0 -D _DEBUG
RELEASEFLAGS = -O2 -D NDEBUG -combine -fwhole-program

You can see above the usage of the different variables. Without the FLAGS settings (which specify to use the GNU variant of the C99 standard, and to look for #included files in the include directory, respectively) the hypothetical code would likely not compile correctly (logically, we assume that this hypothetical code does in fact keep header files in that location, and does make use of C99 extensions). The debug and release flags, on the other hand, contain various optimization directives and declarations: the NDEBUG definition causes assert()s to be taken out (among other things); the combine and fwhole-program flags instruct GCC to assume that the files it is working on comprise the whole program, and to optimize accordingly (this only works for C at present); and the O number specifies the level of optimization to apply. Finally, CFLAGS holds user-optional choices, as promised. In this example, I have chosen to make the compiler very strict about errors and warnings (pedantic, Wall, Wextra), instructed it to tune the output program for my specific machine architecture, and finally asked for the inclusion of copious amounts of GDB-specific debugger information. For maximum portability you should not assume GDB, but in practice it is fine for me.

Now let's define some variables to hold important files related to the build. We'll need the name of the program we're building, which I've called foomatic-widget, a list of source files, header files, common headers on which all files depend, and the object files that our sources will compile to. The application name and common headers we can just specify, but keeping track of all our source and header files could be a pain. I've therefore used a Make feature where we can call out to the shell, in this case to get a list of all files ending in .c and .h. Likewise, the list of object files is built by taking all the source files, and replacing their extensions with .o. This all looks like this:


TARGET  = foomatic-widget
SOURCES = $(shell echo src/*.c)
COMMON  = include/definitions.h include/debug.h
HEADERS = $(shell echo include/*.h)
OBJECTS = $(SOURCES:.c=.o)

Finally, we define some paths used for installing our program in a more permanent fashion. By convention, the DESTDIR variable is used, even though we don't declare it, as this allows the user to test installation to any directory by specifying a DESTDIR on the command-line. These variables are defined this way:


PREFIX = $(DESTDIR)/usr/local
BINDIR = $(PREFIX)/bin

Build Targets

Now we get on to the main business of Make: building things. Make uses the concept of targets to represent sets of instructions that you want it to run. The first target listed is the default one used if Make is invoked without specifying a target. Otherwise, you can run a different one with make targetname. By convention, the all target builds the project fully, and is the default. Targets can also have prerequisites: targets that will be processed prior to the current one, or files that, if changed, will cause the target to be rerun. If the target itself is a file, Make intelligently determines whether it needs to be rerun based on its prerequisites' times of last modification.

I typically just make all depend on the actual name of the executable, defined in $(TARGET) above. This ensures that the executable is built if you simply run Make. Optionally, you can also define other targets as prerequisites; for example, I often include a run of the indent or cppcheck utilities, depending on the nature of the project.

Now we have to let Make know how to build the $(TARGET) that all depends on. We do this by defining it as a new target, which depends on the $(OBJECT) files of each component, as well as the common headers. This target, however, actually contains a rule on how to build it, which will be run when all the prerequisites have been satisfied. This rule is on a new line, and must be indented with tabs. This is a common pitfall, though many editors will make sure that you don't accidentally use spaces here unless you really want to. The rule simply consists of a call to our compiler, defined above, with all the flags that we also defined, and a list of the object files to link. The first two rules then look like this:


all: $(TARGET)
 
$(TARGET): $(OBJECTS) $(COMMON)
  $(CC) $(FLAGS) $(CFLAGS) $(DEBUGFLAGS) -o $(TARGET) $(OBJECTS)

Now, you may have noticed that we're building with the debug settings. How then, do you produce something for day-to-day usage? Why, with another target, invoked with make release and looking like this:

 
release: $(SOURCES) $(HEADERS) $(COMMON)
  $(CC) $(FLAGS) $(CFLAGS) $(RELEASEFLAGS) -o $(TARGET) $(SOURCES)

You may also, later in the development cycle, wish to compile your program with profiling information. The way I've implemented this functionality is with another feature of Make, namely modifying variables. The first target below causes the CFLAGS variable to include a profiling option, and then the actual target causes the application to be built with the new set of flags.

 
profile: CFLAGS += -pg
profile: $(TARGET)

Administrative Targets

We should also define some administrative targets, which will let us move files around or remove them as needed. A subset of the ones suggested by the GNU Makefile conventions are below:


install: release
  install -D $(TARGET) $(BINDIR)/$(TARGET)
 
install-strip: release
  install -D -s $(TARGET) $(BINDIR)/$(TARGET)
 
uninstall:
  -rm $(BINDIR)/$(TARGET)

clean:
  -rm -f $(OBJECTS)
  -rm -f gmon.out
 
distclean: clean
  -rm -f $(TARGET)

The install and install-strip targets provide us with a mechanism to put our final built binary in some appropriate path, as defined in the environment variables above, and using the standard install utility (the naming is a bit confusing: we have both an install Make target and a system utility). The latter option strips debugging symbols from the binary in the process. Both targets depend on the release target, so we can expect that to be built as per the process described above. Uninstall provides the reverse functionality.

The two cleaning-related options are also standard; they differ only in that distclean restores the directory to the pristine state it would be distributed in, i.e. the compiled binary is also removed. The commands in these targets are preceded by a minus sign, telling Make to continue even if the command yields an error (like if the files don't exist).

With these targets in place, we should also take a moment to consider what would happen if we were to actually create a file named, for example, release or install. Make would start deciding whether to run these targets based on the freshness of those files — clearly not the behaviour we want. We can work around this by defining these targets as PHONY, which tells make to always execute them (solving our problem) and to not bother searching for prerequisites (slightly improving performance). We do this as follows:


.PHONY : all profile release \
  install install-strip uninstall clean distclean

Objects

Our application target above depends on a whole bunch of object files. We could list them all individually, or we could allow Make to build them implicitly (it's pretty smart and can mostly figure it out), but we can do even better. We can define a wildcard rule that will match all object files, and build them just the way we want. We could also define one or two object files individually, if they were special cases for some reason.

This wildcard rule makes use of a few special variables. The first one you'll see is %.o. That is the actual wildcard that matches object files. We can use a similar syntax to make it depend on the right source file as a prerequisite. We also need to know about the $@ and $< variables, which refer to the current target and the first prerequisite, respectively. The rule can then be built like this:

 
%.o: %.c $(HEADERS) $(COMMON)
  $(CC) $(FLAGS) $(CFLAGS) $(DEBUGFLAGS) -c -o $@ $<

You may have noticed that the above rule has all header files as a prerequisite. This is to be on the safe side, in case other parts of the program that are relevant to that file were changed. Depending on the size of your project, that may represent a significant amount of time wasted needlessly. If you're not averse to some really gruesome syntax, and want to rectify the problem, and if you're using GNU Make only, you can do better.

Using a feature of GNU Make known as second expansion, you can dynamically determine the specific headers to care about by calling out to GCC with the -MM option, which makes it list the headers included by a particular file. Second expansion allows us to evaluate variables a second time, later on in their lifecycle, where the surrounding context may have changed. For details on the deep magic going on here, consult the actual manual, but you should be able to get a rough idea of what's going on from the following implementation:


.SECONDEXPANSION:
 
$(foreach OBJ,$(OBJECTS),$(eval $(OBJ)_DEPS = $(shell gcc -MM $(OBJ:.o=.c) | sed s/.*://)))
%.o: %.c $$($$@_DEPS)
  $(CC) $(FLAGS) $(CFLAGS) $(DEBUGFLAGS) -c -o $@ $<

The final product

Our shiny new makefile is reproduced below in its entirety:


SHELL = /bin/sh
CC    = gcc
 
FLAGS        = -std=gnu99 -Iinclude
CFLAGS       = -pedantic -Wall -Wextra -march=native -ggdb3
DEBUGFLAGS   = -O0 -D _DEBUG
RELEASEFLAGS = -O2 -D NDEBUG -combine -fwhole-program
 
TARGET  = foomatic-widget
SOURCES = $(shell echo src/*.c)
COMMON  = include/definitions.h include/debug.h
HEADERS = $(shell echo include/*.h)
OBJECTS = $(SOURCES:.c=.o)
 
PREFIX = $(DESTDIR)/usr/local
BINDIR = $(PREFIX)/bin
 
 
all: $(TARGET)
 
$(TARGET): $(OBJECTS) $(COMMON)
  $(CC) $(FLAGS) $(CFLAGS) $(DEBUGFLAGS) -o $(TARGET) $(OBJECTS)

release: $(SOURCES) $(HEADERS) $(COMMON)
  $(CC) $(FLAGS) $(CFLAGS) $(RELEASEFLAGS) -o $(TARGET) $(SOURCES)

profile: CFLAGS += -pg
profile: $(TARGET)
 
 
install: release
  install -D $(TARGET) $(BINDIR)/$(TARGET)
 
install-strip: release
  install -D -s $(TARGET) $(BINDIR)/$(TARGET)
 
uninstall:
  -rm $(BINDIR)/$(TARGET)
 
 
clean:
  -rm -f $(OBJECTS)
  -rm -f gmon.out
 
distclean: clean
  -rm -f $(TARGET)
 
 
.SECONDEXPANSION:
 
$(foreach OBJ,$(OBJECTS),$(eval $(OBJ)_DEPS = $(shell gcc -MM $(OBJ:.o=.c) | sed s/.*://)))
%.o: %.c $$($$@_DEPS)
  $(CC) $(FLAGS) $(CFLAGS) $(DEBUGFLAGS) -c -o $@ $<
 
# %.o: %.c $(HEADERS) $(COMMON)
#    $(CC) $(FLAGS) $(CFLAGS) $(DEBUGFLAGS) -c -o $@ $<
 
 
.PHONY : all profile release \
  install install-strip uninstall clean distclean

For more detailed documentation, consult the GNU Make Manual or the GNU Makefile Conventions document.


blog comments powered by Disqus