GNU/Linux Application Programming (Programming Series)
Creating a binary in a compiled language often involves lots of steps to compile all of the source files into object code and then invoking the linker to put the object code modules together into an executable. The necessary steps can all be performed by hand, but this becomes tedious very quickly. Another solution would be to write a shell script to perform the commands each time. This is a better solution but has a number of drawbacks for larger projects and tends to be hard to maintain over the life of the project. Building software has a number of unique requirements that justify the development of a tool that is specifically targeted at automating the software build process. The developers of UNIX recognized this requirement early on and developed a utility named make to solve the problem. This chapter is an introduction to GNU make , the open source implementation of the make utility commonly used in Linux software development.
An Example Project
The approach used in this chapter will be to introduce a simple example project and then show how to build the project starting with a command line solution and progressing to a fairly complete GNU make implementation. The examples in this chapter will show various ways to build a project consisting of four source files. The diagram shown in Figure 5.1 illustrates the directory layout of the project.
Compiling by Hand
The simplicity of the example project makes it very easy to compile by hand. Executing the following command in the top-level project directory will generate the application named appexp with a single command.
gcc -o appexp src/main.c src/app.c src/bar.c src/lib.c
This command runs the GCC wrapper program that invokes the preprocessor, compiler, and linker to turn these four c-files into an executable. The single command approach is acceptable for such a simple project, but for a larger project this would be impractical . The following set of commands breaks the compilation into the incremental steps commonly seen in a more complicated build process.
gcc -c -o main.o src/main.c gcc -c -o app.o src/app.c gcc -c -o bar.o src/bar.c gcc -c -o lib.o src/lib.c gcc -o appexp main.o app.o bar.o lib.o
The first four commands in this series turn the c-files in the src directory into object files in the top-level directory. The last command invokes the linker to combine the four generated object files into an executable.
A Build Script
Typing any of the commands described in the previous section would get pretty tedious if it had to be done every time the application needed to be rebuilt. The next obvious step would be to put these commands into a script so that a single command could perform all of the steps needed to build the application. The following listing shows the contents of the buildit script that might be written to automate the build process.
Listing 5.1: The buildit Script (on the CD-ROM at ./source/ch5/buildit )
|
1: #!/bin/sh 2: # Build the chapter 5 example project 3: 4: gcc -c -o main.o src/main.c 5: gcc -c -o app.o src/app.c 6: gcc -c -o bar.o src/bar.c 7: gcc -c -o lib.o src/lib.c 8: gcc -o appexp main.o app.o bar.o lib.o
|
The script collects the commands outlined in the previous section into one place. It allows the developer and user of the source code to build the application with the simple ./buildit command line. Also, it can be revision controlled and distributed with the source code to ease the understanding needed by those trying to build an application. One of the disadvantages of the build script is that it rebuilds the entire project every time it is invoked. For the small example in this chapter, this does not cause a significant time increase in the development cycle, but as the number of files increases , rerunning the entire build process turns into a significant burden . One of the major enhancements of the make utility over a shell script solution is its capability to understand the dependencies of a project. Understanding the dependencies allows the make utility to rebuild only the parts of the project that need updating due to source file changes.
A Simple Makefile
The make utility uses a developer-created input file to describe the project to be built. GNU make uses the name Makefile as the default name for its input file. Thus, when the make utility is invoked by typing the make command, it will look in the current directory for a file named Makefile to tell it how to build the project. The following listing illustrates a basic Makefile used to build the example project.
Listing 5.2: Simple Makefile (on the CD-ROM at ./source/ch5/Makefile.simple )
|
1: appexp: main.o app.o bar.o lib.o 2: gcc -o appexp main.o app.o bar.o lib.o 3: 4: main.o : src/main.c src/lib.h src/app.h 5: gcc -c -o main.o src/main.c 6: 7: app.o : src/app.c src/lib.h src/app.h 8: gcc -c -o app.o src/app.c 9: 10: bar.o : src/bar.c src/lib.h 11: gcc -c -o bar.o src/bar.c 12: 13: lib.o : src/lib.c src/lib.h 14: gcc -c -o lib.o src/lib.c
|
Line 1 illustrates the basic construct in Makefile syntax, the rule . The portion of the rule before the colon is called the target, while the portion after the colon is the rule dependencies . Rules generally have commands associated with them that turn the prerequisites into the target. In the specific case of line 1, the target of the rule is the appexp application that depends on the existence of main.o , app.o , bar.o , and lib.o before it can be built. Line 2 illustrates the command used to invoke the linker to turn the main.o , app.o , bar.o, and lib.o object files into the appexp executable. It is important to note that one of the idiosyncrasies of Makefile syntax is the need to put a hard tab in front of the commands in a Makefile; this is how the make utility differentiates commands from other Makefile syntax. The make utility uses rules to determine how to build a project. When the make utility is invoked on the command line, it parses the Makefile to find all of the target rules. It then attempts to build a target with the name all; if the all target has not been defined, then make will build the first target it encounters in the Makefile.
For the Makefile in Listing 5.2, the default target is the appexp application because its rule (line 1) occurs first in the Makefile. The neat part about the Makefile rule syntax is that the make utility can chain the rules together to create the whole build process. When the make utility is invoked, it will begin to build the project by trying to build the default rule: rule 1 in the example Makefile. The rule on line 1 tells the make utility that to build appexp , it must have the files main.o , app.o , bar.o , and lib.o . make will check for the existence of those files to determine if it has everything needed to build the application. If one of the prerequisite files is missing or is newer than the target, then make will start searching the target rules to determine if it has a rule to create the prerequisite.
Once an appropriate rule is identified, then the process will start again by ensuring that the new rule s prerequisites exist. Thus the make utility chains rules together into a tree of dependencies that must be satisfied to build the original target. make will then start executing the commands associated with the rules at the leaves of the tree to build the prerequisites needed to move back toward the root of the tree. In the example Makefile, the process would start with the default rule on line 1. If the object files didn t exist yet, then the make utility would find the rules to make them. First it would find a rule to create main.o , which occurs on line 4. Next, make would examine the rule on line 4 and realize that the
gcc -c -o main.o src/main.c gcc -c -o app.o src/app.c gcc -c -o bar.o src/bar.c gcc -c -o lib.o src/lib.c gcc -o appexp main.o app.o bar.o lib.o
Comparing this to the script in Listing 5.1, we can see that we have reimplemented the simple build script using GNU make . So why use GNU make instead of a build script? After all, it seems like a more complicated way to implement the same steps the build script took care of for us. The answer is the capability of GNU make to build things in an incremental fashion based upon the dependencies that are set up in the Makefile. For example, suppose after building the appexp program we discover an error in the app.c source file. To correct this, we would edit the app.c source file and then rebuild the executable. If we were using the Listing 5.1 script, then all of the source files would get rebuilt into objects, and those objects would get linked into an executable. On the other hand, with the make utility, the rebuild would be accomplished with the following two commands.
gcc -c -o app.o src/app.c gcc -o appexp main.o app.o bar.o lib.o
How did GNU make eliminate the need for the other commands? Since GNU make understands each step that goes into the creation of the executable, it can examine the dates on the files to determine that nothing in the dependency tree for main.o , bar.o , and lib.o changed, and thus these object files don t need to be re-created. Conversely, it examines app.o and realizes one of its dependencies, namely app.c , did change, so it needs to be rebuilt. If you understand the dependency tree that is represented in the Makefile syntax, then you understand the power of make over a simple build script. The Makefile syntax also provides other capabilities beyond those that have been discussed in this section; the rest of this chapter will consider some of the more useful capabilities of GNU make .
Категории