Writing Makefiles

Alexander Neville

2024-01-08

Make is a utility designed to automatically rebuild parts of a program when its components are modified. Make refers to a user-defined makefile to identify the relationships between files. If the time stamp of a file is newer than that of another file that depends on it, Make will run the specified actions to rebuild the output from the specified inputs. Make is often used in the build process of a C program, but can be used to great effect automating tasks in which an output file depends on a number of other files and should be rebuilt when they change.

Rules

A makefile is composed of rules, amongst other directives. A rule has a target (which may be one or more files), an optional list of prerequisites and an optional recipe.

target: prerequisites
    recipe

Consequently, a rule can be said to set out when and how a target should be updated. By default, the first named target is built when make is invoked. This target is considered a goal. Make will try to build a goal by building its prerequisites, which may themselves be targets of another rule.

Explicit Rules

An explicit rule is as above. Explicit rules are not provided by Make by default, as is the case with implicit rules. An explicit rule may have more than one target, in which case the targets are treated as independent or grouped. A target may feature in multiple explicit rules, though preferably only one recipe is provided for a target.

Implicit Rules

Implicit rules capture typical use cases and define rules for a class or set of files, often those with a common extension. Make provides a catalogue of built-in implicit rules. It is possible create new implicit rules in addition to overriding and cancelling built-in implicit rules. The -r flag can be used to cancel all such built-in rules. The section of the Make manual page detailing the list of built-in implicit rules is here.

A common example of an implicit rule is the compilation of a C source file. In this example, the makefile only explicitly details how to create a target, called app from the prerequisite app.o. No rule is provided in this makefile for creating the object file.

app: app.o
    cc -o app app.o

Make recognises that object files are often generated from a correspondingly named .c source file and provides a recipe to build it.

$ ls
app.c  makefile
$ make
cc    -c -o app.o app.c
cc -o app app.o
$ ls
app  app.c  app.o  makefile

Pattern Rules

A pattern rule may be used to define a new implicit rule. A pattern rule contains exactly one % character in the target. The % character matches one or more characters. The string matched by the % expression is called the stem. Should % occur in the prerequisites, it is replaced by the stem from the target. A substitution does not need to occur in the prerequisites of a pattern-rule, nor does a pattern rule need to have any prerequisites or even a recipe. A pattern rule is likely to use an automatic variable in its recipe. In this case $@ matches the name of the target that caused the rule to be run and $< is the name of the first prerequisite.

%.o: %.c
    cc -c $< -o $@
app: app.o
    cc -o app app.o

This pattern rule overrides the implicit rule for C source file compilation.

Static Pattern Rules

Static pattern rules are analogous to implicit rules for a specified set of files. The syntax is targets: target-pattern: prerequisite-pattern; recipe. Here is an example, using automatic variables and static pattern matching to override the implicit rule for generating an object file for the files app.o and lib.o.

app: app.o lib.o
    cc -o $@ $^
app.o lib.o: %.o: %.c; cc -c -o $@ $<

Double-Colon Rules

An uncommon type of rule is the double-colon form, resembling target :: prerequisites ; recipe. A target must appear in only double-colon or only normal rules, not both. The target of such a rule is updated if any of its prerequisites are newer than it, but unusually a target without prerequisites is always updated. Many double-colon rules may exist for the same target, including a recipe. This rule is used in the rare case that the recipe to update a target depends on which prerequisites have changed.

$ ls
app.c  makefile
$ make
cc -c app.c -o app.o
cc -o app app.o

PHONY Targets

If the first named target happens to be a named action rather than a file to be built, this could cause some undesired behaviour. In the example given here, the clean target is written first.

clean:
    - rm -f target
    - rm -f prerequisite
target: prerequisite
    cp prerequisite target
prerequisite: source
    cp source prerequisite

If Make were invoked without an argument and the file clean did not exist, this makefile would cause the clean up recipe to run. This example demonstrates the use of a hyphen at the beginning of a recipe line. The hyphen is stripped by Make before the line is executed and Make will ignore any errors generated by executing the command.

$ ls
makefile  source
$ make
rm -f target
rm -f prerequisite
$

Named actions without any prerequisites, such as clean, should not be executed unless the name of the target is passed as an argument. Moving the clean rule to the end of the file may be an adequate solution to this problem; the recipe for the rule named target would be executed instead, which depends on prerequisite, which in turn depends on the file named source.

target: prerequisite
    cp prerequisite target
prerequisite: source
    cp source prerequisite
clean:
    - rm -f target
    - rm -f prerequisite

The clean target is not a prerequisite of any rule that is used to build the file target and therefore it does not run. It can be run by passing its name as an argument to Make.

$ ls
makefile  source
$ make
cp source prerequisite
cp prerequisite target
$ ls
makefile  prerequisite  source  target
$ make clean
rm -f target
rm -f prerequisite
$ ls
makefile  source

This is not a perfect solution, however. If a file named clean exists, then the rule will not be run.

$ ls
makefile  source
$ touch clean
$ ls
clean  makefile  source
$ make clean
make: 'clean' is up to date.

The builtin .PHONY target can be used to express that the rule named clean does not refer to a file and its recipe will not build the target. This example also illustrates that rules beginning with a single . will not be considered a default target, unless followed immediately by a /.

.PHONY: clean
./target: prerequisite
    cp prerequisite target
prerequisite: source
    cp source prerequisite
clean:
    - rm -f target
    - rm -f prerequisite

This version of the makefile behaves as intended, building files named target and prerequisite from the file source and running the clean target when Make is explicitly passed the name of the rule, even in the presence of a file matching its name.

$ ls
clean  makefile  source
$ make
cp source prerequisite
cp prerequisite target
$ ls
clean  makefile  prerequisite  source  target
$ make clean
rm -f target
rm -f prerequisite
$ ls
clean  makefile  source

Action rules can be handled another way: using another rule without prerequisites or a recipe. The target of this rule is given as a prerequisite of the action, causing the target to be updated and consequently the action to be run.

./target: prerequisite
    cp prerequisite target
prerequisite: source
    cp source prerequisite
clean: FORCE
    - rm -f target
    - rm -f prerequisite
FORCE:

Multiple Targets

An explicit rule may have more than one target. If the usual target: prerequisites; recipe syntax is used, then the targets are considered independent. The compact rule may be separated into a number of rules with common prerequisites and recipes. The automatic variable $@ may be used to access the target that triggered the rule.

app: app.o lib.o
    cc -o app app.o lib.o
app.o lib.o: lib.h
# ^ is equivalent to:
# app.o: lib.h
# lib.o: lib.h

This examples states that both object files are dependent on a common header file and changes to it should result in the re-compilation of the source files. In this example, the implicit rule to do so is used.

$ ls
app.c  lib.c  lib.h  makefile
$ make
cc    -c -o app.o app.c
cc    -c -o lib.o lib.c
cc -o app app.o lib.o
$ ls
app  app.c  app.o  lib.c  lib.h  lib.o  makefile
$ touch lib.h
$ make
cc    -c -o app.o app.c
cc    -c -o lib.o lib.c
cc -o app app.o lib.o

If &: is used in place of :, a number of targets is considered grouped. The recipe is assumed to update all of the targets and the rule is triggered if any of the targets are out of date.

one two &: prerequisite
    touch one
    touch two

The example invocation below demonstrates that rule is run if just one of the prerequisites is out of date.

$ ls
makefile  prerequisite
$ make
touch one
touch two
$ touch prerequisite
$ touch two
$ make
touch one
touch two

Order-only Prerequisites

A recipe is usually run if any of a rule’s prerequisites are newer than the target. More often than not, this is the desired behaviour, though in rare cases one may want a recipe to run only if the prerequisite does not exist and not in the case that the prerequisite is newer than the target. Such a prerequisite is called an order-only prerequisite. These prerequisites are placed after all (possibly zero) normal prerequisites, separated by a | character.

target: | directory
    touch target
directory:
    mkdir -p directory

In this example, the recipe for updating the file named target is not run when the prerequisite directory exists, despite the prerequisite being newer.

$ ls
makefile
$ make
mkdir -p directory
touch target
$ ls
directory  makefile  target
$ touch directory
$ make
make: 'target' is up to date.

Variables

A variable is a stand-in for a string of text, known as the variable’s value. The values of variables may be used in any part of a makefile and can be substituted with the $(variable-name) syntax. ${variable-name} is also acceptable. Variables may be redefined repeatedly and its value reflects that of it’s most recent definition. The $ character may be escaped by preceding it with another $.

Variable Flavours

The expansion of a variable’s value may occur at the point of definition or at the point of substitution. The difference between the two flavours is detailed here.

The single = sign can be used for variable assignment. This form is called recursively-expanded variable assignment. The value of the variable is expanded at the point of substitution. In the given example, make recursive will print two, as the value of a is expanded in the rule, where the most recent definition of b is two.

b = one
a = $(b)
b = two
recursive:
    @echo $(a) # two

The := and ::= forms are identical and called simply-expanded variable assignment. The behaviour of this example is more predictable, as the value of x is expanded at the point of definition, at which point the value of y is one.

y = one
x := $(y)
y = two
simple:
    @echo $(x) # one

Appending to a Variable’s Value

Values may additionally be assigned to variables using the += form, which appends a value to a variable. If the variable has not previously been defined, it acts as =. If the variable was previously assigned a value, then the behaviour depends on how that value was assigned.

If the variable was defined as simply-expanded, then += is equivalent to:

var := initial
var := $(var) appended

The following example consequently echoes one two.

a = one
b := $(a)
b += two
a = three

simple:
    @echo $(b) # one two

In the case of recursively-expanded variables, += is roughly equivalent to:

var = initial
tmp = $(var)
var = $(tmp) appended

Consequently, this example outputs three two.

a = one
b = $(a)
b += two
a = three

recursive:
    @echo $(b) # three two

Multi-Line Variables

Multi-line variables are easily set with the define directive.

variable = target
define recipe :=
touch target
echo $(variable)
endef
variable = not observed

target:
    $(recipe)

The define directive is followed by the name of the variable and the type of assignment. If the latter is omitted, it is assumed to be =.

Automatic Variables

Automatic variables are computed automatically by Make for each rule. Their values are not consistent between rules and their values depend on the targets and prerequisites, in which they may not be used. Complete reference for the automatic variables can be found here. The most often-used automatic variables are listed here.

All of these examples are only a single character long and hence the brackets may be omitted; $(@) is equally valid.

Conditionals

Conditional expressions are used to compare variables to value and conditionally include or ignore parts of a makefile. Conditionals are textual features and Make effectively does not parse sections of the makefile that follow the false branch of a conditional, if it exists.

A conditional is formed by a conditional directive followed by any number of lines that are included when the condition is true; any number of else conditional directives (or simply else without a conditional directive) and the mandatory endif directive.

In this example, the value Success is assigned to to the variable output if the value of input is 0. Note the absence of an else statement, which is optional.

input = 0
output = Error
ifeq ($(input),0)
output = Success
endif

target:
    @echo $(output)

Equally, any number of else conditional directives may appear in a conditional.

input = 2
ifeq ($(input),0)
output = none
else ifeq ($(input),1)
output = one
else ifeq ($(input),2)
output = couple
else
output = several
endif

target:
    @echo $(output) # couple

Other conditional directives include ifneq, obviously the opposite of ifeq; ifdef, which determines whether a variable name has a non-empty value; and ifndef, which is the opposite of ifdef.

Functions

Function calls resemble variable references and may be substituted in the same places. Functions additionally accept arguments and are written $(function arg,arg,arg). Curly brackets are of course acceptable as well. A function’s arguments are separated from its name by a single white space character and multiple arguments are separated by commas. These two characters may need to be hidden within a variable if they appear as an argument. Complete function reference can be found here.

See Also

Or return to the index.