Here's the Makefile I've built up over the last two years which I use as a template (most often verbatim apart from target name(s)). It's meant to be used for C exclusively.

I've sort of cobbled it together, so it's very well possible I made a bone-headed mistake somewhere, hence why I'd like some more experienced eyes to review it! If there's anything that needs explaining, please comment and I'll update the post.

TARGETS := target

CC := gcc

CFLAGS       := -c -W -Wall -pedantic -O3
DEBUG_CFLAGS := -c -W -Wall -pedantic -g -Og

LDFLAGS       := -lm 

BINDIR    := ./bin
DEBUGDIR  := ./debug
OBJDIR    := ./obj
SRCDIR    := ./src
TESTDIR   := ./test

SOURCES      := $(wildcard $(SRCDIR)/*.c)
MAINS        := $(TARGETS:%=$(OBJDIR)/%.o)
OBJECTS      := $(filter-out $(MAINS), $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o))

TESTSOURCES := $(wildcard $(TESTDIR)/*.c)

all  : $(TARGETS)
test : $(TESTS)

$(TARGETS): % : $(OBJDIR)/%.o $(OBJECTS)
    $(CC) -o $@ $^ $(LDFLAGS)

$(OBJECTS): $(OBJDIR)/%.o : $(SRCDIR)/%.c
    mkdir -p $(OBJDIR)
    $(CC) $(CFLAGS) $^ -o $@

$(MAINS): $(OBJDIR)/%.o : $(SRCDIR)/%.c
    mkdir -p $(OBJDIR)
    $(CC) $(CFLAGS) $^ -o $@

    $(CC) -o $@ $^ $(DEBUG_LDFLAGS)

    mkdir -p $(DEBUGDIR)
    $(CC) $(DEBUG_CFLAGS) $^ -o $@

$(TESTS): $(BINDIR)/%_test : $(TESTDIR)/%_test.c $(filter-out $(MAINS), $(OBJECTS))
    mkdir -p $(TESTDIR)
    mkdir -p $(BINDIR)
    $(CC) -o $@ $^ $(LDFLAGS)

    rm -f $(MAINS) $(OBJECTS) $(TARGETS) $(TESTS)

2 Answers 2


Not Enough CFLAGS

You currently have c -W -Wall -pedantic -O3. First, -c shouldn’t be there; it just stops you from using CFLAGS in a single-stage compile. Move it to the targets that create object files.

You don’t specify a version of the language on the command line, despite asking for -pedantic warnings. GCC defaults to -std=c90, while I would normally use -std=c17, but whatever your project uses, make it explicit and make sure everyone knows what they’re supposed to be using.

There are a lot more useful warnings that you haven’t enabled, for example, implicit conversion between signed and unsigned. I typically use -std=c17 -Wall -Wextra -Wpedantic -Wconversion -Wdeprecated.

If you set out to remove all warnings (disabling any specific ones that you decide not to fix, and fixing the rest), you might want -Werror.

You don’t specify a submodel, but it’s very unlikely that you would want to release software that needs to run on the very earliest x86-64 CPUs without AVX or even cmpxchg16b. Or if you’re compiling and testing binaries that only you will use, and what you share will be the source, you should always add -march=native.

You likely also want -flto for whole-program optimization, in your release build.

I typically set the POSIX feature-test macros in a header that I include at the top of all source files, but you could set that here if you prefer.

Consider a Few Debug Flags

You currently have -Og, which is a good choice for debug optimization. There is no reason not to also add -march=native. If you wrote any tail-recursive calls, you might also need -foptimize-sibling-calls to avoid a stack overflow.

Do You Really Want to Compile and Link Every file in the Directory?

For example, if you create fop.o instead of foo.o by a typo, should that always be linked in? Better to keep a list of what object files you’re supposed to have.

What if a Header Changes?

Listing the object files also lets you specify header files as dependencies, so that the correct files get recompiled when you edit a header.

Your make clean Does not Clean Up Your Debug Builds

Your debug target creates *.do and *.db files, which your clean target never cleans.

  • \$\begingroup\$ About the C version, that is a recent omission. I used to compile with std=c89 but my recent systems programming class uses system calls that are not available, and I opted for the unsafe but easy answer of just removing the argument. \$\endgroup\$
    – ice-wind
    Oct 25, 2023 at 12:32
  • \$\begingroup\$ @ice-wind System calls that aren’t available are typically enabled by the _XOPEN_SOURCE (and optionally _POSIX_C_SOURCE) macros. Unless they’re GNU extensions, or ANSI later than C89. The man page should tell you what standard they conform to. \$\endgroup\$
    – Davislor
    Oct 25, 2023 at 13:50
  • \$\begingroup\$ If they’re POSIX, see man feature_test_macros for the correct value to set. \$\endgroup\$
    – Davislor
    Oct 25, 2023 at 13:52

Some desirable additions:

  • Annotate the targets that don't generate a file matching the target name:

    .PHONY: all clean debug test
  • Ensure that any failing command doesn't leave a partially-written output file:


    Most modern C compilers are reasonably good at avoiding this problem, but it's effectively free to get Make to support you here. It's a good habit to have in Makefiles.

  • Use built-in variable $(RM) instead of writing rm -f in full. This might ease the path towards portability beyond just POSIX, too.

  • mkdir -p can fail if another target is concurrently creating the same directory. Either make the directory an order-only dependency, or use a command like @test -d $(@D) || mkdir $(@D) 2>/dev/null || test -d $(@D) (I generally create a variable for that, rather than writing it out in every rule that needs a target directory creating).

A reasonably diligent build setup won't even attempt to compile the release version unless the tests pass. I suggest making each $(OBJDIR)/%.o depend on the corresponding debug object's tests to facilitate that.

  • \$\begingroup\$ Thank you, those all seem to be good pieces of advice ! I am going to implement some if not all of them. \$\endgroup\$
    – ice-wind
    Oct 25, 2023 at 12:30

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.