Makefiles


I use Makefiles in many of my projects and like many people I have a love/hate relationship with them. The main draw of using makefiles is that make is a very ubiquitous tool in most operating systems, and even though they may not be fully compatible with each other (some have their own quirks and extensions) they can be easily adapted to work on many circumstances. Makefiles are used for automation and compilation, normally expecting at least one input file and one output file.

With make, we can make sure we avoid unnecessary computing work. For example if you compile three files a.c, b.c and c.c into separate object files that are later linked into a final out executable, if we make changes to only b.c we want to avoid recompiling a.c and c.c since sometimes these compilations take a long time to complete. I also use Makefiles to generate LaTeX papers in different formats, not only for pure programming tasks. Sometimes I used them as glorified shell scripts that give me different targets I can use.

Syntax

The syntax and rules for Makefiles are a bit arcane and many people seem to have trouble with them. As usual, keeping things simple is generally preferred, and although I’m no Makefile wizard, I’m quite satisfied with this tool, specially compared with other, more complex build systems. Pay attention to the use of tab characters \t for indentation, otherwise make will likely not work.

Targets and prerequisites

Make rules follow the following format:

target: prerequisite | order-only-prerequisites
    commands...

The target is the name of the rule or output file to be generated. Prerequisites are the rules that are necessary in order to build the target. Normally the prerequisites are recursively built left to right if some of the prerequisite files change, the target will be generated. There are occasions where we don’t want this to happen, for example we only want to build the prerequisite once and don’t care if it has changed. This is useful for directory creation and such.

exe: a.o b.o | $(BUILD_DIR)
    touch exe
    echo "GOOD"

exe2: a.o b.o $(BUILD_DIR)
    touch exe2
    echo "BAD"

$(BUILD_DIR):
    mkdir -p $(BUILD_DIR)

In this case make exe2 will keep trying to build the target, whereas make exe knows that no rebuilding is necessary, since we ignore the changes for the build directory.

Variable assignment

We can assign variables in different ways:

Built-in functions

There are a number of built-in functions that are common for most make implementations. This is by no means an

Implicit rules

By default, we have a number of implicit rules built into make. These can be overridden or disabled if desired (by adding .SUFFIXES: at the start of the file).

For example for C files:

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

The percent symbol % denotes pattern substitution, so this means that for each a.c b.c c.c file we will generate the corresponding a.o b.o c.o file.

Notice those magic variables $@ and $<? This is where a lot of confusion with Makefiles stems. I pretty much always have to look at a reference when I have to use these things:

My standard makefile

I tend to copy my makefiles from project to project, adjusting things as needed. For example, for my C projects as use something like this:

.POSIX:
.SUFFIXES:
.PHONY: main run clean

# Source code location and files to watch for changes.
SRC_DIR     := src
BUILD_DIR   := build
SRC_MAIN    := $(SRC_DIR)/main.c
SRC_OBJ     :=
OBJECTS     := $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR)/%.o, $(SRC_OBJ))
WATCH_SRC   := $(shell find $(SRC_DIR) -name "*.c" -or -name "*.s" -or -name "*.h")
INC_DIRS    := $(shell find $(SRC_DIR) -type d)
INC_FLAGS   := $(addprefix -I,$(INC_DIRS))

# Output names and executables.
TARGET := hello
BIN    := $(BUILD_DIR)/$(TARGET)

# Main compilation tool paths.
CC       := gcc
LD       := ld
AS       := as
OBJDUMP  := objdump

# Compiler and linker configuration.
CFLAGS         := -Wall -Wextra -pedantic
CFLAGS         += $(INC_FLAGS)
LDFLAGS        :=
LDLIBS         :=
RELEASE_CFLAGS := -O2 -DNDEBUG
DEBUG_CFLAGS   := -O0 -DDEBUG -g

# Setup debug/release builds.
DEBUG ?= 0
ifeq ($(DEBUG), 1)
    CFLAGS += $(DEBUG_CFLAGS)
else
    CFLAGS += $(RELEASE_CFLAGS)
endif

main: $(BIN)

# Compile and link everything in one go.
$(BIN): $(SRC_MAIN) $(OBJECTS) $(WATCH_SRC) | $(BUILD_DIR)
    $(CC) $(CFLAGS) $(LDFLAGS) -o $(BIN) $(SRC_MAIN) $(OBJECTS) $(LDLIBS)

# Run the program
run: $(BIN)
    ./$(BIN)

# Remove build directory.
clean:
    rm -rf $(BUILD_DIR)

# Create the build directory.
$(BUILD_DIR):
    mkdir -p $(BUILD_DIR)

# Inference rules for C files.
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR)
    $(CC) $(CFLAGS) -c $< -o $@

We want to make our files posix compatible, so we make sure to specify it as the first non comment line. We also mark our PHONY targets (targets that don’t generate any files).

By default make already have a series of inference rules, but I like to structure my code in a certain way, so .SUFFIXES: removes these, that way I can avoid having object files spread around my source tree. All intermediate objects and the final executable will be stored in the BUILD_DIR folder and my code resides on a SRC_DIR directory. If I am using a unity build approach, I’ll just generate the main object file (SRC_MAIN), but sometimes I like to compile larger/more complex targets as separate .o files (SRC_OBJ).

Instead of generating dependency trees, I just tell the build system which files to watch for changes (WATCH_SRC). TARGET and BIN setup the main name of the executable and the final binary file that gets generated.

Depending on which architecture I’m compiling for, I will need to set specific compilers for C and assembly (as) or the linker ld. Similarly I configure the flags for the C compiler and linker with the usual CFLAGS and LDFLAGS and any external pre-compiled libraries with LDLIBS. I also setup different optimization flags for debug and release builds.

This makefile is supposed to serve as a base and I’ll grow it or change it depending on the project, but in general my user interface remains the same:

I can configure compilation parameters by passing them to make directly. For example to generate a debug build in a separate directory, with a different target name we can use:

make run DEBUG=1 BUILD_DIR=build-debug TARGET=debug

I’ll typically add other makefile variables if I need to pass compilation parameters for example I use the following to pass some macro values in one of my projects:

KBD_PATH   ?= /dev/input/event1
MOUSE_PATH ?= /dev/input/mice
C_DEFINES  := -DKBD_PATH=\"$(KBD_PATH)\" -DMOUSE_PATH=\"$(MOUSE_PATH)\"
CFLAGS += $(C_DEFINES)

Resources