构建工具#

根据源文件的数量和模块之间的相互依赖性,手动编译 Fortran 项目可能会变得相当复杂。除非使用正确的工具来自动执行这些任务,否则支持不同的编译器和链接器或不同的平台会变得越来越复杂。

根据项目的大小和项目的目的,可以使用不同的构建自动化选项。

首先,你的集成开发环境可能提供了一种构建程序的方法。一个流行的跨平台工具是 Microsoft 的 Visual Studio Code,但也有其它工具,例如 AtomEclipse PhotranCode::Blocks。它们提供了一个图形用户界面,但通常是针对编译器和平台的。

对于较小的项目,基于规则的构建系统 make 是一种常见的选择。根据定义的规则,它可以执行从更新的源文件(重新)编译目标文件、创建库和链接可执行文件等任务。要为你的项目使用 make,你必须在 Makefile 中编码这些规则,它定义了所有最终程序、中间目标文件或库以及实际源文件的相互依赖关系。有关简短介绍,请参阅 关于 make 的指南

autotools 和 CMake 等维护工具可以通过高级描述生成 Makefile 或 Visual Studio 项目文件。它们从编译器和平台细节中抽象出来。

哪些工具是你项目的最佳选择取决于许多因素。选择一个你喜欢使用的构建工具,它不应该在开发时妨碍你。花更多的时间在构建工具上而不是实际的开发工作上会很快变得令人沮丧。

此外,请考虑构建工具的可访问性。如果仅限于特定的集成开发环境,你项目中的所有开发人员都可以访问吗?如果你使用的是特定的构建系统,它是否适用于你正在开发的所有平台?你的构建工具的进入壁垒有多大?考虑构建工具的学习曲线,如果你必须先学习一门复杂的编程语言才能添加新的源文件,那么完美的构建工具将毫无用处。最后,考虑其它项目正在使用什么,你依赖的项目以及使用(或将使用)你的项目作为依赖项的项目。

使用 make 作为构建工具#

最著名和最常用的构建系统称为 make。它按照名为 Makefilemakefile 的配置文件中定义的规则执行操作,这通常会导致从提供的源代码编译程序。

小技巧

如需深入的 make 教程,请查看其信息页面。这个信息页面有一个在线版本,可用。

我们将从干净的源目录中的基础知识开始。创建并打开文件 Makefile,我们从一个名为 all 的简单规则开始:

all:
	echo "$@"

保存 Makefile 后,通过在同一目录中执行 make 运行它。你应该看到以下输出:

echo "all"
all

首先,我们注意到 make$@ 替换了规则的名称,第二个需要注意的是 make 总是打印它正在运行的命令,最后,我们看到运行 echo "all"

备注

按照惯例,我们总是将 Makefile 的入口点称为 all,但你可以选择任何你喜欢的名称。

备注

如果你的编辑器工作正常,你应该不会注意到它,但是你必须使用制表符缩进规则的内容。如果你在运行上述 Makefile 时遇到问题并看到类似的错误

Makefile:2: *** missing separator.  Stop.

缩进可能不正确。在这种情况下,将第二行中的缩进替换为制表符。

现在我们想让我们的规则更复杂,因此我们添加另一个规则:

PROG := my_prog

all: $(PROG)
	echo "$@ depends on $^"

$(PROG):
	echo "$@"

注意我们如何在make中声明变量,你应该总是用:=声明你的局部变量。要访问变量的内容,我们使用$(...),注意我们必须将变量名括在括号中。

备注

变量的声明通常使用 := 完成,但 make 确实支持 recursively 扩展 变量以及 =。通常,需要第一种声明,因为它们更可预测并且没有递归扩展的运行时开销。

我们引入了规则 all 的一个依赖,即变量 PROG 的内容,同时我们修改了打印输出,我们想查看这个规则的所有依赖,这些依赖存储在变量 $^ 中。现在对于我们以变量 PROG 的值命名的新规则,它与我们之前对规则 all 所做的事情相同,注意 $@ 的值如何依赖于它所使用的规则。

通过运行make再次检查,你应该看到:

echo "my_prog"
my_prog
echo "all depends on my_prog"
all depends on my_prog

在对规则 all 执行任何操作之前,已正确解析并评估了依赖关系。让我们只运行第二条规则:输入make my_prog,你只会在终端中找到前两行。

下一步是使用 make 执行一些实际操作,我们在这里获取上一章的源代码,并在我们的 Makefile 中添加新规则:

OBJS := tabulate.o functions.o
PROG := my_prog

all: $(PROG)

$(PROG): $(OBJS)
	gfortran -o $@ $^

$(OBJS): %.o: %.f90
	gfortran -c -o $@ $<

我们定义了代表目标文件的OBJS,我们的程序依赖于那些OBJS,并且对于每个目标文件,我们创建一个规则以从源文件中生成它们。我们介绍的最后一条规则是模式匹配规则,%tabulate.otabulate.f90 之间的通用模式,它将我们的目标文件 tabulate.o 连接到源文件 tabulate.f90 .有了这个集合,我们运行我们的编译器,这里是 gfortran 并将源文件转换为目标文件,由于 -c 标志,我们还没有创建可执行文件。注意这里依赖项的第一个元素使用了$&lt;

在编译完我们尝试链接程序的所有目标文件后,我们不直接使用链接器,而是使用 gfortran 来生成可执行文件。

现在我们使用 make 运行构建脚本:

gfortran -c -o tabulate.o tabulate.f90
tabulate.f90:2:7:

    2 |    use user_functions
      |       1
Fatal Error: Cannot open module file ‘user_functions.mod’ for reading at (1): No such file or directory
compilation terminated.
make: *** [Makefile:10: tabulate.f90.o] Error 1

我们记得我们的源文件之间存在依赖关系,因此我们将此依赖关系显式添加到 Makefile

tabulate.o: functions.o

现在我们可以重试并发现构建工作正常。输出应该看起来像

gfortran -c -o functions.o functions.f90
gfortran -c -o tabulate.o tabulate.f90
gfortran -o my_prog tabulate.o functions.o

你现在应该在目录中找到 four 个新文件。运行 my_prog 以确保一切按预期工作。让我们再次运行make

make: Nothing to be done for 'all'.

使用可执行文件 make 的时间戳可以确定,它比 tabulate.ofunctions.o 都更新,而后者又比 tabulate.f90functions.f90 更新。因此,该程序已经是最新的最新代码,无需执行任何操作。

最后,我们将看看一个完整的 Makefile

# Disable all of make's built-in rules (similar to Fortran's implicit none)
MAKEFLAGS += --no-builtin-rules --no-builtin-variables
# configuration
FC := gfortran
LD := $(FC)
RM := rm -f
# list of all source files
SRCS := tabulate.f90 functions.f90
PROG := my_prog

OBJS := $(addsuffix .o, $(SRCS))

.PHONY: all clean
all: $(PROG)

$(PROG): $(OBJS)
	$(LD) -o $@ $^

$(OBJS): %.o: %
	$(FC) -c -o $@ $<

# define dependencies between object files
tabulate.f90.o: functions.f90.o user_functions.mod

# rebuild all object files in case this Makefile changes
$(OBJS): $(MAKEFILE_LIST)

clean:
	$(RM) $(filter %.o, $(OBJS)) $(wildcard *.mod) $(PROG)

由于你从 make 开始,我们强烈建议始终包含第一行,就像 Fortran 的 implicit none 一样,我们不希望隐式规则以令人惊讶和有害的方式弄乱我们的 Makefile

接下来,我们有一个配置部分,我们在其中定义变量,如果你想切换编译器,可以在这里轻松完成。我们还引入了 SRCS 变量来保存所有源文件,这比指定目标文件更直观。我们可以通过使用函数 addsuffix 附加 .o 后缀来轻松创建目标文件。 .PHONY 是一个特殊规则,它应该用于你的Makefile 的所有入口点,这里我们定义了两个入口点,我们已经知道_all_,新的_clean_ 规则再次删除所有构建工件,这样我们确实从一个干净的目录开始。

此外,我们稍微更改了目标文件的构建规则,以考虑附加 .o 后缀而不是替换它。请注意,我们仍然需要在 Makefile中明确定义相互依赖关系。我们还添加了对 Makefile 本身的目标文件的依赖,以防你更改编译器,这将允许你安全地重新构建。

现在你已经对 make 有了足够的了解,可以用它来构建小型项目了。如果你打算更广泛地使用 make,我们也为你编译了一些提示。

小技巧

在本指南中,我们避免并禁用了许多常用的 make 功能,如果使用不当,这些功能可能会特别麻烦,如果你对使用 make 没有信心,我们强烈建议你远离内置规则和变量,但显式声明所有变量和规则。

你会发现 make 是一种强大的工具,可以自动化简短的相互依赖的工作流程并构建小型项目。但是对于较大的项目,你可能很快就会遇到一些限制。通常,make 不会单独使用,而是与其它工具结合使用以完全或部分生成 Makefile

递归扩展变量#

在许多项目中常见的是递归扩展变量(用 = 而不是 := 声明)。变量的递归扩展允许乱序声明和其它使用 make 的巧妙技巧,因为它们被定义为在运行时扩展的规则,而不是在解析时定义。

例如,使用此代码段声明和使用 Fortran 标志(flag)将完全正常:

all:
	echo $(FFLAGS)

FFLAGS = $(include_dirs) -O
include_dirs += -I./include
include_dirs += -I/opt/some_dep/include

运行 make 后,你应该会找到预期的(或可能是意外的)打印输出

echo -I./include -I/opt/some_dep/include -O
-I./include -I/opt/some_dep/include -O

小技巧

使用 += 附加到未定义的变量将产生一个递归扩展的变量,该状态将被继承以进行所有进一步的附加。

虽然这似乎是一个有趣的功能,但它往往会导致令人惊讶和意想不到的结果。通常,在定义像编译器这样的变量时,根本没有理由实际使用递归扩展。

使用 := 声明可以轻松实现相同的目的:

all:
	echo $(FFLAGS)

include_dirs := -I./include
include_dirs += -I/opt/some_dep/include
FFLAGS := $(include_dirs) -O

重要

总是将 Makefile 视为一整套规则,在评估任何规则之前必须对其进行完全解析。

你可以使用你最喜欢的任何类型的变量,当然应该小心混合它们。重要的是要了解这两种类型之间的差异及其各自的含义。

注释和空格#

有一些关于空格和注释的警告,在使用 make 时可能会不时弹出。首先,make 不知道除了字符串之外的任何数据类型,默认分隔符只是一个空格。这意味着 make 将很难尝试构建文件名中有空格的项目。如果遇到这种情况,重命名文件可能是手头最简单的解决方案。

另一个常见的问题是前导和尾随空格,一旦引入,make 会很高兴地携带它,实际上它在比较make 中的字符串时确实有所作为。

这些可以通过以下注释来介绍

prefix := /usr  # path to install location
install:
	echo "$(prefix)/lib"

虽然注释将被 make 正确删除,但尾随的两个空格现在是变量内容的一部分。运行 make 并检查是否确实如此:

echo "/usr  /lib"
/usr  /lib

要解决此问题,你可以移动注释,或者使用 strip 函数去除空白。或者,你可以尝试 join 字符串。

prefix := /usr  # path to install location
install:
	echo "$(strip $(prefix))/lib"
	echo "$(join $(join $(prefix), /), lib)"

总而言之,这些解决方案都不会使你的 Makefile 更具可读性,因此,在编写和使用 make 时要特别注意空格和注释。

Meson 构建系统#

在你了解了 make(我们称之为低级构建系统)的基础知识之后,我们将介绍 meson,一个高级构建系统。当你在低级构建系统中指定如何构建程序时,你可以使用高级构建系统来指定要构建的内容。高级构建系统将为你处理如何为低级构建系统生成构建文件。

有很多可用的高级构建系统,但我们将专注于 meson,因为它的构建是特别用户友好的。 meson 的默认低级构建系统称为 ninja

让我们看一个完整的 meson.build 文件:

project('my_proj', 'fortran', meson_version: '>=0.49')
executable('my_prog', files('tabulate.f90', 'functions.f90'))

我们已经完成了,下一步是使用 meson setup build 配置我们的低级构建系统,你应该会看到类似于此的输出

The Meson build system
Version: 0.53.2
Source dir: /home/awvwgk/Examples
Build dir: /home/awvwgk/Examples/build
Build type: native build
Project name: my_proj
Project version: undefined
Fortran compiler for the host machine: gfortran (gcc 9.2.1 "GNU Fortran (Arch Linux 9.2.1+20200130-2) 9.2.1 20200130")
Fortran linker for the host machine: gfortran ld.bfd 2.34
Host machine cpu family: x86_64
Host machine cpu: x86_64
Build targets in project: 1

Found ninja-1.10.0 at /usr/bin/ninja

此时提供的信息已经比我们在 Makefile 中提供的任何信息更详细,让我们使用 ninja -C build 运行构建,它应该显示类似

[1/4] Compiling Fortran object 'my_prog@exe/functions.f90.o'.
[2/4] Dep hack
[3/4] Compiling Fortran object 'my_prog@exe/tabulate.f90.o'.
[4/4] Linking target my_prog.

build/my_prog 中查找并测试你的程序,以确保其正常工作。我们注意到 ninja 执行的步骤与我们在 Makefile(包括依赖项)中编码的步骤相同,但我们不必指定它们,再次查看你的 meson.build 文件:

project('my_proj', 'fortran', meson_version: '>=0.49')
executable('my_prog', files('tabulate.f90', 'functions.f90'))

我们只指定我们有一个 Fortran 项目(恰好需要特定版本的 meson 来支持 Fortran)并告诉 meson 从文件 tabulate.f90functions.f90 构建一个可执行的 my_prog。我们不必告诉 meson 如何构建项目,它自己解决了这个问题。

备注

meson 是一个跨平台的构建系统,你刚刚为你的程序指定的项目可用于为你的本地操作系统编译二进制文件或为其它平台交叉编译你的项目。同样,meson.build 文件是可移植的,也可以在不同的平台上工作。

meson 的文档可以在 meson-build 网页中找到。

创建 CMake 项目#

meson类似,CMake 也是一个高级构建系统,通常用于构建 Fortran 项目。

备注

CMake 遵循稍微不同的策略,并为你提供完整的编程语言来创建构建文件。这样做的好处是你几乎可以使用 CMake 完成所有操作,但你的 CMake 构建文件也可能变得与你正在构建的程序一样复杂。

首先创建包含内容的文件 CMakeLists.txt

cmake_minimum_required(VERSION 3.7)
project("my_proj" LANGUAGES "Fortran")
add_executable("my_prog" "tabulate.f90" "functions.f90")

meson 类似,我们已经完成了 CMake 构建文件。我们使用 cmake -B build -G Ninja 配置我们的低级构建文件,你应该会看到类似于此的输出

-- The Fortran compiler identification is GNU 10.2.0
-- Detecting Fortran compiler ABI info
-- Detecting Fortran compiler ABI info - done
-- Check for working Fortran compiler: /usr/bin/f95 - skipped
-- Checking whether /usr/bin/f95 supports Fortran 90
-- Checking whether /usr/bin/f95 supports Fortran 90 - yes
-- Configuring done
-- Generating done
-- Build files have been written to: /home/awvwgk/Examples/build

你可能会对 CMake 尝试使用编译器 f95 感到惊讶,幸运的是,这只是大多数系统上的 gfortran 的符号链接,而不是实际的 f95 编译器。为了给 CMake 一个更好的提示,你可以导出环境变量 FC=gfortran 重新运行现在应该显示正确的编译器名称

-- The Fortran compiler identification is GNU 10.2.0
-- Detecting Fortran compiler ABI info
-- Detecting Fortran compiler ABI info - done
-- Check for working Fortran compiler: /usr/bin/gfortran - skipped
-- Checking whether /usr/bin/gfortran supports Fortran 90
-- Checking whether /usr/bin/gfortran supports Fortran 90 - yes
-- Configuring done
-- Generating done
-- Build files have been written to: /home/awvwgk/Example/build

以类似的方式,您可以使用你的英特尔 Fortran 编译器来构建你的项目(设置 FC=ifort)。

CMake 提供了对几个低级构建文件的支持,因为默认是特定于平台的,我们将只使用 ninja,因为我们已经将它与 meson 一起使用。和以前一样,使用 ninja -C build 构建你的项目:

[1/6] Building Fortran preprocessed CMakeFiles/my_prog.dir/functions.f90-pp.f90
[2/6] Building Fortran preprocessed CMakeFiles/my_prog.dir/tabulate.f90-pp.f90
[3/6] Generating Fortran dyndep file CMakeFiles/my_prog.dir/Fortran.dd
[4/6] Building Fortran object CMakeFiles/my_prog.dir/functions.f90.o
[5/6] Building Fortran object CMakeFiles/my_prog.dir/tabulate.f90.o
[6/6] Linking Fortran executable my_prog

build/my_prog 中查找并测试你的程序,以确保其正常工作。 ninja 执行的步骤有些不同,因为通常有不止一种方法可以编写低级构建文件来完成构建项目的任务。幸运的是,我们不必关心自己,而是让我们的构建系统为我们处理这些细节。

最后,我们将简要回顾一下完整的 CMakeLists.txt 以指定我们的项目:

cmake_minimum_required(VERSION 3.7)
project("my_proj" LANGUAGES "Fortran")
add_executable("my_prog" "tabulate.f90" "functions.f90")

我们指定我们有一个 Fortran 项目,并告诉 CMake 从文件 tabulate.f90functions.f90 创建一个可执行的 my_prog。 CMake 知道如何从指定的源构建可执行文件的详细信息,因此我们不必担心构建过程中的实际步骤。

小技巧

CMake’s official reference can be found at the CMake webpage. It is organised in manpages, which are also available with your local CMake installation as well using man cmake. While it covers all functionality of CMake, it sometimes covers them only very briefly.

The SCons build system#

SCons is one another high-level cross-platform build system with automatic dependency analysis that supports Fortran projects. SCons configuration files are executed as Python scripts but it could be successfully used without knowledge about Python programming. The using of Python scripting if needed allows to handle build process and file naming in more sophisticated manner.

备注

SCons doesn’t automatically passes the external environment variables such as PATH therefore it will not find programs and tools that installed into non-standard locations unless are specified or passed via appropriate variables. That guaranties that build isn’t affected by external (especially user’s) environment variables and build is repeatable. The most of such variables and compiler options and flags are required to be configured within special “isolated” Environments (please refer to User Guide for additional information).

The SCons doesn’t use external low-level build systems and relies on own build system. The ninja support as external tool to generate ninja.build file is highly experimental (available since scons 4.2) and required to be enabled explicitly by additional configuration.

The simple SCons project is SConstruct file that contains:

Program('my_proj', ['tabulate.f90', 'functions.f90'])

The next step is to build our project with command scons:

scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
gfortran -o functions.o -c functions.f90
gfortran -o tabulate.o -c tabulate.f90
gfortran -o my_proj tabulate.o functions.o
scons: done building targets.

or with scons -Q to disable extended output:

gfortran -o functions.o -c functions.f90
gfortran -o tabulate.o -c tabulate.f90
gfortran -o my_proj tabulate.o functions.o

Find and test your program my_prog at the same directory as source files (by default) to ensure it works correctly.

To cleanup the build artifacts run scons -c (or scons -Qc):

scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Cleaning targets ...
Removed functions.o
Removed user_functions.mod
Removed tabulate.o
Removed my_proj
scons: done cleaning targets.

In our SCons SConstruct file

Program('my_proj', ['tabulate.f90', 'functions.f90'])

we specified executable target name my_proj (optional, if omitted the first source file name is used) and list of source files ['tabulate.f90', 'functions.f90']. There is no need to specify project source files language – it’s detected automatically by SCons for supported languages.

The list of source files could be specified with SCons Glob function:

Program('my_proj', Glob('*.f90'))

or using SCons Split function:

Program('my_proj', Split('tabulate.f90 functions.f90'))

or in more readable form by assigning variable:

src_files = Split('tabulate.f90 functions.f90')
Program('my_proj', src_files)

In case of Split function the multiple lines could be used with Python “triple-quote” syntax:

src_files = Split("""tabulate.f90
                     functions.f90""")
Program('my_proj', src_files)

小技巧

SCons official webpage provides: User Guide with extended description of various aspects how to handle the build process, Frequently Asked Questions page and man page.