The following chapter is designed to provide the MM5 user with both an overview of the UNIX make command and an understanding of how make is used within the MM5 system. UNIX supplies the user with a broad range of tools for maintaining and developing programs. Naturally, the user who is unaware of their existence, doesn't know how to use them, or thinks them unnecessary will probably not benefit from them. In the course of reading this chapter it is hoped that you not only become aware of make but that you will come to understand why you need it.
In the same way you use dbx when you want to "debug" a program or sort when you need to sort a file, so you use make when you want to "make" a program. While make is so general that it can be used in a variety of ways, its primary purpose is the generation and maintenance of programs.
But why the bother of a separate make utility in the first place? When you wrote your first program it probably consisted of one file which you compiled with a command such as "f77 hello.f". As long as the number of files is minimal you could easily track the modified files and recompile any programs that depend on those files. If the number of files becomes larger you may have written a script that contains the compiler commands and reduces the amount of repetitive typing. But as the number of files increases further your script becomes complicated. Every time you run the script every file is recompiled even though you have only modified a single file. If you modify an include file it is your responsibility to make sure that the appropriate files are recompiled. Wouldn't it be nice to have something that would be smart enough to only recompile the files that need to be recompiled? To have something that would automatically recognize that a buried include file have been changed and recompile as necessary? To have something that would optimize the regeneration procedure by executing only the build steps that are required. Well, you do have that something. That something is make.
When you begin to work on larger projects, make ceases to be a nicety and becomes a necessity.
The most basic notion underlying make is that of dependencies between files. Consider the following command:
f77 -o average mainprog.o readit.o meanit.o printit.o
Consider the object file mainprog.o and its associated source code file mainprog.f. Since a change in mainprog.f necessitates recompiling mainprog.o, we say that mainprog.o is dependent upon mainprog.f. Using the same reasoning we see that the final program average is dependent upon mainprog.o. average in this context is the target program (the program we wish to build). In this fashion we can build up a tree of dependencies for the target, with each node having a subtree of its own dependencies (See Figure 3.1). Thus the target average is dependent upon both mainprog.o and mainprog.f, while mainprog.o is dependent only upon mainprog.f. Whenever mainprog.f is newer than mainprog.o, average will be recompiled.
make uses the date and time of last modification of a file to determine if the dependent file is newer than the target file. This is the time you see when using the UNIX ls command. By recognizing this time relationship between target and dependent, make can keep track of which files are up-to-date with one another. This is a reasonable approach since compilers produce files sequentially. The creation of the object file necessitates the pre-existence of the source code file. Whenever make finds that the proper time relationship between the files does not hold, make attempts to regenerate the target files by executing a user-specified list of commands, on the assumption that the commands will restore the proper time relationships between the source files and the files dependent upon them.
The make command allows the specification of a heirarchical tree of dependency relationships. Such relationships are a natural part of the structure of programs. Consider our program average (See Figure 3.1). This application consists of 6 include files and four source code files. Each of the four source code files must be recompiled to create the four object files, which in turn are used to create the final program average. There is a natural dependency relationship existing between each of the four types of files: include files, FORTRAN sources, object, and executables. make uses this relationship and a specification of the dependency rules between files to determine when a procedure (such a recompilation) is required. make relieves people who are constantly recompiling the same code of the tedium of keeping track of all the complexies of their project, while avoiding inefficiency by minimizing the number of steps required to rebuild the executable.
Even with a small number of files the dependency relationships between files in a programming project can be confusing to follow. In the case of mm5 with hundreds of files, an English description would be unuseable. Since make requires some definition of the dependencies, it requires that you prepare an auxillary file -- the makefile -- that describes the dependencies between files in the project.
There are two kinds of information that must be placed in a makefile: dependency relations and generation commands. The dependency relations are utilized to determine when a file must be regenerated from its supporting source files. The generation commands tell make how to build out-of-date files from the supporting source files. The makefile therefore contains two distinct line formats: one called rules, the other commands.
A rule begins in the first position of the line and has the following format:
The name or names to the left of the colon are the names of target files. The names to the right of the colon are the files upon which our target is dependent. That is, if the files to the right are newer than the files to the left, the target file must be rebuilt.
A dependency rule may be followed by one or more command lines. A command line must begin with at least one tab character; otherwise, it will not be recognized by make and will probably cause make to fail. This is a common cause of problems for new users. Other than this, make places no restrictions are command lines - when make uses command lines to rebuild a target, it passes them to the shell to be executed. Thus any command acceptable to the shell is acceptable to make.
make Macro definitions and usage look very similar to UNIX environment variables and serve much the same purpose. If the Macro STRING1 has been defined to have the value STRING2, then each occurrence of $(STRING1) is replaced with STRING2. The () are optional if STRING1 is a single character.
The name of a dependency file, derived as if selected for use with an implicit rule. |
|
The basename of the current target, derived as if selected for use with an implicit rule. |
|
In the Makefile you may notice a line beginning with .SUFFIXES near the top of the file, and followed by a number of targets (e.g., .f.o). In addition, you may notice that the MAKE macro is commonly defined using the -r option. These definitions are all designed to deal with what are known as make's implicit suffix rules.
An implicit suffix rule defines the relationship between files based on their suffixes. If no explicit rule exists and the suffix of the target is one recognized by make, it will use the command associated with the implicit suffix rule. So if there is no explicit rule in the Makefile which deals with the target mainprog.o, make will recognize the suffix .o to indicate that this is an object file and will look in its list of implicit suffix rules to decide how to update the target. If there is a file named mainprog.f in the directory, make will compile mainprog.o using the .f.o rule. If instead there is a file named mainprog.c, make will compile mainprog.o using the .c.o implicit suffix rule. If both source files are in the directory, the rule used is dependent on the particular implementation of make.
The -r option to make turns off the implicit suffix rules. So on most platforms we do not use the implicit suffix rules, preferring to define our own suffix rules. We do this by specifying which files with suffixes use suffix rules - this is done with the .SUFFIXES macro. We then define what these rules in low-level makefiles. For example, one of the suffix rule we specify is
<tab> $(FC) -c $(FCFLAGS) $*.F
The reason we have this suffix rule
is that all our Fortran files are named *.F, which will be subject to cpp
(c pre-processor) before being compiled.
average : mainprog.o readit.o meanit.o printit.o
f77 -o average mainprog.o readit.o meanit.o printit.o
readit.o : readit.f unit.include data.include
meanit.o : meanit.f data.include sum.include
printit.o : printit.f data.include sum.include
average : mainprog.o readit.o meanit.o printit.o
f77 -o $@ mainprog.o readit.o meanit.o printit.o
readit.o : readit.f unit.include data.include
meanit.o : meanit.f data.include sum.include
printit.o : printit.f data.include sum.include
OBJS = mainprog.o readit.o meanit.o printit.o
readit.o : readit.f unit.include data.include
meanit.o : meanit.f data.include sum.include
printit.o : printit.f data.include sum.include
OBJS = mainprog.o readit.o meanit.o printit.o
readit.o : unit.include data.include
meanit.o : data.include sum.include
The make rules, defined dependencies (sometimes not the default ones), and compiler/loader options are defined in Makefiles . The syntax for the make command is in general
# Top-level Makefile for TERRAIN
# Macros, these should be generic for all machines
RM_LIST = *.o *.f core .tmpfile terrain.exe data_area.exe rdem.exe
# Targets for supported architectures
if [ $$? = 0 ]; then echo "Compiling for CRAY" ; \
"RM = $(RM)" "RM_LIST = $(RM_LIST)" \
"MAKE = $(MAKE)" "CPP = /opt/ctl/bin/cpp" \
"CPPFLAGS = -I. -C -P -D$(NCARGRAPHICS) -DRECLENBYTE" \
"LOCAL_LIBRARIES= -L/usr/local/lib -lncarg -lncarg_gks -lncarg_c -lX11 -lm" ) ; \
if [ $$? = 0 ]; then echo "Compiling for Compaq" ; \
"RM = $(RM)" "RM_LIST = $(RM_LIST)" \
"MAKE = $(MAKE)" "CPP = /usr/bin/cpp" \
"CPPFLAGS = -I. -C -P -D$(NCARGRAPHICS)" \
"FC = f77" "FCFLAGS = -I. -convert big_endian -fpe" \
"LOCAL_LIBRARIES= -L/usr/local/ncarg/lib -lncarg -lncarg_gks -lncarg_c -lX11 -lm" ) ; \
if [ $$? = 0 ]; then echo "Compiling for SGI" ; \
"RM = $(RM)" "RM_LIST = $(RM_LIST)" \
"MAKE = $(MAKE)" "CPP = /lib/cpp" \
"CPPFLAGS = -I. -C -P -D$(NCARGRAPHICS)" \
"FC = f77" "FCFLAGS = -I. -n32" \
"LDOPTIONS = -n32" "CFLAGS = -I. -n32" \
"LOCAL_LIBRARIES= -L/usr/local/ncarg/lib -L/usr/local/lib -lncarg -lncarg_gks -lncarg_c -lX11 -lm" ) ; \
if [ $$? = 0 ]; then echo "Compiling for HP" ; \
"RM = $(RM)" "RM_LIST = $(RM_LIST)" \
"MAKE = $(MAKE)" "CPP = /opt/langtools/lbin/cpp" \
"CPPFLAGS= -I. -C -P -D$(NCARGRAPHICS) -DRECLENBYTE" \
"FC = f77" "FCFLAGS = -I. -O" \
"LDOPTIONS = " "CFLAGS = -Aa" \
"LOCAL_LIBRARIES= -L/usr/local/ncarg/lib -L/usr/local/lib -lncarg -lncarg_gks -lncarg_c -lX11 -lm" ) ; \
if [ $$? = 0 ]; then echo "Compiling for SUN" ; \
"RM = $(RM)" "RM_LIST = $(RM_LIST)" \
"MAKE = $(MAKE)" "CPP = /usr/ccs/lib/cpp" \
"CPPFLAGS=-I. -C -P -D$(NCARGRAPHICS) -DRECLENBYTE" \
"LDOPTIONS = " "CFLAGS = -I." \
"LOCAL_LIBRARIES= -L/usr/local/ncarg/lib -L/usr/openwin/lib -L/usr/dt/lib -lncarg -lncarg_gks -lncarg_c -lX11 -lm" ) ; \
if [ $$? = 0 ]; then echo "Compiling for IBM" ; \
"RM = $(RM)" "RM_LIST = $(RM_LIST)" \
"MAKE = $(MAKE)" "CPP = /usr/lib/cpp" \
"CPPFLAGS = -I. -C -P -D$(NCARGRAPHICS) -DRECLENBYTE" \
"FC = xlf" "FCFLAGS = -I. -O -qmaxmem=-1"\
"LDOPTIONS = " "CFLAGS = -I." \
"LOCAL_LIBRARIES= -L/usr/local/lib32/r4i4 -lncarg -lncarg_gks -lncarg_c -lX11 -lm" ) ; \
( $(RM) terrain.exe ; $(LN) src/terrain.exe . ) ;
echo "Making terrain deck for Compaq" ; \
( cp Templates/terrain.deck.dec terrain.deck ) ; \
echo "Making terrain deck for CRAY" ; \
( cp Templates/terrain.deck.cray terrain.deck ) ; \
echo "Making terrain deck for SGI" ; \
( cp Templates/terrain.deck.sgi terrain.deck ) ; \
echo "Making terrain deck for HP" ; \
( cp Templates/terrain.deck.hp terrain.deck ) ; \
echo "Making terrain deck for SUN" ; \
( cp Templates/terrain.deck.sun terrain.deck ) ; \
echo "Making terrain deck for IBM" ; \
( cp Templates/terrain.deck.ibm terrain.deck ) ; \
"CPPFLAGS = -I. -C -P -DDEC" )
( $(CD) src ; $(MAKE) clean "CD = $(CD)" "RM = $(RM)" "RM_LIST = $(RM_LIST)" )
# Lower level Makefile for TERRAIN
$(CPP) $(CPPFLAGS) -D$(MACH) $(FIX01) $*.F > $*.f
$(CPP) $(CPPFLAGS) -D$(MACH) $(FIX01) $*.F > $@
$(FC) -c $(FCFLAGS) $(FIX01) $*.f
OBJS =ia.o anal2.o bint.o bndry.o crlnd.o crter.o dfclrs.o exaint.o \
finprt.o fudger.o interp.o label.o lakes.o \
latlon.o llxy.o mxmnll.o nestll.o oned.o \
outpt.o output.o pltter.o rdldtr.o replace.o rflp.o setup.o sint.o \
smth121.o smther.o smthtr.o terdrv.o terrain.o tfudge.o vtran.o \
xyobsll.o hiresmap.o plots.o crvst.o \
crvst30s.o nestbdy.o crsoil.o equate.o labels.o labelv.o patch.o\
plotcon.o watercheck.o crlwmsk.o soil_tg.o water_vfr.o check_data.\
terrestial_info.o write_fieldrec.o
cray dec hp ibm sgi sun default:
@echo "you need to be up a directory to make terrain.exe"
all:: terrain.exe data_area.exe rdem.exe
$(FC) -o $@ $(LDOPTIONS) $(OBJS) $(LOCAL_LIBRARIES)
OBJS1 = latlon.o llxy.o mxmnll.o nestll.o rflp.o setup.o outpt.o vtran.o\
search.o data30s.o data_area.o
$(FC) -o $@ $(OBJS1) $(LDOPTIONS) $(LOCAL_LIBRARIES) $(LDLIBS)
OBJS2 = cr30sdata.o read30s.o rdem.o ia.o
$(FC) -o $@ $(OBJS2) $(LDOPTIONS) $(LOCAL_LIBRARIES) $(LDLIBS)
# -------------------------------------------------------------------------
# DO NOT DELETE THIS LINE -- make depend depends on it.
anal2.o: parame.incl nestdmn.incl
bndry.o: maps.incl option.incl
crlnd.o: parame.incl paramed.incl ltdata.incl fudge.incl option.incl
crlnd.o: maps.incl nestdmn.incl trfudge.incl ezwater.incl
crlwmsk.o: parame.incl paramesv.incl paramed.incl maps.incl nestdmn.incl
crsoil.o: parame.incl paramesv.incl paramed.incl ltdata.incl
crter.o: parame.incl paramed.incl nestdmn.incl option.incl ltdata.incl
crvst.o: parame.incl paramed.incl ltdata.incl
crvst30s.o: parame.incl paramed.incl nestdmn.incl maps.incl ltdata.incl
data_area.o: parame.incl maps.incl nestdmn.incl ltdata.incl
finprt.o: option.incl parame.incl paramesv.incl headerv3.incl
interp.o: option.incl ltdata.incl
labels.o: paramesv.incl vs_cmn2.incl
labelv.o: paramesv.incl vs_cmn2.incl
latlon.o: maps.incl option.incl
mxmnll.o: parame.incl maps.incl option.incl
output.o: option.incl paramesv.incl ltdata.incl headerv3.incl nestdmn.incl
output.o: maps.incl namelist.incl vs_cmn2.incl vs_cmn1.incl
pltter.o: parame.incl maps.incl nestdmn.incl option.incl paramesv.incl
pltter.o: vs_cmn1.incl vs_cmn2.incl
rdldtr.o: paramed.incl paramesv.incl space.incl
replace.o: parame.incl option.incl paramesv.incl vs_cmn1.incl maps.incl
search.o: parame.incl maps.incl nestdmn.incl ltdata.incl option.incl
setup.o: ezwater.incl parame.incl paramesv.incl maps.incl nestdmn.incl
setup.o: fudge.incl trfudge.incl option.incl ltdata.incl namelist.incl
setup.o: vs_cmn1.incl vs_cmn2.incl vs_data.incl
terdrv.o: paramed.incl parame.incl paramesv.incl maps.incl nestdmn.incl
terdrv.o: option.incl ltdata.incl trfudge.incl space.incl vs_cmn1.incl
terrain.o: parame.incl paramesv.incl maps.incl nestdmn.incl option.incl
tfudge.o: parame.incl paramesv.incl vs_cmn1.incl maps.incl nestdmn.incl
xyobsll.o: maps.incl option.incl