Chapter 3: MAKE UTILITY

 

 

3.1 The UNIX make Utility

 

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.

 

 

3.2 make Functionality

 

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.

 

 

3.3 The Makefile

 

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.

 

 

3.4 Sample make Syntax

 

targetfile :

dependencies

< tab >

command1

< tab >

command2

 

 

myprog.exe:

mysource1.f mysource2.f

< tab >

f77 -o myprog.exe mysource1.f mysource2.f

 

A rule begins in the first position of the line and has the following format:

targetfile: dependencies.

 

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.

 

 

3.5 Macros

 

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.

 

MyFlags = -a -b -c -d

 

In this example every usage of $(MyFlags) would be replaced by make with the string "-a -b -c -d" before executing any shell command.

 

 

3.6 Internal Macros

 

$@

name of the current target

$<

The name of a dependency file, derived as if selected for use with an implicit rule.

$?

The list of dependencies that are newer than the target

$*

The basename of the current target, derived as if selected for use with an implicit rule.

D

directory path, $(@D), $(<D)

F

file name, $(@F), $(<F)

 

 

3.7 Default Suffixes and Rules

.f.o:

 

< tab >

$(FC) $(FFLAGS) -c $<

 

 

.f:

 

< tab >

$(FC) $(FFLAGS) $(LDFLAGS) $< -o $@

 

 

.SUFFIXES:

 

< tab >

.o .c .f

 

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

 

.F.o:

<tab> $(RM) $@

<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.
 
 

3.8 Sample Program Dependency Chart

 

 

 

3.9 Sample Program Components for make Example

 

 

3.10 makefile Examples for the Sample Program

 

 

#

# first makefile example

#

average : mainprog.o readit.o meanit.o printit.o

         f77 -o average mainprog.o readit.o meanit.o printit.o

 

mainprog.o : mainprog.f

         f77 -c mainprog.f

 

readit.o : readit.f unit.include data.include

         f77 -c readit.f

 

meanit.o : meanit.f data.include sum.include

         f77 -c meanit.f

 

printit.o : printit.f data.include sum.include

         f77 -c printit.f

 

 

 

 

 

#

# second makefile example

#

average : mainprog.o readit.o meanit.o printit.o

         f77 -o $@ mainprog.o readit.o meanit.o printit.o

 

mainprog.o : mainprog.f

         f77 -c $<

 

readit.o : readit.f unit.include data.include

         f77 -c $<

 

meanit.o : meanit.f data.include sum.include

         f77 -c $*.f

 

printit.o : printit.f data.include sum.include

         f77 -c $*.f

 

 

 

 

 

#

# third makefile example

#

OBJS = mainprog.o readit.o meanit.o printit.o

average : $(OBJS)

         f77 -o $@ $(OBJS)

 

readit.o : readit.f unit.include data.include

         f77 -c $<

 

meanit.o : meanit.f data.include sum.include

         f77 -c $<

 

printit.o : printit.f data.include sum.include

         f77 -c $<

 

 

#

# fourth makefile example

#

.f.o:

         rm -f $@

         f77 -c $*.f

 

OBJS = mainprog.o readit.o meanit.o printit.o

 

average : $(OBJS)

         f77 -o $@ $(OBJS)

 

readit.o : unit.include data.include

meanit.o : data.include sum.include

printit.o : data.include sum.include

 

 

3.11 Make Command Used in MM5 Preprocessing Programs

 

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

 

make "rule1" "rule2"

 

 

3.12 An Example of Top-level Makefile

 

# Top-level Makefile for TERRAIN

 

# Macros, these should be generic for all machines

 

.IGNORE:

 

AR = ar ru

CD = cd

LN = ln -s

MAKE = make -i -f Makefile

RM = /bin/rm -f

RM_LIST = *.o *.f core .tmpfile terrain.exe data_area.exe rdem.exe

NCARGRAPHICS = NCARG

#NCARGRAPHICS = NONCARG

 

# Targets for supported architectures

 

default:

  uname -a > .tmpfile

  grep CRAY .tmpfile ; \

  if [ $$? = 0 ]; then echo "Compiling for CRAY" ; \

  ( $(CD) src ; $(MAKE) all \

  "RM = $(RM)" "RM_LIST = $(RM_LIST)" \

  "LN = $(LN)" "MACH = CRAY" \

  "MAKE = $(MAKE)" "CPP = /opt/ctl/bin/cpp" \

  "CPPFLAGS = -I. -C -P -D$(NCARGRAPHICS) -DRECLENBYTE" \

  "FC = f90" "FCFLAGS = -I." \

  "LDOPTIONS = " "CFLAGS = " \

  "LOCAL_LIBRARIES= -L/usr/local/lib -lncarg -lncarg_gks -lncarg_c -lX11 -lm" ) ; \

else \

  grep OSF .tmpfile ; \

  if [ $$? = 0 ]; then echo "Compiling for Compaq" ; \

  ( $(CD) src ; $(MAKE) all \

  "RM = $(RM)" "RM_LIST = $(RM_LIST)" \

  "LN = $(LN)" "MACH = DEC" \

  "MAKE = $(MAKE)" "CPP = /usr/bin/cpp" \

  "CPPFLAGS = -I. -C -P -D$(NCARGRAPHICS)" \

  "FC = f77" "FCFLAGS = -I. -convert big_endian -fpe" \

  "LDOPTIONS = " "CFLAGS = " \

  "LOCAL_LIBRARIES= -L/usr/local/ncarg/lib -lncarg -lncarg_gks -lncarg_c -lX11 -lm" ) ; \

else \

  grep IRIX .tmpfile ; \

  if [ $$? = 0 ]; then echo "Compiling for SGI" ; \

  ( $(CD) src ; $(MAKE) all \

  "RM = $(RM)" "RM_LIST = $(RM_LIST)" \

  "LN = $(LN)" "MACH = SGI" \

  "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" ) ; \

else \

  grep HP .tmpfile ; \

  if [ $$? = 0 ]; then echo "Compiling for HP" ; \

  ( $(CD) src ; $(MAKE) all \

  "RM = $(RM)" "RM_LIST = $(RM_LIST)" \

  "LN = $(LN)" "MACH = HP" \

  "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" ) ; \

else \

  grep SUN .tmpfile ; \

  if [ $$? = 0 ]; then echo "Compiling for SUN" ; \

  ( $(CD) src ; $(MAKE) all \

  "RM = $(RM)" "RM_LIST = $(RM_LIST)" \

  "LN = $(LN)" "MACH = SUN" \

  "MAKE = $(MAKE)" "CPP = /usr/ccs/lib/cpp" \

  "CPPFLAGS=-I. -C -P -D$(NCARGRAPHICS) -DRECLENBYTE" \

  "FC = f77" "FCFLAGS = -I." \

  "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" ) ; \

else \

  grep AIX .tmpfile ; \

  if [ $$? = 0 ]; then echo "Compiling for IBM" ; \

  ( $(CD) src ; $(MAKE) all \

  "RM = $(RM)" "RM_LIST = $(RM_LIST)" \

  "LN = $(LN)" "MACH = IBM" \

  "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" ) ; \

  fi ; \

  fi ; \

  fi ; \

  fi ; \

  fi ; \

  fi ; \

  ( $(RM) terrain.exe ; $(LN) src/terrain.exe . ) ;

 

terrain.deck:

  uname -a > .tmpfile

  grep OSF .tmpfile ; \

  if [ $$? = 0 ]; then \

  echo "Making terrain deck for Compaq" ; \

  ( cp Templates/terrain.deck.dec terrain.deck ) ; \

    else \

  grep CRAY .tmpfile ; \

  if [ $$? = 0 ]; then \

  echo "Making terrain deck for CRAY" ; \

  ( cp Templates/terrain.deck.cray terrain.deck ) ; \

    else \

  grep IRIX .tmpfile ; \

  if [ $$? = 0 ]; then \

  echo "Making terrain deck for SGI" ; \

  ( cp Templates/terrain.deck.sgi terrain.deck ) ; \

    else \

  grep HP .tmpfile ; \

  if [ $$? = 0 ]; then \

  echo "Making terrain deck for HP" ; \

  ( cp Templates/terrain.deck.hp terrain.deck ) ; \

    else \

  grep SUN .tmpfile ; \

  if [ $$? = 0 ]; then \

  echo "Making terrain deck for SUN" ; \

  ( cp Templates/terrain.deck.sun terrain.deck ) ; \

    else \

  grep AIX .tmpfile ; \

  if [ $$? = 0 ]; then \

  echo "Making terrain deck for IBM" ; \

  ( cp Templates/terrain.deck.ibm terrain.deck ) ; \

  fi; \

  fi; \

  fi; \

  fi; \

  fi; \

  fi;

 

code:

  ( $(CD) src ; $(MAKE) code \

  "MAKE = $(MAKE)" \

  "CPP = /usr/bin/cpp" \

  "CPPFLAGS = -I. -C -P -DDEC" )

 

clean:

  ( $(CD) src ; $(MAKE) clean "CD = $(CD)" "RM = $(RM)" "RM_LIST = $(RM_LIST)" )

  $(RM) $(RM_LIST)

 

 

3.13 An Example of Low-level Makefile

 

# Lower level Makefile for TERRAIN

# Suffix rules and commands

#######################

FIX01 =

#######################

 

.IGNORE:

 

.SUFFIXES: .F .f .i .o

 

.F.o:

          $(RM) $@

          $(CPP) $(CPPFLAGS) -D$(MACH) $(FIX01) $*.F > $*.f

          $(FC) -c $(FCFLAGS) $*.f

          $(RM) $*.f

 

.F.f:

          $(CPP) $(CPPFLAGS) -D$(MACH) $(FIX01) $*.F > $@

 

.f.o:

          $(RM) $@

          $(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

 

SRC        =$(OBJS:.o=.f)

 

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

 

terrain.exe: $(OBJS)

            $(FC) -o $@ $(LDOPTIONS) $(OBJS) $(LOCAL_LIBRARIES)

 

code:        $(SRC)

#

# for preprocessor 1

#

 

OBJS1        = latlon.o llxy.o mxmnll.o nestll.o rflp.o setup.o outpt.o vtran.o\

            search.o data30s.o data_area.o

 

SRC1         = $(OBJS1:.o=.i)

 

data_area.exe: $(OBJS1)

              $(RM) $@

              $(FC) -o $@ $(OBJS1) $(LDOPTIONS) $(LOCAL_LIBRARIES) $(LDLIBS)

 

code1:         $(SRC1)

 

#

# for preprocessor 2

#

 

OBJS2          = cr30sdata.o read30s.o rdem.o ia.o

 

SRC2           = $(OBJS2:.o=.i)

rdem.exe:       $(OBJS2)

               $(RM) $@

               $(FC) -o $@ $(OBJS2) $(LDOPTIONS) $(LOCAL_LIBRARIES) $(LDLIBS)

 

code2:          $(SRC2)

 

# -------------------------------------------------------------------------

# 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

crlwmsk.o: ltdata.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

exaint.o: parame.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

llxy.o: maps.incl

mxmnll.o: parame.incl maps.incl option.incl

nestbdy.o: parame.incl

nestll.o: 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

replace.o: nestdmn.incl

rflp.o: 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

sint.o: parame.incl

smth121.o: parame.incl

smthtr.o: parame.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

terdrv.o: vs_cmn2.incl

terrain.o: parame.incl paramesv.incl maps.incl nestdmn.incl option.incl

terrain.o: ezwater.incl

terrestial_info.o: maps.incl

tfudge.o: parame.incl paramesv.incl vs_cmn1.incl maps.incl nestdmn.incl

vtran.o: parame.incl

xyobsll.o: maps.incl option.incl

 

clean:

          $(RM) $(RM_LIST)