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:
VAR = main.c: Verbatim assignment. Expressions in the right side will not be evaluated.VAR := $(wildcard *.c): Assignment with expression expansion/evaluation.VAR != find . -name '*.c': Shell output.VAR ?= 1: Conditional assignment. If this value was not passed as en environmental variable or directly during the make call.VAR += -flag: Appending to the preceding value of the variable.
Built-in functions
There are a number of built-in functions that are common for most make implementations. This is by no means an
$(SRCS:.c=.o): Text substitution.$(shell ..): Run a shell command.(foreach var,list,text): Looping functions. For example:FILES := $(foreach DIR,$(DIRS),$(wildcard $(DIR)/*)).(if ..),(or ..),(and ..): Logic functions.$(error ..),$(warning ..),(info ..): Displays an error/waring or info text.$(addprefix pfx,list): Adds a prefix to all elements of the given list.
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:
$@: Target name (in our case%.o)$<: Name of the first prerequisite.$?: Name of all prerequisites newer than the target.$^: Name of all prerequisites with spaces between them, without duplicates.$+: Like$^, but those prerequisites listed more than once appear in the order they were listed in the makefile.$|: Name of all order-only prerequisites with spaces between them.$*: The stem from which an implicit rule matches. e.g. if target isdir/a.foo.band the patterndir/a.%.b, the stem isdir/foo
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:
make: will build the project.make run: will build and run or open the resulting file.make clean: Will remove thebuildfolder and/or intermediate and transitory files.
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)