OMG WTF Makefiles!

If, like many developers, you haven't written a line of C or C++ since you left school, but suddenly find yourself needing to write in those languages, this article is for you. Or maybe you're in school, but you need to write a moderately complex program (more than one file). This article is also for you.

My Instructor Never Covered This

It's almost a given that your programming education didn't include information on creating Makefiles. If you learned to program in college there's a better than even chance that two things are true:

  • Your programs didn't have more than one or two files.
  • Your instructor didn't know how to write a Makefile either.

This isn't really the fault of your school. Formal education about computers tends to be about the scientific and mathematical aspects (i.e. actual Computer Science) or the engineering aspects of writing code (i.e. Software Engineering), which is going to focus on how to solve problems and high level concepts.

Makefiles aren't about either of those things. Makefiles are about automating the building of your software, which is very much about the business of getting software into the hands of users. It's not sexy, certainly won't win you the praise of your thesis advisor, and even among a group of fellow code slingers won't qualify as scintilating cocktail conversation.

What Do I Need

The first thing you're going to need is a version of make. The good news is that it tends to be installed with your compiler as part of your C and C++ software ecosystem. The bad news is that there are a lot of different versions of make.

  • OSX - GNU Make (just called make) is part of the command line development tools, which you can get by installing X-Code. Don't actually use X-Code for writing any of your software though. It will make you hate programming.
  • Linux - GNU Make (just called make) is part of the build core that you probably installed when you realized you needed to write C.
  • *BSD - You have BSD Make (just called make as well), which offers a different set of features than GNU Make.
  • Windows - The good news is that you have a lot of options to choose from. That's also the bad news. If you have Visual C++ installed, it also installed a program called nmake, which serves the same purpose as make but has a different syntax. If you install one of the mingw family of compilers, you'll have a mingw-make.exe file running around, which is GNU Make.

After that you'll also need a decent code text editor. Religious wars are fought over text editors, so I'll just say that you should use the one that lets you get work done. Atom with the Fairy Floss theme is a personal favorite.

What Does It Do?

Make is essentially shell scripting with a dependency manager built in. So if you can handle basic shell scripting, you can probably handle a Makefile.

A Makefile has four main parts:

  • Targets. These are the things you want to build. There will certainly be a target for the final product, and for intermediate things that you need to build the final product.
  • Dependencies. With each target there's also a list of the things that it depends on. They will be built first before you build the target.
  • Recipies. A recipe is a set of shell commands which will be used to assemble the dependencies into the target.
  • Magic Variables. So many magic variables, many of which are indistinguishable from line noise.

A Simple Program

Let's start with a dirt simple “Hello, World” program to demonstrate the simplest case.

#include <stdio.h>
#include <stdlib.h>
 
int main(int argc, char **argv) {
 
    printf("Hello, world.\n");
 
    return EXIT_SUCCESS;
}

There is also another file, Makefile (and the name must be a exactly that, including case, with no extension), which contains the instructions to build the program.

hello: hello.o
    $(CC) $^ -o $@

Already we're looking at line noise as valid syntax, so let's break this down:

hello: this is the target “hello,” and means that it indicates the dependencies and recipe to make a file called “hello.”

hello.o is a dependency of the hello target. If this file is changed, it is necessary to rebuild “hello.” This is also true if this file does not exist.

The lines below the target are the recipe. Each line of the recipe is indented by exactly one tab. The recipe continues until it runs out of tab indented lines. Please note that make is very particular about the tabs. It will not accept four spaces, or eight spaces. It must be the tab charcter (ASCII 9). No other indent will do. This means that you must use an editor capable of actually inserting tabs. Most code editors are aware of this requirement and will insert a tab if they are aware that they are working on a Makefile.

$(CC) is a magic variable. By convention this points to your C compiler (not your C++ compiler). Make will select a suitable default value, but you can change it if you need. You might do that if you are compiling for a platform other than the one you are building the program on, for instance.

$^ is another magic variable. It expands to the full list of the dependencies you listed for the target.

$@ is also magic. It expands to the name of the target. By convention the -o switch to a C compiler sets the name of the output file, so we're telling the compiler to produce the file “hello” (i.e. our target).

Running The Build

~$ make
cc    -c -o hello.o hello.c
cc hello.o -o hello
~$ 

That's odd: when I ran make I got two lines of output, one of which is clearly from the recipe I provided, but the first line doesn't seem to correspond to any recipe in the file. If I run the resulting program though:

~$ ./hello
Hello, world.
~$

So it's definitely my program, without any apparent extra stuff. So what gives?

Hidden Targets

Because make was created to make building C and C++ programs easier, it has a built in rule to compile C files to object files (i.e. they end in .o). You can influence how these files are compiled by setting another magic variable.

Let's say that you want your program compile with debugging symbols left in, just in case hello crashes and you need a backtrace. By convention the compiler flag for that is -g. To compile all of the .o files with debugging symbols, you would put the following into the Makefile:

CFLAGS=-g

This defines a variable CFLAGS which have a value of -g.

The secret recipe for turning a .c file into an .o file is something like:

$(CC) $(CFLAGS) -c -o file.o file.c

So whatever you assign to CFLAGS becomes part of the command line to the compiler which produces the .o files.

Programs With Many Files

In a program with many files, you would quickly get tired of putting all of the dependencies onto the same line as the target.

The quick way is to assign all of the object file names to a variable.

Let's say that our program has become something of a world traveller, and now also includes a few additional files:

hola.c
nihao.c
bonjour.c

We can add the objects to a variable:

OBJS=hola.o nihao.o bonjour.o

And now our Makefile looks like this:

CFLAGS=-g
OBJS=hello.o hola.o nihao.o bonjour.o
 
hello: $(OBJS)
        $(CC) $^ -o $@

Now if I run make I get the following output:

~$ make
cc -g   -c -o hello.o hello.c
cc -g   -c -o hola.o hola.c
cc -g   -c -o bonjour.o bonjour.c
cc -g   -c -o nihao.o nihao.c
cc hello.o hola.o bonjour.o nihao.o -o hello

You can see that our CFLAGS value is being added to the build. You can also see that each of the .o files which we included as dependencies is being built and then linked as part of the final step.

Compiler Tricks In Your Makefile

There are some features of your compiler which are probably not immediately obvious if you haven't built large projects before.

Include Paths

If you are using outside libraries, or even providing your own, you probably need to use .h files which are not part of your project or the standard system headers. If you're using the features of libframistan, which is installed in /opt/framistan and has the subfolders include and lib you'll want to tell your compiler about the include files.

You tell your compiler to use an additional directory for .h files with the -I flags. To to make use of the files in /opt/framistan/include you need to add -I/opt/framistan/include to every compiler invocation.

Much like we previously showed with the debug symbol flag -g, we can add this to the CFLAGS variable and the desired option will be used for compiling every file.

CFLAGS=-g -I/opt/framistan/include

External Libraries

If you're programming in C, there's a better than average chance that you're using an external library. For our mythical libframistan, the library file itself is named libframistan.a and it resides in /opt/framistan/lib.

To tell our compiler where to find a library that isn't in the standard places, we use the -L flags followed by the directory. So for our case the full option looks like -L/opt/framistan/lib.

To get the compiler to actually link the library, we need to provide an additional option for the library itself, -l with the library name. By convention the leading lib and everything after the first '.' character are understood to be part of the name, so the full option is -lframistan

Unlike the include directories, library options are only needed when building the final program, so they can just become part of the recipe, like this:

$(CC) $^ -o $@ -L/opt/framistan/lib -lframistan

That gets pretty tiresome pretty quickly, especially if you have more than one library, so it's common to assign that to a variable as well:

LIBS=-L/opt/framistan/lib -lframistan
 
$(CC) $^ -o $@ $(LIBS)

Additional Targets

So far I've shown just a single target in our Makefile. But most Makefiles in the real world have more than one target, because there's usually more than one task that you want to automate. It's often desirable to get rid of build artifacts, for instance. By convention the target to do this is called “clean.”

clean:
    rm -f *.o
    rm -f hello

Order Matters

Now here's a fun little tidbit. Depending on if you put this new target before or after the “hello” target we created earlier, when you type make you will get different results.

By default make builds the first target that it finds in the Makefile. When we had just one target, this was fine, because we didn't have any other targets to choose from. But now we have two different targets.

I usually prefer to have the default target that I'm going to build during my development process as the first target in the file, so I can just type make and the right thing happens.

Now I have three options for using my Makefile:

  • make builds the first target (in my Makefile this is “hello”).
  • make hello builds the “hello” target, which creates an executable called hello.
  • make clean builds the “clean” target, which deletes all of the object files (*.o) and the hello executable.

Further Reading

There's plenty more that we could go into about Makefiles, but this “short” article is already long enough.

The definitive resource on Makefiles, especially the GNU variety, is the GNU Make Manual. Fortunately it's full of examples of how to use some of the more complicated features.