Most of the work I do these days is with the GNU toolset, and that's a good thing. Even if I'm coding for, say, an Atmel AVR or an ARM processor, I have the familiar or Linux/Unix-style C compiler and tools. Between Unix and Linux, I've used these tools for a very long time and they are very comfortable to me.
However, sometimes you get too comfortable. Tools grow and change over time, but how often do you learn new features. Sure, I know some of them exist, but I tend to stick with my old familiar patterns.
This week I was building some code that is targeting multiple outputs and to do it nicely I was forced to break out of my comfort zone and use a few features of Make (and gcc) that I haven't tried before.
I've mentioned before that I'm lazy, and that's certainly true. I like tools to do the work for me. But apparently I'm not as lazy as some programmers. For example, consider Make's default rules. Sure, I know they are there, but I don't mind typing a command line into a Makefile just to be sure I have complete control of things. But where I draw the line is having to manage the dependency graph of my code -- especially as this was a pretty complex program with lots of parts.
I usually just write a pretty basic makefile with explicit command lines and -- for a small project -- I just manually set up the dependencies. But for big projects, I like makedepends. This automatically scans your source files and tells you what .H files they rely on.
But this project was different. My plan was to have all my source code in one directory and then have separate output directories (and Makefiles) for each target build. It turns out, basic Make was not meant to handle that neither was makedepends.
I figured while I had the Make manual open to look up some new features, I'd bite the bullet and give up my command lines for default rules where possible.
I learned a few interesting things. First of all, Make has a way to set search paths for various items. You can set the VPATH variable and Make will look for prerequisites and targets that it can't find in the current directory. That turned out to be what I needed.
If you need more control you can use the vpath directive (which is lowercase, unlike the uppercase variable). If you like to keep your headers in a separate directory, for example, you could write: "vpath %.h ../headers"
That solved part of my problem. But I could never quite get makedepends to sort out the directory structure. I decided to use the gcc method of managing dependencies.
If you run gcc with the -MM option, any -D options you use to define preprocessor symbols, and a source file name, you'll get an output like this:
rfp.o: rfp.cpp rfp.h rs232.h iobase.hOddly enough, that looks just like a Make rule. So you could -- if you weren't as lazy as I am -- run this command on each source file and make a separate Makefile for each source file (traditionally these files have a .d extension, as in rfp.d). The problem is, you have to remember to rebuild these .d files when something changes. And that's what Make is for!
The answer is contained in the Make manual. You want to make the .d file depend on the same files so that if any of them change, the .d file gets regenerated.
I modified the example in the help a bit, and put it in a file I can include with all my Makefiles:
%.d: %.c @set -e; rm -f $@; \ $(CC) -MM $(CFLAGS) $< | sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' > $@; %.d: %.cpp @set -e; rm -f $@; \ $(CXX) -MM $(CXXFLAGS) $< | sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' > $@; DEPS0=$(SRCS:.cpp=.d) DEPS=$(DEPS0:.c=.d) ifneq ($(MAKECMDGOALS),clean) include $(DEPS) endif
This assumes you have all your source files named in a SRCS variable. The two rules cause gcc to scan .c and .cpp files for dependencies. The sed regular expression transforms the rules to this format:
rfp.o rfp.d: rfp.cpp rfp.h rs232.h iobase.h
Then the include reads in all the generated Makefiles (unless you are running the clean target). This way you get rules for every C or C++ file that depends on the correct set of headers. Good thing I switched to the default rules or my sed command would have to be much more complex!
The only time I've had any trouble is when you delete a header. Probably a good idea to erase all the .d files in your Makefile's clean rule.
Old habits die hard. But a little work up front will pave the way for future laziness. If you want to read more about debugging Makefiles, check out John Graham-Cumming's DDJ article on the subject. What's your favorite Make trick?