A drop-in makefile for lazy C++ programmers

Build systems! Don't we all love them? Isn't it fun to hear people complain that the GNU Autotools are "designed to be as indecipherable as possible"? Isn't it just great to see people rage about CMake having "inherited the worst of almost every build system"? Does it not make you smile how recursive make is famously considered harmful, yet the Linux kernel uses it excessively?

I don't really have an opinion on all these things: I never had to set up the build system for a really large project and as a pure end user of these systems I can't really complain about any of them. Both CMake and Scons look decent though and I will probably try to use one of them for my next project. Just for the fun of it.

What I have been using for quite a while now to build my own, rather small C++ projects is a handwritten makefile. I originally cooked it up to build hVMC, but it ended up being so useful that I kept copying it around. It is actually quite fancy:

  • It requires almost no changes when used for a new project. I usually just copy the makefile and change one line (which only holds the name of the project).
  • It automatically looks at the source files and determines which objects the project has and the dependencies between the source files. I don't have to do anything when I add a new source file.
  • It builds things out-of-source and supports having three different builds: A release build, a debugging build and a profiling build (with debugging symbols and (almost) full optimization). The build can be selected with make BUILD=release|profile|debug and ends up in a directory named bin.release|profile|debug.

I put the makefile in this GitHub Gist, but since it's a bit cryptic I want to go through it line by line:

# disable default implicit rules
MAKEFLAGS += --no-builtin-rules
.SUFFIXES:

The first block disables all builtin implicit rules. We are not going to use these anyway and since they come at a small performance hit there is no reason not to disable them.

# project name
PROJECT  = myproject

This sets the name of the project that is being built. This is the only line I usually change when I use the makefile for a new project. The project name is used as the filename for the final executable.

# common compiler/linker flags
CXX      = g++
CXXFLAGS = -std=c++11 -Wall -Wextra -Wpedantic
LDFLAGS  =
DEFINES  =

Standard stuff: Compiler to be used, compiler flags, linker flags and defines for the preprocessor. Adjust the flags as needed. Note that I only tested this makefile with GCC and wrappers around GCC. We later use GCC's -MM flag to let it generate the dependency info, so if you want to use something different than GCC here, you might get into trouble there ...

# build specific compiler/linker flags
BUILD ?= release
ifeq ($(BUILD), debug)
  CXXFLAGS += -Og -g
  DEFINES  += -DVERBOSE=1
else ifeq ($(BUILD), profile)
  CXXFLAGS += -march=native -O3 -g
  DEFINES  += -DNDEBUG
else ifeq ($(BUILD), release)
  CXXFLAGS += -march=native -O3 -flto
  LDFLAGS  += -fuse-linker-plugin -s
  DEFINES  += -DNDEBUG
else
  $(error unrecognized BUILD)
endif
BUILDDIR = bin.$(BUILD)

The default is a release build, so that is what you get if you don't specify anything on the command line. Depending on the build we extend our flags and finally set the directory in which we build accordingly. The profiling build has full optimization, but does not use GCC's link-time optimization, as it confuses debuggers and profilers. It also has all the debugging symbols. The -DNDEBUG macro I set for profiling and release builds is actually a standard C/C++ macro which disables assertions in the code, so it should always be used for release (and profiling) builds.

# find the hash of the git commit we are building from
GIT_HASH  = $(shell git rev-parse --short HEAD 2> /dev/null || echo *unknown*)
DEFINES  += -DGIT_HASH=\"$(GIT_HASH)\"

I really like Git so making a Git repository is usually the first thing I do when working on some code. These two lines determine the commit hash of the current HEAD and make it available to the preprocessor. I like to put the commit hash into the --version output of my programs, just so that binaries know where they came from. If you are not using Git (you should!) or if you don't need this, you can just delete these two lines.

Ok, this was the easy part. Now it gets a bit tricky!

# generate lists of object files (all and existing only)
OBJECTS_ALL = $(addprefix $(BUILDDIR)/,$(patsubst %.cpp,%.o,$(wildcard *.cpp)))
OBJECTS_EXT = $(wildcard $(BUILDDIR)/*.o)

The first thing we need to do is to look at the source code and determine which objects files the project has. There will be one .o file for every .cpp file in the project. Header files (.hpp) should not be compiled. It's technically possible, but pointless as their contents will be compiled with the .cpp files which include them. The first line generates a list of all object files participating in the build by taking the .cpp files, replacing the extension with .o and prefixing the filenames with our build directory. The second line generates a list of already existing object files by looking for files with a .o extension in the build directory. We will later pull in the dependency information for the already existing object files. Note that we don't need dependency information for objects files which do not exist yet, as we will have to compile them regardless of their dependencies.

# define implicit rule to compile a cpp file
$(BUILDDIR)/%.o : %.cpp | $(BUILDDIR)
	$(CXX) $(CXXFLAGS) $(DEFINES) -c $< -o $@
	$(CXX) $(CXXFLAGS) $(DEFINES) -MM $< -MT $@ > $(BUILDDIR)/$*.dep

The next step is to define an implicit rule for how source code files should be compiled. The first line in the rule is just the standard compiler invocation. The second line in the rule calls the compiler again with the -MM flag to let it generate the dependency info, which we save (with a .dep extension) next to the object file in the build directory. Note that our implicit rule depends on the existence of the build directory. The weird syntax with the pipe is called an order-only prerequisite and is needed to prevent make from rebuilding everything when the timestamp of the build directory changes, which happens basically all the time ...

# main executable
$(BUILDDIR)/$(PROJECT) : $(OBJECTS_ALL) | $(BUILDDIR)
	$(CXX) $(CXXFLAGS) $^ $(LDFLAGS) -o $@

Nothing special here. We get our executable by linking all object files together.

# build directory creation
$(BUILDDIR) :
	mkdir $(BUILDDIR)

Making the build directory is also straightforward.

# pull in dependency info for existing object files
-include $(OBJECTS_EXT:.o=.dep)

If an objects file already exists we have to decide if it has to be rebuilt or not, so we need to know the dependencies of the corresponding source file. Luckily we generated this dependency info after we generated the object files, so we have a .dep file for each .o file in the build directory, which we can just include now.

# obligatory clean target
clean :
	rm -rf bin.*/

Not much to say here. The cleanup target simply deletes all build directories.

And that's all there is to it: Take the makefile, copy it into the folder with your sources, adjust the project name and the compiler/linker flags as needed, and type make. The rest should work automagically! :)

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *