A makefile is a data base that tells the
makeutilit how to recompile a system. In the default use case,
$: make <filename>checks whether
filenameis out of date and, if so, recompiles it. In the way I use makefiles,
$: make <rule>executes a predefined rule to accomplish a certain task like cleaning a particular dataset.
Rules consist of a target (the name of a file to be modified), prerequisites (other files on which the target depends on), and commands to be run in order to update the traget based on changes in the prerequisites.
A rule tells
makewhen a target is out of date and how to update it. A target is out of date if it doesn’t exist or is older then one of its prerequisite files.
$: makeexecutes the first specified rule,
$: make <rule>executes a particular rule.
A normal prerequisite makes both an order statement and a dependency statement: the order statement ensures that all commands needed to produce the prerequisete are fully executed before any commands to produce the target, while the dependency statement ensures that the target is updated every time a prerequisite changes. Occasionally, we want a prerequisite to invoke the order without the dependency statement (i.e. target is not udpated when the prerequisite changes, but when target is being updated, then the prerequisite commandas are run first). We can do this by writing the rule as
target: normal-prerequisites | order-only-prerequisites.
makedoes its work in two phases: during the read-in phase, it reads the makefile and internalises variables and rules to construct a dependency graph of all targets and their prerequisies; during the target-update phase, it determines what rules to update in what order and executes the commandas to do so.
As a result, variable and function expansion can happen either immediately (during the read-in phase) or deferred (after the read-in phase), and gives rise to two flavours of variables: recursively expanded variables, defined by
varname = valueare expanded at the time the variable is substituted during the target-update phase. Before that point,
varnamecontains the content of
valueverbatim (e.g. if
$(othervar), then that last string is the value of
varname). In contrast, simply expanded variables, defined by
varname := valueis expanded immediately when the variable is defined during the read-in phase (and
varnamewould be bound to the value of
othervarin the above example).
To define a variable containing all csv files in a directory, do
csvs := $(wildcard *.csv). The
wildcardfunction is needed here so that the wildcard gets expanded during function creation (as opposed to creating the variable with value
*.csv). I could also create a list containing the same files but with a parquet extensions like so: `parqs := $(patsubst %.csv,%.parquet,$(wildcard *.csv)).
$^is a list of all prerequisites,
$@is the target,
$<the first prerequisite.
If a target is an action to be performed rather than a file to be updated, then it’s called a
phony target. In this case, telling
makethat we’re using a phone target explicitly by prepending the rule with a line like
.PHONY : nameofruleis useful for two reasons:
makedoesn’t think of a file called
nameofruleas the target (which, if it did, would mean that our rule never gets run because it has no prerequisites so that
makewould think of
nameofruleas always up to date) and it doesn’t check for implicit commands to update the target, which improves performance.
Commands begin with a tab and, unless specified otherwise, are executed by
bin/sh. You can set a different shell by changing the value of the
SHELLvariable (I usually use
SHELL := /bin/bash. Each line that begines with a tab and appears within a rule context (anything between the start of one rule and another) is interpreted as a command and sent to the shell.
The only thing
makedoes with commands is to check for
\before newline, and for variables to expand (if you want
$to appear in the command, use
$$). To prevent
makefrom echoing a command, prepend it with
Prepend a command with
-if you want
maketo continue regardless of errors. This can be useful for commands like
-rm tempfile.csv, where you probaby want to continue even if the file didn’t exist and could thus not be removed.
Define a phony target:
.PHONY: clean clean: rm *.csv
$: make run all rules:
.PHONY: all all : rule1 rule2 .PHONY: rule1 rule1: mkdir hello .PHONY: rule2 rule2: rm -rf hello