March 12, 2017

Tom's guide to make and Makefiles

Many people are frightened of makefiles and make. I am hoping this simple guide will give a different perspective on things and help people use this great tool.

Being lazy

Using make is all about being lazy. Once I have a makefile properly set up, all I have to do to work on a project is to edit the source files and then type:
make
Then when I want to burn code into flash (or ROM in the old days), I type something like:
make flash
Doing things like this is part of being professional. I work on a lot of projects, and sometimes I need to work on a project after leaving it forgotten for several years. I know that I can find the source directory, edit files, and type make. I can also hand off the project to others and a proper makefile serves as a sort of documentation of how the project is built and handled.

I don't want to have to remember (or type) long command names and sequences of switches and options. Apart from the tedium of typing (and retyping when I make mistakes), I simply won't remember - and my mind is better used focusing on the problem at hand. If someone else has to jump into one of my projects, there is no substitute for the makefile.

Keeping it simple

Some people seem to think that Makefiles are their opportunity to create some kind of complex masterpiece using every make feature known to man. Then other people come upon these disasters and get frightened away. My goal in this guide is not to give an exhaustive demonstration of every esoteric feature in make, but rather to show how I use make in my day to day work.

The most simple example

So, let us imagine you have a hello world program written in C and want a makefile to go along with it. The following will do the job:
hello:	hello.c
	cc -o hello hello.c
There you have it, understand the above and you will understand the central concept of all makefiles. Consider the following diagram which shows the 3 components of the above two line makefile:
TARGET:	SOURCE
	COMMAND
The thing to the left of the colon is the target, the thing we want to make.
The thing to the right of the colon is the source (or sources), namely the files you have to start with
The command is the command to run that converts the source (or sources) into the target.

Something to watch out for

This is important, and is a stumbling block for even the most experienced. The white space to the left of the command is a single TAB! Honestly this was an inexplicable bad decision on the part of whoever designed make, but at this point we are stuck with it and just have to get used to it. If your editor maps tabs into spaces, you will get cryptic errors, and after being puzzled for a while, will remember this warning and sort this out.

Some added complexity

What if your program is built from multiple source files? What if you need to run more than one command to build your target.
hello:	hello.c extra.c
	cc -c hello.c
	cc -c extra.c
	cc -o hello hello.o extra.o
Here we list the sources, separated by spaces, then use as many lines as necessary to describe the process of making the target. Simple enough, eh?

A target without any sources

I use these sorts of pseudo targets all of the time in order to preserve complex commands that I find myself typing over and over as I work on a project. If you type "make" without an argument, it will try to make the first target in the makefile. However you can tell make which target you want to build like this:
make clean
Most of my makefiles have a target to handle this near the end that looks like this:
clean:
	rm -f hello hello.o extra.o
Here there are no SOURCES, just a target, which is perfectly fine. Make will just run all the commands that follow. So you can have a target "fred" with a bunch of commands that follow and then use "make fred" to run those commands. This lets me put what would otherwise be a bunch of little random shell scripts into my makefile which both preserves them in one place and helps me be lazy.

Providing for "make clean" is a sort of good practice. The idea is to delete everthing the makefile would be able to make anyway so that typing the following is harmless:

make clean
make
I have an entry to support "make term" in one of my current projects which runs the terminal emulator that I use to do debugging. Once again this is about being lazy. I don't have to remember switches and option or even the name of the terminal software.
term:
        picocom -b 115200 /dev/ttyUSB0

Make variables

An important rule to live by in all aspects of programming is "don't repeat yourself", also known as the "DRY" principle. If you find yourself writing the same code over and over, put it in a subroutine. If you use some constant number in many places, define a symbol for it and use the symbol. One beneficial side effect of this is that when you need to make a change you do it in one place, rather than trying to find every place that you repeated yourself. To define a symbolic value in make you do something like this:
PORT = /dev/ttyUSB0
This is simple enough. An assignment statement with as much white space as you care to use. This can later be referenced by wrapping it in parenthesis and slapping a dollar sign in front.
term:
        picocom -b 115200 $(PORT)
Some people get foolishly and needlessly carried away doing this. My rule of thing is to define anything that you expect will change now and then and stick those definitions near the start of the makefile. Also define things that are used in many places.

Something I haven't told you yet

If you have read all that I have written so far, you will think that a Makefile is like a mutant form of shell scripting. And that has been my intention thus far. What I haven't told you about yet is what was the original motivation for writing make in the first place. In the old days, computers were slow and compiling source files took a signficant amount of time. Somebody realized that if they were building some big piece of software that consisted of a dozen source files and only made edits to one, the other 11 were being needlessly recompiled, wasting their valuable time. Make looks at file timestamps and decides if a given software component is "up to date" or not. For example consider this example from above:
hello:	hello.c extra.c
	cc -c hello.c
	cc -c extra.c
	cc -o hello hello.o extra.o
Let us say you just edit extra.c and then type "make". Make will look at the timestamp on the target "hello" and realize that it needs to be recompiled. If you type make again, it will do nothing, realizing it is up to date.

This can depend on that

To get the full advantage out of this scheme, you need to break things up, perhaps with something like this:
hello:	hello.o extra.o
	cc -o hello hello.o extra.o

hello.o: hello.c
	cc -c hello.c

extra.o: extra.c
	cc -c extra.c
Here if we type "make" it will try to make the first target in the file, namely "hello". It will look for the sources and decide if hello needs to be rebuilt at all, but first it looks for any dependency lines that build the sources and check if those may need to be rebuilt, going on and on as necessary.

So let's say we have built hello and left the files laying around (not running "make clean" or otherwise deleting any files). Then we edit extra.c and type make. Make will notice that extra.o is out of date and rebuild it. Then it will notice hello is out of date and relink it. However it will not need to recompile hello.c

This is no big deal on a simple piece of software like this, but consider something like an operating system built from hundreds of source files and you will see the point. Whenever "make" is processing a line with a colon separating a target from sources, it will make this timestamp evaluation. These are called "dependency lines".


Have any comments? Questions? Drop me a line!

Tom's home page / [email protected]