CS 371 Lab #8: the "make" program

You can do this lab during lab time or on your own; you should learn as much as you can about the "make" program in two hours. Your classmates are your study partners. "make" is fair game on the final exam, we will be using it heavily for your project. Make a list of questions to ask me about "make" in class.

Introduction

Make is a program which is used to maintain, update, and regenerate related programs and files.

Make provides a simple mechanism for maintaining the latest version of programs that result from many operations on a number of files. It is possible to tell Make the sequence of commands that create certain files, and the list of files that require other files to be current before the operations can be done. Whenever a change is made in any part of the program, the Make command will create the proper files with  minimum effort.

You can look up the online make manual for more information or type in your command line as:

        man make

In the simplest case, make is executed without explicit parameters:

        make
 

Makefile

Make reads an input file ("makefile") that defines which files make up a program. In addition, the makefile shows the relationships between the files, which comprise a DEPENDENCY graph. Make takes responsibility for recompiling any source files that has been changed since the last time it had been compiled and also for recompiling any source file that depends on other source files that have changed. This means you no longer have to remember which files have recently changed. You don't have to recompile everything, just in case. And, once you've written in the makefile the complete (for large programs, very long) rules for compiling and linking, you don't have to remember them any more.

make can also do other routine tasks often required in program administration. For example, make can be used to list and make backup copies for only those source files that had been changed since their last printing and backup.

When you run make with no arguments, it searches for a file named makefile in the current directory, or if there is no file by that name then Makefile. Make has a -f option, followed by an arbitrary filename, that specifies an alternative makefile with a non-default name.

A makefile contains the following information:

1. Macro definitions at the beginning of the description file have the following form:

        name = string

e.g.    CFLAGS = -O

2. Information on file dependencies followed by executable commands
A dependency line shows the relationship between a target file name given to the left of the colon, and the files that the target depends on, given to the right of the colon. The command line(s), also called RULES that follow the dependency line are assumed to transform the dependency files into the target file. The basic format for dependency rule is:

        target: [ dependency ...]
                               [ command line ....]

As shown above, the commands are grouped below the dependency line and are typed on lines that begin with a tab. Subsequent lines that start with a tab symbol are taken as the command lines that comprise the targets rule. A common error is to use space characters instead of the leading tab. Lines that start with a # are treated as comments. If make finds a makefile, it begins the dependency check with the first target entry in the file. You can also indicate the target explicitly as a parameter when calling make.
 

A simple example of a Makefile

        # Makefile for creating an executable file from two independently compiled

        # C source files

        CFLAGS= -O -g

        CC= gcc

        try: try.o functions.o

                $(CC) $(CFLAGS) -o try try.o functions.o
        # note: this line starts with a tab symbol

        try.o: try.c

                $(CC) $(CFLAGS) -c try.c

        functions.o: functions.c

                $(CC) $(CFLAGS) -c functions.c

        clean:

                rm try try.o functions.o
 

The first two lines define two macros, one for compilation flags and the other the CC (Compiling Command) macro.

When make is run on this makefile it sets its target to the first target in the Makefile - the file try.

The executable file try depends on try.o and functions.o .The command for creating the try is

        gcc -O -g -o try try.o functions.o

In order to complete the creation of the file try,  it will set subtargets try.o and functions.o, as described in the dependency rule. The -o option places the output in file try.  This applies regardless  to  whatever sort of output gcc is producing, whether it be an executable file, an object file, an  assembler  file or preprocessed C code. The  -g  option  produces debugging information in the operating system's native  format . GDB can  work with this debugging information. With the option  `-O'  the compiler tries to reduce code  size  and execution time. Without `-O', the compiler's goal is to reduce the cost  of  compilation  and  to make debugging produce the expected results.

The try.o file depends on try.c and is created by the command

        gcc -O -g -c try.c

Similarly, functions.o depends on functions.c and is created by the command

        gcc -O -g -c functions.c

Another way to do the same is to run

        make try

indicating the main target explicitly.

The last two lines define a rule used to remove the executable and object files. It will work when this target is indicated explicitly:

        make clean

Because the target clean is not mentioned in other dependency rules it can be achieved only when called explicitly. Note that in both cases make uses (by default) the same Makefile.
 

Assignment

C++ and Make

Learning "make" well is an important 371 topic whether or not it we use it for the semester project...and, we will use it for the semester project. You should read and learn about "make" as per the description above. Then we should consider makefiles for C++ projects. Large C++ project makefiles tend to have some of the following issues:

Header file dependencies
You don't just say "foo.o depends on foo.cpp", you say "foo.o: foo.cpp x.h y.h z.h..." that is, list all .h files including .h files included by .h files.
Subclass dependencies
Subclasses' .cpp files need to be recompiled if their superclass is modified, so add them to the makefile dependency list
Application Includes and Libraries
The compiler can find system includes and libraries like -lm or standard C++ libraries, but you will need -L and -I to tell it where to look for your own libraries and header files.
Directory Issues
Actual (non-library) source code for an application may be scattered across multiple directories. Different compilers handle compiles of non-local sources varyingly. Usually its best to write a makefile for each directory.
Platform issues
It is usually possible, but difficult, to write makefiles that will work on different OS platforms and compilers. Makefiles tend to have extra options and symbols needed for specific platforms. Modern makes tend to support an "include" mechanism to handle such variations, and are increasingly machine-generated by automatic configuration tools such as autoconf.
For this lab, you should write a Makefile for compiling a C++ program named "fps". The source code is stored in two files, fps.cpp (a main() function) and data.cpp (a class with some instance data). Your Makefile should meet the following requirements:
  1. It builds a library file named libfps.a (read manual page for "ar") from fps.o and data.o
  2. A "link" rule should build target executable named fps from libfps.a.
  3. The compile rules should build targets like fps.o from sources like fps.cpp
  4. Your makefile should be able to clean up the working directory (remove .o, .a and similar by-products of compilation)
Now, add your data file(s) (created in an earlier lab, hopefully by now accessible via the class CVS repository) to the makefile with a rule that bundles them together into a single .a file. Modify your data files so that they place their instances in global singleton class or other collection. Can you see how to modify fps.cpp to generate one random instance out of each type of object we have so far?

Each of you please turnin to the TA your makefile. Put the joint merged source code base (makefile and main program that initializes all the instances) on the project web site.

By the way, there is an interesting relationship between makefiles and the UML class diagrams you have been doing. The makefile encodes a "dependency graph", and inheritance between classes is one form of dependency. This means that the inheritance relationships in your class diagrams will result in makefile dependencies, or in other words, your makefile dependency graph will be a superset of the inheritance graph.