Skip to main content

A Practical Guide to Make

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:

  • To execute A, I must execute B, C, and F
  • To execute B, I must execute C
  • To execute C, I must execute F and D
  • To execute D, I must execute F

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 -o main.o -c main.c main.h

module.o: module.c module.h
	gcc -o module.o -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 module.o 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.

Pattern Rules

One of the most powerful, but also most arcane features of Make is pattern rules. Pattern rules allow you to generalize the command to build a particular type of file beyond one single instance of that file. A pattern rule’s target and dependencies can be defined in terms of a single base part of file name. As an example, a pattern rule to build a C .o file might associate %.o (the target) with the dependencies %.h and %.c.

Consider a trivial example:

%.baz: %.txt
	cp $< $@

And a shell session with this Makefile:

[cad@kronos][11:09][~/Desktop/tmp]
(zsh) $ make bar.baz
cp bar.txt bar.baz
[cad@kronos][11:09][~/Desktop/tmp]
(zsh) $ make foo.baz
cp foo.txt foo.baz
[cad@kronos][11:09][~/Desktop/tmp]
(zsh) $ ls
bar.baz  bar.txt  foo.baz  foo.txt  Makefile

As you can see, % is a sort of context-sensitive variable we can use to associate a single common action with different possible input and output files. As an example, in most C programs, every C source code file will be compiled into an object file in the same way, it is thus redundant to specify a separate rule for each and every C file individually.

Now, adding in what we have learned about pattern rules, let’s re-write our ideomatic C Makefile one again to use them:

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

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

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

In this very simple example, we haven’t saved all that much space, we just got rid of one single rule. However, keep in mind that pattern rules will scale to arbitrarily many files. With the Makefile above, we could have the helloprog target depend on dozens of object files, but still have all the object files generated by a single pattern-rule.

As an aside, you should also familiarize yourself with Make’s Implicit Rules which can sometimes be used to simplify your Makefile even further; Implicit rules are built-in pattern rules that know how to build common file types such as C object files, and equivalents for other languages. Personally, I do not make use of implicit rules, because I feel that they make the build process too opaque and intractable to developers not deeply familiar with Make. Nevertheless, implicit rules are a longstanding and mature feature of Make which any C developer should be familiar with.

Best Practices

  • Be careful about which commands should fail the build when they fail, and which you can safely ignore failures of using the - character.

  • Always try to name rules according to the file they produce as output (when applicable) - this allows make to cleverly avoid rebuilding files whose sources have not changed on disk since the last build.

  • When a rule depends on the contents of one or more files, such as compiling a program from source, always list the files which the rule takes as input as it’s dependencies. This is for the same reason as above - Make will cleverly avoid rebuilding object files if the object file exists, and it’s corresponding source file on disk has not changed.

  • Do not run make clean excessively, for the same reason as the previous points; Make is smart and will re-use object files (and similar) if they already exist and their dependencies have not changed.

  • Typically, when working with compiled languages, you should create variables for your compiler and the flags for the same. For example, in C, it is convention to store your compiler in CC and your compiler flags in CFLAGS. For example, you might set CC to gcc and CFLAGS to -Wall -Werror. Then in your rules, you can use these variables rather than writing out your compiler and all it’s options - this will save you lots of times if you decide you want to add another compiler flag or anything of that sort.

Common Gotchas and Pitfalls

  • Make targets must be indented by the tab character - this is NOT the same thing as some number of spaces.

  • Makefiles must be named makefile or Makefile, or make will not see them.

  • If you run the command make without a specifying a target, it simply executes the first target in the Makefile.

  • On case-sensitive systems such as Linux, makefile and Makefile are separate files (unlike Mac and Windows) - creating both in the same directory will cause unexpected consequences.

The Future of Build Systems

In the modern age, much of software development is moving away from Make. It is becoming increasingly common for new languages to implement first-party build and dependency management tools. Even in cases where language developers do not do so, community projects often spring up to create language-specific build tools. Many of these build tools offer much more advanced features than Make does, as they can be tailored to their specific language, rather than acting as a general-case solution. Nevertheless, Make is still widely used for C and C++ projects large and small, and it is unlikely that Make will be going away any time soon in that space. Make also has the benefit of being a general-case solution, which makes it useful for tasks beyond compiling C and C++ code (case in point, this website is generated by a Makefile).

Some popular build systems for Java include

All three of these tools are fairly similar, and solve more or less the same problems: managing dependencies and compiling projects. I have personally only used Maven, and even then not much, so I am unqualified to make an educated recommendation between the three. Anecdotally, Maven seems to be the most popular for large-scale enterprise applications.

For Rust, the first-party build and dependency management system is Cargo.

For C and C++, CMake has become popular in recent years. CMake is a tool for generating and executing Makefiles in an automated fashion, but does not handle dependency installation and management as Cargo, Gradle, Maven and Ant do.

There are may other language-specific build tools aside from these - you should investigate the ones available for your language of choice… it is frequently better to use a purpose-build solution rather than a general purpose one.

Finally, in the general-purpose build system space, Make has a new competitor: Sake. Sake offers many of the same features and benefits of Make, with a few key differences:

  • Sake uses the standardized YAML syntax, rather than inventing it’s own.
  • Sake build files are self-documenting.
  • Sake is fully cross-platform, and runs equally well as a native Windows application as a native Linux application.
  • Sake uses file hashes, rather than filesystem timestamps to determine if a dependency or target is out of date. This can make it more reliable on some systems.

Further Reading

Edit History

  • 2018-02-22 - removed “Using Make with Java” section, it was just an anecdote and did not provide a useful example, nor useful information.
  • 2018-03-02 - Added Pattern Rules section.
  • 2018-03-02 - Added “The Future of Build Systems” section.
  • 2018-03-06 - Added “Further Reading” Section.
  • 2019-11-25 - Update section headers to new style.
  • 2020-01-18 - Fix some minor formatting errors.
  • 2020-02-25 - Use gcc -o to set output file names explicitly (thanks James!).
  • 2023-03-14 - Formatting tweaks while migrating to Hugo.