Over the years, Linux (well, the operating system that is commonly known as Linux which is the Linux kernel and the GNU tools) has become much more complicated than its Unix roots. That’s inevitable, of course. However, it means old-timers get to slowly grow into new features while new people have to learn all in one gulp. A good example of this is how software is typically built on a Linux system. Fundamentally, most projects use make — a program that tries to be smart about running compiles. This was especially important when your 100 MHz CPU connected to a very slow disk drive would take a day to build a significant piece of software. On the face of it, make is pretty simple. But today, looking at a typical makefile will give you a headache, and many projects use an abstraction over make that further obscures things.

In this article, I want to show you how simple a makefile can be. If you can build a simple makefile, you’ll find they have more uses than you might think. I’m going to focus on C, but only because that’s sort of the least common denominator. A make file can build just about anything from a shell prompt.

No IDE?

You may scoff and say that you don’t need a makefile. Your IDE builds your software for you. That may be true, but a lot of IDEs actually use makefiles under the covers. Even IDEs that do not often have a way to export a makefile. This allows you to more easily write build scripts or integrate with continuous release tools which usually expect a makefile.

One exception to this is Java. Java has its own set of build tools that people use and so make isn’t as common. However, it is certainly possible to use it along with command line tools like javac or gcj .

The Simplest Makefile

Here’s a simple makefile:

hello: hello.c

That’s it. Really. This is a rule that says there is a file called hello and it depends on the file hello.c. If hello.c is newer than hello, then build hello. Wait, what? How does it know what to do? Usually, you’ll add instructions about how to build a target (hello) from its dependencies. However, there are default rules.

If you create a file called Makefile along with hello.c in a directory and then issue the make command in that directory you’ll see the command run will be:

cc hello.c -o hello

That’s perfectly fine since most distributions will have cc pointing to the default C compiler. If you run make again, you’ll see it doesn’t build anything. Instead, it will report something like this:

make: 'hello' is up to date.

To get it to build again, you’ll need to make a change to hello.c, use the touch command on hello.c, or delete hello. By the way, the -n command line option will tell make to tell you what it will do without actually doing it. That’s handy when you are trying to figure out what a particular makefile is doing.

If you like, you can customize the defaults using variables. The variables can come from the command line, the environment, or be set in the makefile itself. For example:

CC=gcc CFLAGS=-g hello : hello.c

Now you’ll get a command like:

gcc -g hello.c -o hello

In a complex makefile you might want to add options to ones you already have and you can do that too (the number sign, #, is a comment):

CC=gcc CFLAGS=-g # Comment next line to turn off optimization CFLAGS+=-O hello : hello.c

In fact, the implicit rule being used is:

$(CC) $(CFLAGS) $(CPPFLAGS) hello.c -o hello

Note the variables always use the $() syntax by convention. In reality, if you have a single character variable name you could omit it (e.g., $X ) but that may not be portable, so the best thing to do is always use the parenthesis.

Custom Rules

Maybe you don’t want to use the default rule. That’s no problem. The make program is a little bit of stickler for file format, though. If you start subsequent lines with a tab character (not just a few spaces; a real tab) then that line (along with others) will run like a script instead of the default rules. So while this isn’t very elegant, here’s a perfectly fine way to write our makefile:

hello : hello.c gcc -g -O hello.c

In fact, you ought to use variables to make your rules more flexible just like the default ones do. You can also use wildcards. Consider this:

% : %.c $(CC) $(CPPFLAGS) $(CFLAGS) $< -o $@

This is more or less the same as the default rule. The percent signs match anything and the $< gets the name of the first (and in this case, only) prerequisite which is hello.c. The $@ variable gives you the name of the target (hello, for this example). There are many more variables available, but these will get you started.

You can have multiple script lines (processed by the default system shell, although you can change that) as long as they all start with a tab. You can also add more prerequisites. For example:

hello : hello.c hello.h mylocallib.h

This gets tedious to maintain, though. For C and C++, most compilers (including gcc) have a way to create .d files that can automatically tell make what files an object depends on. That’s beyond the scope of this post, but look up the -MMD option for gcc, if you want to know more.

I Object

Normally, in a significant project, you won’t just compile C files (or whatever you have). You’ll compile the source files to object files and then link the object files at the end. The default rules understand this and, of course, you can write your own:

hello : hello.o mylib.o hello.o : hello.c hello.h mylib.h mylib.o : mylib.c mylib.h

Thanks to the default rules, that’s all you need. The make program is smart enough to see that it needs hello.o, it will go find the rule for hello.o and so on. Of course, you can add one or more script lines after any of these if you want to control what happens for the build process.

By default, make only tries to build the first thing it finds. Sometimes you’ll see a fake target as the first thing like this:

all : hello libtest config-editor

Presumably, then, you’ll have rules for those three programs elsewhere and make will build all three. This works as long as you never have a file name “all” in the working directory. To prevent that being a problem, you can declare that target as phony by using this statement in the makefile:

.PHONY: all

You can attach actions to phony targets, too. For example, you’ll often see something like:

.PHONY: clean clean: rm *.o

Then you can issue make clean from the command line to remove all the object files. You probably won’t make that the first target or add it to something like all. Another common thing to do is create a phony target that can burn your code to a microcontroller so you can issue a command like “make program” to download code to the chip.

Stupid Make Tricks

Usually, you use make to build software. But like any other Unix/Linux tool, you’ll find people using it for all sorts of things. For example, it would be easy to build a makefile that uses pandoc to convert a document to multiple formats any time it changes.

The key is to realize that make is going to build a dependency tree, figure out what pieces need attention, and run the scripts for those pieces. The scripts don’t have to be build steps. They can copy files (even remotely using something like scp), delete files, or do anything else a Linux shell script could do.

But Wait, There’s More

There’s a lot more to learn about make, but with what you already know you can do a surprising amount. You can read the manual, of course, to learn more. If you look at a significant makefile like the one for Arduino projects, you’ll see there’s a lot there we didn’t talk about. But you still should be able to pick through a lot of it.

Next time, I’ll talk a little about schemes that take a different input format and generate a makefile. There are a few reasons people want to do this. One is the abstraction software might determine dependencies or make other changes to the generated makefile. In addition, most of these tools can generate build scripts for different platforms. As an example, if you’ve seen automake or cmake , these are the kinds of tools I’m talking about.

However, you don’t need anything that fancy for a lot of small projects like we often do on a Raspberry Pi or other embedded system. Basic make functionality will take you a long way. Especially for a project targeting a single kind of computer with a small number of files and dependencies.