Revised: October 2003.
This article is an introduction to a useful programming aid called MAKE. We will discuss the use of MAKE in terms of C examples. However, MAKE is useful when programming in other languages as well.
A typical use of MAKE involves storing (in a file called "Makefile") a specification of the steps required to compile a program. The steps are invoked with a short command e.g. "make prog". Thus MAKE affords a simple economy of typing which can be very valuable in itself, but MAKE does more.
In cases where a program is composed of several separately compiled modules, MAKE examines the modification times of relevant files and recompiles only those components that have been modified since the previous compilation. This can significatly decrease the turnaround time during program testing.
In summary, the programmer specifies the relationships (dependencies) between the components of a program and the steps (commands) to build a program in a file called "Makefile". The program "make" then executes the commands in the Makefile to compile the program.
(What Make does for you)
Compiling a program that consists of more than one source file involves two steps: compiling each module into its respective object file and then linking the object files into the executable program. Object files are intermediate code files created by the compiler which by themselves are not executable, but when linked together, are. They are easily identified by their .o extension. Object files are created with the -c option on the compiler. For example, the commands
cc -c prog.c cc -c func.c
will compile the source files prog.c and func.c into the object files prog.o and func.o. An additional command to the compiler,
cc prog.o func.o
will compile prog.o and func.o into a working a.out file which you can execute. The process of putting together .o files is called "linking." If you want to compile the object files into a filename other than a.out, you can specify it with -o name in the compile command. Eg, instead of the above command, use
cc prog.o func.o -o myprog
This will link prog.o and func.o into an executable called myprog. If you make changes to, say, prog.c, all you need to do is recompile prog.c into an object file, then link the object file into the program using the existing func.o object file.
cc -c prog.c cc prog.o func.o -o myprog
As you can see, only the modified source files need to be recompiled. Since the object files for the unchanged source files still exist and are up to date, they can simply be reused.
These are the basic steps for compiling a program with more than one source file. MAKE allows you to automate this by specifying the compilation commands in a file which MAKE uses, called the "Makefile".
Shell scripts allow you to automate certain tasks. You can use them to examine printer queues, like zebras do. You can use them to automate any series of commands, and even do different things using an if statement. You can repeat statements using a for or a foreach loop.
Aliases are an alternative to shell scripts that can be used when you only want to run a one line command. You could make an alias by typing the following:
alias rl 'rlogin'
That line would be typed in at the shell (normally % sign) prompt, or added into your .cshrc file. Then, you could type in 'rl ieng' to rlogin to ieng9.
Shell scripts are generally written for the Bourne shell (sh), although C shell scripts are not uncommon. As a reference for writing Bourne shell scripts, please see the man page by typing "man sh". There are many shell scripts installed on ACMS systems, so it shouldn't be hard to find examples. Your .login and .cshrc files are basic examples of C shell scripts.
Please refer to the man pages and the section of this article that describes how to get more information for recommended books.
Let's continue with the example we started earlier... We have two source files, prog.c and func.c. The prog.c file has an #include "head.h" statement, the func.c file has no includes, and the executable program is to be named "myprog". One way to construct a Makefile would be as follows:
myprog: prog.o func.o
cc prog.o func.o -o myprog
prog.o: prog.c head.h
cc -c prog.c
func.o: func.c
cc -c func.c
Note that your #include files are also specified in the dependencies. If program modules depend on #include files, then you will want them recompiled if the #include files are changed. However, since system #include files (such as stdio.h) never change, they don't need to be listed as dependencies.
This Makefile says that myprog is dependent on prog.o and func.o. That is, prog.o and func.o are required to exist and be up-to-date in order to put together the program myprog. Likewise, prog.o is dependent on prog.c and head.h. Finally func.o is dependent on func.c.
When MAKE sets about making a target, it first insures that each of the dependency items is up-to-date. If it finds that the target item is older than any of the dependency items or does not exist, it performs the associated commands to remake the target. Note that the Makefile and all files referenced by MAKE should be in the same directory.
Let's assume the program's sourcefiles (prog.c, func.c, head.h) and the Makefile have been created. At this point the directory listing might look like this:
% ls Makefile func.c prog.c head.h
From the the section "Creating a Makefile", our Makefile looks like this:
myprog: prog.o func.o
cc prog.o func.o -o myprog
prog.o: prog.c head.h
cc -c prog.c
func.o: func.c
cc -c func.c
We will now use MAKE to create prog.o, func.o, and myprog with the command:
% make myprog
MAKE looks for a file specifically called "Makefile". If it does not find one, MAKE will not work.
MAKE starts at the first target, "myprog". It notes that there is a dependency on prog.o and so proceeds to the target "prog.o". Now MAKE sees that prog.o in turn is dependent upon prog.c and head.h. MAKE then checks to see if prog.c is current. Since there is no prog.c target in the Makefile and prog.c exists, MAKE is satisfied that prog.c is up-to-date. Likewise for head.h. MAKE returns to the business of assuring that prog.o is up-to-date. Since prog.o does not exist, MAKE invokes the command "cc -c prog.c" to make a current prog.o. MAKE returns to "myprog" and continues to the next dependency, func.o. MAKE goes through a sequence of steps to create func.o similar to those described for prog.o. Returning to "myprog", MAKE sees that there are no more dependencies to check. Since "myprog" does not exist, MAKE invokes the command "cc prog.o func.o -o myprog" and the job is done.
Now let's say we modify the func.c file and we want to remake the executable, myprog. Running "make myprog" as before will do just that. As usual, it starts at "myprog", finds the dependency prog.o, then proceeds to the target "prog.o". In checking the dates and times of prog.c and head.h, it sees that neither of these dependencies has changed since "prog.o" was made. Therefore, MAKE skips the step of remaking "prog.o". It's a different story for "func.o". MAKE finds that the dependency "func.c" is newer than "func.o" so the command "cc -c func.c" is run to recompile "func.o". MAKE returns to "myprog", sees that there are no more dependencies, and then notes that func.o is newer than myprog. So the command "cc prog.o func.o -o myprog" is executed and MAKE is finished
If the system is busy and your MAKE process compiles many files it will take a long time to complete. You may want to start it and leave it running in the background while you go do something else. If you use the C-shell, use this model:
% make target >& LOG < /dev/null &
The ">& LOG" part will create a log of messages from the MAKE process. The "< /dev/null" part will allow the port you are using to be released properly when you logout. The final "&" requests background processing. See Job Control for more information about running jobs in the background.
If you use the Bourne shell, use this model:
$ nohup make target > LOG 2>&1 &
The "nohup" allows the MAKE process to continue running after you log out. The "> LOG 2>&1" part creates a log of messages from the MAKE process. The final "&" requests background processing.
MAKE is merely the medium to automate procedures -- you, the programmer, can tell it how to do almost anything. It is not restricted to compiling programs. Makefiles can include "targets" with commands to manage the files in your programming project, or even do things not related to compiling at all, such as running gtroff on a set of word-processing files. Here are some examples
clean:
rm *.o core
print:
lpr -Papm2337 *.c *.h
backup:
cp *.c *.h Makefile backupdir
The command make clean removes all of the .o files and the core file (if it exists) from your directory. make print prints all of your .c and .h files to the apm2337 printer. make backup copies your .c files, .h files, and the Makefile into a directory called "backupdir" (which you should create prior to making the first backup).
Try the Unix manual:
% man make
Or read "Make - A Program for Maintaining Computer Programs" by S. I. Feldman in the Unix Programmer's Manual Supplementary Documents 1. Both of the above sources discuss the more complex and powerful aspects of MAKE that were not included in this introductory article.
#
# @(#)Makefile.customer 1.2
#
# Makefile for the example programs for Olit programmers
#
## This Makefile illustrates some techniques employed in
## production Makefiles.
##
## 1) Setting and referencing variables
##
## 2) Using variables to specify compiler and loader flags
##
## 3) Using implied dependancies and rules. Notice that although this
## particular example was set-up to compile a program "unit_test1"
## from a C source file "unit_test1.c", no explicit rules are given
## for making unit_test1.o from unit_test.c. In fact, unit_test.c is
## not explicitly mentioned.
##
## 3) Using the special variable $a to denote the current target name
##
## 4) Using conventional target names: "all" for all the executables
## this Makefile knows how to make; "clean" to remove all expendable
## files.
##
## 5) The Makefile is based on a template designed to minimize the
## amount of work necessary to adapt it to make a particular program.
DESTDIR = /usr/openwin
INCLUDE = -I${DESTDIR}/include
# If you want to compile for debugging, change "-O" to "-g"
CFLAGS = ${INCLUDE} -O
# if you want special to pass special loader options to ld, set
# LDFLAGS= ...
LIBS = -L${DESTDIR}/lib \
-lXol \
-lXt \
-lX11
ALL = unit_test1
all: ${ALL}
$(ALL): $$@.o
$(RM) $@
$(CC) -o $@ $@.o $(CFLAGS) $(LDFLAGS) $(LIBS)
clean:
rm -f core ${ALL} *.o