This is a makefile for a project I'm currently working on. I was wondering, if there was a way to write it more concisely, especially concerning tests and libraries. I would like to maintain the current directory structure.
CC = gcc
SRC_DIR = src
TEST_DIR = test
INC_DIR = include
OBJ_DIR = build
BIN_DIR = bin
EXE = $(BIN_DIR)/maumau_server
SRC = $(wildcard $(SRC_DIR)/*.c)
OBJ = $(SRC:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)
OBJ += $(OBJ_DIR)/mongoose.o
OBJ += $(OBJ_DIR)/cjson.o
TEST_OBJ = $(OBJ_DIR)/unity.o
CPPFLAGS = -I$(INC_DIR) -Ilibs/mongoose -Ilibs/cjson -Ilibs/unity/src -MMD -MP
CFLAGS = -Wall -Wextra -Wpedantic -std=gnu99
LDFLAGS =
LDLIBS =
.PHONY: all clean debug test
# --- misc ---
all: $(EXE)
@:
debug: CFLAGS += -g3
debug: $(EXE)
@:
$(BIN_DIR) $(OBJ_DIR):
mkdir -p $@
clean:
@$(RM) -rf $(OBJ_DIR) $(BIN_DIR)
# --- tests ---
test: $(BIN_DIR)/test_card $(BIN_DIR)/test_user
for test in $^; do echo; ./$$test; done
$(BIN_DIR)/test_%: $(OBJ_DIR)/test_%.o $(OBJ_DIR)/%.o $(TEST_OBJ) | $(BIN_DIR)
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
$(OBJ_DIR)/test_%.o: $(TEST_DIR)/test_%.c | $(OBJ_DIR)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
# --- libs ---
$(OBJ_DIR)/unity.o: libs/unity/src/unity.c | $(OBJ_DIR)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
$(OBJ_DIR)/mongoose.o: libs/mongoose/mongoose.c | $(OBJ_DIR)
$(CC) $(CPPFLAGS) $(CFLAGS) -DMG_ENABLE_DIRLIST=0 -c $< -o $@
$(OBJ_DIR)/cjson.o: libs/cjson/cJSON.c | $(OBJ_DIR)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
# --- EXE ---
$(EXE): $(OBJ) | $(BIN_DIR)
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
-include $(OBJ:.o=.d)
Here is some more info on how the thing is supposed to work:
The project has the following directories:
- src/: for .c files
- include/: for .h files
- test/: for test source files
- libs/: for external libraries
- bin/: for the resulting binary files
- build/: for all other build artifacts
The following phony targets exist:
all
: build the main binarydebug
: build the main binary with debug symbolstest
: build and run the test binariesclean
: remove build artifacts (bin/ and build/ directories)
1 Answer 1
Make works best when the targets are to be built in the current working directory (but can find sources anywhere suitable using VPATH), so it's best to be in the target directory when invoking Make. You have found that you had to re-write the built-in pattern rules because your working directory isn't the output directory.
That's also the right way to produce multiple builds with different compiler options. At the moment, it's very easy to create deliverables that are linked from a mix of "debug" and "release" objects:
make debug
touch main.c
make all
If instead, we have debug/Makefile
:
VPATH=../src
include ../Makefile
CFLAGS += -g3
and the obvious similar release/Makefile
, then we not only don't have to rewrite the built-in rules, but we also never have both debug and non-debug objects in the same build. And we can actually build from anywhere, which can be an advantage when building other users' code (where we wouldn't normally have write permission).
The difference between debug and normal builds is unlike my expectations. I would always include debugging information in all builds, and strip it out later where size is important - you want to keep it around for when users send you core dumps that you need to interpret.
Conversely, the options I actually would expect to differ (-DNDEBUG -O3
in release builds and perhaps -O0
in debug builds) don't seem to be present.
It's weird to require users to link installed library headers into your source directory. Much better to find them using the provided tools:
some-object.o: CFLAGS += $(shell pkg-config --libs SuiteSparse_Mongoose)
$(EXE): LIBS += $(shell pkg-config --libs SuiteSparse_Mongoose)
I would be more bullish about tests, and make the all
target depend on passing the unit tests before it even attempts to build anything:
all: tests $(EXE)
Then there's never a situation in which failing tests can be overlooked or ignored.
There's no install
target, which makes it harder for anyone to create a package from your program. As a starting point, I'd probably write
.PHONY: install
install:
install -D -d $(DESTDIR)/usr/bin
install -m 755 $(DESTDIR)/usr/bin/$(EXE)