Make is an awesome build tool that is seeing a bit of a resurgence. It’s a powerful tool, but can be complicated to get started with. There are no good guides around that help you learn make in a simple and straightforward way, add to that that most are heavily focused on C code, and it quickly gets too much.

Make is not limited to C, and most examples of Makefiles I’ve come across are complicated and difficult to understand. It doesn’t have to be that way.

Getting Started With Make

The first thing to understand about Make is that it was designed for C. Out of the box, it will do its best to compile a C file with no configuration.

Drop this into a file called hello_world.c somewhere:

#include<stdio.h>

int main() {
	printf("Hello World\n");
	return 0;
}

Now open up a terminal, and from the same directory, type:

make hello_world

As long as you have a C compiler installed, you should now see a file called hello_world in the current directory. Run ./hello_world and you should see the familiar output.

Make Makes Files

Running make is always in the format make <target>, where <target> is usually the file you want to create. It doesn’t always have to be this way, but that’s how make is designed to be used.

From the same directory, if you were to run make hello_world again, you should see:

make: 'hello_world' is up to date.

This is because make will track changes to files, and only re-run a target if the source file is newer than the file you want to build. This is a great time saver when you have a long list of dependencies, and only one has changed, as make will only re-compile those that need changing.

Edit hello_world.c, and change Hello World! to Goodbye World, then run make hello_world again, this time, it should compile, and running ./hello_world should now print Goodbye World.

Makefiles

Most of us don’t write C on a day-to-day basis, so instead, we need to tell make how to work with our language of choice, or do the things we want it to do. To do that, we need a Makefile.

A Makefile is a list of targets, denoted by a colon, followed by a list of shell commands to run, which must be indented by a single tab by default. It looks like this:

test:
	echo 'Hello World'

Create a new file in your current directory, and call it Makefile. Add the above, and then run make test. You should see the command make has run, followed by its output:

echo 'Hello World'
Hello World

Phony Targets

What we’ve just done goes against how make was designed. Remember, targets should be files we want to create, not arbitrary names, and we didn’t create a file called test, instead we just used it to echo a message.

This can cause problems for make. If you were to have a file or directory that was called test, make wouldn’t run this target. Let’s see that in action:

touch test
make test

You should see a familiar message:

make: 'test' is up to date.

In these instances, what you want to do is list the target as phony, which is a way of telling make: “this doesn’t really generate a file”. Makefiles have a special target for this, called .PHONY, and it’s just a list of phony targets. Update your Makefile to match the below:

test:
	echo 'Hello World'

.PHONY: test

Now run make test again and you should now see Hello World, even though we have a file called test.

Something Useful

Let’s put this to some use, and see a real example by using make to build a tiny static site from Markdown. The only external tool here is Pandoc, which converts between document formats.1

Create a directory for source pages:

mkdir pages

Create pages/index.md with a little bit of Markdown:

# Hello World

This page was generated with make.

Now open up your Makefile and replace it with the following:

public/index.html: pages/index.md
	mkdir -p public
	pandoc pages/index.md -s -o public/index.html

We haven’t listed this as phony, because this actually creates a file called public/index.html. Let’s run our new target:

$ make public/index.html
mkdir -p public
pandoc pages/index.md -s -o public/index.html

You should now have a public/index.html file. Running make public/index.html for a second time should yield the familiar up-to-date message.

make: 'public/index.html' is up to date.

Dependent Files

Let’s try something a little different. Create a tiny HTML snippet that pandoc should splice into each page’s <head>:

echo '<meta name="generator" content="make">' > head.html

Pandoc copies this snippet into the document head when you pass --include-in-header=head.html, so edits to head.html change the HTML file on disk, but make only reacts to prerequisites. If head.html is not listed next to public/index.html, you get the stale up-to-date message after changing the snippet, because make doesn’t know about that relationship yet. List prerequisites on the same line as the target name.

public/index.html: pages/index.md head.html
	mkdir -p public
	pandoc pages/index.md -s --include-in-header=head.html -o public/index.html

After updating your Makefile to match the above example, run make public/index.html again, then run it once more: you should get the usual up-to-date message. Edit head.html, run make public/index.html again, and make will re-run pandoc.

In simple forms, think of it like this:

create-this-file: when-this-file-is-newer
	by-doing-this

If you have multiple file dependencies, simply list them on the same line, separated by a space.

Dependent Targets

Imagine a scenario where you don’t keep head.html in source control, and instead you generate it by some other means. You probably want a target in your Makefile to generate it, but then your public/index.html target doesn’t have that file on disk until that target has run.

This is ok. Make can nest targets as dependencies of each other, and will run them in order. This example is contrived, but please bear with me.

Remove head.html:

rm head.html

We’re now back to a pretty blank state, but if we were to run make public/index.html, it would fail:

make: *** No rule to make target 'head.html', needed by 'public/index.html'.  Stop.

Let’s add a rule to generate head.html:

head.html:
	echo '<meta name="generator" content="make">' > head.html

public/index.html: pages/index.md head.html
	mkdir -p public
	pandoc pages/index.md -s --include-in-header=head.html -o public/index.html

With this change make knows how to generate head.html for our public/index.html target. Run make public/index.html and see what happens:

echo '<meta name="generator" content="make">' > head.html
mkdir -p public
pandoc pages/index.md -s --include-in-header=head.html -o public/index.html

Nice! Let’s try make head.html and make public/index.html:

make: 'head.html' is up to date.
make: 'public/index.html' is up to date.

Excellent. If we were to remove head.html, make would regenerate it before updating public/index.html. You could also run make head.html on its own; a later make public/index.html would pick up the new header snippet when the timestamps say it should.

Converting Files

Let’s add another page:

cat > pages/about.md <<'EOF'
# About

This is another page generated with make.
EOF

Now let’s add a target to our Makefile which will output the HTML files in a public directory, to keep in line with what make expects:

head.html:
	echo '<meta name="generator" content="make">' > head.html

public/index.html: pages/index.md head.html
	mkdir -p public
	pandoc pages/index.md -s --include-in-header=head.html -o public/index.html

public/about.html: pages/about.md head.html
	mkdir -p public
	pandoc pages/about.md -s --include-in-header=head.html -o public/about.html

You can run it with make public/about.html:

mkdir -p public
pandoc pages/about.md -s --include-in-header=head.html -o public/about.html

That’s better, but we don’t want to be doing that for all of our Markdown files, there could be hundreds of them.

Fortunately, Make has you covered.

Pattern Targets

Pattern targets are exactly the same as normal targets, except they allow you to treat all files that match a pattern in the same way, rather than specifying a specific file. For us this is really useful, as we can re-write our rule like so:

public/%.html: pages/%.md head.html
	mkdir -p public
	pandoc $< -s --include-in-header=head.html -o $@

The only slightly dodgy things there are the $@ variable, which matches the target name, so make public/index.html would be public/index.html, and the $< variable, which means “the first dependency”: for make public/index.html that is pages/index.md, and for make public/about.html it is pages/about.md.

Ordering of dependencies is important when you use $<. If we were to put head.html before pages/%.md, then $< would always be head.html, no matter which page you tried to build. That’s not what we want.

Now we have that setup, run make public/index.html and we’ll see what happens:

make: 'public/index.html' is up to date.

Looks promising, let’s remove it and make sure it works:

rm public/index.html
make public/index.html

And your generated file should be back in public/index.html.

Pulling it Together

Whilst pattern targets help us avoid having hundreds of rules in our Makefile, it would be really annoying to have to type make public/index.html or make public/about.html ad infinitum. Fortunately, make has our back here too.

We can now add a site target that builds them both:

.PHONY: site

site: public/index.html public/about.html

Here, all we’re doing is telling make that it needs to build both of our files as dependencies of the site target. This allows us to build our entire project with make site. Try it out:

rm -f public/*
make site

You should see both files get built and placed in the public directory. If you were to remove a single file, and run make site again, it should only build that file:

rm -f public/index.html
make site

And you should see public/index.html has been re-created.

Variables

Having to keep a list of all of our files as dependencies of the site target isn’t much better than what we had before. There’s a good chance we’ll miss one eventually, and it will get messy to debug. Instead, it’s better to store this in a variable that’s dynamically populated. Update your Makefile to match the following:

SOURCE_FILES := $(wildcard pages/*.md)
PUBLIC_FILES := $(patsubst pages/%.md, public/%.html, ${SOURCE_FILES})

head.html:
	echo '<meta name="generator" content="make">' > head.html

.PHONY: site

site: ${PUBLIC_FILES}

public/%.html: pages/%.md head.html
	mkdir -p public
	pandoc $< -s --include-in-header=head.html -o $@

Here, we’ve added two variables to the top of the file. They don’t have to be there, but that’s kind of a make convention.

Make has a number of ways of assigning variables, this one, using the := symbol, is the simplest form of assignment, which simply means “store this as the value”. The other types are a little more complex, and we don’t need them here, so I will cover them in another post.

On the right hand side of the assignment, we have a make function call. This is denoted by the $(). The first word inside the dollar-brackets is the name of the function we’re calling. It’s then followed by a comma separated list of arguments for the function. There is no comma after the function name.

Make has a number of built in functions, the two we’re using here are wildcard and patsubst. You can probably guess what they do by their names. wildcard will take a wildcard file path and return a space separated list of all files from the filesystem that matches that name, here that’s a list of all of our .md files in the pages/ directory.

The second, patsubst, will match the first argument, and replace it with the second, in the string provided as the third argument. We can use the % character as a simple wildcard match and replace.

The result is that SOURCE_FILES contains the string pages/about.md pages/index.md, and PUBLIC_FILES contains the string public/about.html public/index.html.

Take a look at our site target. Here, we’re just making the dependency our PUBLIC_FILES variable (which is wrapped dollar-braces ${} to “echo” it), which, as I said above evaluates to the string public/about.html public/index.html, so those two targets become our dependencies. Any new files we add will get picked up and added to the variables, and therefore added as dependencies to our site target. Go ahead and try it out, see what happens.

Wrapping Up

The astute among you would have noticed that we’re invoking pandoc for every single file for our site target when we could convert several files in one go. You lose the ability to only build what’s changed with that target, but the trade-off is minimal in this instance. I did it the way I did here for the sake of example.

The second thing you may have noticed, if you’re playing around a little, is that there’s no way to make the wildcard function recursive. For that, you need another built-in function: shell. The shell function allows you to call out to the shell below and capture the resulting output. If you want to collect a list of files recursively, the best way is a combination of shell and find, like so:

SOURCE_FILES := $(shell find pages/ -type f -name '*.md')

Conclusion

I hope this post has given you a good grounding in how to use make, why things are the way they are, and how it should be used for the greatest effect. There is far more to make, and especially to writing good Makefiles than what I’ve outlined here, but this foundation should allow you to start using make, and proceed to my next post, Makefile Variables.