gmake

Instructions and hints for beginners

Moderators: arango, robertson

Post Reply
Message
Author
User avatar
kate
Posts: 4091
Joined: Wed Jul 02, 2003 5:29 pm
Location: CFOS/UAF, USA

gmake

#1 Unread post by kate »

In a previous article, I provided an introduction to make, the standard Unix project management tool. At that time I wrote:
Make has a portable subset of features, with system-dependent extensions. If you want to use extensions, I suggest sticking with those supported by gnu make (gmake), since it is available most everywhere.
Over the years, the community has moved from the stance of writing portable Makefiles to a stance of just using a powerful, portable make. The make bible from O'Reilly has come out in a new (third) edition, with the new title of Managing projects with GNU Make and a new author, Robert Mecklenburg, 2005, ISBN 0-596-00610-1. If you have been considering learning more about make, and perhaps dusting off your Makefiles, read this book.

Make Rules

The core of make hasn't changed in decades, but concentrating on gmake allows one to make use of its nifty little extras designed by real programmers to help with real projects. The first change that matters to my Makefiles is change from suffix rules to pattern rules. I've always found the .SUFFIXES list to be odd, especially since .f90 is not in the default list. Good riddance to all of that! For a concrete example, the old way to provide a rule for going from file.f90 to file.o is:

.SUFFIXES: .o .f90 .F .F90
.f90.o:
<TAB> $(FC) -c $(FFLAGS) $<


while the new way is:

%.o: %.f90
<TAB> $(FC) -c $(FFLAGS) $<


In fact, going to a uniform make means that we can figure out what symbols are defined and use their standard values - in this case, $(FC) and $(FFLAGS) are the built-in default names. If you have any questions about this, you can always run make with the -p (or --print-data-base) option. This prints out all the rules make knows about, such as:

# default
COMPILE.f = $(FC) $(FFLAGS) $(TARGET_ARCH) -c


Printing the rules database will show variables that make is picking up from the environment, from the Makefile, and from its built-in rules - and which of these sources is providing each value.

Assignments

In the old days, I only used one kind of assignment: = (equals sign). Gmake has several kinds of assignment (other makes might as well, but I no longer have to know or care). An example of the power of gnu make is shown by an example from my Cray X1 Makefile. There is a routine which runs much more quickly if a short function in another file is inlined. The way to accomplish this is through the -O inlinefrom=file directive to the compiler. I can't add this option to FFLAGS, since the inlined routine won't compile with this directive - it is only the one file that needs it. I had:

FFLAGS = -O 3,aggress -e I -e m
FFLAGS2 = -O 3,aggress -O inlinefrom=lmd_wscale.f90 -e I -e m

lmd_skpp.o:
<TAB> $(FC) -c $(FFLAGS2) $*.f90


The first change I can make to this using other assignments is:

FFLAGS := -O 3,aggress -e I -e m
FFLAGS2 := $(FFLAGS) -O inlinefrom=lmd_wscale.f90


The := assignment means to evaluate the right hand side immediately. In this case, there is no reason not to, as long as the second assigment follows the first one (since it's using the value of $(FFLAGS)). For the plain equals, make doesn't evaluate the right-hand side until its second pass through the Makefile. However, gmake supports an assignment that avoids the need for FFLAGS2 entirely:

lmd_skpp.o: FFLAGS += -O inlinefrom=lmd_wscale.f90

What this means is that for the target lmd_skpp.o only, append the inlining directive to FFLAGS. I think this is pretty cool!

One last kind of assignment is to set the value only if there is no value from somewhere else (the environment, for instance):

FC ?= mpxlf90_r

If we use := or =, we would override the value from the environment.

Portability

Using gmake across all platforms solves some portability issues, but not all. For instance, there is an example in chapter one which uses the pr command with some gnu-only options. The IBM has an older pr in /usr/bin and the gnu version in /opt/freeware/bin. The example failed until I changed my path to make the gnu version my default.

The book has a chapter on portability with an emphasis on cygwin, which is great if you use cygwin. There is also an example of extracting the machine-dependent parts of the Makefile into a series of include files:

MACHINE := $(shell uname -sm | send 's/ /-/g')
include $(MACHINE)-defines.mk


running uname -sm on the IBM gives differing values for the -m option between iceberg and iceflyer - perhaps uname -s is enough for my needs. Having an automatic way to set the MACHINE type is nice, but our users are using more than one Fortran compiler under Linux, so knowing we're on Linux is only half the battle.

As I've described before, I have been using imake to build Makefiles for all the different systems I have access to. Robert Mecklenburg claims that gmake is powerful enough to eliminate the need for imake. I'll let you know if I agree as I read more of this book and continue to update my Makefiles.
Last edited by kate on Wed Mar 15, 2006 12:20 am, edited 3 times in total.

User avatar
kate
Posts: 4091
Joined: Wed Jul 02, 2003 5:29 pm
Location: CFOS/UAF, USA

#2 Unread post by kate »

Gnu Make Part 2

Last time I mentioned that there is a new book out about Gnu Make (Managing projects with GNU Make, Robert Mecklenburg, O'Reilly, 2005). In the past, I was generating Makefiles for a variety of computers by using imake, a tool that comes with the X11 window software. The key to imake is separating out the parts of the Makefile that depend on what computer you are on, the parts that depend on the project you are building, and the parts that don't change between your Fortran projects (or X11 projects).

This was not too ungainly as long as our ocean model was a serial code. Then we started generating different Makefiles for MPI/OpenMP/Serial on each system we were using. Before long, we had 20-30 Makefiles! It's time to tame that beast. In a previous article, we talked about using automake/autoconf to generate the Makefile on the system we use. Somehow that solution wasn't a good enough fit to our needs and never replaced imake. Using gnu make, I think we finally have a better way.


include

One reasonably portable make features is the include directive. It can be used to clean up the Makefile by putting bits in an include file. The syntax is simply:

include file

and we are using it to include the list of sources to compile and the list of dependencies. This is how we are now keeping the project information neat. We can also include a file with the system/compiler information in it, assuming we have some way of deciding which file to include. Last time I showed the example from the book, using uname to generate a file name. I decided to modify it slightly for our needs.

I have a make variable called FORTRAN, which is set by the user of the Makefile. This value is combined with the result of uname -s to provide a machine and compiler combination. For instance, ftn on Linux is the Cray cross-compiler. This would link to a different copy of the NetCDF library and use different compiler flags than the Intel compiler on Linux.

# The user sets FORTRAN:
FORTRAN := ftn

MACHINE := $(shell uname -s)
MACHINE += $(FORTRAN)
MACHINE := $(shell echo $(MACHINE) | sed 's/[\/ ]/-/g')
include $(MACHINE).mk

Now, instead of having 30 Makefiles, we have about 12 include files and we pick one at compile time. In this example, we will pick Linux-ftn.mk, containing the Cray cross-compiler information. The value Linux comes from the uname command, the ftn comes from the user, the two are concatenated, then the space is converted to a dash by the sed command. The sed command will also turn the slash in UNICOS/mp into a dash; the native Cray include file is UNICOS-mp-ftn.mk.

The other tricky system is CYGWIN, which puts a version number in the uname output, such as CYGWIN_NT-5.1. Gnu make has quite a few built-in functions plus allows the user to define their own functions. One of the built-in functions allows us to do string substitution:

MACHINE := $(patsubst CYGWIN_%,CYGWIN,$(MACHINE))


In make, the % symbol is a sort of wild card, much like * in the shell. Here, we match CYGWIN followed by an underscore and anything else, replacing the whole with simply CYGWIN. Another example of a built-in function is the substitution we do in:

objects = $(subst .F,.o,$(sources))

where we build our list of objects from the list of sources. There are quite a few other functions - see the book or the gnu make manual for a complete list.

Conditionals

One reason we had so many Makefiles was having a separate one for each of the serial/MPI/OpenMP versions on each system (if supported). For instance, the name of the IBM compiler changes when using MPI; the options change for OpenMP. The compiler options also change when using 64-bit addressing or for debugging, which we were manually setting. A better way to do this is to have the user select 64-bit or not, MPI or not, etc, then do the right thing later.

Gnu make supports two kinds of if test, ifdef and ifeq (plus the negative versions ifndef, ifneq). The example from the book is:

ifdef COMSPEC
# We are running Windows
else
# We are not on Windows
endif

An example from the IBM include file is:

FC := xlf95_r
FFLAGS := -qsuffix=f=f90 -qmaxmem=-1 -qarch=pwr4 -qtune=pwr4
ARFLAGS := -r -v

ifdef LARGE
FFLAGS += -q64
ARFLAGS += -X 64
LDFLAGS += -bmaxdata:0x200000000
NETCDF_INCDIR := /usr/local/pkg/netcdf/netcdf-3.5.0_64/include
NETCDF_LIBDIR := /usr/local/pkg/netcdf/netcdf-3.5.0_64/lib
else
LDFLAGS += -bmaxdata:0x70000000
NETCDF_INCDIR := /usr/local/include
NETCDF_LIBDIR := /usr/local/lib
endif

ifdef DEBUG
FFLAGS += -g -qfullpath
else
FFLAGS += -O3 -qstrict
endif

We also test for MPI and OpenMP and change things accordingly. To test for equality, an example is:

ifeq ($(MPI),on)
# Do MPI things
endif

or
ifeq "$(MPI)" "on"
# Do MPI things
endif


The user has to set values for the MPI, DEBUG, and LARGE switches in the
Makefile *before* the compiler-dependent piece is included:


MPI := on
DEBUG :=
LARGE :=


Be sure to use the immediate assign with the := when setting these flags.

The conditional features supported by gnu make are very handy and have replaced the need for so many Makefiles in our build system. We have further tidied it up by putting all the include files in their own subdirectory. The next episode will be on putting the sources into multiple directories.

User avatar
kate
Posts: 4091
Joined: Wed Jul 02, 2003 5:29 pm
Location: CFOS/UAF, USA

#3 Unread post by kate »

Gnu Make Part 3 - More Than One Directory

In our journey with make, one friend sent me to this web site:

http://make.paulandlesley.org/rules.html

It contains Paul Smith's Rules of Makefiles. He also has these web pages:

http://make.paulandlesley.org/vpath.html
http://make.paulandlesley.org/multi-arch.html

Last time we covered using conditionals and include files to clean up our Makefile. So far we have been assuming that everything is in one directory. Over time, our code has grown to encompass over a hundred source files. This was getting unwieldy, so the time has finally come to put things in subdirectories. As in many things, there's more than one way to do it. Some of the choices:

* recursive make or nonrecursive make (see http://aegis.sourceforge.net/auug97.pdf ).

* object files go in the source directories, the top directory, or into a build directory.

We have chosen to go with nonrecursive make and Paul's third rule - life is simpler if the objects go into the current (top) directory.

Some Details

The top directory has the master Makefile, which includes a Makefile bit from each subdirectory, called Module.mk. The top Makefile starts with an empty list of sources. In each subdirectory, we find local sources by simply listing all the .F files and appending this to the master list:

In Makefile:

sources :=
include somedir/Module.mk


In somedir/Module.mk:

local_src := $(wildcard $(subdirectory)/*.F)
sources += $(local_src)


Here, we are using the wildcard function to search the subdirectory for it's local sources. Each subdirectory is resetting the local_src variable, but that's OK because we're saving the values in the global sources variable. The other sneaky thing here is the user-defined subdirectory function, from the book Managing Projects with GNU Make by Robert Mecklenburg:

subdirectory = $(patsubst %/Module.mk,%, \
$(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST)))


This does the right thing to figure out which subdirectory we are in from make's internal list of the Makefiles it is parsing. It depends on all the subdirectory include files being called Module.mk.

Library Details

The directory structure we are using has the top directory, an Include directory, several directories which contain sources for libraries, and the directory for the main program. There is also a directory for the compiler-specific Makefile components, which we talked about last time.

Here is a complete example of a library Makefile component:

local_lib := libNLM.a
local_src := $(wildcard $(subdirectory)/*.F)
path_srcs += $(local_src)

local_src := $(patsubst $(subdirectory)/%.F,%.F,$(local_src))
local_objs := $(subst .F,.o,$(local_src))

libraries += $(local_lib)
sources += $(local_src)

$(local_lib): $(local_objs)
$(AR) $(ARFLAGS) $@ $^


The only thing that changes from one to the next is the name of the library to build. I'm actually keeping track of the sources with and without the subdirectory part of their name. The objects will go into the top directory, so they shouldn't have the directory in their list. I only need the path_srcs for creating the dependency information; make itself knows to look in the directories because of a vpath command. We are also updating a libraries variable, adding the local library to the global list.

Main Program

The main program is in a directory called Drivers and its Module.mk is similar to the library one:


local_src := $(wildcard $(subdirectory)/*.F)
path_srcs += $(local_src)

local_src := $(patsubst $(subdirectory)/%.F,%.F,$(local_src))
local_objs := $(subst .F,.o,$(local_src))

sources += $(local_src)

$(BIN): $(libraries) $(local_objs)
$(LD) $(FFLAGS) $(LDFLAGS) $(local_objs) -o $@ $(libraries) $(LIBS)


Instead of a rule for building a library, we have a rule for building a binary. In this case, the name of the binary depends on if it's parallel or not and is defined elsewhere. The binary depends on the libraries getting compiled first, as well as the local sources. During the link, the $(libraries) are compiled from the sources in the other directories, while $(LIBS) are exteral libraries such as NetCDF and mpich.

Top Level Makefile

Now we get to the glue that holds it all together. We first initialize some of the global lists:


#--------------------------------------------------------------------------
# Initialize some things.
#--------------------------------------------------------------------------

clean_list := core *.o *.mod *.f90 lib*.a
sources :=
path_srcs :=
libraries :=

objects = $(subst .F,.o,$(sources)


Next is the subdirectory function we already presented, followed by the user-defined switches and the compiler-dependent includes we presented last time. Then we have the pattern rules we described in part one. Finally we get to the meat of the includes:


.PHONY: all

all: $(BIN)

modules := Adjoint Ice Modules Nonlinear Representer \
Support Tangent Utility Drivers

includes := Include Adjoint Nonlinear Tangent Drivers

vpath %.F $(modules)
vpath %.h $(includes)

include $(addsuffix /Module.mk,$(modules))

CPPFLAGS += $(patsubst %,-I%,$(includes))

.PHONY: clean

clean:
$(RM) $(clean_list) $(BIN)


all is the first target that gets seen by make, making it the default target. In this case, we know there is only the one binary, whose name we know - the book shows how to do more than one binary. The modules are the list of subdirectories containing a Module.mk we need to include. clean is the target that removes all the cruft we don't want to keep. Both all and clean are phony targets in that no files of those names get generated - make has the .PHONY designation for such targets.

Conclusion

I've presented bits of a make system that works for us in a portable, parallel ocean circulation model written in Fortran. I've probably glossed over things - feel free to contact me for details or clarifications. The GNU make book has more of a Java slant, plus presents a Makefile for making the book itself.

User avatar
arango
Site Admin
Posts: 1367
Joined: Wed Feb 26, 2003 4:41 pm
Location: DMCS, Rutgers University
Contact:

New ROMS/TOMS Makefile

#4 Unread post by arango »

Future versions of ROMS/TOMS will have the following directory structure:

Code: Select all

Adjoint/    Drivers/   Lib/        Obsolete/     SeaIce/   Version
Bin/        External/  Modules/    Programs/     Tangent/  makefile
Compilers/  Include/   Nonlinear/  Representer/  Utility/
Notice the uppercase in the directories names. Some care was taken to repeat an uppercase letter so we can use auto-complete when processing. ROMS/TOMS is partitioned into several directories as follows:

Code: Select all

Adjoint/                ROMS/TOMS Adjoint model
Bin/                    executable scripts: cpp_clean, sfmakedepend ...
Compilers/              makefile rules and MakeDepend
Drivers/                ROMS/TOMS drivers
External/               Input scripts (*.in), varinfo.dat
Include/                cppdefs.h, globaldefs.h, set_bounds.h, tile.h, wrf_io_flags.h
Lib/                    ROMS/TOMS need libraries, PARPACK, etc
Modules/                ROMS/TOMS declaration modules
Nonlinear/              ROMS/TOMS nonlinear model
Obsolete/               Obsolete files
Programs/               Other support programs
Representer/            ROMS/TOMS representer tangent linear model
SeaIce/                 ROMS/TOMS Sea Ice
Tangent/                ROMS/TOMS tangent linear model
Utility/                ROMS/TOMS generic routines, IO, etc
Version                 ROMS/TOMS Version
makefile                only one file!
The make file rules are locate in the Compilers directory which currently contain the following make include files (*.mk):

Code: Select all

AIX-xlf.mk     Darwin-f90.mk  Linux-ifc.mk    MakeDepend       SunOS-ftn.mk
ALPHA-f90.mk   IRIX64-f90.mk  Linux-ifort.mk  MakeDepend.orig  UNICOS-mk-f90.mk
CYGWIN-f90.mk  Linux-ftn.mk   Linux-path.mk   OSF1-f90.mk      UNICOS-mp-ftn.mk
CYGWIN-g95.mk  Linux-g95.mk   Linux-pgi.mk    SunOS-f95.mk
Each file name is generated by concatenating operating system (uname -s) and the chosen compiler (value of Makefile macro FORT), as described by kate above. You may need to modify these rules files to specify the correct path for the NetCDF and ARPACK libraries. The ARPACK library is only used in the Generalized Stability Theory (GST) analysis used by the tangent linear and adjoint models.

The new makefile has the following section where the user select the value for FORT. Notice that it is prossible to have several compiler in the same operating sytem (i.e. Linux):

Code: Select all

#--------------------------------------------------------------------------
#  We are going to include a file with all the settings that depend on
#  the system and the compiler. We are going to build up the name of the
#  include file using information on both. Set your compiler here from
#  the following list:
#
#  Operating System        Compiler(s)
#
#     AIX:                    xlf
#     ALPHA:                  f90
#     CYGWIN:                 f90, g95
#     Darwin:                 f90
#     IRIX:                   f90
#     Linux:                  ifc, ifort, pgi, path, g95
#     SunOS:                  f95
#     UNICOS-mp:              ftn
#     SunOS/Linux:            ftn (Cray cross-compiler)
#
#  Feel free to send us additional rule files to include! Also, be sure
#  to check the appropriate file to make sure it has the right paths to
#  NetCDF and so on.
#--------------------------------------------------------------------------

        FORT := pgi
The Bin subdirectory contains the cpp_clean and sfmakedepend perl scripts used by the makefile. The cpp_clean is used to clean the *.f90 after C-preprocessing. The sfmakedepend is used to create ROMS/TOMS compiling dependency file MakeDepend. This is a very important file. To create the dependency file just execute:

% make depend

This only needs to be done when new files are added or file association via the USE command is changed.

Caution

The new ROMS/TOMS makefile only works with GNU make version 3.80 or higher. To check which version do you have in your system just execute the command:

% make -v

Update GNU make if necessary. This can be done very easily.

Lastly, with the new makefile you can compile in parallel by executing:

% make -j jobs

where jobs is an integer indicating the number of CPUs to compile simultaneously. This is a wonderful capability. In the previous structure of the ROMS/TOMS makefiles the order was critical and parallel compiling was not possible. This is not longer the case :D

User avatar
m.hadfield
Posts: 521
Joined: Tue Jul 01, 2003 4:12 am
Location: NIWA

#5 Unread post by m.hadfield »

Here's a cool little recipe for Gnu make debugging

To print the value of any variable, add the following target.

print-%:
<TAB>@echo $* = $($*)

Then "make print-MACHINE", for example, will print the name of the MACHINE variable.

This could be in the same file as the rest of the make declarations, or in a "helper" file which you bring in as necessary on the make command line.

The document describing this is at

http://tinyurl.com/8ax3j

I found links to this and other interesting make info at

http://www.electric-cloud.com/resources/

User avatar
m.hadfield
Posts: 521
Joined: Tue Jul 01, 2003 4:12 am
Location: NIWA

#6 Unread post by m.hadfield »

In an earlier article on this thread, Kate Hedstrom described a way to support special compilation commands for one target:
However, gmake supports an assignment that avoids the need for FFLAGS2 entirely:

lmd_skpp.o: FFLAGS += -O inlinefrom=lmd_wscale.f90

What this means is that for the target lmd_skpp.o only, append the nlining directive to FFLAGS. I think this is pretty cool!
Yes this is cool and it's used in the ROMS 2.2 make files, not only for lmd_skpp.o on the Cray but also for adding extra CPPFLAGS when generating mod_strings.f90. But I've just discovered it doesn't work quite as I understood it.

The key term is "target" as opposed to "file". The extra FFLAGS above are added when building the target lmd_skpp.o and its dependencies. What this means in practice depends on what targets you direct Gnu make to build and on the dependency tree that is then constructed. If you just issue the gmake command in the usual way, which causes Gnu make to build its default target (named "all"), then as far as I can tell the extra FFLAGS are used only in compiling lmd_skpp.o. But if you start with an empty directory and call gmake lmd_skpp.o then Gnu make determines that there are various other files that need to be compiled before it compiles lmd_skpp and it adds the extra FFLAGS when compiling them too! At least that's what I observed (though I was actually trying to customise compilation for a file other than lmd_skpp).

Does this matter? Usually not, but there might be situations where it does.

User avatar
kate
Posts: 4091
Joined: Wed Jul 02, 2003 5:29 pm
Location: CFOS/UAF, USA

#7 Unread post by kate »

Next in the series of ARSC HPC newsletter articles on make, just in time for the new ROMS:

More fun with Gnu Make

Two years ago I described how to take advantage of some of the new features in gnu make. I got as far as describing a build system for our ocean model in which we split the source code into multiple directories. At that time we were happy with letting all the object and other temporary files get created in the top level directory, from which the make was launched.

Time passed, the model has grown. The number of these temporary files is such that we've been wanting to redirect them to a Build directory. The gnu make bible (Managing Projects with GNU Make by Robert Mecklenburg, O'Reilly, 2004) provides a couple of examples showing how to do this for C programs. One way requires the user to cd into the Build directory first, but we've opted to keep the Makefile in the top directory and type "make" from there.

Goals
As a reminder, our source files have an extension of .F and need to be preprocessed by cpp into an intermediate .f90 file. Some contain F90 modules which produce a .mod file as well as a .o file when compiled. Most of our subdirectories will have the resulting .o files archived into a library which will be linked to the main program. We want the .f90, .o, .mod, and libxx.a files to all end up in the Build directory. The book example has a whole directory tree under Build, but we're lazy and we'll let them all just land in ./Build.

User-defined make functions
Mecklenburg wrote:GNU make supports both built-in and user-defined functions. A function invocation looks much like a variable reference, but includes one or more parameters separated by commas. Most built-in functions expand to some value that is then assigned to a variable or passed to a subshell. A user-defined function is stored in a variable or macro and expects one or more parameters to be passed by the caller.
We start off by defining a bunch of functions in the top Makefile:

Code: Select all

#--------------------------------------------------------------------------
#  Make functions for putting the temporary files in $(SCRATCH_DIR)
#--------------------------------------------------------------------------

# $(call source-dir-to-binary-dir, directory-list)
source-dir-to-binary-dir = $(addprefix $(SCRATCH_DIR)/, $(notdir $1))

# $(call source-to-object, source-file-list)
source-to-object = $(call source-dir-to-bindary-dir,   \
                   $(subst .F,.o,$(filter %.F,$1)))

# $(call f90-source, source-file-list)
f90-source = $(call source-dir-to-binary-dir,     \
                   $(subst .F,.f90,$1))

# $(call make-library, library-name, source-file-list)
define make-library

   libraries += $(SCRATCH_DIR)/$1
   sources   += $2

   $(SCRATCH_DIR)/$1: $(call source-dir-to-binary-dir,    \
                      $(subst .F,.o,$2))
       $(AR) $(ARFLAGS) $$@ $$^
       $(RANLIB) $$@
endef

# $(call one-compile-rule, binary-file, f90-file, source-files)
define one-compile-rule
  $1: $2 $3
       cd $$(SCRATCH_DIR); $$(FC) -c $$(FFLAGS) $(notdir $2)

  $2: $3
       $$(CPP) $$(CPPFLAGS) $$< > $$@

endef

# $(compile-rules)
define compile-rules
  $(foreach f, $(local_src),       \
    $(call one-compile-rule,$(call source-to-object,$f), \
    $(call f90-source,$f),$f))
endef
  • 1. We define a function to convert the path from the source directory to the Build directory, called source-dir-to-binary-dir. Note that the Build directory is called $(SCRATCH_DIR) here. All it does is strip off the leading directory with the the built-in function "notdir", then paste on the Build directory.

    2. Next comes source-to-object, which calls the function above to return the object filename when given the source filename. Our version assumes that all sources have a .F extension.

    3. A similar function is f90-source, which returns the name of the intermediate source which is created by cpp from our .F file.

    4. The makefile fragment in each library source directory invokes make-library, which takes the library name and the list of sources as its arguments. The function adds its library to the global list of libraries and provides rules for building itself. The double dollar signs are to delay the variable substitution. Note that we call source-dir-to-binary-dir instead of source-to-object - this is a work-around for a make bug.

    5. The next, one-compile-rule, takes three arguments: the .o filename, the .f90 filename, and the .F filename. From these, it produces the make rules for running cpp and the compiler. It would be possible to compile from the top directory and put the .o file in Build with the appropriate arguments, but I don't know how to get the .mod file into Build short of a "mv" command. Likewise, if we compile in the top directory, we need to know the compiler option to tell it to look in Build for the .mod files it uses. Doing a "cd" to Build before compiling simplifies these steps.

    6. The last, compile-rules, is given a list of sources, then calls one-compile-rule once per source file.
Other Makefile changes

Don't forget to set the $(SCRATCH_DIR) variable:

Code: Select all

 SCRATCH_DIR := Build
  clean_list += $(SCRATCH_DIR)/*
and add its files to vpath:

Code: Select all

vpath %.f90 $(SCRATCH_DIR)
vpath %.o $(SCRATCH_DIR)
One annoyance is that the compile phase of one-compile-rule didn't seem to be working. I was forced to keep the pattern rule for .o files:

Code: Select all

%.o: %.f90
<TAB>  cd $(SCRATCH_DIR); $(FC) -c $(FFLAGS) $(notdir $<)
Makefile fragments

The library directory Module.mk files are now shorter:

Code: Select all

local_lib  := libNLM.a
local_src  := $(wildcard $(subdirectory)/*.F)

$(eval $(call make-library,$(local_lib),$(local_src)))

$(eval $(compile-rules))
The eval forces it to evaluate later in the build phase. The main directory Module.mk is a little trickier:

Code: Select all

local_src  := $(wildcard $(subdirectory)/*.F)
#local_objs := $(call source-to-object,$(local_src))
local_objs := $(subst .F,.o,$(local_src))
local_objs := $(addprefix $(SCRATCH_DIR)/, $(notdir $(local_objs)))

sources    += $(local_src)

ifdef LD_WINDOWS
$(BIN): $(libraries) $(local_objs)
        $(LD) $(FFLAGS) $(local_objs) -o $@ $(libraries) $(LIBS_WIN32) $(LDFLAGS)
else
$(BIN): $(libraries) $(local_objs)
        $(LD) $(FFLAGS) $(LDFLAGS) $(local_objs) -o $@ $(libraries) $(LIBS)
endif

$(eval $(compile-rules))
I couldn't get it to use the commented out version of local_objs, I don't know why. I could tell from the output of "make -p" that local_objs was empty, though, until I tried this version.

Final warnings

I'm going ahead with this change for my code, though it may cause trouble for some:
  • 1. We're a little closer to the gnu make bugs here, and probably need a newer version of gnu make than before. I've been testing it with version 3.80.

    2. This Makefile takes more memory than the old one and lead to a malloc failure from make on my Mac laptop. The older Makefile works just fine on that system.

    3. The Makefile dependencies get just a little trickier every change we make. Note that F90 has potentially both include and module use dependencies. The book example uses the C compiler to produce its own dependencies for each source file into a corresponding .d file to be included by make. Our Fortran compilers are not so smart. For these hairy compiles, it's critical to have accurate dependency information unless we're willing to "make clean" between compiles. I've got an old Perl script that seems to still be doing the job, but I cross my fingers every time I have to mess with it.

Post Reply