Data Structures - Lab Exercise 2
Using Make for G++ Compilations


This week's lab introduces an important tool: The make facility. This facility allows you to easily maintain and keep your files up to date when you are working on a project that is split accross several cxx files (and their associated header files). For each of your future assignments, you should keep all of the files for that assignment in one directory, and create a make file to manage the files in that directory.
Copy an Example Makefile
If you haven't already done so, please make a subdirectory called 2270, and within that directory make another subdirectory called labs. Change to this directory now, and then copy a file named makefile from the main's program directory:

cp ~main/programs/labs/makefile .

Don't forget the period at the end, meaning "copy to the current directory". If you don't already have them from last week, then you should also copy these three files to your directory: demo2.cxx, throttle.cxx, and throttle.h.

What's in a Makefile?
The file makefile is the usual input file for a tool called make. The purpose of make is to help you maintain and update a collection of related program files. The collection usually has header files, cxx files, and compiled files--all of which depend on each other. For example, consider last week's demo2 program. The final product is an executable file named demo2, which is created by linking together two other compiled files, demo2.o and throttle.o. The two compiled files were created by compiling demo2.cxx and throttle.cxx (both of which used the header file throttle.h). The complete dependencies among the files can be drawn like this:
                          demo2 (Executable File)
                             ^        ^
                             |        |
                       demo2.o        throttle.o
                       ^     ^        ^      ^
                       |     |        |      |
               demo2.cxx     throttle.h      throttle.cxx
    
The upward arrows in the file express how each file is created. For example, the file demo2.o is created by compiling demo2.cxx and also includes throttle.h. To state the matter simply:

If demo2.cxx or throttle.h changes, then demo2.o must be regenerated by giving the compiler command:
g++ -Wall -c -gstabs demo2.cxx

This requirement to sometimes regenerate demo2.o is one of the dependencies that the example makefile expresses. To see this dependency, use emacs to open up the file named makefile.

Near the bottom of the file you'll find these two lines:

demo2.o: demo2.cxx throttle.h
        g++ -Wall -c -gstabs demo2.cxx

The first line is called a target line, which begins with a file name and a colon. After the colon is a list of more file names. Here's how to interpret the line: The file before the colon (called the target file) depends on the other files (after the colon). Whenever one of the files after the colon changes, the make tool knows that the target file needs to be regenerated. After the target line, there is a series of commands that tell exactly how to regenerate the target file. For the case of demo2.o, we only need the one g++ command to regenerate the file. (Notice that we included the -c flag to indicate that we should only compile and not create an executable file yet. We also included the -gstabs flag in case we want to use the debugger.)

There is one other peculiar requirement: The command lines (such as the g++ command) must each begin with a tab (not with 8 spaces!).

As a second example of a dependency, the executable file demo2 is created by compiling together the object files demo2.o and throttle.o. If either of these two object files should change, then demo2 also needs to be recreated. Here is the appropriate target line and command from our makefile:

demo2$(SUFFIX): demo2.o throttle.o
        g++ -Wall -gstabs demo2.o throttle.o -o demo2

This target line says that if demo2.o or throttle.o should happen to change, then the demo2 must be regenerated with the g++ command that is shown. The string $(SUFFIX) is added to the end of the name of the file demo2. On a Unix system, this string is defined to be an empty string, so that the whole file name is just demo2. On an MS-Windows system, this string is defined to be ".exe" so that the whole file name becomes demo2.exe. Can you find the part of the makefile that defines the variable $(SUFFIX)? It is an if-statement that depends on whether or not another variable called COMSPEC is defined. (COMSPEC is something that is defined for MS-Windows, but not for Unix.)

Using Make to Regenerate a Specified Target File
In order to illustrate how the make facility works, start by getting rid of all the object files and executable files. You can do this with the remove command:

rm throttle.o demo2.o demo2

There are two simple ways to use the make facility to automatically regenerate your files. The first approach regenerates a specific file. For example, suppose you want to regenerate throttle.o. Then you can use the make command, as shown here:

make -k throttle.o

The make command will find the dependency information in the makefile file. It sees that throttle.o depends on other files, so it will first ensure that those files are present (and regenerate them if necessary). In this example, the two files throttle.h and throttle.cxx are necessary for generating throttle.o. These two files are present, so the make command proceeds to generate throttle.o, using the g++ command that is specified in the makefile. When the g++ command is executed, it is displayed on the screen, so you will see this appear on the screen:

g++ -Wall -c -gstabs throttle.cxx

After this command finishes, you should list the files in your directory, where you will find the object file throttle.o is once again present.

Using Make without Specifying a Target File
You may also use the make command without specifying a file, like this:

make -k

Without a specified file, the make command will regenerate the first target that it finds in makefile. Try this now, and you will see that the executable file demo2 is regenerated, since demo2 is the first target file in makefile. During the process of regenerating the demo2 file, the make command had to carry out several steps. In the first step, the make command realizes that demo2 depends on throttle.o and also on demo2.o. But the file demo2.o is not present. So, the make command first regenerates demo2.o, and then it can proceed to regenerate the executable file demo2. On the screen you'll see the two steps displayed:

g++ -Wall -c -gstabs demo2.cxx
g++ -Wall -gstabs demo2.o throttle.o -lm -o demo2

Using Make from within Emacs
The best feature of the make command is how it automatically keeps track of exactly which object files and executable files need to be recompiled. As an example, you should now change one of your source files. I suggest that you use emacs to make a small change to the demo2.cxx program, perhaps adding another small output statement. Then save the new demo2.cxx (CTRL-x s) and from within emacs give the compile command (ESC x compile RETURN).

What happens? The emacs compile command automatically issues the "make -k" command. The make facility realizes that demo2.cxx has changed, and therefore the object file demo2.o is regenerated with the command:

g++ -Wall -c -gstabs demo2.cxx

Next, the make facility realizes that demo2.o has just changed, and therefore the executable file demo2 is regenerated with:

g++ -Wall -gstabs demo2.o throttle.o -lm -o demo2

But, notice that the object file throttle.o was not recompiled. The dependencies in the makefile were sufficient to show that throttle.o did not need recreation.

Using Make with Special Targets
A makefile can also have special target lines that carry out special actions rather than regenerate files. There are two such target lines at the bottom of the example makefile:

clean:
	$(RM) demo2$(SUFFIX)
	$(RM) demo2.o
	$(RM) throttle.o

all:
        @make $(EXPENDABLES)

The special target "clean" simply removes all of the files that can be generated by the makefile. (The command to remove a file varies between Unix and MS-Windows. In Unix, the variable $(RM) is defined to be the rm command with the f option to force removal even if the file is read protected; in MS-Windows, the variable $(RM) is defined to be the del command.)

The special target "all" regenerates demo2 by executing the make command itself. If there were more executables, then you could add more make commands to the all target. The @ symbol in front of make command suppresses printing of the command, so that "make. . ." will not be printed on the screen. The two special targets can be activated just like any other target. For example:

make -k all

Using Make on Your PC
If you are using the CSCI 1300 software on your PC, then you can also build and use makefiles. The primary differences:
  1. The name of a target for an executable file should have ".exe" added to the end. For example: demo2.exe instead of just demo2.
  2. The rm command won't work to remove files, and the del command for a PC will remove only one file at a time. So, you'll have to modify the clean and all targets to use the del command to remove one file at a time.

    The makefile that we provided for this example works fine for either Unix or on a PC (because of the two variable SUFFIX and RM). Note that when the value of a makefile variable is defined, you just use the name (such as SUFFIX or RM). But when the variable is used, you must put the name in parentheses with a dollar sign in from (such as $(SUFFIX) or $(RM)).


Michael Main (main@colorado.edu)