The Makefile Quagmire
or
How I learned to stop Worrying and Love GMake
Thomas Stover, 2006
What is Make?
If you have not heard of make, it will save you a world of pain.
This article is an introduction and discussion of Makefiles,
GNU Make, and related issues. Examples of complete GMake projects are given.
Some sort of Make program can be found on most Unix environments as
well as a "behind the scenes" technology in some IDEs (integrated
development environment) such as Visual Studio.
At a conceptual level they all share at least one purpose: to automate both
the task of repetitive complex steps in a build process, as well as
intelligently deciding which if any of the steps in such a process need to
be performed for a given result. From there features vary widely.
GNU Make or gmake has support for most features you will ever need and then some.
Not only is it almost certainly available on any given Linux system,
it can be installed on practically every Unix, and even on Windows.
Be warned however that the world of make is not without its nightmares.
Read on, and in minutes learn what has taken me years to sort out.
How to Use GMake
Lets start with a real example and build on it from there.
Take the following source for a C program.
example1.c
#include <stdio.h>
int function_one(int)
{
return 1;
}
int main(void)
{
int x;
x = function_one();
return x;
}
|
Now let say we use a command such as gcc -o example1 example1.c to build the program.
Thats not really that big of deal to type out is it?. What if it were something like
gcc -Wall -g -O2 -o example1 example1.c? Still not too terrible, but we can see how
things could begin to be a pain to type out correctly each time. One obvious thing to do
would be to simply place that command in a shell script. That way if we wanted to build
the program again we could just run something like ./build. Our first Makefile example
is essentially just that. Although in this case it would seem more complicated
than a shell script, this demonstrates a foundation for what is to come. Take the following
file named Makefile (case sensitive).
Makefile
#This is a comment in our very first Makefile
#it will build our program, example1 when we type make
example1 : example1.c
gcc -o example1 example1.c
|
Important: you must place a real tab (not spaces) before the "gcc" or it will not work.
First place both example1.c and Makefile in your current working directory.
If we are on a Linux or Cygwin installation that has make installed we can simply type
make. The same goes for an Msys installation on windows (see details about make on windows
bellow). If we are on a BSD or other Unix system that has its own native version of make
GNU Make, if installed, is typically named gmake. Running make should output the commands
that its running, and then you should have the example1 executable in the directory as well.
You do not have to specify that our make file is Makefile, because make looks by default
for a file with that specific name. At this point we are no better than a shell script.
Lets try something new.
Without modifying example1.c or deleting example1, run make again. You should get a message
along the lines of make: Nothing to be done for "example1". GMake has figured out
that example1 is newer than its dependencies. By newer or older we mean the time stamps on the files.
Change something about the source file, save it, and run make again.
The two lines that are not comments compose a Makefile rule. The first line of a rule
defines a target and dependencies for that target. The lines beginning with tabs that
follow are the commands to produce the target. The first target in a Makefile
is the one gets built by default. A target can be specified on the command line. For example,
we could have just as well run make example1. A target's dependencies, to the right
of a target separated by a colon, must be either a file's name or another Makefile target. This forms
the basic mechanism by which make decides what needs to be done to reach a given target.
When programs like make were originally developed compiling a single file could takes several
minutes, and a large project could takes days to build. If someone wanted to test out a simple
change to one file, waiting forever just to find out a semicolon was left out
was unacceptable. By using make you can setup a build system that understands what the minimum
amount of work needed to be done is in any scenario, and automatically get started on whatever that
may be all by typing make target. Computers continuously growing faster still, can
now usually build anything fairly quickly. There are still software packages in existence
that can take hours or days to build on top of the line hardware, but more commonly today
an invocation of make is used to do something in 4 seconds rather than 4 minutes.
Even if you think your project is never going to be big enough to warrant such as setup, it is
good practice for the ones that do. Beyond that a Makefile is a good way to add utility
functions and alternative build options to your project. Common examples include things
like install scripts, different executables in the same project, programs with different
features or linked with different libraries, and the cleanup of temporary files generated
during a build. Also by using Makefiles in standard ways will we see in future articles
how GNU Autoconf can help create dynamically created customized site source trees. In such
a setup support for different compilers, host operating systems, and libraries can by
included in your project. Lets move on to another example. Start over with the following
files. First create two subdirectories obj and src.
The source files will be placed in the src subdirectory, and the Makefile still in the
parent directory.
./src/functions.h
#ifndef _functions_h_
#define _functions_h_
#define MAGIC_NUMBER 4
int function_one(void);
int function_two(void);
int function_three(void);
#endif
|
|
./src/function_one.c
#include "functions.h"
int function_one(void)
{
return MAGIC_NUMBER + 1;
}
|
|
./src/other_functions.c
#include "functions.h"
int function_two(void)
{
return MAGIC_NUMBER + 2;
}
int function_three(void)
{
return MAGIC_NUMBER + 3;
}
|
|
./src/example2.c
#include <stdio.h>
#include "functions.h"
int main(void)
{
printf("1() = %d\n", function_one() );
printf("2() = %d\n", function_two() );
printf("3() = %d\n", function_three() );
return 0;
}
|
|
Makefile
#Makefile for example 2
CC = gcc
FLAGS = -g
HEADERS = ./src/functions.h
example2 : $(HEADERS) ./src/example2.c ./obj/function_one.o ./obj/other_functions.o
$(CC) $(FLAGS) -o example2 ./src/example2.c ./obj/function_one.o ./obj/other_functions.o
./obj/function_one.o : $(HEADERS) ./src/function_one.c
$(CC) $(FLAGS) -o ./obj/function_one.o -c ./src/function_one.c
./obj/other_functions.o : $(HEADERS) ./src/other_functions.c
$(CC) $(FLAGS) -o ./obj/other_functions.o -c ./src/other_functions.c
#comments can go anywhere
clean:
rm -f ./obj/*.o
rm -f example2
|
This examples adds several new features. One difference to note here between gmake
and at least the make included with BSD derivatives, is gmake supports the relative file
names used above while BSD's make requires full paths to be used on anything not in the
current directory. Again running make will build the example. Let us examine
how this example is different from the first. The first three lines after the
comment are Makefile variables. They can be used anywhere in the Makefile by the
notation $(variable_name), which make will make a text substitution for the value
of the variable. Alluding again to the future article about autoconf, by placing
the command for gcc itself as makefile variable, a different compiler could be
used by changing only one value. The FLAGS variable illustrates how by changing
one variable we could decide to build support for debugging or not. The HEADERS
variable demonstrates the use of a variable as a target dependency.
Next notice the splitting the program into four source files. Using a real
build system encourages the use of multiple files. Not only will a build after
a small change run faster as a project grows larger, but a cleaner more
organized source tree evolves as time moves on. With experience an organizational
style that takes into account the needs of the Makefile, source tree layout,
and the build system at large can come naturally. Play around with the source
files and run make again and again. What happens when different combinations
of the source files get changed before running make? In this example I also
present the way I prefer to place temporary object files and other none
source code temporary files in a separate directory. This is entirely optional as
make will still work without doing that. I however love clean directories, and
I hate all those nasty temp files everywhere. Plus, if I want to delete the
temp files, they are all in one place.
That brings us to the last target, clean. A make clean here will always
run because it has no dependencies to compare against. A clean target is
a common Makefile utility target used to remove any temporary files and
prepare for a complete build from scratch.
In the next example we will take the functions in the files function_one.c,
and other_functions.c and place them in a static library. Our example's
main file will be a client driver for that lib. As the project is kept
small to be easy to understand, you should imagine how these simple
concepts could be used on a larger scale with far more complicated interdependencies.
Take the same source files from the last example, but use this new Makefile.
Makefile
#Makefile for example 3
CC = gcc
FLAGS =
LIBHEADERS = ./src/functions.h Makefile
LIBOBJS = ./obj/function_one.o ./obj/other_functions.o
LIBDIR = /usr/lib/
INCLUDEDIR = /usr/include/
testlibrary : $(LIBHEADERS) ./src/example2.c libfunctions.a
$(CC) $(FLAGS) -o testlibrary ./src/example2.c libfunctions.a
libfunctions.a : $(LIBOBJS)
ar rcs libfunctions.a $(LIBOBJS)
./obj/function_one.o : $(LIBHEADERS) ./src/function_one.c
$(CC) $(FLAGS) -o ./obj/function_one.o -c ./src/function_one.c
./obj/other_functions.o : $(LIBHEADERS) ./src/other_functions.c
$(CC) $(FLAGS) -o ./obj/other_functions.o -c ./src/other_functions.c
clean :
rm -f ./obj/*.o
rm -f example2 libfunctions.a
install : libfunctions.a
install libfunctions.a $(LIBDIR)
install ./src/functions.h $(INCLUDEDIR)
binpackage : libfunctions-1.0.tgz
libfunctions-1.0.tgz : libfunctions.a
mkdir libfunctions
cp libfunctions.a libfunctions
cp ./src/functions.h libfunctions
tar -czf libfunctions-1.0.tgz ./libfunctions
rm -rf libfunctions
|
Let's rundown the new things in this version. Notice that the FLAGS macro is blank. This is to show
that it is perfectly fine to have empty values for variables. Next besides changing HEADERS
to LIBHEADERS, the Makefile itself is added. With this little optional trick if you make a
change to the actual make file, make will just rebuild everything instead of having to go
through make clean; make. I find it handy for times such as just after you do a
svn update that really warrant a clean build without you realizing it. The other new
variables illustrate how things such as installation locations that users will have a good chance
of wanting to change should be easy to find and self explanatory. Also observe that the
dependencies of dependencies do not have to be explicitly listed as make derives those for us.
Two new utility targets have been added, install, for installing the library and its
interface header, and binpackage, for creating a redistributable binary package.
Beware of Features
Although there are plenty of other features that can be placed in your Makefile, be warned
that beyond what we have covered here it gets sloppy and pointless. I strongly advocate clean,
human readable Makefiles. One of the worst things that you can have is to end up with a dog
and pony show for a build system. Trust me, I've learned that the hard way. You will undoubtedly
want more from your Makefile based build system as time goes on. For that there are other
software projects such as autoconf, that can by supplementing your Makefiles, accomplish a great
deal without having to try to use make for something it's not.
One of the most irritating Make behaviors certainly needs to be mentioned. Most people, books,
and other sources seem to love what is known as implied rules. I hate them mostly
because they make it so hard to learn and understand what in the world Make is really
doing and what it's all about. This is especially so for the beginner who is also new to gcc.
In some cases if a rule is not explicitly defined for an object file for example, Make might
think it knows what to do anyway if there is a source file named a certain way. I strongly
discourage the use of this "Makefile shorthand" because requiring someone to remember these
things on top of everything else causes a Makefile to be just that much harder to read. So,
if you ever start swearing that Make just did some steps by itself you are not crazy. Just check
for any typos and make sure everything is explicitly listed.
For a detailed look at more of the dark side of make, I will refer you to other sources,
such as this paper,
Working with Makefiles.
Parallel Processing and GNU Make
In the previous example, the library's two source files could be compiled at the
same time since neither depends on each other. As a Make project grows, more and
more situations arise were Make could be working on more than one target at a time.
If we were using a dual core machine for example, then we could actually do more than one task at
once. GMake's command line option, make -j max_number_of_simultaneous_jobs
allows use to take advantage of SMP hardware. I recently tested an almost 99% performance
increase while building various source trees from scratch on a dual core athlon with make -j 2.
Of course if I had a 32-way Sun server to myself I could have just as easily gone
with gmake -j 32. Even if you do not have SMP hardware at your disposal you
should check out distcc. Distcc along with GMake
allows a compile farm to be built with as many workstations or servers you can spare.
If you find that your Makefile builds everything fine normally, but fails when used with
simultaneous jobs, it is probably due to an unlisted dependency. Start by double checking
the dependencies for the target that failed.
Other Uses for Make
GMake is by no means limited to working with C, or C++. Any language that
requires compiling or processing that can be divided into separate
tasks can utilize the advantages of a Make build system. Even more
interesting is that Make can be found in use for all kinds of things
beyond the scope of programing. One area is the intelligent automation
of document rendering procedures, such as composing html files from
separate header, footer, banner, and body files. It is not unheard of to see
a Make system drive a scientific or render batch process.
Make has even been used to calculate dependencies at boot time to
intelligently start service daemons in the correct order. Anything that is
based on performing incremental steps towards a larger goal is a potential
candidate for a Make's help.
Recursive Make
A recursive Make is when a command in a Makefile's target itself calls make.
Sometimes we see this when a project includes the entire source tree of another
project such as a library along with its distribution for convenience or other
reason. Other times the purpose of this technique is bring the Makefile down
to a smaller size. A large project might be divided into several source directories
each with its own Makefile. One master Makefile in the tree's root directory includes
all the others. The issues start when you want Make to be aware of dependency
relationships across different files. What happens instantly after setting it all
up though is that your once logical, but large single Makefile becomes a complete disaster.
Don't say I didn't warn you. Still not convinced recursive make is totally stupid?
Consult these links that made me a believer.
An excellent
paper on what's wrong with recursive Makefiles
Analisys of the better
parallelism in non-recursive Makefiles
Automake
GNU Automake is tool that generates Makefiles from automake input files. It is closely
related to GNU Autoconf, yet still a separate piece of software entirely. Many consider
it the way to work with Make. As I make my own way through the maze of open
source centric contemporary Unix software engineering tools and practices, I have come
to the contrarian viewpoint that Automake is worse than useless. I have spent several
days many times fighting it in an attempt to do something close to what I wanted; it was
futile. Automake is one of those uppity academic software packages that aims to take
complete control over as much of your project as possible. If you don't understand
why it's so inflexible, well that's simply because you are not cool enough. You can
literally take one of the tiny examples above, covert it to an automake based project,
and end up with a Makefile that is totally incomprehensible. As it runs to produce
a clean build you would see page after page of nonsense scroll by, burning through
cpu cycles for somewhere in the neighborhood of 20,000% longer than what you had to
start with. If you have ever downloaded and built the code of a big open source project
and wondered what in the world it was doing when you typed make, it is likely that
70% or more of that is fluff from using automake. Automake loves the recursive make,
and it is a control freak about file locations and and other details. Often things
like supporting cross compilers becomes even more complicated that need be.
I have yet to realize a use for it.
So why does anyone use it? It is not that it doesn't really accomplish its goals,
its that it inflicts as much pain as it can along the way. One aim is to be able
to render a Makefile for any version of Make out there. This is useful and impressive,
but with the ability to install GMake (which itself uses automake) almost anywhere -
it's a hard sell.
GNU Make on Windows
GMake can even be used on windows in a few different ways. The first
is if you use cygwin. Next MinGW (a gcc for windows)
comes with natively compiled version of gmake with some significant features missing.
A better version comes with MSys (a clean
subset of cygwin for development purposes with a nice installer). Remember that you
can put any commands you want in a Makefile rule. This includes windows programs such
as visual studio's command line compiler. If you are interested in this also check
out WCC. On
that same note, be aware that Visual Studio uses its own incarnation of make nmake.
Its pretty useless overall though, but if you are looking to migrate a Visual Studio
project to a real build system examine the nmake command file for a starting place.
Prelude to Autoconf
One very important thing we have not seen is how to conditionally change the
dependencies for a target without editing the Makefile.
For instance suppose we needed to have different features available in different
editions of our library in example 3. If we could somehow adjust the value
of the LIBOBJS variable, and by organizing our source files accordingly then
we would have a means to do so.
There are strategies
for trying this, such as consulting environment variables or the really
obscure Makefile if. I have found with much headache to not even try these.
In my opinion the only real way to that sort of thing is by dynamicly regenerating
the Makefile itself. That is exactly what GNU Autoconf is for, and that is a story
for another day.
© 2006, 2011 C. Thomas Stover
cts at techdeviancy.com
back
|