I have created a Makefile template file which I hope to be able to easily drop in to a folder and have useful targets ready. I am, however, not very familiar with make
's language and am looking for suggestions about correctness/readability, or ways to make this file simpler. The template is only intended to be used for C code (not C++).
The Makefile expects the project structure to be as follows -
project_root/
|--Makefile
|--build/ <--- Binaries
|--src/ <--- Source files
|--obj/ <--- Objects and pre-compiled headers
|--inc/ <--- Header files
This is the full Makefile -
# r - diables built-in rules
# R - diables built-in variables
MAKEFLAGS += -rR
# Make all targets depend on this Makefile
.EXTRA_PREREQS:= $(abspath $(lastword $(MAKEFILE_LIST)))
# Program name
P=
INCLUDE_DIR=inc
OBJECT_DIR=obj
BUILD_DIR=build
SOURCE_DIR=src
CC=gcc
CFLAGS=-Wall -Wextra -Wpedantic -I$(INCLUDE_DIR) -I$(OBJECT_DIR) # OBJECT_DIR contains the precompiled headers
LDFLAGS=
DBGFLAGS=-fsanitize=address,undefined,integer-divide-by-zero -fno-omit-frame-pointer
LDLIBS=
HEADERS=algos sorts array shared # Header files
UNITS=algos sorts array # Compilation units
# Generate lists of headers, sources, objects, and object-with-debugging-symbols
HDRS=$(patsubst %, $(OBJECT_DIR)/%.h.gch, $(HEADERS))
SRCS=$(patsubst %,$(SOURCE_DIR)/%.c, $(UNITS))
OBJS=$(patsubst %, $(OBJECT_DIR)/%.o, $(UNITS))
OBJS_DEBUG=$(patsubst %,$(OBJECT_DIR)/%.o.debug, $(UNITS))
# build main binary by default
.PHONY: default
default: $(BUILD_DIR)/$(P)
# build main binary, main binary with debug symbols, and main binary built for valgrind
.PHONY: all
all: $(BUILD_DIR)/$(P) $(BUILD_DIR)/$(P)_debug $(BUILD_DIR)/$(P)_grind
# Build pre-compiled header file
.SECONDARY: $(OBJECT_DIR)/%.gch
$(OBJECT_DIR)/%.gch: $(INCLUDE_DIR)/%
@echo Building pre-compiled header: $@
@mkdir -p $(OBJECT_DIR)
$(CC) $(CFLAGS) $(LDFLAGS) $< -o $@
# Build object file
.SECONDARY: $(OBJECT_DIR)/%.o
$(OBJECT_DIR)/%.o: $(SOURCE_DIR)/%.c $(DEPS)
@echo Building object: $@
@mkdir -p $(OBJECT_DIR)
$(CC) $(CFLAGS) $(LDFLAGS) $< -c -o $@
# Build object file with debugging symbols
.SECONDARY: $(OBJECT_DIR)/%.o.debug
$(OBJECT_DIR)/%.o.debug: LDFLAGS+=-g
$(OBJECT_DIR)/%.o.debug: $(SOURCE_DIR)/%.c $(DEPS)
@echo Building object with debug symbols: $@
@mkdir -p $(OBJECT_DIR)
$(CC) $(CFLAGS) $(LDFLAGS) $< -c -o $@
# Build main binary
$(BUILD_DIR)/$(P): $(OBJS)
@echo Building $@
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(LDFLAGS) $^ -O3 -o $@
# Build binary with debugging symbols. Uses -g and ASan. To be used for valgrind
$(BUILD_DIR)/$(P)_debug: LDFLAGS+=-g
$(BUILD_DIR)/$(P)_debug: $(OBJS_DEBUG)
@echo Building $@
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(LDFLAGS) $(DBGFLAGS) $^ -o $@
# Build binary with debugging symbols. Uses -g only. Cannot be used for valgrind
$(BUILD_DIR)/$(P)_grind: LDFLAGS+=-g
$(BUILD_DIR)/$(P)_grind: $(OBJS_DEBUG)
@echo Building $@
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(LDFLAGS) $^ -o $@
# Run program
.PHONY: run
run: $(BUILD_DIR)/$(P)
$^
# Start gdb on debug build
.PHONY: debug
debug: $(BUILD_DIR)/$(P)_debug
gdb $^
# Run valgrind on grind build
.PHONY: grind
grind: $(BUILD_DIR)/$(P)_grind
valgrind --track-origins=yes --leak-check=full --leak-resolution=high $^
# Run linter
.PHONY: lint
lint: $(SRCS)
clang-tidy\
--quiet\
--checks=*,\
-llvmlibc-restrict-system-libc-headers,\
-altera-id-dependent-backward-branch,\
-modernize-macro-to-enum,\
-altera-unroll-loops,\
-llvm-include-order,\
-bugprone-reserved-identifier,\
-cert-dcl37-c,\
-cert-dcl51-cpp,\
-misc-no-recursion,\
-google-readability-todo\
$^\
-- -I$(INCLUDE_DIR) # Have to include this or clang will complain about not being able to find header files. See https://stackoverflow.com/a/56457021/15446749
# Format all sources
.PHONY: format
format: $(SRCS)
clang-format --fallback-style=LLVM -i $(SRCS)
.PHONY: clean
clean:
rm -f $(OBJECT_DIR)/*.o
rm -f $(OBJECT_DIR)/*.o.debug
rm -f $(OBJECT_DIR)/*.h.gch
rm -f $(BUILD_DIR)/$(P){,_debug,_grind}
rmdir obj
rmdir build
-
\$\begingroup\$ May I suggest using cmake instead of making makefile templates? \$\endgroup\$Ajinkya Kamat– Ajinkya Kamat2023年05月03日 21:54:04 +00:00Commented May 3, 2023 at 21:54
2 Answers 2
I'm not a C developer, so I don't know what's idiomatic in that community. That said:
- The linter flags could probably be in a configuration file, to avoid cluttering up actual code.
- Making all flags depend on the Makefile itself is an interesting pattern. Do you have a reference discussing it? I feel like it could possibly cause more problems than it solves.
- The clean target references both
OBJECT_DIR
andobj
directly. You probably want to always referenceOBJECT_DIR
. - Why not delete
OBJECT_DIR
andBUILD_DIR
recursively? Nothing else should be writing to those directories, after all. - Idiomatically, uppercase variables are free to be overridden at runtime. But
HDRS
etc look like they should not be overridden, so they could be lowercase. - Rather than the "Program name" comment, why not rename the variable to the self-explanatory
PROGRAM_NAME
?
-
\$\begingroup\$ I made all the targets depend on the Makefile itself so that any changes to the recipes would cause a rebuild. I am not sure what kind of downsides this approach would have though.. \$\endgroup\$ZarakshR– ZarakshR2023年05月03日 22:05:37 +00:00Commented May 3, 2023 at 22:05
-
\$\begingroup\$ What is
DEPS
? \$\endgroup\$vnp– vnp2023年05月05日 05:03:22 +00:00Commented May 5, 2023 at 5:03
You're making it hard for yourself by wanting your targets in subdirectories. Make works best for building targets in the current directory. If you can remove that requirement, that would save the effort and risk you introduced by re-writing the built-in rules.
Note that Make is quite happy for your sources to live elsewhere, and that's what the VPATH
mechanism enables.
We can put that to good use by having separate build directories for optimised and unoptimised output, with each invoking the shared Makefile and VPATH
pointing at the sources directory. Again, that saves you rewriting the standard rules.
It appears that all the object files depend on all the headers. That's crippling Make's ability to rebuild only the files whose sources have changed. I recommend using -MD
in your CFLAGS
to get GCC to generate dependency files as a side-effect of compilation, and using them if they exist by writing
-include *.d
The makefile has no .DEBUG_ON_ERROR
target and no comment to explain why it's omitted. I'm guessing that's an oversight.
I recommend using $(RM)
instead of writing rm -f
explicitly, as the former is more idiomatic.