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.

  • 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, and Waf, 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.

See Also