Fortran Wiki
Build tools

Fortran Build Tools

  • CMake - CMake is a cross-platform, open-source build system with first-class Fortran support. It knows most Fortran compilers and includes automatic Fortran dependency resolution and can be used for unit and regression testing, software building, system introspection, cross-compilation, and redistributable software packaging. CMake is similar to (but simpler than) autotools in that it does not compile software directly. CMake can generate files for various back-ends including traditional Makefiles, Ninja, Visual Studio, Xcode, and others.

  • Meson - Meson is a cross-platform, open-source build system. It is a relatively new entrant but already has good support for most Fortran compilers. It is similar to CMake in that it generates files for various backends including Ninja, Visual Studio, and Xcode. Meson does not generate Makefiles, relying solely on Ninja for Linux and Unix support.

  • Waf - Waf is a cross-platform, open-source build system. It differs from autotools, CMake, and Meson in that it also performs the compilation instead of relying on a third-party tool like Make. Waf has good Fortran support, including automatic Fortran dependency resolution, unit testing, software building and redistributable software packaging. Instead of using a domain-specific language like CMake, Waf scripts are written in Python.

  • JAMS Makefile - The JAMS Makefile provides a portable, versatile way of compiling Fortran, C, C++, and mixed projects. It uses GNU make with user given configuration files for the specific compiler at given computing system. Fortran dependencies are built with a Python script.

  • fake - a shell script for generating Fortran makefiles.

  • FoBiS.py - a Python script for (automatic) building of modern complex Fortran project.

  • sfmakedepend - searches for Fortran style includes, C preprocessor includes, and module dependencies.

  • makedepf90 - Automatic dependency resolution and makefile creation given a set of Fortran sources. Resolves module dependencies, Fortran and C preprocessor includes and external procedures

The rest of this page contains some sample scripts for CMake, Meson, Waf, and the JAMS Makefile along with a brief introduction to each. In all cases we have a project with the following directory structure:

   project/
        - src/
             - hello.f90
             - tools.f90
             - tools.h

CMake

A typical CMake project has a file called CMakeLists.txt in the project directory:

   project/
        - CMakeLists.txt
        - src/
             - hello.f90
             - tools.f90
             - tools.h

The file contains the instructions to build the software. For example:

cmake_minimum_required(VERSION 3.5)

project(hello)
enable_language(Fortran)

if(CMAKE_Fortran_COMPILER_ID MATCHES "GNU")
    set(dialect "-ffree-form -std=f2008 -fimplicit-none")
    set(bounds "-fbounds-check")
endif()
if(CMAKE_Fortran_COMPILER_ID MATCHES "Intel")
    set(dialect "-stand f08 -free -implicitnone")
    set(bounds "-check bounds")
endif()
if(CMAKE_Fortran_COMPILER_ID MATCHES "PGI")
    set(dialect "-Mfreeform -Mdclchk -Mstandard -Mallocatable=03")
    set(bounds "-C")
endif()

set(CMAKE_Fortran_FLAGS_DEBUG "${CMAKE_Fortran_FLAGS_DEBUG} ${bounds}")
set(CMAKE_Fortran_FLAGS "${CMAKE_Fortran_FLAGS} ${dialect}")

#
# Compile.
#
file(GLOB_RECURSE sources  src/*.f90 src/*.h)
add_executable(prog ${sources})

1) The first line declares the CMake version required. If you don’t know what to put here, just take the first two digits of your current CMake version:

$ cmake --version
cmake version 3.5.1

CMake suite maintained and supported by Kitware (kitware.com/cmake).

In my case, I write cmake_minimum_required(VERSION 3.5)

2) Then comes the project name, and the language. CMake has excellent support for Fortran, but you do have to specify the language, and the name of the language is case-sensitive. For example, enable_language(fortran) will give an error.

project(hello)
enable_language(Fortran)

3) The next lines add a few compiler-specific flags. CMake understands most Fortran compilers including the compilers from GNU, Intel, Absoft, PGI, PathScale, and IBM (XL and VisualAge).

if(CMAKE_Fortran_COMPILER_ID MATCHES "GNU")
    set(dialect "-ffree-form -std=f2008 -fimplicit-none")
    set(bounds "-fbounds-check")
endif()
...

set(CMAKE_Fortran_FLAGS_DEBUG "${CMAKE_Fortran_FLAGS_DEBUG} ${bounds}")
set(CMAKE_Fortran_FLAGS "${CMAKE_Fortran_FLAGS} ${dialect}")

The set() command sents the value of a variable. The compiler flags are stored in three different variables:

  • CMAKE_Fortran_FLAGS – Used for all builds.
  • CMAKE_Fortran_FLAGS_DEBUG – Used only for debugging builds.
  • CMAKE_Fortran_FLAGS_RELEASE – Used only for release builds.

4) Finally, we read all the *.f90 and *.h files into the sources variable and add ask CMake to compile the executable. CMake understands Fortran module dependencies and will compile the modules in the correct order.

file(GLOB_RECURSE sources  src/*.f90 src/*.h)
add_executable(prog ${sources})

CMake generates a lot of files, so it is recommended that you build the program in a separate directory. On Linux and Unix, CMake defaults to creating a traditional Makefile, but CMake can also produce Ninja files to seed-up compilation (but see note below).

$ mkdir build
$ cd build
build $ cmake ..
build $ make

To take advantage of the Ninja build system, you must install Kitware’s Ninja branch which contains enhancements needed to compile Fortran programs. The -GNinja flag instructs CMake to generate Ninja files:

build $ cmake -GNinja ..
build $ ninja

You can use -D flags to override internal variables. For example:

build $ cmake -GNinja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_Fortran_COMPILER=ifort -DCMAKE_INSTALL_PREFIX=/usr/local ..
build $ ninja

A full list of variables is available in the CMake wiki.

Meson

A typical Meson project has a file called meson.build in the project directory:

   project/
        - meson.build
        - src/
             - hello.f90
             - tools.f90
             - tools.h

The file contains the instructions to build the software and it looks broadly similar to a CMakeLists.txt file:

project('hello', 'fortran')

#
# Compiler flags
#
dialect = ''
bounds =  ''
if meson.get_compiler('fortran').get_id() == 'gcc'
    dialect = [ '-ffree-form','-std=f2008','-fimplicit-none']
    bounds = '-fbounds-check'
endif
if meson.get_compiler('fortran').get_id() == 'intel'
    dialect = ['-stand f08','-free','-implicitnone']
    bounds = '-check bounds'
endif
if meson.get_compiler('fortran').get_id() == 'pgi'
    dialect = [ '-Mfreeform','-Mdclchk',
                '-Mstandard','-Mallocatable=03']
    bounds = '-C'
endif

add_global_arguments(dialect, language : 'fortran')
add_global_arguments(bounds, language : 'fortran')

#
# Done.
#
sources = ['src/hello.f90', 'src/hello.h', 'src/tools.f90']
executable('prog', sources)

1) The first line indicates the project name and language:

project('hello', 'fortran')

2) The next lines add a few compiler-specific flags. The if-statement uses regular expressions to match the name of the compiler. The script syntax looks similar to modern scripting languages like Python.

if meson.get_compiler('fortran').get_id() == 'gcc'
    dialect = [ '-ffree-form','-std=f2008','-fimplicit-none']
    bounds = '-fbounds-check'
endif
...

add_global_arguments(dialect, language : 'fortran')
add_global_arguments(bounds, language : 'fortran')

3) Finally, we instruct Meson to compile the executable. Meson should be able to handle Fortran module dependencies and compile the modules in the correct order.

sources = ['src/hello.f90', 'src/hello.h', 'src/tools.f90']
executable('prog', sources)

Meson also generates a lot of files, On Linux and Unix, CMake defaults to creating Ninja files. Meson requires Ninja version 1.5 or later.

$ mkdir build
$ meson build
$ cd build
build $ ninja

Waf

Warning: I have had trouble getting Waf working on my system. The instructions below may be incomplete. –DC

Waf projects are expected to include the waf executable as part of the source distribution. In addition, there is a file called wscript that contains the instructions to build the software. The typical directory structure is,

   project/
        - waf
        - wscript
        - src/
             - hello.f90
             - tools.f90
             - tools.h

A sample wscript file could look like this:

def options(ctx):
	ctx.load('compiler_fc')
	ctx.add_option('--debug', action='store', default=False, help='Debug build')
	ctx.add_option('--release', action='store', default=False, help='Release build')

def configure(ctx):
	ctx.load('compiler_fc')
	
	print ctx.env.FC_NAME
	
	if ctx.env.FC_NAME == 'GFORTRAN':
		ctx.env.append_unique('FCFLAGS', ['-ffree-form', '-std=f2008'])
		ctx.env.append_unique('FCFLAGS', '-fimplicit-none')
		
		if ctx.options.debug:
			ctx.env.append_unique('FCFLAGS', ['-O0', '-g'])
			ctx.env.append_unique('FCFLAGS', '-fbounds-check')
		if ctx.options.release:
			ctx.env.append_unique('FCFLAGS', '-O3')
		
	elif ctx.env.FC_NAME == 'IFORT':
		ctx.env.append_unique('FCFLAGS', ['-free', '-stand f08', '-implicitnone'])
		
		if ctx.options.debug:
			ctx.env.append_unique('FCFLAGS', ['-g', '-check bounds'])
		if ctx.options.release:
			ctx.env.append_unique('FCFLAGS', '-fast')
		
	elif ctx.env.FC_NAME == 'PGFORTRAN':
		ctx.env.append_unique('FCFLAGS', ['-Mfreeform', '-Mstandard'])
		ctx.env.append_unique('FCFLAGS', ['-Mdclchk', '-Mallocatable=03'])
		
		if ctx.options.debug:
			ctx.env.append_unique('FCFLAGS', ['-g', '-C'])
		if ctx.options.release:
			ctx.env.append_unique('FCFLAGS', '-fast')
	
	ctx.check_fortran()

def build(bld):
	bld(features = 'fc fcprogram',
	    source   = ['src/hello.f90', 'src/hello.h', 'src/tools.f90'],
	    target   = 'prog')

Waf files are Python scripts, and waf commands correspond to functions in the file. For example, ./waf configure will run the configure() function. In the project directory, do this:

$ ./waf configure
$ ./waf build

Waf creates a build directory where the project is built.

JAMS Makefile

Compiling a program from source code is an elaborate process. The compiler has to find all source files, of course. It has to know all dependencies between the source files. For C programs, it has to find all the header (.h) files. For Fortran programs, it has to find the module (.mod) files, which are produced by the compiler itself, which means that the files have to be compiled in a certain order. Last but not least, the compiler and linker have to find external libraries and use appropriate compiler options.

Different solutions exist for this problem, the two most prominent being GNU’s configure and Kitware’s CMake. One almost always has to give non-standard directories on the command line, e.g.

configure --with-netcdf=/path/to/netcdf
cmake -DCMAKE_NETCDF_DIR:STRING=/path/to/netcdf

Therefore, one has to know all installation directories, configure and cmake options, etc. for the current computer (system), or load the appropriate, matching modules, which is tedious if you or a team is working on several computers such as your local computer for development and one or two clusters or supercomputers for production. This can be externalised in CMake by giving a script with -C or -P once all information was gathered.

The JAMS Makefile project follows a similar idea that the information about the current computer (system) must only be gathered once and stored in a config file. The user can then easily compile the same code on different computer (systems) with different compilers in debug or release mode, by simply telling on the command line, for example:

make system=mcinra compiler=gnu release=debug

This uses the system specific files mcinra.alias to look for the default GNU compiler, which is version 9.2 in this case and then uses all variables set in the file mcinra.gnu92. The user has to provide mcinra.alias and mcinra.gnu92 populated with the directories and specific compiler options for the GNU compiler suite 9.2 on the macOS system mcinra. Checking the same code with another compiler would be (given mcinra.intel_ exists):

make system=mcinra compiler=intel release=debug

After checking with debug compiler options, one can simply compile the release version of the program by typing:

make system=mcinra compiler=intel release=release

All the options can also be set in the Makefile itself.

Once the user has gathered the information about his/her system, compiling any other project is by simpling changing the path to the source files. In the example project, this would be:

make system=mcinra compiler=intel release=release SRCPATH=/path/to/project/src

The JAMS Makefile project includes examples for different operating systems, i.e. Unix (e.g. gadi), Linux (e.g. explor), macOS (e.g. mcinra), and Windows (e.g. uwin). The system mcinra provides examples for different compilers, i.e. the GNU compiler suite, the Intel compiler suite, the NAG Fortran compiler, and the PGI Fortran compiler.

The project provides some standard configurations for the GNU compiler suite such as homebrew on macOS, ubuntu on Linux, and cygwin and ubuntu (_uwin_) on Windows.

An example .alias file for homebrew on macOS is (_homebrew.alias_):

ifneq (,$(findstring $(compiler),gnu gfortran gcc))
    icompiler := gnu
endif

which says that you can type compiler=gnu or compiler=gfortran or compiler=gcc on the command line or set it in Makefile. The homebrew.gnu file then looks like:

# Programs
F90 := /usr/local/bin/gfortran
FC  := $(F90)
CC  := /usr/bin/cc  # clang, same as /usr/bin/gcc
CPP := /usr/bin/cpp

# Flags
ifeq ($(release),debug)
    F90FLAGS += -pedantic-errors -Wall -W -O -g -Wno-maybe-uninitialized
    FCFLAGS  += -pedantic-errors -Wall -W -O -g -Wno-maybe-uninitialized
    CFLAGS   += -pedantic -Wall -W -O -g -Wno-uninitialized
else
    F90FLAGS += -O3 -Wno-aggressive-loop-optimizations
    FCFLAGS  += -O3 -Wno-aggressive-loop-optimizations
    CFLAGS   += -O3
endif
F90FLAGS += -cpp -ffree-form -ffixed-line-length-132
FCFLAGS  += -ffixed-form -ffixed-line-length-132
CFLAGS   +=
MODFLAG  := -J# space significant
DEFINES  += -D__GFORTRAN__ -D__gFortran__

# OpenMP
F90OMPFLAG := -fopenmp
FCOMPFLAG  := -fopenmp
COMPFLAG   := -fopenmp
LDOMPFLAG  := -fopenmp
OMPDEFINE  := -D__OPENMP__

# Linking
LIBS  += -L$(GNULIB)
RPATH += -Wl,-rpath,$(GNULIB)

and there can be special section for external libraries such as for LAPACK or the generic coordinate transformation software PROJ:

# LAPACK
LAPACKDIR  :=
LAPACKFLAG := -framework Accelerate
LAPACKDEF  := -D__LAPACK__

# PROJ
PROJ4DIR  := /usr/local/
PROJ4FLAG := -lproj

which can then be chosen with lapack=true or proj4=true on the command line, or set at the beginning of the Makefile.

See github.com/mcuntz/jams_makefile for details.

See Also