Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

🏠 Back to Blog

Make Files

Why do Make files exist?

Make files are used for automation. Typically as a step in the software development lifecycle (compilation, builds, etc.). However, they can be used for any other task that can be automated via the shell.

Make files must be indented using tabs, not spaces

Makefile Syntax

Makefiles consist of a set of rules. Rules typically look like this:

targets: prerequisites
	command
	command
	command
  • The targets are file names, separated by spaces. Typically, there is only 1 per rule.
  • The commands are a series of steps typically used to make targets.
  • The prerequisites are also file names, separated by spaces. These files need to exist before the commands for the target are run. These are dependencies to the targets.

Example

Let’s start with a hello world example:

hello:
	echo "Hello, World"
	echo "This line will print if the file hello does not exist."

There’s already a lot to take in here. Let’s break it down:

  • We have one target called hello
  • This target has two commands
  • This target has no prerequisites

We’ll then run make hello. As long as the hello file does not exist, the commands will run. If hello does exist, no commands will run. It’s important to realize that I’m talking about hello as both a target and a file. That’s because the two are directly tied together. Typically, when a target is run (aka when the commands of a target are run), the commands will create a file with the same name as the target. In this case, the hello target does not create the hello file.

Let’s create a more typical Makefile - one that compiles a single C file. But before we do, make a file called blah.c that has the following contents:

// blah.c
int main() { return 0; }

Then create the Makefile (called Makefile, as always):

blah:
	cc blah.c -o blah

This time, try simply running make. Since there’s no target supplied as an argument to the make command, the first target is run. In this case, there’s only one target (blah). The first time you run this, blah will be created. The second time, you’ll see make: ‘blah’ is up to date. That’s because the blah file already exists. But there’s a problem: if we modify blah.c and then run make, nothing gets recompiled.

We solve this by adding a prerequisite:

blah: blah.c
	cc blah.c -o blah

When we run make again, the following set of steps happens:

  • The first target is selected, because the first target is the default target
  • This has a prerequisite of blah.c
  • Make decides if it should run the blah target. It will only run if blah doesn’t exist, or blah.c is newer than blah

This last step is critical, and is the essence of make. What it’s attempting to do is decide if the prerequisites of blah have changed since blah was last compiled. That is, if blah.c is modified, running make should recompile the file. And conversely, if blah.c has not changed, then it should not be recompiled.

To make this happen, it uses the filesystem timestamps as a proxy to determine if something has changed. This is a reasonable heuristic, because file timestamps typically will only change if the files are modified. But it’s important to realize that this isn’t always the case. You could, for example, modify a file, and then change the modified timestamp of that file to something old. If you did, Make would incorrectly guess that the file hadn’t changed and thus could be ignored.

Make Clean

clean is often used as a target that removes the output of other targets, but it is not a special word in Make. You can run make and make clean on this to create and delete some_file.

Note that clean is doing two new things here:

  • It’s a target that is not first (the default), and not a prerequisite. That means it’ll never run unless you explicitly call make clean
  • It’s not intended to be a filename. If you happen to have a file named clean, this target won’t run, which is not what we want. See .PHONY later in this tutorial on how to fix this
some_file: 
	touch some_file

clean:
	rm -f some_file

Variables

Variables can only be strings. You’ll typically want to use :=, but = also works.

Here’s an example of using variables:

files := file1 file2
some_file: $(files)
	echo "Look at this variable: " $(files)
	touch some_file

file1:
	touch file1
file2:
	touch file2

clean:
	rm -f file1 file2 some_file

targets

The ‘all’ target

Making multiple targets and you want all of them to run? Make an all target. Since this is the first rule listed, it will run by default if make is called without specifying a target.

all: one two three

one:
	touch one
two:
	touch two
three:
	touch three

clean:
	rm -f one two three

Multiple targets

When there are multiple targets for a rule, the commands will be run for each target. $@ is an automatic variable that contains the target name.

all: f1.o f2.o

f1.o f2.o:
	echo $@
# Equivalent to:
# f1.o:
#	 echo f1.o
# f2.o:
#	 echo f2.o

Reference

Var assignment

foo  = "bar"
bar  = $(foo) foo  # dynamic (renewing) assignment
foo := "boo"       # one time assignment, $(bar) now is "boo foo"
foo ?= /usr/local  # safe assignment, $(foo) and $(bar) still the same
bar += world       # append, "boo foo world"
foo != echo fooo   # exec shell command and assign to foo
# $(bar) now is "fooo foo world"

= expressions are only evaluated when they’re being used.

Magic variables

out.o: src.c src.h
  $@   # "out.o" (target)
  $<   # "src.c" (first prerequisite)
  $^   # "src.c src.h" (all prerequisites)

%.o: %.c
  $*   # the 'stem' with which an implicit rule matches ("foo" in "foo.c")

also:
  $+   # prerequisites (all, with duplication)
  $?   # prerequisites (new ones)
  $|   # prerequisites (order-only?)

  $(@D) # target directory

Command prefixes

PrefixDescription
-Ignore errors
@Don’t print command
+Run even if Make is in ‘don’t execute’ mode
build:
    @echo "compiling"
    -gcc $< $@

-include .depend

Find files

js_files  := $(wildcard test/*.js)
all_files := $(shell find images -name "*")

Substitutions

file     = $(SOURCE:.cpp=.o)   # foo.cpp => foo.o
outputs  = $(files:src/%.coffee=lib/%.js)

outputs  = $(patsubst %.c, %.o, $(wildcard *.c))
assets   = $(patsubst images/%, assets/%, $(wildcard images/*))

More functions

$(strip $(string_var))

$(filter %.less, $(files))
$(filter-out %.less, $(files))

Building files

%.o: %.c
  ffmpeg -i $< > $@   # Input and output
  foo $^

Includes

-include foo.make

Options

make
  -e, --environment-overrides
  -B, --always-make
  -s, --silent
  -j, --jobs=N   # parallel processing

Conditionals

foo: $(objects)
ifeq ($(CC),gcc)
  $(CC) -o foo $(objects) $(libs_for_gcc)
else
  $(CC) -o foo $(objects) $(normal_libs)
endif

Recursive

deploy:
  $(MAKE) deploy2

Further reading