Makefiles-Introduction

Separate Compilation

When a large program must be written, compiling the entire program every time a change is made to it can be time-consuming and wasteful of computing and human resources. When more than one person is working on the same program, there is a problem of  only one person at a time being able to edit a source program file. Consequently, most programming languages support separate compilation--a way to write the program in multiple files, compile them separately, and then link the various program parts together to form a single program. A separate compilation feature facilitates the construction of libraries of reusable program components, a highly desirable feature in view of the costs of developing all software from scratch.

A common organization of C++ programs utilizes header files and source files. A header file, usually named with a suffix of .h contains type declarations, constant declarations, external variable declarations, function prototypes, and/or class declarations. Source files, usually named with a suffix of .cxx or .cpp, contain the definitions for the declarations that appear in header files. Under this organization,

Usually [but not always] a header file has a corresponding source file--for example, header file hello.h has a source file helloimplementation.cpp. The name of the file indicates that the components defined therein are associated with a Hello abstract data type.

Dependencies

Under this organization, a source file will typically #include the header files for the program component it wants to use. In order for component C1 of a program to use another component, C2, then C1 needs only have the information available in a header file to be compiled. The implementation of C2 can be changed without affecting C1. Thus, C2 can be compiled separately. However, any change to the specification for C2 [in the header file for C2] will require that C1 be recompiled since that change can affect the things for which C1 is using C2. Consider the following example, which shows three files that make up a program [omitted details are represented by ellipses]:

// hellomain.cpp
#include <iostream>
#include "hello.h"
int main(void) {
    Hello HiThere;
   HiThere.Display();
    return 0;;
}
   
// hello.h
#ifndef HELLO_H
#define HELLO_H
class Hello
{
public:
    Hello();
    void Display();
 private:  
   char Msg[71];
};
#endif
   
// helloimplementation.cpp
#ifndef HELLOIMPLEMENTATION_CPP
#define HELLOIMPLEMENTATION.CPP
#include <iostream>
#include "hello.h"
#include <cstdlib>
using namespace std;
Hello::Hello()
{
   strcpy(Msg, "Hello, there!");
}
void Hello::Display()
{
   cout<<Msg<<endl;
}
#endif
 
 
 
...

This example follows a standard pattern:

Note that this organization sets up some dependencies among the files and the contents of those files:

These dependencies are pictured below:

cstlib

hello.h

iostream

Header Files

\      

 

|

|

|

\ /
X
/ \

|

|

|

 

helloimplementation.cpp

hellomain.cpp

C++ Source Files
  | |
 

helloimplementation.o

hellomain.o

Object Files
 

\

 

/

 

hellomain

Executable File

The executable depends on two object files, while the object files depend on their respective source files and header files. In the process of building the executable [program] named hellomain, the source ( .cpp) files must be compiled to object  ( .o) files, which are then linked together to become an executable file (hellomain).

The dependencies in this example show that whenever a change is made to hello.h, then the program components in hellomain.cpp and in helloimplementation.cpp need to be recompiled. Even if the change in hello.h were only within a comment, the safest course is to recompile any files that #include the one that has changed.

Using make to Manage Dependencies

In a large program, there can be hundreds of header and source files. Keeping track of dependencies and what changes require what recompilations can be a nightmare. Fortunately, a tool exists in the UNIX environment to manage dependencies and to take whatever actions are necessary to bring a program up-to-date in terms of recompiling what is necessary. As a bonus,the same tool can be used to help keep directories cleaned up. Using that tool does take a little work and understanding of how it works on your part, but the effort to learn and use make can result in a payoff of reduced frustration and reduced program development time. make is indispensable for managing large programs, but is useful for smaller programs as well. You will be required to use make in all projects for this course.

What is a Makefile ?

The input to make is a makefile, an ordinary text file that contains rules on how to build a program. A rule has the following format:

target:   prerequisite-list
 TAB construction-commands

The target is the name of a file that depends on the files in the prerequisite-list. The construction-commands, on a line that must start with a TAB character, are regular commands to the shell that construct [usually compile and/or link] the target file. The make utility executes the construction-commands when the modification time of one or more files in the prerequisite-list is more recent than the modification time of the target.

Each of the prerequisites can be a target named in another rule.

The following makefile corresponds to the dependency graph shown above. [make is picky about rules, so be sure to use a tab character (represented by TAB in the examples) at the start of a line containing a construction-command.]

# Makefile for the hellomain program (comment lines begin with #)
hellomain: hellomain.o helloimplementation.o
 TAB c++ -o hellomain hellomain.o helloimplementation.o
hellomain.o: hellomain.cpp hello.h
 TAB c++ -c hellomain.cpp
helloimplementation.o: helloimplementation.cpp hello.h
 TAB c++ -c helloimplementation.cpp

Since standard include files, such as iostream.h, seldom change, most people do not include such dependencies in prerequisite-lists. This makefile has three targets:

A makefile can have any name, but it is typical to name it either makefile or Makefile.   Both these names are defaults for UNIX make and are searched for in your current working directory if make is invoked without arguments. For more in-depth information on arguments to make and using a makefile by another name, see man make.

Executing the makefile

This makefile is executed by the command

make

entered at a command prompt.

What does make do?

Make interprets each rule as follows:

Check that the target, a file having the same name in the working directory,   is up-to-date. [A target is up-to-date if it exists and if each of the files listed in its prerequisite-list both exists and has a modification date/time that is earlier than that of the target.  i.e., has not been changed since the last make.] If the target  is not up-to-date, then bring it up-to-date by:

  1. ensuring that each of the files listed in the prerequisite-list is up-to-date in accordance with any rule listing that file as a target. [If a file is not up-to-date, then apply the rule listing that file as a target to bring it up-to-date.]
  2. applying the construction-command for this rule to create/update the target.

When make starts execution, it starts using the first rule that appears in the file. In this example, the first rule applied is for the target demo. Let's assume that only the source files exist. Then make takes the following steps, starting with the rule listing hellomain as a target.

hellomain: hellomain.o helloimplementation.o
    c++ -o hellomain hellomain.o helloimplementation.o

Since file hellomain is not up-to-date [since it does not exist], Step 1 is applied, checking that each prerequisite, hellomain.o and then helloimplementation.o, is up-to-date.

hellomain.o: hellomain.cpp hello.h
    c++ -c hellomain.cpp

Since the files listed in the prerequisite-list for target hellomain.o (hellomain.cpp and hello.h) are up-to-date [since they exist and no rules list either as a target], Step 2 is applied, causing the the construction-command

c++ -c hellomain.cpp

to be executed.

helloimplementation.o: helloimplementation.cpp hello.h
 TAB c++ -c helloimplementation.cpp

Since the files listed in the prerequisite-list for target helloimplementation.o (helloimplementation.cpp and hello.h) are up-to-date [since they exist and no rules list either as a target], Step 2 is applied, causing the the construction-command

c++ -c helloimplementation.cpp

to be executed.

Since all the files in the prerequisite-list for demo have been brought up-to-date, Step 2 is applied, causing the construction-command

c++ -o hellomain hellomain.cpp helloimplementation.cpp

to be executed.

 

Your assignment, create the three files for the hello example and the makefile in a separate directory.  Type in make.  If you have no errors, you should get a hellomain created.  Fix any errors (if any).  Run hellomain.  Now type in make.  What happens?  Now remove all the .o files and hellomain.  Start a scriptfile.  Execute make.  Print the script file and write the answer to the "What happens?" question.