profile for Kells1986 at Stack Overflow, Q&A for professional and enthusiast programmers

Tuesday, 17 September 2013

Makefile Tutorial

Introduction


I thought I'd write a brief tutorial about Makefiles, as this was a subject I had little understanding of while at university, and it wasn't until I began my current job that I realised how powerful they were.

The main use of a Makefile is to simplify the process of compiling large multi-file projects. However, this is not their only use. They can also be used in a similar way to shell scripts, to perform other tasks for you.

Makefiles generally consist of targets, dependencies, commands and variables. A Makefile is generally in the following format

VARIABLE=something 

target: dependency1 dependency2...
        command to make target

Note: There must be a tab before the command! Spaces are not adequate, you will get mysterious errors if you don't include a tab.

Create a fresh directory somewhere and create a file called "Makefile", then copy in the following commands:

NAME=James

all: hello myname

hello:
echo "Hello"

myname:
echo "My Name Is "$(NAME)

mynamedep: hello myname

echo "This was written using dependencies"

Now, if you run make you will see the commands in the Makefile and the result of their execution. There are three rules here, one for the target all, one to target hello, and one to target myname. You can run make and supply a specific target to just run that command, for instance running make myname will execute the rule to the target myname. If you don't specify a target then it will run for target all. If you don't specify a target called all, it will work on the target you specify at the top of the Makefile. 

There are two targets that make use of dependencies, these are all,and mynamedep. You can list a dependency next to a target and make will check if the dependency target needs to be built before it can continue. This comes in useful for compiling later, the above example is a little contrived.

You will notice that there is also a variable being used, much like you might see in a shell script, however in Makefiles these variables are braced in parentheses when they are referenced. I'll talk more about variables later.

Compiling

Suppose that you had two files, main.c and functions.c. The first file main.c contains only the "main" function, but inside main, functions from functions.c are called. How would we write a Makefile for that?

Well we need to compile functions.c to get our object file functions.o, and then compile main.c to generate main.o, and finally we need to link functions.o and main.o to create ourprogram.

A really simple file would look like this:

#ourprogram needs main.o and functions.o
ourprogram: main.o functions.o
        gcc main.o functions.o -o ourprogram

#main.o is the target and needs main.c and functions.o
main.o: main.c functions.o
        gcc -c main.c

#functions.o is the target and depends on functions.c
functions.o: functions.c
        gcc -c functions.c

The above Makefile would work just fine, when we call "make" our code would build, assuming there were no compiler errors. However, if we wanted to tweak this Makefile it would be kind of laborious - say we wanted to use a different compiler we would have to find and replace the instances of gcc.

We could use variables to make this easier to maintain:

CC=gcc
EXEC=ourprogram

#ourprogram needs main.o and functions.o
$(EXEC): main.o functions.o
        $(CC) main.o functions.o -o $(EXEC)

#main.o is the target and needs main.c and functions.o
main.o: main.c functions.o
        $(CC) -c main.c

#functions.o is the target and depends on functions.c
functions.o: functions.c
        $(CC) -c functions.c 

Now if we want to change the name of our program we can change the variable EXEC, or we can change the compiler by just editing the CC variable. We could also have written the line $(CC) main.o functions.o -o $(EXEC) as $(CC) main.o functions.o -o $@. The $@ variable is a special variable that references the target name.

We can also store more than one thing in a variable, and make will iterate through. For example, we could store all of the objects we need in a variable:

CC=gcc
EXEC=ourprogram
OBJECTS=main.o functions.o

#ourprogram needs main.o and functions.o
$(EXEC): $(OBJECTS)
        $(CC) $(OBJECTS) -o $@

#main.o is the target and needs main.c and functions.o
main.o: main.c functions.o
        $(CC) -c main.c

#functions.o is the target and depends on functions.c
functions.o: functions.c
        $(CC) -c functions.c 

Now we are beginning to see how Makefiles are so powerful. They can do other really cool stuff like working out which file to compile to generate the correct object. This means that we can replace the final two rules with a single rule, if we add another variable containing our source files, and one more containing compiler flags:

 CC=gcc
EXEC=ourprogram
OBJECTS=main.o functions.o
SOURCES=main.c functions.c
CFLAGS=-c

$(EXEC): $(OBJECTS)
        $(CC) $(OBJECTS) -o $(@)

$(OBJECTS): $(SOURCES)
    $(CC) $(CFLAGS) $*.c -o $@

There we have a complete Makefile that can easily be extended to incorporate more files, change the name of the executable, change the compiler, or change the compiler flags.

I hope that this has been a comprehendible tutorial and that you've managed to follow along. I remember when I was new to Linux that tutorials could quickly go out of my depth and look a little daunting. There are plenty more resources for learning about Makefiles, but there is one book in particular I'd recommend if you are keen to learn more, 21st Century C

5 comments:

  1. My thoughts:

    - There is nothing special about the target named "all". The default target is the first target in the Makefile, no matter what it's called. "all" is just a common convention.

    - This *really* should mention how critical it is that the lines with commands start with a tab - and that it be a genuine tab, and not a bunch of spaces that certain editor configurations might decide to replace tabs with.

    - It would be good to note that there are implicit rules - even just the one for making .o files from .c files (by compiling with $(CC) and also passing -c and $(CFLAGS)), which is the best known one, can save you a lot of typing (and distraction from the important parts of the Makefile - the dependencies - resulting in ease of maintenance). That last Makefile will work just fine without the definitions of SOURCES or CFLAGS, or any part of the rule for making $(OBJECTS) from $(SOURCES). (And -c isn't really something that should go into CFLAGS anyway, as it's specific to a build process that goes via .o files.) Try typing "make foo.o" in a directory containing "foo.c" without a Makefile present.

    One does not experience the true power of makefiles until one takes advantage of implicit and template rules. "make -p" shows the full resultant set of make rules, and typing it in a directory without a Makefile will show only the implicit ones.

    ReplyDelete
  2. Your comments are of course all valid, however the target audience I had in mind was students/beginners who may have never written a Makefile before. As such I tried to make the steps I took as clear and verbose as possible. To do this some of the 'magic' that make performs as standard is included.

    ReplyDelete
  3. Good article, but as I was reading it I had the same thoughts as the first two points that were made by Anonymous.

    make is notoriously pissy about tabs, and gives fairly useless error messages or unexplainable behaviour if you use spaces instead. There's also no good reason for the tab requirement other than an initial bad decision and tradition. I'd have appreciated a big blinking warning about this when I started with makefiles.

    You could replace "If you don't specify a target then it will run for target all." with "If you don't specify a target then it will run for the first target that appears in your makefile, which traditionally named 'all'." It's no more confusing, and a bit more accurate.

    But I agree with you about explaining the default rules -- no need to confuse beginners with it.

    Hope you do more in this series.

    ReplyDelete
    Replies
    1. Thanks, I've edited it a little as I spotted a couple of typos, I've tried to address the tab issue and the all target more clearly.

      Delete
  4. Very nice tutorial. Your last iteration doesn't get the dependencies right though. If you build from scratch and then edit main.c only and re-build, both function.o and main.o get rebuilt even though only main.o needs to be rebuilt. If instead you use a pattern matching rule like this...

    %.o: %.c
    [TAB]$(CC) -c $< -o $@

    ... then only the correct file gets rebuilt.

    As an earlier comment points out, there is a default rule that does pretty much the same thing so you could delete this altogether but if you want to avoid relying on predefined "magic" this rule should work and also get the dependencies right.

    ReplyDelete