The Blog of Charles Daniels

A Practical Guide to Make

Posted 2017-07-15

A Practical Guide to Make

Contents

Introduction

In this article, you will learn how to use the Make build system to compile software projects. This article is targeted at those who have experience programming, but are not deeply familiar with the shell or commandline build systems. However, this article does assume you have basic familiarity with the UNIX commandline.

I decided to write this article because, in my experience observing my peers, and in 2 semesters of TAing an intro to UNIX class, Make seems to perpetually be a sticking point for new users. This article is in particular targeted at my peers and students who have often completed one or more semesters of a computing degree, and are competent programmers, but have not had the benefit of UNIX experience.

Background - What is Make?

The true purpose and utility of Make are often neglected in courses and articles which discuss it. Make is often regarded and used as simply a tool for chaining shell commands together. While Make can indeed do this, using make as a glorified shell script parser leaves many of it's most powerful and useful features neglected.

Make is as much a tool for executing commands and scripts as it is a dependency resolution system. In Make, instructions on how to build a piece of software are split up into targets, each of which may have zero or more dependencies. A given target cannot be executed until all of it's dependencies have, and multiple targets can share overlapping dependencies.

Consider the following:

If you then wished to execute "A", figuring out the most efficient order in which to execute the tasks B-F without repeating or executing a task without it's dependencies, is a quite difficult problem, the complexity of which grows multiplicatively with the number of targets and dependencies.

Make Syntax

Make uses a very simple syntax. Targets are strings which start in column zero and are terminated by a :, and followed by a list of dependencies. The command to be executed within a target are indented below the target name by a single tab character (8 spaces will not work).

Consider the example from the previous section encoded as a Makefile:

A: B C F
    echo "executing A"

B: C
    echo "executing B"

C: F D
    echo "executing C"

D: F
    echo "executing D"

F:
    echo "executing F"

Executing this make file with the command make A yields the following output

echo "executing F"
executing F
echo "executing D"
executing D
echo "executing C"
executing C
echo "executing B"
executing B
echo "executing A"
executing A

Thus, we can see that F->D->C->B->A was the most efficient order in which to execute the tasks such that each task's dependencies were executed before the task itself.

Notice that the commands being executed are displayed alongside their output, this can be turned off by preceding the command with an @ symbol, for example:

A: B C F
    @echo "executing A"

B: C
    @echo "executing B"

C: F D
    @echo "executing C"

D: F
    @echo "executing D"

F:
    @echo "executing F"

yields the output:

executing F
executing D
executing C
executing B
executing A

A Simple C Program

Consider the following files comprising a simple C program:

main.c

#include "main.h"

int main() {
    hello();
    printf("Hello from main.c!\n");
}

main.h

#ifndef MAIN_H

#include<stdio.h>
#include "module.h"

#define MAIN_H
#endif

module.c

#include "module.h"

void hello() {
    printf("Hello from module.c!\n");
}

module.h

#ifndef MODULE_H

#include<stdio.h>

void hello();

#define MODULE_H
#endif

The corresponding Makefile might look like:

helloprog: main.o module.o
    gcc -o helloprog main.o module.o

main.o: module.o main.c main.h
    gcc -c main.c main.h

module.o: module.c module.h
    gcc -c module.c module.h

clean:
    -rm module.o
    -rm main.o
    -rm helloprog
    -rm module.h.gch
    -rm main.h.gch

Lets dissect line-by-line...

helloprog: main.o module.o - the helloprog target requires that main.o and module.o be built before it can run.

gcc -o helloprog main.o module.o - this gcc command produces a program named helloprog by linking the object files main.o module.o. For those unfamiliar with C, .o files are analogous to .class files in Java.

main.o: module.o main.c main.h - the target main.o requires that module.o be built first, and also depends on the contents of the files main.c and main.h. This is one of the clever features of make - if main.o is already present and main.c and main.h have not changed since the last time the target was built, the target can simply be skipped - more on this later.

gcc -c main.c main.h - generate the object files for main

module.o: module.c module.h - the target moduleo depends on the files module.c and module.h. Notice that it depends on no other Make target.

clean: - it is a convention that a Makefile should provide a target named clean which removes any build artifacts such as object files, binaries, debug symbols, and so on. Notice that the lines in this target are preceded by -, this instructs make to proceed even if one of the command fails. Without the - character, if say module.o did not exist, but main.o did, the command rm module.o would fail, causing make clean to exit without removing main.o.

Using Variables

Standard Variables

In GNU make, variables are assigned in one of four ways. The simplest is with the := operator, and take the form VARNAME := value. The other three methods are beyond the scope of the tutorial, but more information can be found here.

Variables can be assigned the output of shell commands, for example THEDATE := $(shell date) would set THEDATE to the output of the shell command date.

Variables are accessed using the syntax $(VARNAME). Note that variables can be accessed from anywhere in the Makefile, but only defined outside of targets.

Consider the example below...

Makefile:

VARNAME:=a string
THEDATE:=$(shell date)

mytarget:
        @echo "the date is $(THEDATE)"
        @echo "VARNAME is: $(VARNAME)"

Output:

the date is Sun May 28 18:18:00 EDT 2017
VARNAME is: a string

Special Variables

NOTE What I refer to as "special variables" are actually referre to as Automatic Variables in the Make documentation.

Automatic variables allow you to quickly write very powerful Makefiles, and write rules so they may be easily copy-pasted or otherwise re-used. For the sake of brevity, I shall not reproduce the full list of automatic variables here, but rather provide a link to the pertinent page in the Make documentation, plus the following example...

Makefile:

myrule: otherrule coolrule
        @echo "target name is: $@"
        @echo "first prerequisite: $<"
        @echo "all prerequisites: $^"
otherrule:
        @echo "hello from otherrule"

coolrule:
        @echo "hello from coolrule"

Output:

hello from otherrule
hello from coolrule
target name is: myrule
first prerequisite: otherrule
all prerequisites: otherrule coolrule

Note that there are other special variables, but these are the ones I find to be the most useful.

Armed with this information, let's rewrite our simple C program's Makefile to be a little cleaner...

Makefile:

helloprog: main.o module.o
        gcc -o $@ $^

main.o: main.c main.h
        gcc -c $^

module.o: module.c module.h
        gcc -c $^

clean:
        -rm module.o
        -rm main.o
        -rm helloprog
        -rm module.h.gch
        -rm main.h.gch

Notice that all three of the compilation-related rules are considerably shorter and easier to type. This is especially handy when writing a target which has many dependencies which later change - any changes can be made in one place.

Best Practices

Using Make with Java

While Make has traditionally been a "C thing", it can also be applied to great effect to Java based projects for compilation, and is indeed my preferred method of building Java programs. Note however that Java has not one, but two build systems geared for Java and it's ecosystem in the form of Ant and Maven. If you are already familiar with one of these, or don't yet know any build system and are shopping for one for a Java project, Make may not be the best choice. That said, I used Make to great effect for Freshmen level Java for two semesters.

Other Uses for Make

Common Gotchas and Pitfalls