Making make Easy

Posted on 2016-04-18

If you come from a systems programming world, make is a pretty amazing tool. Being able to keep track of a dependence graph of files, and using the last modified time-stamp to automate tasks seems pretty clever and general. It’s small, composable, versatile — a true unix tool.

But if you are something of a beginner to C/C++, you may feel a little indignant that you are being forced to write your dependencies in two different places (both the includes in your source files and the makefile itself). Yeah, it is important to compile your project in chunks and link later, but shouldn’t gcc or clang be able to figure out how, all by itself?

There are reasons why things aren’t this way, and many of them are legacy reasons (and, yes, many of them are actually good reasons). All the same, I long ago set out to make something of an “automated general makefile.” Many of my students have asked me how exactly it works, so I decided to make this step-by-step walk-through of making a general makefile that should work with most simple C/C++ projects.

Typical Object and Executable Rules

For a typical project (especially for beginners), chances are most of your makefiles look the same, and are probably broken into two main sets of rules: object rules, and executable rules.

Object Rules

Most rules in your makefile likely look something like this:

mylib.o : mylib.c mylib.h
    $(CC) mylib.c -c -o mylib.o

This is stupidly redundant! Despite popular opinions, computers are supposed to make your life easier. Makefiles provided a number of automatic variables to fix this. Here are arguably the three most important:

Variable Meaning
$@ Name of the target.
$< Name of the first prerequisite.
$^ Name of all the prerequisites.

This is made more complicated if you are dealing with archives, or are dealing with duplicates in your linking process. I’m just going to cover the basics. With this lovely information, let’s rewrite this rule!

mylib.o : mylib.c mylib.h
    $(CC) $< -c -o $@

There we go! Much more readable… Ok, maybe not more readable, but this will help us start to generalize things.

Executable Rules

Just like object files, most simple projects have nearly identical final rules for their executable.

exec : thing1.o thing2.o thing3.o thing4.o thing5.o
    $(CC) thing1.o thing2.o thing3.o thing4.o thing5.o -o exec

Again, we can much more conciesly express this with

exec : thing1.o thing2.o thing3.o thing4.o thing5.o
    $(CC) $^ -o $@

But These Rules Are Still Pretty Redundant!

Oh, yeah, totally! Let’s see how we can fix that… First thing is first — lets make a list of all the files we have!

FILES := thing1.c thing2.c thing3.c thing4.c thing5.c

Ok, so now what? Well, we probably will have an object file for each one of those source files.

OBJECTS := $(FILES:.c=.o)

That little chunk of magic essentially says make a copy of FILES, replacing the .c extension with .o. Note that the whitespace matters here (and in general everywhere for makefiles).

Now that we have a list of files and objects, let’s make a simple makefile which builds every object file, and links them all together into one executable!

# List our files
FILES := thing1.c thing2.c thing3.c thing4.c thing5.c
# Turn those files into a list of objects
OBJECTS := $(FILES:.c=.o)

all : exec

# Object file rules
%.o : %.c
    $(CC) $< -c -o $@

# Executable rule
exec : $(OBJECTS)
    $(CC) $^ -o $@

Yeah, now we be jammin’! So what’s going on with the %.o rule? Well this is a pattern rule, which does pretty much what you’d expect. Anything looking for a rule of the form stem.o that doesn’t exist will be matched, and will depend on stem.c. (Right now I am ignoring header files, don’t worry!)

Detecting Files

Ok, we’re getting somewhere, but chances are we probably want an object file for every .c file in our project. Can we make make automatically build our FILES list? Yes, of course we can, that’s why I asked the question. While we’re doing this, let’s try and clean things up a bit.

# Compiler
CC       := clang

# Directories and targets
SRCDIR   := src
BUILDDIR := build
TARGET   := myprog

# Build a list of files automatically!
SRCEXT   := c
SOURCES  := $(shell find $(SRCDIR) -type f -name *.$(SRCEXT))
OBJECTS  := $(patsubst $(SRCDIR)/%, $(BUILDDIR)/%,$(SOURCES:.$(SRCEXT)=.o))

.SECONDEXPANSION:

all : $(TARGET)

$(TARGET) : $(OBJECTS)
    $(CC) $^ -o $@

$(BUILDDIR)/%.o : $(SRCDIR)/%.c
    @mkdir -p $$(dirname $@)
    $(CC) -o $@ -c $<

Starting to look pretty fancy, right? Other than the general cleaning, this adds a way for make to automatically detect the source files that will be needed when compiling.

SOURCES := $(shell ...) uses a make function called shell. Essentially the function just runs it’s arguments as a shell command, and evaluates to its result. We used find to look for all files ending in SRCEXT in the SRCDIR directory. Now make can know what files to compile, without us having to tell it!

OBJECTS := $(pathsubst ...) uses the pathsubst function to make sure our object files go to BUILDDIR instead of SRCDIR. This will make sure we don’t have junk files everywhere when we make.

.SECONDEXPANSION and the mkdir are just housekeeping. It keeps our compiler from complaining that there isn’t a directory to put our object files. See the make manual on secondary expansion to get an idea of what is going on.

What about Headers?

Right now, if you edit one of your .c/.cc/.cpp files, make will know to recompile properly. But let’s say you edit one of your header files — none of our rules depend on those header files!

We could try something like this:

# A poor attempt at dealing with header files.
$(BUILDDIR)/%.o : $(SRCDIR)/%.c $(HDIR)/%.h
    $(CC) -o $@ -c $<

But this assumes that for every .c file there is a .h file. Even for small and simple projects this is a pretty bad assumption! We are going to have to get pretty fancy here. How can we automatically figure out what files are included by a given source file?

The -M Compilation Flags

Luckily this is a solved problem. Consider the following C program

// Dumb C Program
#include "myheader.h"

Pretty crazy, I know. Now check this out.

$ # Run your compiler with -M
$ clang -M new.c
new.o: new.c myheader.

The -M flag essentially says “read this file, and build me makefile rules!” There are some variations on this flag, bug we will use just a few.

Flag Meaning
M Build makefile dependencies, and stop.
MM Build makefile dependencies, ignore files on the system path (like stdio.h).
MP Give a dumby rule for included files. Prevents make from spitting out an error if the header is deleted.
MD Compile the file like normal WHILE building makefile dependencies.
MF Give a name to this “mini-makefile”

Using Sub-Makefiles

So how is our makefile going to use this? With the include directive. By simply including this files, make will know about these subtle dependencies. Putting it together gives us something like:

# Compiler
CC       := clang

# Directories and targets
SRCDIR   := src
BUILDDIR := build
TARGET   := myprog

# Build a list of files automatically!
SRCEXT   := c
SOURCES  := $(shell find $(SRCDIR) -type f -name *.$(SRCEXT))
OBJECTS  := $(patsubst $(SRCDIR)/%, $(BUILDDIR)/%,$(SOURCES:.$(SRCEXT)=.o))
DEPS     := $(OBJECTS:%.o=%.d)

.SECONDEXPANSION:

all : $(TARGET)

$(TARGET) : $(OBJECTS)
    $(CC) $^ -o $@

$(BUILDDIR)/%.o : $(SRCDIR)/%.c
    @mkdir -p $$(dirname $@)
    $(CC) -c -o $@ -MMD -MP -MF ${@:.o=.d} $<

# Include all dependence information from the last build
-include $(DEPS)

Finally! We have a makefile that just… generally works!

A Final Cleanup

Before we call this good, let’s do one last cleanup. Part of this cleanup should involve our project structure. I am going to assume my project looks something like this.

project
| -- src     (Source Files)
| -- include (Header Files)
| -- lib     (3rd Party Libraries)
| -- build   (Object Files)
| -- bin
|    | -- target

This is a pretty typical project layout — if you’re not sure how to organize your simple C / C++ project, consider that layout. So, without really much explanation, here is our final makefile:

# Compiler and flags
CC       := clang
CFLAGS   := $(CLAGS) -Wall -Wextra -O3
INC      := -I include
LIB      := -lmath -pthread #For example

# Directories and targets
SRCDIR   := src
BUILDDIR := build
TARGET   := bin/myprog

# List of files
SRCEXT   := c
SOURCES  := $(shell find $(SRCDIR) -type f -name *.$(SRCEXT))
OBJECTS  := $(patsubst $(SRCDIR)/%, $(BUILDDIR)/%,$(SOURCES:.$(SRCEXT)=.o))
DEPS     := $(OBJECTS:%.o=%.d)

# Colors
GREEN  = \e[0;32m
YELLOW = \e[1;33m
RED    = \e[0;31m
NC     = \e[0m

.SECONDEXPANSION:
.PHONY: clean

all : $(TARGET)

$(TARGET) : $(OBJECTS)
    @echo -e "$(GREEN) Linking... $(NC)"
    $(CC) $(LIB) $^ -o $@

$(BUILDDIR)/%.o : $(SRCDIR)/%.c
    @echo -e "$(GREEN) Building $@... $(NC)"
    @mkdir -p $$(dirname $@)
    $(CC) $(CFLAGS) $(INC) -c -o $@ -MMD -MP -MF ${@:.o=.d} $<

clean:
    @echo -e "$(GREEN) Cleaning... $(NC)";
    $(RM) -r $(BUILDDIR) $(TARGET)

-include $(DEPS)

As long as your project is of the given simple structure, this will just automatically work!

Limitations

You might want to know why everyone doesn’t do this. Just glossing through some reasons quickly:

  1. Sometimes it doesn’t make sense to have a one-to-one relationship with source files and object files.
  2. This will not yield a fast makefile. Now, if it’s speed you need, consider investigating one of the alternative build systems, like ninja. This is more directed at smaller projects.
  3. No support for the portability power that is provided by something like cmake or autotools.

What this file really does is give you a simple solution to build systems in C, which is easily human editable if need be. Also, hopefully you’ve learned a few things about how to write makefiles, and organize your C / C++ projects.

Tyler Cecil's Blog
Comments mean a lot!
More Posts Write Me