Writing a small and complete makefile

I have to admit that my past with make is not all marry and nice. At first I have learned it by looking through the existing makefiles and from some old online tutorials. Then, at the university, they literally said nothing about it and I just continued to use it as whatever. My life continued and each time I wrote a makefile it looked totally different.

At last, I sat down calmly and read the manual and, oh dear, many things just became easier. Now, let me tell you about my common mistakes and wrong assumptions I had.

Please note that this article is no intended to cover the very basics.

First of all, which is probably the most revolutionary thing about make I have learned, one does not need a makefile at all. Make is ridiculously smart (and quite stupid, too). The key are implicit rules and implicit variables.

$ ls
example.c
$ make example
cc	example.c	-o example

As you can see, there is no makefile. We call make with a target name and it starts to think about it's life. When analyzing the given target it starts to look for implicit rule by going through possible implicit prerequisites and it encounters example.c in the directory. There is a matching implict rule for it. It starts to look if it can do something about the example.c. There is nothing to do as there is no implict rule liek that. It goes back to original target and builds it.

Let's say we don't want to type in target name each time we want to build. Nothing easier. Default target is fist one that appears in the makefile:

$ ls
example.c Makefile
$ cat Makefile
example:
$ make
cc	example.c	-o example

Obviously, such simple cases are quite unusual. Still, they are quite illustrative. However, there is more to it. As I already mentioned there are also implicit variables. They are used from within implicit rules and can be modified for various purposes.

There is a common redundancy involving implicit variables that I have noticed in multitude of makefiles I have seen and used. Variables describing compilers are very often pointlessly reassigned:

CC:=gcc
CXX:=g++

I can think of cases when assigning them a new value is the way to go but I have yet to see such case in the wild. Of course, one could argue that default CC is cc and not gcc. That's true. However, in most cases this argument can be invalidated by a simple readlink -f $(which cc).

Now then, let's write a small useful and complete makefile. I have a directory with various code snippets in it. I do my quick experiments there to check if particular things work as expected in limited and controlled environment. Most of them are usually built from just a single file. Of course, I could put an entire command in terminal each time, do a lot of ^Rs or whatever. Each time one of the snippets would require a library, it would become more and more of a bother. I figured out, I could write an extendable makefile for that:

SNIPS:=$(basename $(wildcard *.cpp) $(wildcard *.c))
all: $(SNIPS)
clean: ; $(RM) $(SNIPS)
.PHONY: all clean

Surprisingly that's enough. First line will create a variable containing all of cpp and c files in the directory with suffixes removed. Next line will add a default target that builds all of the snippets uisng implict rules for its dependencies. Clean will do whatever clean does. The last line makes both all and clean a phony target, one that is not associated with a file name.

What does it mean it is extendable? If one wants to add a library to one of the targets - just add a line! Please note, that this will work in a lot of other cases and it's a nice way of assigning variables for selected targets e.g.:

fcgi: LDLIBS:=-l:libfcgi.a

It is also worth noting that variables can be populated from environment:

$ ls
example.c
$ export LDLIBS=-lfcgi
$ make example
cc	example.c	-lfcgi	-o example

Of course, out in the wild building is usually more complicated than just a single file with few libraries and without arcane linking. Once you start working, keep in mind whatever make has already prepared for you. As an example/exercise:

CXXFLAGS += $(shell pkg-config --cflags sdl2)
LDLIBS += $(shell pkg-config --libs sdl2)
objects := $(patsubst %.cpp,%.o,$(wildcard %.cpp))

app: $(objects)
	$(CXX)	-o $@	$^	$(LDFLAGS)	$(LDLIBS)

.PHONY: clean

clean:
	$(RM) app $(objects)

Those were just few selected notes I made and considered worth writing about. All in all, I highly encourage everyone to read GNU make manual and its manpage, especially if they haven't done it yet. It's surprisingly easy to read it and, while it's probably just me, it was extremely fun to read.

See also