• No results found

Most game development projects consist of multiple source files, for the simple reason that it is impractical to manage thousands of lines of code in a single file. Since a large project can involve many source files, it would be wasteful to recompile everything if only one file had been changed since the program was last compiled. This happens, however, if all of the files are given to gcc at once on the command line. For instance, the Linux version of Civilization: Call To Power consists of more than 500,000 lines of C++ code in well over 100 files, and a full recompile of the entire source tree takes nearly an hour (whereas a partial rebuild assisted by Make usually takes 15 to 20 minutes).

The Make utility speeds up software development by automatically determining which files actually need to be recompiled after changes have been made. Make also eliminates the need to type long command lines to rebuild programs, since it stores all of the required commands and invokes them as needed.

Although Make has a lot of functionality, its basic usage is quite simple. It is based ontargets, which are sets of directions for maintaining the components (object files, libraries, and so on) of a program. Targets specify the name of the component to track, the source files and other targets that the component depends on, and the commands for rebuilding the target. The instructions for building a component are called rules, and the list of files that a component depends on are calleddependencies. When make is invoked upon a certain target, it checks that target’s dependency list first. If any of the dependencies have been changed since the target was last rebuilt, the target’s rules are executed. Make also recursively rebuilds any out-of-date targets in the dependency list. This is extremely convenient for large, modular programming projects.

Creating Makefiles

Make looks for targets and rules in a file calledMakefile ormakefile. This file can contain any number of targets. If Make is started with no command-line options, it automatically attempts to rebuild the first target it encounters. Consider the following makefile:

program: file1.c file2.c graphics.a gcc -c file1.c file2.c

graphics.a: graphics.c draw.c gcc -c graphics.c draw.c

ar rcs graphics.a graphics.o draw.o ranlib graphics.a

This file describes how to build an executable called programand a static library called graphics.a. (Don’t worry about the commands for building the library—we’ll discuss libraries later in this chapter.) program depends on

file1.c,file2.c, andgraphics.a. If any of these have been modified since

programwas last built, Make will rebuildprogram. graphics.ais also a target, and it depends ongraphics.cand draw.c. The indented lines under each target are rules. If program needs to be rebuilt, Make will execute the two rules that have been provided. These lines must be indented with tab characters; spaces will not work. Make is rather particular about syntax.

Variable Substitution

The Make utility provides convenient access to environment variables. Makefiles can set, combine, and retrieve environment variables as text strings and can include these variables in targets and rules. It is common to use the variable CC

to represent the C compiler command (which in our case is gcc), CFLAGSto represent the standard set of command-line options to pass to the compiler, and

LDFLAGSto represent the options to pass to the linker (which is normally just the C compiler but is sometimes explicitly invoked with theld command). For example, the previous makefile can be rewritten as follows to take advantage of variable substitution:

CC=gcc

CFLAGS=-O2 -W -Wall -pedantic LIBS=-lSDL -lpthread

program: file1.c file2.c graphics.a

$(CC) $(CFLAGS) -c file1.c file2.c

graphics.a: graphics.c draw.c

$(CC) $(CFLAGS) -c graphics.c draw.c ar rcs graphics.a graphics.o draw.o ranlib graphics.a

As you can see, variables are substituted into the makefile with the$(VARNAME)

notation. This is a literal text substitution, and it takes place before the rule is otherwise processed. What if you want to add to the end of a variable without destroying its old contents? You might try something like this:

FOO=bar

FOO=$(FOO) baz FOO=$(FOO) qux

At a glance, it would appear that the FOOvariable would end up with the value

bar baz qux. However, Make does not normally evaluate variables until they are used (in targets), soFOOactually ends up with the string $(FOO) qux. There are two solutions to this problem. GNU Make (the default Make on Linux systems) provides a:=operator for assignments, which causes its right-hand side to be evaluated before the variable is assigned. It also provides a +=operator for directly appending to variables. A more portable solution would be to assign

bar,baz, andqux to three different variables and to combine them all at once:

BAR=bar BAZ=baz QUX=qux

FOO=$(BAR) $(BAZ) $(QUX)

This (hacked) solution allows the variableFOO to be constructed correctly when it is used in a rule. It is a rather ugly way to do so, however, so we suggest using the GNU Make extensions.

Although the use of variables might lengthen a makefile, they can provide a nice bit of abstraction. Variables make it easy to modify the options used throughout the build process without changing the whole makefile.

Implied Rules

Since C files are almost always compiled with the cccommand (which is a symbolic link to thegcccommand on Linux machines), there is really no need to specify build rules for each source file in the project. Make allows forimplied

build rules. That is, if a target is followed by no rules and does not specify any dependencies (or it simply does not exist), Make will attempt to use adefault

build rule based on the target’s file extension.

For example, let’s say thatfoo.cis a C source file containing the function bar

and thatmain.c is a C source file containing amain function that callsbar. The following makefile will build the program. Notice that there is no target for

foo.o—it is referenced by thefootarget, and Make assumes that it should create the target by compiling the filefoo.c. (Actually, Make knows of several different source file types, C being perhaps the most common.) When Make automatically invokes the C compiler, it adds theCFLAGSvariable to the command line.

CFLAGS=-O2 -W -Wall -pedantic foo: foo.o main.c

gcc foo.o main.c -o foo

Phony Targets

Programmers often use Make for purposes other than building executables. It’s really a general-purpose project management tool. For instance, I’m currently using a makefile so that I don’t have to delete a bunch of files and then run LATEX, MakeIndex, and dvips every time I want a preview of this book. Consider the following makefile:

foo: foo.c

gcc foo.c -o foo clean:

rm *.o rm foo

The cleantarget has no dependencies and is therefore built only when it is specifically requested on the command line. The commandmake cleancauses

all object files as well as the executablefooto be deleted and therefore serves to force a complete rebuild of the project. Programmers commonly include aclean

target in their makefiles for convenience.

In a more general sense, Make is often used as a simple interface to complex commands. Targets used for this purpose do not actually describe a build process but rather a set of commands to be executed when the target is

requested. But what happens if such a “phony” target has the same name as a file in the current directory? For instance, what if there is a file called clean? Make would detect that this file exists and would decide not to build the target. Make provides a special pseudo-target called .PHONYfor this purpose. .PHONY

takes a dependency list, just as other targets do, but no build rules. .PHONY’s dependencies are marked as phony targets and will always be built when

requested, regardless of any existing file by the same name. Here is the previous makefile, rewritten to use the .PHONYtarget.

foo: foo.c gcc foo.c -o foo .PHONY: clean clean: rm *.o rm foo Error Handling

In the event of an error, Make immediately stops and prints an error message (in addition to whatever was printed by the command that failed). Make detects errors by the return codes of the rules it executes: a return code of zero indicates success, and anything else indicates an error. Most UNIX commands follow this convention. If there is a syntax error in the makefile itself, Make will complain about it and exit.