1
\$\begingroup\$

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
toolic
15.1k5 gold badges29 silver badges211 bronze badges
asked May 3, 2023 at 20:02
\$\endgroup\$
1
  • \$\begingroup\$ May I suggest using cmake instead of making makefile templates? \$\endgroup\$ Commented May 3, 2023 at 21:54

2 Answers 2

2
\$\begingroup\$

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 and obj directly. You probably want to always reference OBJECT_DIR.
  • Why not delete OBJECT_DIR and BUILD_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?
answered May 3, 2023 at 21:45
\$\endgroup\$
2
  • \$\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\$ Commented May 3, 2023 at 22:05
  • \$\begingroup\$ What is DEPS? \$\endgroup\$ Commented May 5, 2023 at 5:03
3
\$\begingroup\$

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.

answered May 6, 2023 at 13:44
\$\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.