Looking at back at the C++ projects I've developed, the Makefiles are a mess. To tell you the truth, I copied one a long time ago and basically have been cutting and pasting it haphazardly ever since. I've decided to change that so I've been reading more about how Make actually works and some best practices for using it going forth. As most of the programs I write are not that complicated I've come up with a generic Makefile I can cut and paste ^W^W^W deploy going forth. I'd like some feedback on it.
Any input (except "you should be using cmake or $(OTHER_TRENDY_NEW_TOOL) instead") is welcome but some specific questions that come to mind are:
Have I forgotten to add something?
Conversely, have I added too much?
Is it worth making this a POSIX makefile. Currently I've only tried it with GNU make on Linux but portability is good. What about BSD or Windows make? Or should I just rely on gmake being available pretty much everywhere?
Do I have all the recommended compiler/linker flags for C++? I would like to be able to choose between g++ and clang atleast so options should be portable between both though if there is something good that only one supports I can special case it. (Can I?)
I'm assuming Visual Studio/Windows in general is a whole separate can of worms so I've ignored it for now but if you know how this Makefile could be adapted for Windows development I'd be happy to know.
Thanks in advance.
PROGRAM=someprogram
SRCDIR:=src
INCDIR:=include
DEPDIR:=deps
BUILDDIR:=build
DESTDIR?=
PREFIX?=/usr/local
BINDIR?=bin
SRC:=$(wildcard $(SRCDIR)/*.cc)
OBJECTS:=$(patsubst $(SRCDIR)/%.cc,$(BUILDDIR)/%.o,$(SRC))
DEPFILES:=$(patsubst $(SRCDIR)/%.cc,$(DEPDIR)/%.d,$(SRC))
CXX?=/usr/bin/g++
STRIP?=/usr/bin/strip
INSTALL?=/usr/bin/install
VALGRIND?=/usr/bin/valgrind
CPPFLAGS:=-I$(INCDIR)
CXXFLAGS:=-std=c++17 -Wall -Wextra -Wpedantic -flto ${CXXFLAGS}
LDFLAGS:=-ffunction-sections -fdata-sections -Wl,-gc-sections $(LDFLAGS)
DEPFLAGS=-MT $@ -MMD -MP -MF $(DEPDIR)/$*.Td
COMPILE.cc=$(CXX) $(DEPFLAGS) $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
LINK.cc=$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)
POSTCOMPILE=mv -f $(DEPDIR)/$*.Td $(DEPDIR)/$*.d && touch $@
all: debug
$(BUILDDIR)/%.o: $(SRCDIR)/%.cc $(DEPDIR)/%.d | $(DEPDIR) $(BUILDDIR)
$(COMPILE.cc) $(OUTPUT_OPTION) $<
$(POSTCOMPILE)
$(BUILDDIR)/%.o: $(SRCDIR)/%.s | $(BUILDDIR)
$(COMPILE.s) $(OUTPUT_OPTION) $<
$(BUILDDIR): ; @mkdir -p $@
$(DEPDIR): ; @mkdir -p $@
$(DEPFILES):
$(PROGRAM): $(OBJECTS)
$(LINK.cc) $(OUTPUT_OPTION) $^
debug: CPPFLAGS += -DDEBUG
debug: CXXFLAGS += -g3
debug: $(PROGRAM)
release: CXXFLAGS += -O2
release: distclean $(PROGRAM)
$(STRIP) --strip-all -R .comment -R .note $(PROGRAM)
memcheck: debug
$(VALGRIND) --suppressions=valgrind.suppressions --quiet --verbose --leak-check=full --show-leak-kinds=all --track-origins=yes --log-file=valgrind.log ./$(PROGRAM)
install: release
$(INSTALL) -m755 -D -d $(DESTDIR)$(PREFIX)/$(BINDIR)
$(INSTALL) -m755 $< $(DESTDIR)$(PREFIX)/$(BINDIR/$(PROGRAM)
clean:
-rm -rf $(BUILDDIR)
-rm -rf $(DEPDIR)
-rm valgrind.log
distclean: clean
-rm $(PROGRAM)
.PHONY: all debug release memcheck install clean distclean
.DELETE_ON_ERROR:
-include $(wildcard $(DEPFILES))
```
-
\$\begingroup\$ Here's a similar question with a possibly useful answer. \$\endgroup\$Edward– Edward2019年10月30日 14:51:08 +00:00Commented Oct 30, 2019 at 14:51
2 Answers 2
It's reasonable to require GNU Make - it's available on all platforms that have their own Make (as far as I know), and trying to cope with the vagaries of all vendors' Make implementations is an exercise in futility. So I think you've taken the right approach here.
You're working against Make by putting build products into subdirectories. It's easier to build them into the working directory, and the sources can be found using VPATH
, then there's no need to copy all the built-in rules (not so bad when all your source files are C++, but when you need to add a few assembler and C files, then the maintenance starts to grow).
Creating separate debug and release binaries in the same build tree is problematic - it can mean a total rebuild when switching from one kind to another. It's more usual to have separate build directories for the two, so you can incrementally build either at any time (of course, they can share the same source files, using VPATH
, and can share most of the Makefile the same way).
That would look something like this:
debug/Makefile
CXXFLAGS += -g3 -DDEBUG
VPATH = ../src:../include
include ../Makefile
release/Makefile
CXXFLAGS += -O2
VPATH = ../src:../include
include ../Makefile
Then, building the release version doesn't affect the objects used to build the debug version, and vice versa.
This target is problematic for a parallel build:
release: distclean $(PROGRAM)
We'll lose some of the files as the distclean
isn't sequenced with respect to $(PROGRAM)
.
I don't like the explicit strip
invocation in the release
target: since install
depends on release, this makes it impossible to build debug-symbol Debian packages the usual way. Just let the packager do the stripping.
I see no value in redefining COMPILE.cc
and LINK.cc
: CXXFLAGS
and LDFLAGS
are provided specifically for you to add your own flags to these lines - just use them.
Speaking of which, we normally use +=
to add to the flags:
CPPFLAGS += -I$(INCDIR)
CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic
CXXFLAGS += -flto
LDFLAGS += -ffunction-sections -fdata-sections
LDFLAGS += -Wl,-gc-sections
DEPFLAGS = -MT $@ -MMD -MP -MF $(DEPDIR)/$*.Td
CPPFLAGS += $(DEPFLAGS)
$(RM)
is provided as a more portable alternative to rm -f
; we could use that in a few places.
.DELETE_ON_ERROR
is often missed - kudos for remembering that.
DESTDIR?=
is a no-op - undefined Make variables already expand to nothing. Just omit this line. It's good that you're correctly allowing this to be set by packaging systems etc.
-
\$\begingroup\$ Haha I am actually a Debian developer so I should have been aware of the strip thing. (I did atleast remember the value of DESTDIR.) But most of the projects will never make it off my laptop let alone into a major Linux distribution. For my purposes it is handy to have the strip done automatically. Perhaps a way to satisfy both uses would be to move the strip options into $(STRIP) so it can be defined as a no-op if relying on e.g. debhelper to do the stripping. What do you think? \$\endgroup\$Jaldhar– Jaldhar2019年10月31日 03:14:51 +00:00Commented Oct 31, 2019 at 3:14
-
\$\begingroup\$ I'm in two minds about your point on build objects in subdirectories. I assume the toplevel is for things like the Makefile, READMEs, Licenses etc. not ephemera. On the other hand as you say it would simplify things. As for build and release in the same build tree, don't I have to do a full rebuild anyway if I switch because I'm using different compile flags in each? \$\endgroup\$Jaldhar– Jaldhar2019年10月31日 03:21:32 +00:00Commented Oct 31, 2019 at 3:21
-
\$\begingroup\$ As for distclean before release. Can I do
release: $(PROGRAM) | distclean
to avoid issues with parallel builds? \$\endgroup\$Jaldhar– Jaldhar2019年10月31日 03:23:46 +00:00Commented Oct 31, 2019 at 3:23 -
\$\begingroup\$ I've edited to show how we can build separate debug and release builds - keep each build's products separate, so we can't accidentally link a mix of debug and release objects. Then we can incrementally build either version without having to
make clean
beforehand. \$\endgroup\$Toby Speight– Toby Speight2019年10月31日 09:02:09 +00:00Commented Oct 31, 2019 at 9:02
I'm writing this as an answer because of space restrictions but this is more of a reply to @TobySpeight.
Thanks to your help I've improved my Makefile to create debug and release builds separately but I had to make additional changes beyonf what you wrote. I am recording them here for future posterity.
I had to change the install target so instead of depending on the release target which no longer exists, it cds to the release directory and builds there first. In my top level Makefile:
install:
@cd release && $(MAKE) install-$(PROGRAM)
...and in the release Makefile:
install-$(PROGRAM): $(PROGRAM)
$(INSTALL) -m755 -D -d $(DESTDIR)$(PREFIX)/$(BINDIR)
$(INSTALL) -m755 $< $(DESTDIR)$(PREFIX)/$(BINDIR/$(PROGRAM)
With the new setup, what happens if a user runs make
in the top-level directory? They are going to get errors. Or what if they run make distclean
in a subdirectory? First I defined a function in the top-level Makefile to tell where we are. This was unexpectedly complicated but this works:
get_builddir = '$(findstring '$(notdir $(CURDIR))', 'debug' 'release')'
I defined two new targets:
checkinbuilddir:
ifeq ($(call get_builddir), '')
$(error 'Change to the debug or release directories and run make from there.')
endif
checkintopdir:
ifneq ($(call get_builddir), '')
$(error 'Make this target from the top-level directory.')
endif
Then I had my $(PROGRAM)
and $(DISTCLEAN)
targets depend on them:
$(PROGRAM): $(OBJECTS) | checkinbuilddir
$(LINK.cc) $(OUTPUT_OPTION) $^
$(STRIP)
distclean: | checkintopdir
cd debug && $(MAKE) clean
cd release && $(MAKE) clean
So this will be my stock Makefile going forward. I still don't deal with e.g. building libraries or multi-binary programs but I'm satisfied with it for now. Thanks once again.