Often times comes the need to use a Makefile for compiling our C/C++ library or application and we may end up making some simple version of it which may work for a while. However, with rapidly growing projects and the need to add different features to our compilation scripts it is better to follow some rules at the time of first making our Makefiles.
To achieve this we must follow some good practices that will allow for a short and concise script that can be read and debugged easily, as well as later make full use of some features GNU Make provides.
Build directory
One common occurance is not making a build directory and
directly compiling our targets into the root of our
project or maybe making the directory without proper
structuring, where all blobs and targets are mangled
together in a single directory, which may make things
hard to find and/or clean. So to best avoid issues like
that, we must make a cache/
and build/
directories
to separate well our targets from our pre-compiled
objects.
Example:
Let's say our project has the following file structure:
.
├── src/
│ ├── services/
│ │ ├── client.c
│ │ ├── client.h
│ │ ├── server.c
│ │ ├── server.h
│ │ ├── mod.c
│ │ └── mod.h
│ ├── utils/
│ │ ├── log.c
│ │ ├── log.h
│ │ ├── mod.c
│ │ └── mod.h
│ ├── lib.c
│ ├── lib.h
│ └── main.c
└── Makefile
Where:
lib.c
compiles to our librarymain.c
compiles to our executable*.c
compile to our object files*.h
are copied to ourinclude/
Target:
.
├── src/
│ └── ...
├── build/
│ ├── bin/
│ │ └── project
│ ├── lib/
│ │ └── libproject.so
│ └── include/
│ └── project/
│ ├── services/
│ │ ├── client.h
│ │ ├── server.h
│ │ └── mod.h
│ ├── utils/
│ │ ├── log.h
│ │ └── mod.h
│ └── lib.h
├── cache/
│ ├── services/
│ │ ├── client.o
│ │ ├── server.o
│ │ └── mod.o
│ ├── utils/
│ │ ├── log.o
│ │ └── mod.o
│ └── lib.o
└── Makefile
Apart from our build/
directory we will need a
cache/
to store all our object files and keep our
build directory clean from all artifacts of compilation.
To achieve this we must make the following definitions:
Sources and targets
# We can define something like "TARGET" as to not have
# to copy the name of our library/application in every
# recipe
TARGET = project
# Source
SRC = $(shell find src/ -name '*.c' ! -name 'main.c')
INC = $(shell find src/ -name '*.h')
# Cache
OBJ = $(SRC:src/%.c=cache/%.o)
# Build
INCLUDE = $(INC:src/%.h=build/include/$(TARGET)/%.h)
LIB = build/lib/lib$(TARGET).so
BIN = build/bin/$(TARGET)
Using the find
command will make it easy to avoid
listing source files manually, which often leads to bugs
by forgetting to add some file to the Makefile.
This command is extremely helpful and comes in handly a lot of times, so it's best to give its manual page a read to make more complex builds in the future.
$ man find
Recipes
We can incorporate the following expesions in our recipes:
Expression | Expands to |
---|---|
$(@D) | Directory of the target |
$@ | Target |
$^ | All dependencies |
$< | First dependency |
Like so:
all: $(INCLUDE) $(LIB) $(BIN)
# Compile the object files
cache/%.o: src/%.c
@mkdir -p $(@D)
$(CC) $^ -o $@ -c
# Copy the include files
build/include/$(TARGET)/%.h: src/%.h
@mkdir -p $(@D)
cp $^ $@
# Build our library using the cached object files
$(LIB): $(OBJ)
@mkdir -p $(@D)
$(CC) $^ -o $@ -shared
# Build our executable using only our library
# Note that `$<` only matches the first dependency
$(BIN): src/main.c $(LIB)
@mkdir -p $(@D)
$(CC) $< -o $@ -l$(TARGET) -Lbuild/lib
clean:
rm -rf build/
rm -rf cache/
Now we can just compile.
$ make all
cp src/utils/log.h build/include/project/utils/log.h
cp src/utils/mod.h build/include/project/utils/mod.h
cp src/services/client.h build/include/project/services/client.h
cp src/services/server.h build/include/project/services/server.h
cp src/services/mod.h build/include/project/services/mod.h
cp src/lib.h build/include/project/lib.h
gcc src/utils/mod.c -o cache/utils/mod.o -c
gcc src/utils/log.c -o cache/utils/log.o -c
gcc src/services/client.c -o cache/services/client.o -c
gcc src/services/server.c -o cache/services/server.o -c
gcc src/services/mod.c -o cache/services/mod.o -c
gcc src/lib.c -o cache/lib.o -c
gcc cache/utils/mod.o cache/utils/log.o cache/services/client.o cache/services/server.o cache/services/mod.o cache/lib.o -o build/lib/libproject.so -shared
gcc src/main.c -o build/bin/project -lproject -Lbuild/lib
You can also mute the output of cp
as to avoid filling
the screen with copy commands:
# Copy the include files
build/include/$(TARGET)/%.h: src/%.h
@mkdir -p $(@D)
- cp $^ $@
+ @cp $^ $@
Define the compiler
This is usually not that much of an issue if you are not making use of some compiler's unique extensions, like GCC's nested functions or Clang's blocks which are commonly used in some modern C projects for simplyfing a lot of procedures.
So to ensure the project is compiled with the C compiler you're working with you should set it up like this:
CC = clang
But to also allow for the developer to compile with any
compiler of their liking by manually setting the
variable in the make
command you should use :=
.
CC := clang
Now we can set CC
manually to change our compiler.
$ make all
...
clang src/utils/mod.c -o cache/utils/mod.o -c
...
clang src/main.c -o build/bin/project -lproject -Lbuild/lib
$ make all CC=gcc
...
gcc src/utils/mod.c -o cache/utils/mod.o -c
...
gcc src/main.c -o build/bin/project -lproject -Lbuild/lib
Define flags
This is very straight forward and mostly just part of C fundamentals, but here's the basic usage of the flags:
C_FLAGS = -Ofast -Wall -pedantic
ifeq ($(CC), gcc)
C_FLAGS += -Wformat -Wlogical-op -fstrength-reduce
else ifeq ($(CC), clang)
C_FLAGS += -fstack-protector
endif
cache/%.o: src/%.c
@mkdir -p $(@D)
$(CC) $(C_FLAGS) $^ -o $@ -c
build/include/$(TARGET)/%.h: src/%.h
@mkdir -p $(@D)
cp $^ $@
$(LIB): $(OBJ)
@mkdir -p $(@D)
$(CC) $(C_FLAGS) $^ -o $@ -shared
$(BIN): src/main.c $(LIB)
@mkdir -p $(@D)
$(CC) $(C_FLAGS) $< -o $@ -l$(TARGET) -Lbuild/lib
$ make all
...
clang -Ofast -Wall -pedantic -fstack-protector-all src/utils/mod.c -o cache/utils/mod.o -c
...
clang -Ofast -Wall -pedantic -fstack-protector-all src/main.c -o build/bin/project -lproject -Lbuild/lib
$ make all CC=gcc
...
gcc -Ofast -Wall -pedantic -Wformat -Wlogical-op -fstrength-reduce src/utils/mod.c -o cache/utils/mod.o -c
...
gcc -Ofast -Wall -pedantic -Wformat -Wlogical-op -fstrength-reduce src/main.c -o build/bin/project -lproject -Lbuild/lib
Secondary expansion
Secondary expansion
allows us to make use of some amazing tricks (as seen in
the hyperlink), but the one that's simplest and nicest
to look at is definitely expanding $(@D)
in the
order-only-prerequisites
section.
By adding a recipe for directories we can make it so that they are created only once and avoid manually creating them inside the recipe of all our targets.
# Enabling feature
.SECONDEXPANSION:
# Compile the object files
cache/%.o: src/%.c | $$(@D)/
$(CC) $^ -o $@ -c
# Copy the include files
build/include/$(TARGET)/%.h: src/%.h | $$(@D)/
cp $^ $@
# Build our library using the cached object files
$(LIB): $(OBJ) | $$(@D)/
$(CC) $^ -o $@ -shared
# Build our executable using only our library
# Note that `$<` only matches the first dependency
$(BIN): src/main.c $(LIB) | $$(@D)/
$(CC) $< -o $@ -l$(TARGET) -Lbuild/lib
# Directory recipe
%/:
mkdir -p $@
$ make all
mkdir -p build/include/project/utils/
cp src/utils/log.h build/include/project/utils/log.h
cp src/utils/mod.h build/include/project/utils/mod.h
mkdir -p build/include/project/services/
cp src/services/client.h build/include/project/services/client.h
cp src/services/server.h build/include/project/services/server.h
cp src/services/mod.h build/include/project/services/mod.h
cp src/lib.h build/include/project/lib.h
mkdir -p cache/utils/
gcc src/utils/mod.c -o cache/utils/mod.o -c
gcc src/utils/log.c -o cache/utils/log.o -c
mkdir -p cache/services/
gcc src/services/client.c -o cache/services/client.o -c
gcc src/services/server.c -o cache/services/server.o -c
gcc src/services/mod.c -o cache/services/mod.o -c
gcc src/lib.c -o cache/lib.o -c
mkdir -p build/lib/
gcc cache/utils/mod.o cache/utils/log.o cache/services/client.o cache/services/server.o cache/services/mod.o cache/lib.o -o build/lib/libproject.so -shared
mkdir -p build/bin/
gcc src/main.c -o build/bin/project -lproject -Lbuild/lib
Now we can be pedantic printing the directories created,
or we can also just mute mkdir
to get a cleaner
output.
%/:
- mkdir -p $@
+ @mkdir -p $@
The final product
Adding everything up from this article we end up with this Makefile:
CC := clang
C_FLAGS = -Ofast -Wall -pedantic
ifeq ($(CC), gcc)
C_FLAGS += -Wformat -Wlogical-op -fstrength-reduce
else ifeq ($(CC), clang)
C_FLAGS += -fstack-protector-all
endif
TARGET = project
SRC = $(shell find src/ -name '*.c' ! -name 'main.c')
INC = $(shell find src/ -name '*.h')
OBJ = $(SRC:src/%.c=cache/%.o)
INCLUDE = $(INC:src/%.h=build/include/$(TARGET)/%.h)
LIB = build/lib/lib$(TARGET).so
BIN = build/bin/$(TARGET)
all: $(INCLUDE) $(LIB) $(BIN)
.SECONDEXPANSION:
cache/%.o: src/%.c | $$(@D)/
$(CC) $(C_FLAGS) $^ -o $@ -c
build/include/$(TARGET)/%.h: src/%.h | $$(@D)/
@cp $^ $@
$(LIB): $(OBJ) | $$(@D)/
$(CC) $(C_FLAGS) $^ -o $@ -shared
$(BIN): src/main.c $(LIB) | $$(@D)/
$(CC) $(C_FLAGS) $< -o $@ -l$(TARGET) -Lbuild/lib
%/:
@mkdir -p $@
clean:
rm -rf build/
rm -rf cache/