Creating GNU Makefiles

For me, makefiles are a love-hate relationship. I pretty much hate their syntax, but they are also ever-so powerful. The basis of make is to use targets and dependencies, a makefile can be written in any order so it is not procedural like a scripting language. Instead to follow a makefile, you have to start at the end target and work your way back towards the beginning to know what make is going to achieve first. Make starts at the end and works out what it has to do in order to achieve completing a target.

It’s very important to note early on that a makefile MUST have TAB characters in it (before commands). You cannot substitute spaces for TABs, they must be real tabs or else the makefile will not work.

So a makefile is made up of targets and dependencies, and commands:

[source]target: dependencies
[TAB]command[/source]

Again, the TAB is important before the command!

Dependencies are optional for a target. Some targets merely execute a commant and this is the simplest target. If we start with a helloworld example using the simplest target type (just a command), we end up with a makefile that looks as simple as this:

[source]all:
gcc helloworld.c -o helloworld.exe[/source]

You can test this makefile out. Generate the file above (remember that TAB!) and save it as Makefile. Then run the following command in the same directory:

[source]make all[/source]

Assuming that you have a correctly setup gcc environment and the helloworld.c example code can compile, you’ll end up with your target created as helloworld.exe.

If you’re running on Linux, don’t fear the .exe file extension – it’s not going to effect you. It’s only a filename!

The example above works for small projects, but in essence you could do the same inside a batch file or bash script very easily and achieve the same thing. Makefile power comes from it’s dependencies handling.

To see how the target and dependecies work we need to write another makefile example which is more long-hand than before to achieve the same result. It does however show exactly how make works backwards to work out what it has to do.

In the command line above, we use GCC to compile and link an executable from a c source file. However, gcc can be used in a way which means these tasks get done separately. First we want to create a compiled object file from each c source file and then we want to link all of the object files into a final executable

So off we go again, working backwards. It is important to note here that all is a very common make target that compiles everything the makefile knows how to compile (generally anyway, there maybe some other special targets). So starting at the end, we always start by generating a target all. Our helloworld example is just one executable – and later on we might want to generate other executables that are part of the same project – so we will make the target all depend on another target which is what generates our helloworld executable:

[source]all: helloworld[/source]

Now later on if we add another executable, we can just add it’s exe target as a dependency of all. So now we can create the helloworld executable link target. It is going to depend on all of the object files which are created from all of the c source files. So it needs a dependency for each of our object files:

[source]helloworld: helloworld.o
gcc helloworld.o -o helloworld.exe[/source]

The dependency above is pretty obvious as the target is the same as the filename we require in the command line. Without it, the command will obviously fail. So make knows before it runs the link command it must first satisfy the helloworld.o target, which we’ll see now:

[source]helloworld.o:
gcc -c helloworld.c[/source]

Finally we reach a target with no dependencies. This is where make will now start to run commands. It can now run the command associated with the target helloworld.o. This simply compiles the c source file into an object file without linking it.

Once make has run the command, and assuming that command run correctly, it will now step back to the helloworld target where it finds that it has satisfies all of the dependencies for the target and can now proceed to execute the command for that target which is the link stage for gcc to create the helloworld executable.

Now make steps back again and sees that it has satisied the target all which is the top level target we have asked it to produce. It can now exit and we’ll have the executable waiting for us.

Here’s that complete makefile:

[source]all: helloworld

helloworld: helloworld.o
gcc helloworld.o -o helloworld.exe

helloworld.o:
gcc -c helloworld.c[/source]

As our project expands, we need to add more files as the funcionality of our program has changed. Let’s add a couple more files to our example, which are really just phantom source files:

[source]all: helloworld

helloworld: helloworld.o functions1.o functions2.o
gcc helloworld.o functions1.o functions2.o -o helloworld.exe

helloworld.o:
gcc -c helloworld.c

functions1.o:
gcc -c functions1.c

functions2.o:
gcc -c functions2.c[/source]

It is proving quite a repetitive task to add new files to this makefile, and as with writing software we should now that common tasks like this should be boiled down to their minimum. Imagine now we want to add some options like -Wall to the c source file compiling targets, there’s a lot of copy and pasting going on!

Make can define and expand variables. For example, we are using GCC, but someone else might be using a different compiler. Most compilers have the same basic syntax, but the options may well be expressed differently. Let’s change the compiler to be a variable, and the c source file compile flags to a variable too. We can turn on warnings at the same time:

[source]# Finally a makefile with some comments!
# Set this to your preferred compiler executable:
CC=gcc

# Include the options here for compiling a c source file to an object file:
CFLAGS=-c -Wall

# The main target "all" – this is a very common target in makefiles. Almost all
# makefiles have an all target
all: helloworld

# Executable link – use the c compiler as the linker
helloworld: helloworld.o functions1.o functions2.o
$(CC) helloworld.o functions1.o functions2.o -o helloworld.exe

helloworld.o:
$(CC) $(CFLAGS) helloworld.c

functions1.o:
$(CC) $(CFLAGS) functions1.c

functions2.o:
$(CC) $(CFLAGS) functions2.c[/source]

So that saved us some grief, but honestly this is still not looking good. Changing our projects source files means editing a lot of the makefile and repeating the same thing all over the makefile. It’s labourious!

Now we’ll start to explore the true power of makefiles as we understand the very basics of what they are about and how make goes about evaluating simple target and dependency lists.

As we can make variables which simple expand to strings, we would like to head to the situation where source files wouldn’t have to be explicitly listed as separate targets. The answer to this is generally achieved using Automatic Variables (http://www.gnu.org/software/make/manual/make.html#Automatic-Variables) in make.

We should investigate Automatic Variables a bit to see what we can do with them, and how they work. The make manual can be a pretty tough read, and experimenting shows you a lot about the behaviour. If something doesn’t make sense, look in the manual to see if there is anything mentioned about what it is you’re trying to do.

Make is clever in that you can provide a standard procedure for making a file with a particular extension. This rule is then applied to any file that doesn’t have an explicit rule to create it.

The simple standard procedure is written by starting a target with a period ‘.’ – let’s change our current file so that we can do one standard procedure rule. We’ll make use of an automatic variable that allows the rule to substitute in the dependant filename:

[source]# Set this to your preferred compiler executable:
CC=gcc

# Include the options here for compiling a c source file to an object file:
CFLAGS=-c -Wall

# The main target "all" – this is a very common target in makefiles. Almost all
# makefiles have an all target
all: helloworld

# Executable link – use the c compiler as the linker
helloworld: helloworld.o functions1.o functions2.o
$(CC) helloworld.o functions1.o functions2.o -o helloworld.exe

.c.o:
$(CC) $(CFLAGS) $<[/source]

Here, we’ve introduced the .c.o target, it is known as an inference rule. As the helloworld (executable) has a dependency on all of the object files listed, it uses the standard procedure rule to build them all.

The GNU make manual has the following cryptic message about $<

$<
The name of the first prerequisite. If the target got its recipe from an implicit rule, this will be the first prerequisite added by the implicit rule (see Implicit Rules).

So now we have a rule that is common for building all of our c sources. But we will need to list the object files, and we need to list them twice! So now it would be best to be able to use another variable to list the objects:

[source]# List the object files here!
OBJS = helloworld.o \
functions1.o \
functions2.o

# Set this to your preferred compiler executable:
CC=gcc

# Include the options here for compiling a c source file to an object file:
CFLAGS=-c -Wall

# The main target "all" – this is a very common target in makefiles. Almost all
# makefiles have an all target
all: helloworld

# Executable link – use the c compiler as the linker
helloworld: $(OBJS)
$(CC) $(OBJS) -o helloworld.exe

.c.o:
$(CC) $(CFLAGS) $<[/source]

But there’s more to programming that just C. What if we’ve got other files which are c++ files? We can quickly define a rule for building those sources and include the c++ compiler:

[source]# List the object files here!
OBJS = helloworld.o \
functions1.o \
functions2.o \
functions3.o

# Set this to your preferred compiler executable:
CC=gcc
CPP=g++

# Include the options here for compiling a c source file to an object file:
CFLAGS=-c -Wall
CPPFLAGS=-c -Wall

# The main target "all" – this is a very common target in makefiles. Almost all
# makefiles have an all target
all: helloworld

# Executable link – use the c compiler as the linker
helloworld: $(OBJS)
$(CC) $(OBJS) -o helloworld.exe

.c.o:
$(CC) $(CFLAGS) $<

.cpp.o:
$(CPP) $(CPPFLAGS) $<[/source]

With a modified helloworld now:

[c]#include <stdio.h>
#include <stdlib.h>

extern char* FN1_GetName(void);
extern char* FN2_GetName(void);
extern char* FN3_GetName(void);

int main(int argc, char* argv[])
{
printf( "Hello World!\n"
"Using %s\n"
"Using %s\n"
"Using %s\n",
FN1_GetName(),
FN2_GetName(),
FN3_GetName() );

return EXIT_SUCCESS;
}[/c]

We must make sure here that everything in the C++ file that is going to be called by our C code is compiled for calling from C. This requires function3.cpp to look like:

[c]#include <cstdio>

extern "C" char* FN3_GetName(void);

char* FN3_GetName(void)
{
return "functions3.cpp";
}[/c]

We include <cstdio> because if the C compiler was used to compile this code, this would make it trip up – so we definitely know that the C++ compiler is being used to compile this code via our .cpp.o inference rule.

Don’t forget you’ll need to create a “clean” target which removes all of the object files too. You might also want to list the sources instead of the objects as that makes a lot more sense!

In order to list sources instead of objects, we can make use of makes text substitution rules:

[source]# List the object files here!
CSRC = helloworld.c \
functions1.c \
functions2.c

CPPSRC = functions3.cpp

OBJS = $(CSRC:.c=.o) $(CPPSRC:.cpp=.o)[/source]

Perhaps we’ll get on to sub-directories in make-102!

Leave a Reply