Skip to content

Makefile

Getting started

Books

References


Makefiles by example

This tutorial was adapted from Bruce A. Maxwell's A Simple Makefile Tutorial.

A Simple Example

Let's start off with the following three files: - hellomake.c represents a typical main program, - hellofunc.c represents a some functional code, and - hellomake.h represents the include file corresponding to hellofunc.c.

Here is the content of the files:

  • hellomake.c

    #include <hellofunc.h>
    
    int main() {
      /* call a function in another file */
      apply_pythagorean_theorem(3.0, 4.0);
    
      return(0);
    }
    

  • hellofunc.c

    #include <hellofunc.h>
    #include <stdio.h>
    #include <math.h>
    
    float apply_pythagorean_theorem(float a, float b) {
      printf("Input: a=%g, b=%g\n", a, b);
      printf("Pythagorean theorem: c=%g\n", sqrt(a*a + b*b));
    }
    

  • hellofunc.h

    /* example include file */
    float apply_pythagorean_theorem(float, float);
    

Normally, you would compile this collection of code by executing the following command:

gcc -I. -o hellomake hellomake.c hellofunc.c -lm
This command compiles the two .c files and names the executable hellomake. The arguments of the command are - hellomake.c hellofunc.c are the source code files, - -o hellomake is the output/executable file that will be created, - -I. tells gcc to look in the current directory (.) for include files, namely our file hellomake.h, - -lm tells gcc to link the executable with the math library.

The typical approach to the test/modify/debug (without a Makefile) cycle is to use the Up arrow in a terminal to go back to your last compile command so you don't have to type it each time, especially once you've added a few more .c files to the mix.

Unfortunately, this approach to compilation has two downfalls. Problem #1, if you lose the compile command or switch computers you have to retype it from scratch, which is inefficient at best. Problem #2, if you are only making changes to one .c file, recompiling all of them every time is also time-consuming and inefficient. So, it's time to see what we can do with a Makefile.

Defining rules

The key functionality of Makefiles are rules. A rule consists of - a name followed by a : colon, - dependencies (files to monitor for changes) after the : colon, and - lines of commands (as we would type in the terminal).

Abstract rule:

[RULE_NAME]: [DEPENDENCY_FILE_1] [DEPENDENCY_FILE_2] [...]
    [COMMAND_1, after a Tab]
    [COMMAND_2, after a Tab]
    [...]

Important: There must be a Tab at the beginning of a command!

Note that there is a Tab before every command in the Makefile that is part of a rule. make will not be happy if is missing or replaced by Space characters.

The first Makefile

The simplest Makefile would look something like this.

Makefile v1:

hellomake: hellomake.c hellofunc.c
    gcc -I. -o hellomake hellomake.c hellofunc.c -lm

If you put this rule into a file called Makefile (or makefile is ok, too) and then type make on the command line it will execute the compile command as you have written it in the Makefile.

Note that make with no arguments executes the first rule in the file. Furthermore, by putting the list of files on which the rule depends on after the : colon, make knows that the rule hellomake needs to be executed if any of those dependency files change.

Hands-on: Run make again and it will know not to recompile. Change one of the .c files and make will recognize the need to recompile. (Unfortunately, if you were to make a change to the header file, hellomake.h, make would not recompile the .c files, even though they needed to be. It will be fixed later.)

Note: To force a recompilation, you must delete the executable manually:

rm hellomake

At this point, we solved problem #1 and we can avoid using the Up arrow repeatedly, looking for the last compile command.

Hands-on: Practice inventing some of rules on your own and putting (harmless) terminal commands to be executed, for example

where_am_i:
    pwd
then execute this rule in the terminal:
make where_am_i

Separate compilation commands for separate source files

To make the compilation more efficient, in terms of compiling source files separately and only if they have been changed.

Makefile v2:

# `make` will run the first rule by default
hellomake: hellomake.o hellofunc.o
    gcc -o hellomake hellomake.o hellofunc.o -lm

hellofunc.o: hellofunc.c hellofunc.h
    gcc -I. -c -o hellofunc.o hellofunc.c

hellomake.o: hellomake.c
    gcc -I. -c -o hellomake.o hellomake.c

By putting the object files, hellomake.o and hellofunc.o in the dependency list and in the rule, make knows it must first compile the .c versions individually, and then build the executable hellomake.

Hands-on: Change one of the .c files and make will recognize the need to recompile only that one file.

Note: To force a recompilation, you must delete all compiled files manually (we will address that later):

rm *.o hellomake

Using variables

In order to make the implementation of the Makefile more efficient, we define variables.

To define a variable, we write

VARIABLE_NAME=DEFINE_SOMETHING
and to use a variable, we enclose the variable name:
$(VARIABLE_NAME)

Special variables

Additionally, the Makefile syntax provides predefined variables. These are special variables: - $@ refers to the target name (left side of the : colon), - $^ refers to the list of dependencies (right side of the : colon), and - $< refers to only the first dependency (first item on right side of the : colon).

These variables make the overall compilation rule more general or generic.

Let's adapt the previous Makefile.

Makefile v3:

# set compiler
CC=gcc
# set compiler flags
CFLAGS=-I.
# set library flags
LIBS=-lm

# `make` will run the first rule by default
hellomake: hellomake.o hellofunc.o
    $(CC) -o $@ $^ $(LIBS)

hellofunc.o: hellofunc.c hellofunc.h
    $(CC) $(CFLAGS) -c -o $@ $<

hellomake.o: hellomake.c
    $(CC) $(CFLAGS) -c -o $@ $<

This Makefile works identically to the previous one, but it is much cleaner and simpler to modify and generalize.

Pattern rules

Similarly to predefined, or special, variables there are predefined patterns for rules. A pattern rule looks like an ordinary rule, except that its target contains exactly one % percent character, for instance, - %.c matches any file name that ends with .c - %.o matches any file name that ends with .o

The pattern rules allow to reduce the number of rules dramatically.

Makefile v4:

# set compiler
CC=gcc
# set compiler flags
CFLAGS=-I.
# set library flags
LIBS=-lm

# `make` will run the first rule by default
hellomake: hellomake.o hellofunc.o
    $(CC) -o $@ $^ $(LIBS)

%.o: %.c hellofunc.h
    $(CC) $(CFLAGS) -c -o $@ $<

Specifying object and header files in variables

To improve the structure of the implementation of our Makefile, we create the variable OBJS that stores all of the object files that we need to generate. And we create the variable DEPS that stores the set of all .h files on which the .c files depend.

Finally, let's also store the program in a variable.

Makefile v5:

# set compiler
CC=gcc
# set compiler flags
CFLAGS=-I.
# set library flags
LIBS=-lm

# set the program name
PROGRAM=hellomake
# list object files
OBJS = hellomake.o hellofunc.o
# list header files
DEPS = hellomake.h

# `make` will run the first rule by default
$(PROGRAM): $(OBJS)
    $(CC) -o $@ $^ $(LIBS)

%.o: %.c $(DEPS)
    $(CC) $(CFLAGS) -c -o $@ $<

Notice that the two rules are entirely generic and can work for most projects.

Additional rules

Since a large project will create many .o files and possibly other files, it is also very valuable to create a rule to clean up (i.e., remove) the build files. This can be done with additional rules.

Another very common rule that is put as the first rule, is called all. This specifies what the default behavior of make should be.

The rules that we will add next are not related to any files, so we need to tell make to not confuse the rules with files named like them. That is done with .PHONY; see in the example.

Makefile v6:

# set compiler
CC=gcc
# set remove command
RM=rm
# set compiler flags
CFLAGS=-I.
# set library flags
LIBS=-lm

# set the program name
PROGRAM=hellomake
# list object files
OBJS = hellomake.o hellofunc.o
# list header files
DEPS = hellomake.h

.PHONY: all
all: $(PROGRAM)

$(PROGRAM): $(OBJS)
    $(CC) -o $@ $^ $(LIBS)

%.o: %.c $(DEPS)
    $(CC) $(CFLAGS) -c -o $@ $<

.PHONY: clean
clean:
    $(RM) $(OBJS)
    $(RM) $(PROGRAM)

Hands-on: Type make clean to execute the clean rule of the Makefile.


References