3
\$\begingroup\$

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))

```
\$\endgroup\$
4
\$\begingroup\$

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.

\$\endgroup\$
  • \$\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 Oct 31 '19 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 Oct 31 '19 at 3:21
  • \$\begingroup\$ As for distclean before release. Can I do release: $(PROGRAM) | distclean to avoid issues with parallel builds? \$\endgroup\$ – Jaldhar Oct 31 '19 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 Oct 31 '19 at 9:02
0
\$\begingroup\$

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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