As I mentioned a while back, I've been wanting to discuss automatic dependency generation using GNU make and GNU gcc. This is something I just recently figured out, thanks to two helpful articles on the web. The following is a discussion of how it works. I'll be going through this material quickly, and I'll be doing as little hand-holding as possible, so hang on tight.
Let's start by looking at the final makefile:
SHELL = /bin/bash
ifndef BC
BC=debug
endif
CC = g++
CFLAGS = -Wall
DEFINES = -DMY_SYMBOL
INCPATH = -I../some/path
ifeq($(BC),debug)
CFLAGS += -g3
else
CFLAGS += -O2
endif
DEPDIR=$(BC)/deps
OBJDIR=$(BC)/objs
# Build a list of the object files to create, based on the .cpps we find
OTMP = $(patsubst %.cpp,%.o,$(wildcard *.cpp))
# Build the final list of objects
OBJS = $(patsubst %,$(OBJDIR)/%,$(OTMP))
# Build a list of dependency files
DEPS = $(patsubst %.o,$(DEPDIR)/%.d,$(OTMP))
all: init $(OBJS)
$(CC) -o My_Executable $(OBJS)
init:
mkdir -p $(DEPDIR)
mkdir -p $(OBJDIR)
# Pull in dependency info for our objects
-include $(DEPS)
# Compile and generate dependency info
# 1. Compile the .cpp file
# 2. Generate dependency information, explicitly specifying the target name
# 3. The final three lines do a little bit of sed magic. The following
# sub-items all correspond to the single sed command below:
# a. sed: Strip the target (everything before the colon)
# b. sed: Remove any continuation backslashes
# c. fmt -1: List words one per line
# d. sed: Strip leading spaces
# e. sed: Add trailing colons
$(OBJDIR)/%.o : %.cpp
$(CC) $(DEFINES) $(CFLAGS) $(INCPATH) -o $@ -c $<
$(CC) -MM -MT $(OBJDIR)/$*.o $(DEFINES) $(CFLAGS) $(INCPATH) \
$*.cpp > $(DEPDIR)/$*.d
@cp -f $(DEPDIR)/$*.d $(DEPDIR)/$*.d.tmp
@sed -e 's/.*://' -e 's/\\\\$$//' < $(DEPDIR)/$*.d.tmp | fmt -1 | \
sed -e 's/^ *//' -e 's/$$/:/' >> $(DEPDIR)/$*.d
@rm -f $(DEPDIR)/$*.d.tmp
clean:
rm -fr debug/*
rm -fr release/*
Let's blast through the first 20 lines of code real quick, seeing as this is all boring stuff. We first set our working shell to bash, which happens to be the shell I prefer (if you don't specify this, the shell defaults to 'sh'). Next, if the user didn't specify the BC environment variable (short for "Build Configuration"), we default it to a value of 'debug.' This is how I gate my build types in the real world; I pass it in as an environment variable. There are probably nicer ways of doing this, but I like the flexibility that an environment variable gives me. Next, we set up a bunch of common build variables (CC
, CFLAGS
, etc.), and we do some build configuration specific setup. Finally, we set our DEPDIR
(dependency directory) and OBJDIR
(object directory) variables. These will allow us to store our dependency and object files in separate locations, leaving our source directory nice and clean.
Now we come to some code that I discussed in my last programming grab bag:
# Build a list of the object files to create, based on the .cpps we find
OTMP = $(patsubst %.cpp,%.o,$(wildcard *.cpp))
# Build the final list of objects
OBJS = $(patsubst %,$(OBJDIR)/%,$(OTMP))
# Build a list of dependency files
DEPS = $(patsubst %.o,$(DEPDIR)/%.d,$(OTMP))
The OTMP
variable is assigned a list of file names ending with the .o
extension, all based on the .cpp
files we found in the current directory. So, if our directory contained three files (a.cpp, b.cpp, c.cpp), the value of OTMP
would end up being: a.o b.o c.o
.
The OBJS
variable modifies this list of object files, sticking the OBJDIR
value on the front of each, resulting in our "final list" of object files. We do the same thing for DEPDIR
, instead prepending the DEPDIR
value to each entry (giving us our final list of dependency files).
Next up is our first target, the all
target. It depends on the init
target (which is responsible for making sure that the DEPDIR
and OBJDIR
directories exist), as well as our list of object files that we created moments ago. The command in this target will link together the objects to form an executable, after all the objects have been built. The next line is very important:
# Pull in dependency info for our objects
-include $(DEPS)
This line tells make to include all of our dependency files. The minus sign at the front says, "if one of these files doesn't exist, don't complain about it." After all, if the dependency file doesn't exist, neither does the object file, so we'll be recreating both anyway. Let's take a quick look at one of the dependency files to see what they look like, and to understand the help they'll provide us:
objs/myfile.o: myfile.cpp myfile.h
myfile.cpp:
myfile.h:
In this example, our object file depends on two files: myfile.cpp and myfile.h. Note that, after the dependency list, each file is listed by itself as a rule with no dependencies. We do this to exploit a subtle feature of make:
If a rule has no prerequisites or commands, and the target of the rule is a nonexistent file, then make imagines this target to have been updated whenever its rule is run. This implies that all targets depending on this one will always have their commands run.
This feature will help us avoid the dreaded "no rule to make target" error, which is especially helpful if a file gets renamed during development. No longer will you have to make clean in order to pick up those kinds of changes; the dependency files will help make do that work for you!
Back in our makefile, the next giant block is where all the magic happens:
# Compile and generate dependency info
# 1. Compile the .cpp file
# 2. Generate dependency information, explicitly specifying the target name
# 3. The final three lines do a little bit of sed magic. The following
# sub-items all correspond to the single sed command below:
# a. sed: Strip the target (everything before the colon)
# b. sed: Remove any continuation backslashes
# c. fmt -1: List words one per line
# d. sed: Strip leading spaces
# e. sed: Add trailing colons
$(OBJDIR)/%.o : %.cpp
$(CC) $(DEFINES) $(CFLAGS) $(INCPATH) -o $@ -c $<
$(CC) -MM -MT $(OBJDIR)/$*.o $(DEFINES) $(CFLAGS) $(INCPATH) \
$*.cpp > $(DEPDIR)/$*.d
@cp -f $(DEPDIR)/$*.d $(DEPDIR)/$*.d.tmp
@sed -e 's/.*://' -e 's/\\\\$$//' < $(DEPDIR)/$*.d.tmp | fmt -1 | \
sed -e 's/^ *//' -e 's/$$/:/' >> $(DEPDIR)/$*.d
@rm -f $(DEPDIR)/$*.d.tmp
This block of code is commented, but I'll quickly rehash what's going on. The first command actually compiles the object file, while the second command generates the dependency file. We then use some sed magic to create the special rules in each dependency file.
Though it's a lot to take in, these makefile tricks are handy to have in your toolbox. Letting make handle the dependency generation for you will save you a ton of time in the long run. It also helps when you're working with very large projects, as I do at work.
If you have a comment or question about this article, feel free to comment.