September 03, 2013
Just because a tool is old doesn’t mean it won’t do the job.
According to Wikipedia, hammers (consisting of a handle and a stone head) date back to about 30,000 BC. Just because a tool is old doesn’t mean it won’t do the job. That’s how I feel about a lot of the old software tools. Sure, sometimes new tools really are better (I really like git, for example, compared to some older version control programs).
Sometimes new tools solve new problems, and that’s good too. I’m not sure a caveman needs a soldering iron. Sometimes, though, I think new tools are just because someone wanted to create a new tool. If you look at build systems, it seems crazy that there are so many. Off the top of my head: make, ant, bake, cmake, qmake, scons, waf, and rake all come to mind. I’m sure there are a dozen others (and the ones built into many of the IDEs). Many of these have some tie in to a specific ecosystem (qmake and Qt programs, for example).
Make is the good old-fashioned hammer. Of course, some of these tools actually just generate makefiles, so they really augment make. The question is: Do you need to augment make?
I’d suggest that for the kind of small- to medium-sized projects I typically create, the answer is no. I am unlikely to have many subprojects and complicated dependencies. I’m certainly not interested in writing XML scripts or managing a GUI just to automate a fairly simple build.
On the other hand, there are some tricks that let make handle some things you don’t always see in “conventional” programming. I thought I’d share some of those tricks.
If you are rusty on make, the idea is very simple (although it can easily get more complicated in practice): A makefile contains lines that list a target followed by a colon and a list of dependent files. After that line (the rule) appears one or more lines, indented by a tab (not spaces), that describes how to build that target (the recipe).
Here’s a really simple makefile:
hello-world : hello-world.c hello-world.h gcc -o hello-world hello-world.c
When the make program reads this file, it compares hello-world with the two files on the right side of the colon. If either file is newer than the target, this causes the recipe to execute. By default, make looks at the first rule in the makefile, although in this case, there’s only one.
That file is almost too simple. However, what if you want to compile and link in separate steps? That’s not very important in this case, but it would be if you had many C files and didn’t want to compile each one, every time you had to compile anything. Here’s a slightly more complex file:
hello-world : hello-world.o gcc -o hello-world hello-world.o hello-world.o : hello-world.c hello-world.h gcc -c -o hello-world.o hello-world.c
This is similar, except that make will try to find rules for the right-hand side before starting the recipe. So the sequence of events is:
- Identify that hello-world is the main rule
- Note that hello-world.o is the dependency
- Check and find a rule for hello-world.o
- Check and find no rules for hello-world.c and hello-world.h
- Check to see if hello-world.o is older than either of the dependencies; run recipe if necessary
- Check to see if hello-world is older than hello-world.o; run recipe if necessary
By default, make looks for a file named Makefile in the current directory and uses the first rule. However, you can specify a different file using -f and you can also specify a target rule by name. So while it wouldn’t make much sense, you could issue the command:
make -f test.makefile hello-world.o
You often see special targets added that solely execute a recipe. For example, you might want to clean up to force a new build:
clean : rm *.o
Then you can say:
Generally, you want to add targets like this to a special rule called .PHONY. The reason is simple. If you ever wound up with a file in the build directory named clean, the rule would stop running (because the file would always be up to date). Naming the rule as phony (along with any other similar rules) fixes this problem:
.PHONY : clean download archive # three phony targets
Obviously, the # character starts a comment. There are a few other subtleties. Make can set variables and also draw variables from the environment. You might see something like:
TARGET = hello-world OBJS = hello-world.o COPT = -g $(TARGET) : $(OBJS) gcc $(COPT) -o $(TARGET) $OBJS
I often use this to write a generic makefile that I can configure per project by changing a few variables at the top. You can even append to a variable:
OBJS = hello-world.o OBJS += pretty-print.o
That has the same effect as setting OBJS to “hello-world.o pretty-print.o” and is useful when you are trying to have something customizable near the top of the file yet still influence the variables later on.
Make will also deduce certain common operations and has special variables that it will use in those cases. For example, it knows how to change a .c file to a .o file (you don’t even have to name the .c file). It will call the C compiler specified in $(CC) and pass it $(COPTS) (and there are defaults for these). However, for embedded development, I never use these built-in steps. I want control of what the file is doing and I don’t want to depend on a hidden string inside the program.
So writing a makefile doesn’t have to be a big deal. The big deal is writing a generic makefile that is easy to use. There are a few tricks that apply mostly to cross compiling and embedded tools, and I’ll talk about those next time.