Getting started with CMake for Fortran
This guide aims to teach you how to use the CMake build system, with a special focus on Fortran (there are many fine C++ tutorials already around, but not many on Fortran). Instead of trying to cover all of the necessary information to become a CMake expert, it instead aims to get you started with a working build system in as little time as possible. If you want to know more, there’s a list of recommended reading at the end of the document. Finally, this guide uses the command-line throughout because it’s the most portable interface across platforms.
Table of Contents
- TL;DR - a simple template
- Installing CMake
- The basics of CMake
- Getting started with CMake
- Building the code
- Further reading
TL;DR - a simple template
If you’ve already read this guide and just want to copy-and-paste the template, here it is.
Top level CMakeLists.txt
file (project_root/CMakeLists.txt
):
cmake_minimum_required(VERSION 3.1)
project(my_project VERSION 0.1
DESCRIPTION "My Fortran program"
LANGUAGES Fortran)
enable_language(Fortran)
# Currently setting the Fortran compiler to use -std=gnu, change this if you
# want a specific standard
set(FVERSION "-std=f95")
set(CMAKE_Fortran_FLAGS "${CMAKE_Fortran_FLAGS} ${FVERSION}")
# Source code
add_subdirectory(src)
install(TARGETS my_exe DESTINATION "bin")
Source directory (project_root/src/CMakeLists.txt
):
set(MY_MODS my_module.f90
another_module.f90
CACHE INTERNAL "")
add_library(mylib "${MY_MODS}")
add_executable(my_exe main.f90)
target_link_libraries(my_exe PRIVATE mylib)
Configure the build by setting the CMake variables CMAKE_Fortran_COMPILER
and
CMAKE_Fortran_FLAGS
, e.g.:
cmake -B build -DCMAKE_Fortran_COMPILER=ifort -DCMAKE_Fortran_FLAGS="-O0 -g"
Compile commands
Build and install to project_root/install
:
cmake -B build -DCMAKE_INSTALL_PREFIX=./install
cmake --build build
cmake --install build
Build using all CPU cores in parallel with the -j
flag:
cmake --build build -j $(nproc)
Installing CMake
macOS
The easiest way to install the latest version of CMake is with the Homebrew package manager by doing:
brew install cmake
Linux
The best way to install CMake on Linux is through your distro’s package manager, e.g.
apt install cmake
on Ubuntu/Debian and derivatives, or
dnf install cmake
on RHEL or Fedora.
Some Linux distributions (e.g. Debian or RHEL) package old and out-of-date versions of CMake. If you need a newer version than the one in your distro’s repository, then you’ll have to install from source or download a pre-built binary from https://cmake.org/download/. This is also useful if your HPC cluster only supplies old versions of CMake.
Windows
CMake is natively supported by Visual Studio: https://docs.microsoft.com/en-us/cpp/build/cmake-projects-in-visual-studio?view=msvc-170. If you can’t or don’t want to use Visual Studio, then the easiest way is to download the pre-built binaries from https://cmake.org/download/.
The basics of CMake
CMake is a cross-platform program to automate the software build process. It’s a higher level tool than a build system like “Make”: you specify the source structure of your code and compiler parameters needed to build it, then CMake generates a set of Makefiles customised to build the code on the target system.
CMake has a few benefits compared to writing Makefiles by hand:
- CMake is cross-platform (that’s what the “C” in “CMake” means): you can write one CMake specification which will work on Windows, Mac and Linux.
- CMake has convenient functions to automatically determine include and link paths for many common libraries such as GSL or LAPACK. For most systems, you don’t need to fuss around with hard-coding library paths, and the exceptions are still pretty simple to deal with.
- CMake has native support for a wide range of languages and compilers, so you can specify high-level desired properties and let CMake figure out the right compiler flags.
- CMake is extremely widely used in open-source software, making it easier for people to use and contribute to your code.
You specify build properties in files called CMakeLists.txt
, which are sort
of like CMake’s equivalent of Makefile
s. These files can be included
throughout your project’s source tree as needed to give you fine-grained
control over how different parts of your code should be compiled and linked
(similar in concept to the old-school technique of “recursive makefiles”).
Getting started with CMake
To start using CMake, make a file in your project’s root directory
called CMakeLists.txt
(the capitalisation is significant). This file will
contain the so-called “top-level”
configuration options for your project - things like the name and version of
your project, which directories to include in the build and compiler options
which should be used for all components of your project.
CMakeLists.txt
is not a Makefile and should not be confused with one. It
simply sets up the parameters of the build. The commands in the file are
executed sequentially by CMake, similar to any other programming language, but
unlike make
it does not actually build any software as it reads it. Keep this
distinction in mind throughout the rest of this guide.
Configuring the project
The top-level CMakeLists.txt
file should always start with some variation on
the following lines (at least until you’re familiar enough with CMake to know
when to leave them out):
cmake_minimum_required(VERSION 3.1)
project(my_project VERSION 0.1
DESCRIPTION "My Fortran program"
LANGUAGES Fortran)
The first command specifies that the build recipe requires a version of CMake
at least as recent as 3.10
- it will immediately terminate the build if
run with an older version of CMake. This is important because CMake has
changed a lot over the years and some of the examples in this guide will
only work for newer versions.
The second command defines the project to be built: its name, version, a
short description and what programming language(s) it uses. This is important
information for configuring the build and applies to everything in the project
directory. Everything other than the project name (my_project
, in this case)
is optional, but for Fortran programs you must have LANGUAGES Fortran
to get
any of CMake’s special Fortran-aware functionality (if you leave it out, CMake
will assume the default language C++). The project name must always be
specified - CMake will issue a warning and make one for you if it’s not
specified.
CMake supports mixing source files in multiple languages and is able to
automatically handle the compiling and linking steps for (most)
mixed-language projects. For example, if we had a mix of C and Fortran, we
would modify the project()
command like so:
project(my_project VERSION 0.1
DESCRIPTION "My Fortran program"
LANGUAGES Fortran C)
And CMake will automatically detect and configure C-specific build options.
CMake can also handle compiling binaries which contain both C and Fortran code. You’ll still need to define an interface using compatible data types (as outlined in e.g. this guide), but CMake will automatically generate the correct compiler and linker commands for hybrid Fortran/C code.
Configuring the Fortran compiler
Fortran builds require a little bit of extra configuration on top of just specifying the language.
Fortran compilers have a wide variety of
supported options and flags, so it’s usually necessary to set different
compiler options for gfortran
than for the Intel or Cray Fortran compilers.
The following lines of code create a variable (internal to CMake) to store the
name of the Fortran compiler, then sets up a conditional if-then-else based on
the compiler:
get_filename_component (Fortran_COMPILER_NAME ${CMAKE_Fortran_COMPILER} NAME)
if (Fortran_COMPILER_NAME MATCHES "gfortran.*")
set (CMAKE_Fortran_FLAGS "<gfortran flags go here>")
elseif (Fortran_COMPILER_NAME MATCHES "ifort.*")
set (CMAKE_Fortran_FLAGS "<Intel flags go here>")
endif()
CMake will attempt to automatically determine the system’s default Fortran
compiler, but this can be overridden by specifying the CMAKE_Fortran_COMPILER
CMake variable when configuring the build. For example, to tell CMake to use
ifort
to build your project, you can do:
cmake -B build -DCMAKE_Fortran_COMPILER=ifort
Compiling an executable
The CMake command to compile an executable (binary) file is add_executable()
,
which takes the following form:
add_executable(exe_name file1.f90 file2.f90 ...)
You can add as many files as you want to the add_executable
statement, which
will add a statement to the Makefile to compile them together into an
executable with the specified name (exe_name
in this case). The executable’s
name also serves as a label used internally by CMake during the build-process,
and is referred to as a target. Many CMake commands act on an exe target and
modify various properties of the build. There can also be multiple executables
per project, each of which requires its own call to add_executable()
and its
own unique name.
Linking to an external library
What if you want to use an external library with your code, such as LAPACK or
GSL? Fortunately, CMake has a set of feature which automate a lot of the fiddly
work of linking with external libraries. The command
target_link_libraries(exe_name <args>)
tells CMake to create a linker
command based on the supplied arguments. According to the CMake
documentation,
the arguments can be one of the following:
- A full path to a library file (e.g.
/usr/lib64/libgsl.so
) - A plain library name, which will be resolved according to the conventions of
the current system. For example, the call
target_link_libraries(my_exe blas)
will generate the linker command-lblas
. CMake can also automatically find and resolve the link path for a number of common libraries via thefind_package()
command, which searches through both pre-configured paths and paths set by environment variables such as modules. See the CMake documentation for more details. - A library target name: corresponding to a library you have created in the current project (more about this below).
The executable given to target_link_libraries
must have already been defined
in the CMake file by the add_executable()
command.
Creating your own library
Even though it’s possible to include multiple files in the add_executable
, it
can quickly get unwieldy for large projects. This is doubly true if you need to
generate multiple binaries which share common files (e.g. files of parameters
or physical constants). An alternative is to group your auxiliary files into a
library and then link that to your binary.
The process for creating and linking a library is very similar to adding an external library, but requires you to compile your library first.
First, you need to add_executable()
with the smallest viable number of files
for each binary (e.g. a “main” loop), since you need to define the binary first
before setting any properties like library dependencies. Then, you can create a
new library with the add_library()
command, which takes a list of files to
compile into the library. Here is an example CMake script:
add_executable(exe1 main1.f90)
add_executable(exe2 main2.f90)
add_library(shared_mods module1.f90 module2.f90)
target_link_library(exe1 shared_mods)
target_link_library(exe2 shared_mods)
Finally, if you have a lot of files to compile into different libraries, you can store the list of files in a CMake variable, which is a little bit cleaner than passing the full list verbatim. For example:
set(QED_MODS uehling.c
uehling_ml.c
electric.c
magnetic.c
CACHE INTERNAL "")
add_library(qed "${QED_MODS}")
Note: the set
command defines a CMake variable, which in this case is a list
of strings (filenames). Variables are referenced by the ${}
syntax, similar
to some Unix shell scripting. The CACHE
part tells CMake to cache and
reuse the value between builds in the same directory (unless you specify
otherwise), while INTERNAL
means that it will not be printed in any
user-facing help messages.
Building the code
Now that you’ve got a working CMake configuration (or downloaded a project which has one), it’s time to actually compile the code. As outlined above, there are three steps to this: configuration, compilation and installation.
The basic steps in the build process are as follows:
- CMake parses the
CMakeLists.txt
file(s), figures out how to build your software for the target platform (usually the one you’re running CMake on), then generates a set ofMakefiles
encoding those build options 1. This is known as the “configuration step”, and corresponds to the command:cmake -B <build_dir> <cmake_flags>
The
-B
flags creates a new “build directory” in the project’s top-level directory which will contain all of the object/module files during the build step. This is called an “out-of-tree” build. You don’t strictly need to do this, but it’s cleaner to keep all of the temporary build files in one place that you can easily delete if you need to. I usually call this folderbuild
.
The <cmake_flags>
are variables you pass to CMake to configure the build
that you don’t want to hard code into the CMakeLists.txt
file and have the
form -D<flag>
, e.g. -DCMAKE_Fortran_COMPILER=gfortran
.
- The
Makefiles
actually compile the code, either through CMake’s wrapper interface via:cmake --build <build_dir>
or directly through the
make
command via:cd build make
This is known as the “build step”. If your build is taking a long time, you can compile the code using multiple CPU processors by passing the
-j
flag tocmake--build
, along with the number of processors to use:cmake --build <build_dir> -j 4
will launch 4 parallel compilation jobs using 4 CPU cores.
CMake
then installs the compiled code to a set of “install directories”, along with any libraries or header files the code needs to run. By default, these files are installed in a system-wide default (which may or may not require root privileges), but this can be overridden during the configuration step. This is known as the “install step” and can again be done through CMake’s wrapper interface:cmake --install <build_dir>
or through a Unix-style
make install
command (in the build directory). If you don’t tell it otherwise, CMake will try to install your code in the default system location, usually something like/usr/
on Unix. You may or may not have permission to install to this directory, so it’s usually a good idea to specify a custom location to install the code (called the “install prefix”). This is specified by passing the--prefix
flag to the install command. For example, the command:cmake --install <build_dir> --prefix=./install
will set the install prefix to a sub-directory called
install
. The executable files will be installed to./install/bin
, libraries will be installed to./install/lib
and so on. If you install the code in a non-standard location, you may need to add the new location to thePATH
variable so your shell knows where to find it. On Unix, this is easiest to accomplish by adding the following line to your.bashrc
file:export PATH=<install_prefix>/bin:$PATH
(This is a bit of an oversimplification, but it’s close enough for our purposes.)
If there are any errors in your CMakeLists.txt
files, they will be reported
during the configuration stage. Errors in the source code files will be
reported during the compilation stage. The installation phase should only fail
due to either insufficient permissions of file-system errors (e.g. disk-full),
provided the first two steps were successful.
If the build is successful, you should see output like the following:
$ cmake -B build
-- The Fortran compiler identification is GNU 11.2.1
-- The C compiler identification is GNU 11.2.1
-- Detecting Fortran compiler ABI info
-- Detecting Fortran compiler ABI info - done
-- Check for working Fortran compiler: /usr/bin/f95 - skipped
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Setting build type to 'RelWithDebInfo' as none was specified.
-- Found PkgConfig: /usr/bin/pkg-config (found version "1.8.0")
-- Found GSL: /usr/include (found version "2.6")
-- Configuring done
-- Generating done
-- Build files have been written to: /home/emily/Code/rhf/build/
$ cmake --build build
[ 1%] Building C object src/qed/CMakeFiles/qed.dir/uehling.c.o
[ 2%] Building C object src/qed/CMakeFiles/qed.dir/magnetic.c.o
[ 4%] Building C object src/qed/CMakeFiles/qed.dir/uehling_ml.c.o
[ 5%] Building C object src/qed/CMakeFiles/qed.dir/electric.c.o
...
[ 97%] Built target rhf_utils
Scanning dependencies of target rhf
[ 98%] Building Fortran object src/CMakeFiles/rhf.dir/rhf.f90.o
[100%] Linking Fortran executable rhf
[100%] Built target rhf
$ cmake --install build --prefix=./install
-- Install configuration: "RelWithDebInfo"
-- Installing: /home/emily/Code/rhf/./install/bin/rhf
-- Installing: /home/emily/Code/rhf/./install/lib64/libtoml-f.a
...
Further reading
This guide only contains the barest minimum needed to get started with CMake and Fortran. If you want to learn more, I recommend the following resources:
- Kitware’s CMake Fortran example: https://gitlab.kitware.com/cmake/community/-/wikis/doc/cmake/languages/fortran/ForFortranExample
- Kitware’s CMake tutorial: https://cmake.org/cmake/help/latest/guide/tutorial/index.html
- An Introduction to Modern CMake: https://cliutils.gitlab.io/modern-cmake/