Make


Compilation by hand

Let's say we have a single C source file foo.c:


      
      

The typical way to compile the file to an executable, foo is:


      gcc -o foo foo.c
      

To be more precise, the above invocation of gcc invokes the preprocessor to process the #include, #define and other such directives, compiles foo.c to an object file, and then links this object file with a few small stub objects to produce the final executable, foo.

We can additionally specify a few flags to make the compiler stricter. A common set of flags is:


      gcc -o foo -Wall -Wextra -Werror foo.c
      

Now, the compilation aborts, as the -Wall option causes the compiler to warn about unused variables, with the -Werror converting the warnings to errors:


      gcc -o foo -Wall -Wextra -Werror foo.c
      (out) foo.c: In function ‘main’:
      (out) foo.c:16:10: error: unused parameter ‘argc’ [-Werror=unused-parameter]
      (out)  main(int argc, char *argv[])
      (out)           ^~~~
      (out) foo.c:16:22: error: unused parameter ‘argv’ [-Werror=unused-parameter]
      (out) main(int argc, char *argv[])
      

We can easily fix the warnings by adding (void)argc; (void)argv; at the start of the main function; the following is the corrected code: foo.c

In more detail, the common -W (warn) flags are:

-Wall
Enable all the warnings about constructions that some users consider questionable, and that are easy to avoid (or modify to prevent the warning)
-Wextra
Enable some extra warning flags that are not enabled by -Wall
-Werror
Make all warnings into errors. With warnings, the compilation will proceed; with errors, the compilation aborts.

If we repeatedly make changes to foo.c (or if we compile several source files as part of our executable), then manually invoking the above gcc command is tiresome and error-prone. Hence, Makefiles.

A simple Makefile

A simple Makefile for compiling foo.c is:


      

In makefile terminology, the line foo: foo.c is a rule, where foo is the target and foo.c is the prerequisite.

Each rule then contains one or more commands, called the recipe (here, the gcc -o foo ... line), which specifies how to create the target from the prerequisites. Note that each line (command) of the recipe must start with a tab character.

When we enter the command make, make looks for a file named Makefile in the current directory. make then invokes a rule's recipe any time its target is out-of-date with respect to one of its prerequisites (in other words, any time we modify foo.c).

Using variables

We can generalize our Makefile a little by using variables. A variable simply stores a string of text that we can use in our rules.



      

Here, CFLAGS is a variable that holds our compilation flags. We access the value of the variable by using the syntax $(CFLAGS)

The $@ and $^ are automatic variables -- variables that make computes afresh for each rule that is executed, based on the target and prerequisites of the rule. Note that automatic variables can only be used in the recipe portion of a rule.

We use two common automatic variables in our rule above:

$@
The rule's target
@^
The names of all the rule's prerequisites

Adding a clean rule

It is good practice to include a special rule, conventionally called clean, to delete any build artifacts (in our running example, the only artifact is the executable that we build, foo).



      

As with any rule, we can explicitly invoke the clean rule by specifying the target's name as an argument to 'make': make clean. (By default, make without any arguments will try to build the first target in the Makefile (here, foo)).

Since there is no file named clean, make clean will always invoke the rule's recipe, hoping to create this very file, which of course, it never does. That's the trick.

However, there's a fly in the ointment: if by some strange chance we do have a file called clean in our directory, then make clean will never run; the rule has no prerequisites, and thus the clean file is always up-to-date.

To make our intention clear to make that clean is a fake rule, and not a file -- even if such a file called clean exists -- we add clean as a prerequisite of the special target, .PHONY. By doing so, make clean will run the recipe regardless of whether we have a file named clean.

Multiple source files

Let's say that we now have the following files:

a.c


      
b.c


      
foo.c


      
foo.h


      
bar.c


      
bar.h


      

In other words, a and b are two programs, each of which uses foo and bar.

Approach 1 (Simple but wasteful)

A simple Makefile for this project is:



      

Note that we create a PHONY target called all to represent both programs. While this Makefile is simple and easy to read, it has the drawback that foo.c and bar.c are compiled twice: once for a and once for b. For a small project, this is no big deal; for a large project, it can waste a lot of time.

Approach 2 (Efficient but verbose)

Rather than recompile foo.c and bar.c twice, we can create rules for compiling these to their respective .o files:



      

Note the use of the automatic variable $<, which represents a rule's first prerequisite.

This approach is more efficient, but there is a lot of redundancy in the rules. For a small number of source files, the redundancy is manageable; for larger projects, it becomes an obstacle to making changes.

Approach 3 (Efficient and succinct)

To remove some of the redundancy, we can use static pattern rules. Per the manual, "Static pattern rules are rules which specify multiple targets and construct the prerequisite names for each target based on the target name." The general form is


      targets ...: target-pattern: prereq-patterns ...:
      	recipe
        ...
      

Each target is matched against the target-pattern. The target-pattern uses the '%' character to capture a portion of the target name; this captured string is called the stem. The '%' character can then be used in the prereq-patterns to substitute the stem.

The following Makefile demonstrates the use of static pattern rules for our project. This new Makefile at first appears cryptic, but becomes easier to understand with practice. Also note the use of the variable $(CC) which is a predefined variable that expands to the system's default C compiler.




      

More information

For our course, the above example Makefiles will usually suffice. However, if you are interested in learning more, the official GNU Make Manual is an outstanding reference and a fairly quick read.

The following books are also helpful:

Managing project with GNU make, 3rd Edition
Meklenburg, et. al
O'Reilly, 2004, ISBN: 0-596-00610-1
(free online access via William & Mary library)

and

The GNU make book
John Graham-Cumming
No Starch Press, 2015 ISBN-13: 978-1-59327-649-2
(free online access via William & Mary library)