3
\$\begingroup\$

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 binary
  • debug: build the main binary with debug symbols
  • test: build and run the test binaries
  • clean: remove build artifacts (bin/ and build/ directories)
G. Sliepen
69k3 gold badges74 silver badges180 bronze badges
asked Jul 18, 2024 at 5:51
\$\endgroup\$
0

1 Answer 1

3
\$\begingroup\$

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)
answered Jul 31, 2024 at 13:06
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.