If you read my earlier article OMG WTF Makefiles, there was a lot of information to take in. And Makefiles built that way can get pretty overwhelming to keep up pretty quickly. In fact, unless I'm forced to use BSD Make, I use GNU Make and take advantage of some nice shortcuts. I'll show you how to keep your Makefile building simple.
Simple Dependency Management
I hate having to update the Makefile every time I add a new class to the project. I feel like my development tools should be able to work it out for me, especially if I use a bit of care in my code organization. Fortunately, GNU Make has this covered. The following snippet will handle most of your dependency management situations:
SRCS=$(wildcard *.c) BASES=$(basename $(SRCS)) OBJS=$(addsuffix .o, $(BASES)) hello: $(OBJS) $(CC) $(CFLAGS) -o $@ $^ $(LIBS)
This is the automatically maintained version of the Makefile from my original article. It's making extensive use of some GNU Make magic. The filename functions provide a lot of useful tools for manipulating file names that can automate your Makefiles.
- SRCS will become a list of all of the files ending in
.c, which are the implementation of your classes and functions.
- BASES is an intermediate list, containing the base names (i.e. the parts before
.c) of every file in the SRCS list.
- OBJS will be the same list as SRCS, but with the
.cchanged to a
.o. This represents the actual list of compiled object files which should go into the final project.
If you're writing an application of much complexity, there's a good chance that you're not only writing actual application code, but possibly libraries which are used by multiple applications in your project. Rather than remembering the right order to enter and build each folder in order to ship your application, you can handle this in your top level Makefile.
Let's say that you have a library 'felgercarb' that is needed by several programs in your application. To keep your code neatly organized, you create several folders in your main project folder:
/project-root | |- felgercarb/ | |- app1/ | |- app2/ | | Makefile
Each of those folders has its own Makefile, which uses rules like we've learned in the previous article and the simplified Makefile rules we learned in this article.
In the top level
Makefile we have four special recipes:
.PHONY: felgercarb app1 app2 felgercarb: $(MAKE) -C felgercarb app1: $(MAKE) -C app1 app2: $(MAKE) -C app2
There are a few things going on here.
.PHONYis a magic target. You list any targets as dependencies which don't generate an output file in this folder. This saves
makethe trouble of looking for them on disk, and it knows that it should always build this targets. This can be a time saver on large builds, especially if the file system is slow, as it is on some build server setups.
$(MAKE)is another magic variable. This is set to whatever the name of the
makeprogram was. By default this is just 'make' but if you're building with a specific make, like
mingw64-make.exeon Windows or
gmakeon BSD, that information is preserved.
The make option
-Cmeans that the next argument is the directory which make should enter and build the default target. Of course you're going to build that default target using the pattern shown in the previous section.
It's worth noting that this trick can be performed infinitely deep, within system limitations. So inside of
app1 there could be another target which builds components in a deeper directory. Perhaps there's a GUI and a CLI version of the app.
Making Nice Makefiles
Our example above for handling multiple directories could use some cleaning up to improve developer happiness. There are a couple of other phony targets that we'll want to add.
.PHONY: felgercarb app1 app2 all clean all: felgercarb app1 app2 felgercarb: $(MAKE) -C felgercarb app1: felgercarb $(MAKE) -C app1 app2: felgercarb $(MAKE) -C app2 clean: $(MAKE) -C felgercarb clean $(MAKE) -C app1 clean $(MAKE) -C app2 clean
Now our default target is
all which has as its dependencies
app2. That means that felgercarb, app1 and app2 will all be built by default. I've also updated
app2 to require
felgercarb as a dependency, so that if I only need one of the apps, I don't need to build the more expensive
all target, and I'll still have fresh dependencies.
Our last target,
clean, we've seen before. In folders where source lives, we want the
clean target to remove intermediate object files and other detritus of the development process. Executed at the root level, we want it to clean all the folders. We're using our
-C trick again to enter another folder, and we're using a third argument to indicate the target we want built. In this case, we want to build the
clean target in each folder.
Anybody who has written code with me in the last few years knows that I'm a big proponent of test driven development. In this practice I don't write any code until I have an automated test as part of my build which fails. Then I write only enough code to make my test pass. I repeat that until the code does everything that it's supposed to do (and no more). To ensure adherence to the practice, I make a successful run of my automated test suite a dependency of the final product. It requires discipline but makes for happier customers. Happy customers pay their bills faster and refer new works faster, so it's worth the trouble.
Setting these builds up isn't trivial though. Next time I'll walk you through setting up this kind of build. It will be a nice wrap up to the series on Makefiles, and will amaze your friends. Following that I'll show you how I do test driven development on a simple project.