Skip to content

Instantly share code, notes, and snippets.

@maxtruxa
Last active August 31, 2023 14:56
Show Gist options
  • Star 56 You must be signed in to star a gist
  • Fork 16 You must be signed in to fork a gist
  • Save maxtruxa/4b3929e118914ccef057f8a05c614b0f to your computer and use it in GitHub Desktop.
Save maxtruxa/4b3929e118914ccef057f8a05c614b0f to your computer and use it in GitHub Desktop.
Generic makefile for C/C++ with automatic dependency generation, support for deep source file hierarchies and custom intermediate directories.
# output binary
BIN := test
# source files
SRCS := \
test.cpp
# files included in the tarball generated by 'make dist' (e.g. add LICENSE file)
DISTFILES := $(BIN)
# filename of the tar archive generated by 'make dist'
DISTOUTPUT := $(BIN).tar.gz
# intermediate directory for generated object files
OBJDIR := .o
# intermediate directory for generated dependency files
DEPDIR := .d
# object files, auto generated from source files
OBJS := $(patsubst %,$(OBJDIR)/%.o,$(basename $(SRCS)))
# dependency files, auto generated from source files
DEPS := $(patsubst %,$(DEPDIR)/%.d,$(basename $(SRCS)))
# compilers (at least gcc and clang) don't create the subdirectories automatically
$(shell mkdir -p $(dir $(OBJS)) >/dev/null)
$(shell mkdir -p $(dir $(DEPS)) >/dev/null)
# C compiler
CC := clang
# C++ compiler
CXX := clang++
# linker
LD := clang++
# tar
TAR := tar
# C flags
CFLAGS := -std=c11
# C++ flags
CXXFLAGS := -std=c++11
# C/C++ flags
CPPFLAGS := -g -Wall -Wextra -pedantic
# linker flags
LDFLAGS :=
# linker flags: libraries to link (e.g. -lfoo)
LDLIBS :=
# flags required for dependency generation; passed to compilers
DEPFLAGS = -MT $@ -MD -MP -MF $(DEPDIR)/$*.Td
# compile C source files
COMPILE.c = $(CC) $(DEPFLAGS) $(CFLAGS) $(CPPFLAGS) -c -o $@
# compile C++ source files
COMPILE.cc = $(CXX) $(DEPFLAGS) $(CXXFLAGS) $(CPPFLAGS) -c -o $@
# link object files to binary
LINK.o = $(LD) $(LDFLAGS) $(LDLIBS) -o $@
# precompile step
PRECOMPILE =
# postcompile step
POSTCOMPILE = mv -f $(DEPDIR)/$*.Td $(DEPDIR)/$*.d
all: $(BIN)
dist: $(DISTFILES)
$(TAR) -cvzf $(DISTOUTPUT) $^
.PHONY: clean
clean:
$(RM) -r $(OBJDIR) $(DEPDIR)
.PHONY: distclean
distclean: clean
$(RM) $(BIN) $(DISTOUTPUT)
.PHONY: install
install:
@echo no install tasks configured
.PHONY: uninstall
uninstall:
@echo no uninstall tasks configured
.PHONY: check
check:
@echo no tests configured
.PHONY: help
help:
@echo available targets: all dist clean distclean install uninstall check
$(BIN): $(OBJS)
$(LINK.o) $^
$(OBJDIR)/%.o: %.c
$(OBJDIR)/%.o: %.c $(DEPDIR)/%.d
$(PRECOMPILE)
$(COMPILE.c) $<
$(POSTCOMPILE)
$(OBJDIR)/%.o: %.cpp
$(OBJDIR)/%.o: %.cpp $(DEPDIR)/%.d
$(PRECOMPILE)
$(COMPILE.cc) $<
$(POSTCOMPILE)
$(OBJDIR)/%.o: %.cc
$(OBJDIR)/%.o: %.cc $(DEPDIR)/%.d
$(PRECOMPILE)
$(COMPILE.cc) $<
$(POSTCOMPILE)
$(OBJDIR)/%.o: %.cxx
$(OBJDIR)/%.o: %.cxx $(DEPDIR)/%.d
$(PRECOMPILE)
$(COMPILE.cc) $<
$(POSTCOMPILE)
.PRECIOUS: $(DEPDIR)/%.d
$(DEPDIR)/%.d: ;
-include $(DEPS)
#include <iostream>
int main() {
std::cout << "Hello, World!\n";
return 0;
}
@maxtruxa
Copy link
Author

maxtruxa commented Jun 20, 2016

Generic Makefile

Features

  • Easy to use (i.e. copy-paste for a small project):
    For basic usage just set BIN to the desired output binary name and SRCS to your input source files.
  • Source files can be placed in subdirectories and reusing a filename somewhere in the source tree does not produce collisions during compilation.
    This is achieved by mirroring the actual source tree in the object file directory. A lot of simple makefiles flatten the file hierarchy, so foo.cpp and bar/foo.cpp both end up producing foo.o, which obviously won't work.
  • Generated object and dependency files are hidden in subdirectories (OBJDIR and DEPDIR, respectively).
    Less clutter in your project directory, yay!
  • Generate a distribution tarball through make dist.
    Included files are modified through DISTFILES and the tarball name through DISTOUTPUT.
  • Available standard targets can be displayed with make help.
  • Standard make features are kept intact:
    • Toolchain selection through CC, CXX, LD, ...
    • Passing flags to tools through CFLAGS, CXXFLAGS, CPPFLAGS, LDFLAGS, ...

Warning: Do _not_ point OBJDIR or DEPDIR to a directory that contains files that should be kept when running make clean/make distclean. The clean and distclean targets provided in this makefile delete those directories recursively.

If you want to point OBJDIR and/or DEPDIR to the current directory (or any directory containing files that shouldn't be deleted), change the clean target like this:

clean:
    $(RM) $(OBJS) $(DEPS)

This solution has one (minor) drawback: When a source file is renamed or deleted, the corresponding object file is not being removed.

Examples

Note: Extra newlines inserted between shell commands for better readability.

Compile Binary

make/make all:

$ tree -a
.
├── makefile
└── test.cpp

0 directories, 2 files

$ make
clang++ -MT .o/test.o -MD -MP -MF .d/test.Td -std=c++11 -g -Wall -Wextra -pedantic -c -o .o/test.o test.cpp
mv -f .d/test.Td .d/test.d
clang++   -o test .o/test.o

$ tree -a
.
├── .d
│   └── test.d
├── makefile
├── .o
│   └── test.o
├── test
└── test.cpp

2 directories, 5 files

$ ./test
Hello, World!

Create Distribution Tar File

make dist:

$ tree -a
.
├── makefile
└── test.cpp

0 directories, 2 files

$ make dist
clang++ -MT .o/test.o -MD -MP -MF .d/test.Td -std=c++11 -g -Wall -Wextra -pedantic -c -o .o/test.o test.cpp
mv -f .d/test.Td .d/test.d
clang++   -o test .o/test.o
tar -cvzf test.tar.gz test
test

$ tree -a
.
├── .d
│   └── test.d
├── makefile
├── .o
│   └── test.o
├── test
├── test.cpp
└── test.tar.gz

2 directories, 6 files

$ make clean
rm -f -r .o .d

$ tree -a
.
├── makefile
├── test
├── test.cpp
└── test.tar.gz

0 directories, 4 files

@Peregring-lk
Copy link

What is the postcompile step used for?

@cmtika
Copy link

cmtika commented Jan 11, 2018

Suppose I have test1.cpp, test2.cpp, ..., testn.cpp and I need to build an executable for each. How do I change this generic make file? I would like to have the choice of running "make all" or "make test1", "make test2", etc.

Thanks!

@malamanteau
Copy link

A million times--thank you! Starting from this makefile template saved me a TON of headaches.

@johan-boule
Copy link

johan-boule commented Apr 10, 2019

Your dist target contradicts the usual practice: it's supposed to build a source tarball. If someone wants to produce a binary tarball, he will use make install DESTDIR=/opt/foofor that.

@jpz
Copy link

jpz commented Jul 5, 2019

This is awesome - thanks very much for publishing this.

I have spent hours trying to fiddle with a makefile for a new project, wanting my obj and dep files to be hidden away from the sources - this works perfectly. I have one minor bit of feedback - I needed to rearrange LINK.o to get it to work for me, as g++ was being order-specific - I needed to do this:

$(BIN): $(OBJS)
        $(LD) -o $@ $^ $(LDFLAGS) $(LDLIBS)

The normal order for your makefile is this:

$(BIN): $(OBJS)
        $(LD) $(LDFLAGS) $(LDLIBS) -o $@ $^

However in my circumstance, I then found undefined references.

Thanks again.

@hertzsprung
Copy link

On line 116, I believe .PRECIOUS = should actually be .PRECIOUS: to avoid deleting .d intermediate files, see https://stackoverflow.com/a/56424855/150884

@maxtruxa
Copy link
Author

@hertzsprung Thanks for catching that typo!

@maxsupermanhd
Copy link

I have maybe an issue here, where running make twice it makes something more, is this supposed to work like that?
buildlog: https://pastebin.com/TqRtgQ7q (2 build commands in a row, no changes to sources)
makefile: https://pastebin.com/iKYuAsCF
@maxtruxa Am I doing something wrong?

@maxtruxa
Copy link
Author

@maxsupermanhd That is strange. Some of the prerequisites must have changed, otherwise make wouldn't have run the recipes a second time.
Two questions: Does this happen reproducibly on every fresh build? And does this happen when running make sequentially (i.e. without -j) as well? I tried it on a sample project of mine and it works fine with and without -j4.

@maxsupermanhd
Copy link

maxsupermanhd commented Apr 12, 2020

Two questions: Does this happen reproducibly on every fresh build? And does this happen when running make sequentially (i.e. without -j) as well? I tried it on a sample project of mine and it works fine with and without -j4.

  1. Yes, every build. But first one builds all and target works fine, second run makes no sense and just making already ready to link files.
  2. With or without -j this is not affecting build (except time).
    Any help will be very useful.
    @maxtruxa

@maxtruxa
Copy link
Author

@maxsupermanhd Depending on your version of make, you can run make with the --trace option which tells you why make is running stuff.

@maxsupermanhd
Copy link

@maxtruxa Ok, I found the main reason of this, after building target make will update objects because of .d files updated. Maybe somehow implement make depend?
Traced compile log: https://pastebin.com/aktN5rVq
Can you fix this or there is my side issue?

@MartinZeman17
Copy link

LDLIBS is actually not defined anywhere

@maxtruxa
Copy link
Author

maxtruxa commented Jul 2, 2020

@MartinZeman17 That's correct. LDLIBS is one of the predefined variables used by implicit Make rules (see here). I used LDLIBS in the linker rules as well, to mirror the behavior of implicit rules but by default there are no libs specified. To make this more obvious, I added an empty assignment to LDLIBS.

@maxtruxa
Copy link
Author

maxtruxa commented Jul 2, 2020

@johan-boule Yes, thank you for pointing that out. Sadly, I haven't had the time to fix that.

@wait-how
Copy link

@maxsupermanhd I had the same problem and got the makefile to handle repeated builds by treating $(DEPDIR)/%.d as an order-only dependancy, so that the creation date of the dependancy files doesn't matter.

@maxtruxa
Copy link
Author

Note to future readers: Save yourself the trouble and just use CMake.

@DukMastaaa
Copy link

How do I specify a source directory? Do I have to list out all of the files that need to be compiled and then make will calculate dependencies? Or can I just tell it to "compile ./src/main.cpp" and then it will look at all of the files in ./src?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment