Using make

Posted on 2019-11-09 by me

I write makefiles just infrequently enough to remember the syntax and structure. They are surely on their way out, but as long as there are c/c++, tex and sphinx-docs, there will be make.

The problem: you write a collection of scripts, and there is some procedure that takes these and puts together some intended output. Examples: compiling in a compiling language (eg c) or producing a PDF from a tex file. Make allows us to automate this sensibly.

Below walks through a super simple example: using pandoc to make a static website from a collection of markdown files. I used this for a while, before I met hugo.

Full code at the bottom.

Make

Make is a pretty old program that automates a build, while using annoying, opaque syntax (relatively). But it has endured for a reason - it is still useful and does precisely what it is intended to do well enough. A full manual can be found in the link.

Make was most concerned with compiling for c programming. As such, it has inbuilt implicit rules for c compiling, - and examples in c can be less transparent AFAICS.

Make rules

Make reads a Makefile. Rules make up the staple part of a makefile’s logic. A rule takes the following form:

<target>: <prequisite 1> <prequisite 2> ...
    <command 1> 
    <command 2> 

Here <target> is the name of the output file. Succeeding it is a list of names of files on which the target file depends. If the target file exists and it has been modified more recently that all the prerequisite files, then make will skip on to the next rule. That is, it assumes there are no new changes. If a prerequisite has been modified, then make will run through the commands.

Note: that commands must be indented (other types of whitespace will fail).

Make variables

You can define and reference variables in make. For example, here we define variables SOURCEDIR and BUILDDIR and later reference the latter:

SOURCEDIR := source # Define
BUILDDIR := build
SOURCEDOCS := $(wildcard $(SOURCEDIR)/*.md) # Use 
... 

Variable references work by strict textual substitution There are variations in syntax which are also acceptable. For example ${SOURCEDIR} would also be substituted with source. See docs for explanation on the subtle differences between definition syntax =, :=,::=.

Special variables

This is where things start getting a bit wtf-ish for me.

Make has some special variables.

Variable Meaning
$@ target
$< first prerequisite
$^ all prerequisites
$? all prerequisites newer than the target
$(@D) directory of target
$(@F) same as $(notdir $@)

See also $% archived target, $+ prerequisites with duplicates, $| all order-only prerequisites, $* stem of implicit rule match. In addition we can extend the use of D and F in analogous, eg $(<D),$(<F) etc.

In our example, we have the following command.

	$(PANDOC) $(PANDOC_OPTIONS) $(PANDOC_HTML_OPTIONS) -o $@ $<

The -o flag informs pandoc that $@ is the output file name, while pandoc implicitly understands $< is the input.

Functions for Transforming Text

The basic function in make looks like

$(function args)

Here are some functions and their usage. See docs for explanation.

function Example:In Example:Out
dir $(dir src/foo.c hacks) src/ ./
notdir $(notdir src/foo.c hacks) foo.c hacks
suffix $(suffix src/foo.c hacks) .c
basename $(basename src/foo.c hacks) src/foo hacks
addsuffix $(addsuffix .c,foo bar) foo.c bar.c
addprefix $(addprefix src/,foo bar) src/foo src/bar
join $(join a b,.c .o) a.c b.o
word $(word 2, foo bar baz) bar
words $(words there are 4 words) 4
firstword $(firstword foo bar) foo

See also wildcard, realpath, abspath.

We use the wildcard.

SOURCEDOCS := $(wildcard $(SOURCEDIR)/*.md)

This will result in a whitespace separated list of all files in source (to where the variable SOURCEDIR is assigned) ending in .md.

Control

Make includes if.. then, and, or, foreach, etc. which we don’t need right now.

Pattern Rules

A pattern rule is a rule in which the target contains precisely one % character.

In our example we have the following pattern rule.

$(BUILDDIR)/%.html : $(SOURCEDIR)/%.md
	$(PANDOC) $(PANDOC_OPTIONS) $(PANDOC_HTML_OPTIONS) -o $@ $<

This creates a rule for any html file in the build directory, and stipulates that its prerequisite is the corresponding markdown file located in the source directory.

.PHONY and clean

All (almost all) make files have these two rules that look a little like this:

.PHONY: all clean
clean:
	- $(RM) $(BUILDHTML)

clean is used to remove all built files, but it doesn’t have any dependencies. To solve this we use a special built in target name .PHONY.

An aside on pandoc and markdown with yaml

Pandoc is a super useful document conversion format. It allows you to write in something easy to write in, like markdown, and convert to something easy to read in, like html or pdf. It hosts a great many options and extensions. See the site for docs.

Using a yaml header inside the markdown, allows us to include some metadata tags to the webpages, like a title.

In our example we instruct pandoc to include a css file located in the static folder.

PANDOC=/usr/bin/pandoc
PANDOC_OPTIONS=--smart --standalone
PANDOC_HTML_OPTIONS=--from markdown+yaml_metadata_block --css ../static/minidark.css --to html5

... 
$(BUILDDIR)/%.html : $(SOURCEDIR)/%.md
	$(PANDOC) $(PANDOC_OPTIONS) $(PANDOC_HTML_OPTIONS) -o $@ $<

Our example make file

Altogether we have the following makefile:

SOURCEDIR := source
BUILDDIR := build
SOURCEDOCS := $(wildcard $(SOURCEDIR)/*.md)

BUILDHTML = $(addprefix $(BUILDDIR)/, $(notdir $(SOURCEDOCS:.md=.html)))

RM=/bin/rm

PANDOC=/usr/bin/pandoc

PANDOC_OPTIONS=--smart --standalone

PANDOC_HTML_OPTIONS=--from markdown+yaml_metadata_block --css ../static/minidark.css --to html5

# Pattern-matching Rules

$(BUILDDIR)/%.html : $(SOURCEDIR)/%.md
	$(PANDOC) $(PANDOC_OPTIONS) $(PANDOC_HTML_OPTIONS) -o $@ $<

.PHONY: all clean
clean:
	- $(RM) $(BUILDHTML)

html: $(BUILDHTML)