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
| Prefix | Description |
|---|---|
- | 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