构建工具#
手动编译您的 Fortran 项目可能会变得相当复杂,具体取决于源文件的数量以及模块之间的相互依赖关系。除非使用正确的工具来自动执行这些任务,否则支持不同的编译器和链接器或不同的平台可能会变得越来越复杂。
根据项目的大小和项目的用途,可以使用不同的构建自动化选项。
首先,您的集成开发环境可能提供了一种构建程序的方法。一个流行的跨平台工具是微软的 Visual Studio Code,但还有其他工具存在,例如 Atom、Eclipse Photran 和 Code::Blocks。它们提供了一个图形用户界面,但通常对编译器和平台非常特定。
对于较小的项目,基于规则的构建系统 make
是一个常见的选择。根据定义的规则,它可以执行诸如从更新的源文件(重新)编译目标文件、创建库和链接可执行文件等任务。要将 make
用于您的项目,您必须在 Makefile
中编码这些规则,该规则定义了所有最终程序、中间目标文件或库以及实际源文件的相互依赖关系。有关简要介绍,请参阅 有关 make
的指南。
像 autotools 和 CMake 这样的维护工具可以通过高级描述生成 Makefile 或 Visual Studio 项目文件。它们抽象了编译器和平台的细节。
哪些工具最适合您的项目取决于许多因素。选择一个您使用起来舒适的构建工具,它不应该在您开发时妨碍您。在使用构建工具上花费比进行实际开发工作更多的时间很快就会让人感到沮丧。
此外,请考虑构建工具的可访问性。如果它仅限于特定的集成开发环境,项目中的所有开发人员都可以访问它吗?如果您使用的是特定的构建系统,它是否可以在您开发的所有平台上运行?您的构建工具的入门门槛有多高?考虑构建工具的学习曲线,完美的构建工具毫无用处,如果您必须先学习一门复杂的编程语言才能添加一个新的源文件。最后,考虑其他项目正在使用什么,您依赖的项目以及那些使用(或将使用)您的项目作为依赖项的项目。
使用 make 作为构建工具#
最著名且最常用的构建系统称为 make
。它根据名为 Makefile
或 makefile
的配置文件中定义的规则执行操作,这通常会导致从提供的源代码编译程序。
提示
有关深入的 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
也支持使用 =
的递归展开变量。通常,需要第一种声明方式,因为它们更可预测并且没有递归展开带来的运行时开销。
我们引入了规则 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.o
和 tabulate.f90
之间的通用模式,它将我们的目标文件 tabulate.o
连接到源文件 tabulate.f90
。有了这套规则,我们运行我们的编译器,这里使用 gfortran
并将源文件转换为目标文件,由于 -c
标志,我们尚未创建可执行文件。请注意此处 $<
用于依赖项的第一个元素。
编译完所有目标文件后,我们尝试链接程序,我们不直接使用链接器,而是使用 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
您现在应该在目录中找到四个新文件。运行 my_prog
以确保一切按预期工作。让我们再次运行 make
make: Nothing to be done for 'all'.
利用可执行文件 make
能够确定的时间戳,可以判断它比 tabulate.o
和 functions.o
都新,而后两者又比 tabulate.f90
和 functions.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 标记将完全正常工作
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
视为一整套规则,它必须在任何规则可以评估之前完全解析。
您可以使用任何您最喜欢的变量类型,当然,混合使用时应谨慎操作。了解两种变量类型之间的区别及其各自的影响非常重要。
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.f90
和 functions.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
类似地,你可以使用你的 Intel 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.f90
和 functions.f90
创建一个可执行文件 my_prog
。CMake 知道如何从指定源代码构建可执行文件的细节,因此我们不必担心构建过程中的实际步骤。
提示
CMake 的官方参考可以在 CMake 网页 上找到。它以手册页的形式组织,这些手册页也可以通过你本地 CMake 安装获得,使用 man cmake
。虽然它涵盖了 CMake 的所有功能,但有时只是非常简要地介绍。
SCons 构建系统#
SCons 是另一个高级跨平台构建系统,具有自动依赖分析功能,支持 Fortran 项目。SCons 配置文件作为 Python 脚本执行,但即使不了解 Python 编程也能成功使用。如果需要,使用 Python 脚本可以更复杂地处理构建过程和文件命名。
注意
SCons 不会自动传递外部环境变量,例如 PATH
,因此它不会找到安装在非标准位置的程序和工具,除非指定或通过适当的变量传递。这保证了构建不受外部(尤其是用户的)环境变量的影响,并且构建是可重复的。大多数此类变量以及编译器选项和标志需要在特殊的“隔离” Environments
中配置(有关更多信息,请参阅 用户指南)。
SCons 不使用外部低级构建系统,而是依赖于自己的构建系统。 ninja
作为外部工具来生成 ninja.build
文件的支持是高度实验性的(从 scons 4.2 开始可用),并且需要通过额外的配置显式启用。
简单的 SCons 项目是包含以下内容的 SConstruct
文件:
Program('my_proj', ['tabulate.f90', 'functions.f90'])
下一步是使用命令 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.
或者使用 scons -Q
禁用扩展输出。
gfortran -o functions.o -c functions.f90
gfortran -o tabulate.o -c tabulate.f90
gfortran -o my_proj tabulate.o functions.o
在与源文件相同的目录(默认情况下)中查找并测试你的程序 my_prog
,以确保它能正常工作。
要清理构建工件,请运行 scons -c
(或 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.
在我们的 SCons SConstruct
文件中
Program('my_proj', ['tabulate.f90', 'functions.f90'])
我们指定了可执行目标名称 my_proj
(可选,如果省略则使用第一个源文件名)和源文件列表 ['tabulate.f90', 'functions.f90']
。无需指定项目源文件的语言——SCons 会自动为支持的语言检测。
源文件列表可以使用 SCons Glob
函数指定
Program('my_proj', Glob('*.f90'))
或使用 SCons Split
函数
Program('my_proj', Split('tabulate.f90 functions.f90'))
或者通过分配变量以更易读的形式指定
src_files = Split('tabulate.f90 functions.f90')
Program('my_proj', src_files)
对于 Split
函数,可以使用 Python 的“三引号”语法使用多行。
src_files = Split("""tabulate.f90
functions.f90""")
Program('my_proj', src_files)
注释和空格#
使用
make
时,空格和注释可能会出现一些需要注意的地方。首先,make
除了字符串之外不知道任何数据类型,默认分隔符只是一个空格。这意味着make
在尝试构建文件名中包含空格的项目时会遇到困难。如果您遇到这种情况,重命名文件可能是最简单的解决方案。另一个常见问题是前导和尾随空格,一旦引入,
make
将愉快地将其携带,并且在make
中比较字符串时,它确实会产生影响。这些可以通过以下注释引入:
虽然
make
会正确删除注释,但现在尾随的两个空格已成为变量内容的一部分。运行make
并检查这是否确实如此要解决此问题,您可以移动注释,或者使用
strip
函数去除空格。或者,您可以尝试join
字符串。总而言之,这些解决方案都不会使您的
Makefile
更具可读性,因此,在编写和使用make
时,谨慎注意空格和注释是明智之举。