一个make的介绍#
我们简要讨论了 make
的基础知识。本章给出了为大型项目扩展 make
的想法和策略。
在详细讨论make
之前,请考虑以下几点:
make
是一个 Unix 工具,在移植到非 Unix 平台时可能会给你带来困难。也就是说,还有不同风格的make
可用,并非所有都支持你想要使用的功能。虽然
make
让你可以完全控制构建过程,但这也意味着你对整个构建过程负责,并且你必须为项目的每个细节指定规则。你可能会发现自己花费大量时间编写和维护Makefile
,而不是开发源代码。你可以使用你的
Makefile
,但请考虑你项目中可能不熟悉make
的其他开发人员。你希望他们花多少时间学习你的Makefile
,他们是否能够调试或添加功能?纯
make
不会扩展。你将很快添加辅助程序来动态或静态生成你的Makefile
。这些引入了依赖关系和可能的错误来源。不应低估测试和记录这些工具所需的工作量。
如果你认为 make
适合你的需要,那么你可以开始编写你的 Makefile
。在本课程中,我们将使用包索引中的真实示例,这些示例(在撰写本文时)使用 make
以外的构建系统。本指南应该提供一个普遍推荐的编写 make
的风格,但也可以作为有用和有趣功能的演示。
小技巧
即使你发现 make
不适合构建你的项目,它也是自动化由文件定义的工作流的工具。也许你可以在不同的环境中利用它的力量。
开始#
对于这一部分,我们将使用 Fortran CSV 模块( v1.2.0)。我们的目标是编写一个 Makefile
来将此项目编译为一个静态库。首先克隆存储库
git clone https://github.com/jacobwilliams/fortran-csv-module -b 1.2.0
cd fortran-csv-module
小技巧
对于这一部分,我们将使用标签 1.2.0
中的代码,以使其尽可能可重现。随意使用最新版本或其它项目。
该项目使用 FoBiS 作为构建系统,你可以检查 build.sh
以获取与 FoBiS 一起使用的选项。我们即将为这个项目编写一个“Makefile”。首先,我们检查目录结构和源文件
.
├── build.sh
├── files
│ ├── test_2_columns.csv
│ └── test.csv
├── fortran-csv-module.md
├── LICENSE
├── README.md
└── src
├── csv_kinds.f90
├── csv_module.F90
├── csv_parameters.f90
├── csv_utilities.f90
└── tests
├── csv_read_test.f90
├── csv_test.f90
└── csv_write_test.f90
我们找到了七个不同的 Fortran 源文件; src
中的四个应该被编译并添加到一个静态库中,而 src/tests
中的三个包含依赖于这个静态库的单独程序。
首先创建一个简单的Makefile
:
# Disable the default rules
MAKEFLAGS += --no-builtin-rules --no-builtin-variables
# Project name
NAME := csv
# Configuration settings
FC := gfortran
AR := ar rcs
LD := $(FC)
RM := rm -f
# List of all source files
SRCS := src/csv_kinds.f90 \
src/csv_module.F90 \
src/csv_parameters.f90 \
src/csv_utilities.f90
TEST_SRCS := src/tests/csv_read_test.f90 \
src/tests/csv_test.f90 \
src/tests/csv_write_test.f90
# Create lists of the build artefacts in this project
OBJS := $(addsuffix .o, $(SRCS))
TEST_OBJS := $(addsuffix .o, $(TEST_SRCS))
LIB := $(patsubst %, lib%.a, $(NAME))
TEST_EXE := $(patsubst %.f90, %.exe, $(TEST_SRCS))
# Declare all public targets
.PHONY: all clean
all: $(LIB) $(TEST_EXE)
# Create the static library from the object files
$(LIB): $(OBJS)
$(AR) $@ $^
# Link the test executables
$(TEST_EXE): %.exe: %.f90.o $(LIB)
$(LD) -o $@ $^
# Create object files from Fortran source
$(OBJS) $(TEST_OBJS): %.o: %
$(FC) -c -o $@ $<
# Define all module interdependencies
csv_kinds.mod := src/csv_kinds.f90.o
csv_module.mod := src/csv_module.F90.o
csv_parameters.mod := src/csv_parameters.f90.o
csv_utilities.mod := src/csv_utilities.f90.o
src/csv_module.F90.o: $(csv_utilities.mod)
src/csv_module.F90.o: $(csv_kinds.mod)
src/csv_module.F90.o: $(csv_parameters.mod)
src/csv_parameters.f90.o: $(csv_kinds.mod)
src/csv_utilities.f90.o: $(csv_kinds.mod)
src/csv_utilities.f90.o: $(csv_parameters.mod)
src/tests/csv_read_test.f90.o: $(csv_module.mod)
src/tests/csv_test.f90.o: $(csv_module.mod)
src/tests/csv_write_test.f90.o: $(csv_module.mod)
# Cleanup, filter to avoid removing source code by accident
clean:
$(RM) $(filter %.o, $(OBJS) $(TEST_OBJS)) $(filter %.exe, $(TEST_EXE)) $(LIB) $(wildcard *.mod)
调用 make
应该按预期构建静态库和测试可执行文件:
gfortran -c -o src/csv_kinds.f90.o src/csv_kinds.f90
gfortran -c -o src/csv_parameters.f90.o src/csv_parameters.f90
gfortran -c -o src/csv_utilities.f90.o src/csv_utilities.f90
gfortran -c -o src/csv_module.F90.o src/csv_module.F90
ar rcs libcsv.a src/csv_kinds.f90.o src/csv_module.F90.o src/csv_parameters.f90.o src/csv_utilities.f90.o
gfortran -c -o src/tests/csv_read_test.f90.o src/tests/csv_read_test.f90
gfortran -o src/tests/csv_read_test.exe src/tests/csv_read_test.f90.o libcsv.a
gfortran -c -o src/tests/csv_test.f90.o src/tests/csv_test.f90
gfortran -o src/tests/csv_test.exe src/tests/csv_test.f90.o libcsv.a
gfortran -c -o src/tests/csv_write_test.f90.o src/tests/csv_write_test.f90
gfortran -o src/tests/csv_write_test.exe src/tests/csv_write_test.f90.o libcsv.a
有几点需要注意,make
构建通常会交织构建工件和源代码,除非你在实现构建目录方面付出了额外的努力。此外,现在源文件和依赖项是明确指定的,即使对于这样一个简单的项目,这也会导致多行。
自动生成的依赖#
Fortran 的 make
的主要缺点是缺少确定模块依赖关系的能力。这通常通过手动添加或使用外部工具自动扫描源代码来解决。一些编译器(如英特尔 Fortran 编译器)还提供生成 make
格式的依赖项。
在深入研究依赖生成之前,我们将概述对依赖问题进行稳健处理的概念。首先,我们想要一种可以独立处理所有源文件的方法,而每个源文件都提供(module
)或需要(use
)模块。生成依赖项时,只知道源文件和模块文件的名称,不需要有关目标文件名称的信息。
如果你检查上面的依赖项部分,你会注意到所有依赖项都是在目标文件而不是源文件之间定义的。为了改变这一点,我们可以从源文件和它们各自的目标文件中生成一个映射:
# Define a map from each file name to its object file
obj = $(src).o
$(foreach src, $(SRCS) $(TEST_SRCS), $(eval $(src) := $(obj)))
注意 obj
声明为递归扩展变量,我们有效地使用这种机制在 make
中定义一个函数。 foreach
函数允许我们遍历所有源文件,而 eval
函数允许我们生成 make
语句并针对 `Makefile[ X296X]。
我们相应地调整依赖关系,因为我们现在可以通过源文件名定义目标文件的名称:
# Define all module interdependencies
csv_kinds.mod := $(src/csv_kinds.f90)
csv_module.mod := $(src/csv_module.F90)
csv_parameters.mod := $(src/csv_parameters.f90)
csv_utilities.mod := $(src/csv_utilities.f90)
$(src/csv_module.F90): $(csv_utilities.mod)
$(src/csv_module.F90): $(csv_kinds.mod)
$(src/csv_module.F90): $(csv_parameters.mod)
$(src/csv_parameters.f90): $(csv_kinds.mod)
$(src/csv_utilities.f90): $(csv_kinds.mod)
$(src/csv_utilities.f90): $(csv_parameters.mod)
$(src/tests/csv_read_test.f90): $(csv_module.mod)
$(src/tests/csv_test.f90): $(csv_module.mod)
$(src/tests/csv_write_test.f90): $(csv_module.mod)
创建映射的相同策略已经用于模块文件,现在它也只是扩展到目标文件。
为了自动生成相应的依赖关系图,我们将在这里使用 awk
脚本
#!/usr/bin/awk -f
BEGIN {
# Fortran is case insensitive, disable case sensitivity for matching
IGNORECASE = 1
}
# Match a module statement
# - the first argument ($1) should be the whole word module
# - the second argument ($2) should be a valid module name
$1 ~ /^module$/ &&
$2 ~ /^[a-zA-Z][a-zA-Z0-9_]*$/ {
# count module names per file to avoid having modules twice in our list
if (modc[FILENAME,$2]++ == 0) {
# add to the module list, the generated module name is expected
# to be lowercase, the FILENAME is the current source file
mod[++im] = sprintf("%s.mod = $(%s)", tolower($2), FILENAME)
}
}
# Match a use statement
# - the first argument ($1) should be the whole word use
# - the second argument ($2) should be a valid module name
$1 ~ /^use$/ &&
$2 ~ /^[a-zA-Z][a-zA-Z0-9_]*,?$/ {
# Remove a trailing comma from an optional only statement
gsub(/,/, "", $2)
# count used module names per file to avoid using modules twice in our list
if (usec[FILENAME,$2]++ == 0) {
# add to the used modules, the generated module name is expected
# to be lowercase, the FILENAME is the current source file
use[++iu] = sprintf("$(%s) += $(%s.mod)", FILENAME, tolower($2))
}
}
# Match an include statement
# - the first argument ($1) should be the whole word include
# - the second argument ($2) can be everything, as long as delimited by quotes
$1 ~ /^(#:?)?include$/ &&
$2 ~ /^["'].+["']$/ {
# Remove quotes from the included file name
gsub(/'|"/, "", $2)
# count included files per file to avoid having duplicates in our list
if (incc[FILENAME,$2]++ == 0) {
# Add the included file to our list, this might be case-sensitive
inc[++ii] = sprintf("$(%s) += %s", FILENAME, $2)
}
}
# Finally, produce the output for make, loop over all modules, use statements
# and include statements, empty lists are ignored in awk
END {
for (i in mod) print mod[i]
for (i in use) print use[i]
for (i in inc) print inc[i]
}
这个脚本对它解析的源代码做了一些假设,所以它不适用于所有的 Fortran 代码(即不支持子模块),但对于这个例子来说就足够了。
小技巧
使用awk
上面的脚本使用了 awk
语言,该语言是为文本流处理而设计的,使用了类似 C 的语法。在 awk
中,你可以定义在某些事件上评估的组,_ 例如当行匹配特定模式时的 _,通常由 正则表达式 表示。
这个 awk
脚本定义了五个组,其中两个使用特殊模式 BEGIN
和 END
分别在脚本开始之前和脚本结束之后运行。在脚本开始之前,我们使脚本不区分大小写,因为我们在这里处理 Fortran 源代码。我们还使用特殊变量 FILENAME
来确定我们当前正在解析哪个文件,并允许一次处理多个文件。
定义了三种模式后,我们正在寻找 module
、use
和 include
语句作为第一个以空格分隔的条目。使用使用的模式,并非所有有效的 Fortran 代码都会被正确解析。一个失败的例子是:
use::my_module,only:proc
为了让 awk
脚本可以解析这个,我们可以在 BEGIN
组之后直接添加另一个组,在处理流的同时修改它
{
gsub(/,|:/, " ")
}
理论上,你需要一个完整的 Fortran 解析器来处理续行和其它困难。这可能可以在 awk
中实现,但最终需要一个巨大的脚本。
另外,请记住,生成依赖项应该很快,在为大型代码库生成依赖项时,昂贵的解析器会产生很大的开销。做出合理的假设可以简化和加快这一步,但也会在构建工具中引入错误源。
使脚本可执行 (chmod +x gen-deps.awk
) 并使用 `./gen-deps.awk $(find src -name ‘*.[fF]90’)[ X114X]。你应该看到这样的输出:
csv_utilities.mod = $(src/csv_utilities.f90)
csv_kinds.mod = $(src/csv_kinds.f90)
csv_parameters.mod = $(src/csv_parameters.f90)
csv_module.mod = $(src/csv_module.F90)
$(src/csv_utilities.f90) += $(csv_kinds.mod)
$(src/csv_utilities.f90) += $(csv_parameters.mod)
$(src/csv_kinds.f90) += $(iso_fortran_env.mod)
$(src/tests/csv_read_test.f90) += $(csv_module.mod)
$(src/tests/csv_read_test.f90) += $(iso_fortran_env.mod)
$(src/tests/csv_write_test.f90) += $(csv_module.mod)
$(src/tests/csv_write_test.f90) += $(iso_fortran_env.mod)
$(src/tests/csv_test.f90) += $(csv_module.mod)
$(src/tests/csv_test.f90) += $(iso_fortran_env.mod)
$(src/csv_parameters.f90) += $(csv_kinds.mod)
$(src/csv_module.F90) += $(csv_utilities.mod)
$(src/csv_module.F90) += $(csv_kinds.mod)
$(src/csv_module.F90) += $(csv_parameters.mod)
$(src/csv_module.F90) += $(iso_fortran_env.mod)
请注意,脚本输出将使用递归扩展变量并且尚未定义任何依赖项,因为可能需要无序声明变量,并且我们不想意外创建任何目标。你可以验证是否存在与上述手写片段中相同的信息。唯一的例外是对 iso_fortran_env.mod
的额外依赖,因为它是一个未定义的变量,它只会扩展为一个空字符串并且不会引入任何进一步的依赖。
现在,你终于可以在你的 Makefile
中包含这个片段来自动生成依赖了:
# Disable the default rules
MAKEFLAGS += --no-builtin-rules --no-builtin-variables
# Project name
NAME := csv
# Configuration settings
FC := gfortran
AR := ar rcs
LD := $(FC)
RM := rm -f
GD := ./gen-deps.awk
# List of all source files
SRCS := src/csv_kinds.f90 \
src/csv_module.F90 \
src/csv_parameters.f90 \
src/csv_utilities.f90
TEST_SRCS := src/tests/csv_read_test.f90 \
src/tests/csv_test.f90 \
src/tests/csv_write_test.f90
# Add source and tests directories to search paths
vpath % .: src
vpath % .: src/tests
# Define a map from each file name to its object file
obj = $(src).o
$(foreach src, $(SRCS) $(TEST_SRCS), $(eval $(src) := $(obj)))
# Create lists of the build artefacts in this project
OBJS := $(addsuffix .o, $(SRCS))
DEPS := $(addsuffix .d, $(SRCS))
TEST_OBJS := $(addsuffix .o, $(TEST_SRCS))
TEST_DEPS := $(addsuffix .d, $(TEST_SRCS))
LIB := $(patsubst %, lib%.a, $(NAME))
TEST_EXE := $(patsubst %.f90, %.exe, $(TEST_SRCS))
# Declare all public targets
.PHONY: all clean
all: $(LIB) $(TEST_EXE)
# Create the static library from the object files
$(LIB): $(OBJS)
$(AR) $@ $^
# Link the test executables
$(TEST_EXE): %.exe: %.f90.o $(LIB)
$(LD) -o $@ $^
# Create object files from Fortran source
$(OBJS) $(TEST_OBJS): %.o: % | %.d
$(FC) -c -o $@ $<
# Process the Fortran source for module dependencies
$(DEPS) $(TEST_DEPS): %.d: %
$(GD) $< > $@
# Define all module interdependencies
include $(DEPS) $(TEST_DEPS)
$(foreach dep, $(OBJS) $(TEST_OBJS), $(eval $(dep): $($(dep))))
# Cleanup, filter to avoid removing source code by accident
clean:
$(RM) $(filter %.o, $(OBJS) $(TEST_OBJS)) $(filter %.d, $(DEPS) $(TEST_DEPS)) $(filter %.exe, $(TEST_EXE)) $(LIB) $(wildcard *.mod)
这里为每个源文件单独生成额外的依赖文件,然后包含在主 Makefile
中。此外,依赖文件作为对对象文件的依赖被添加,以确保在编译对象之前生成它们。依赖关系中的管道字符定义了没有时间戳依赖关系的规则顺序,因为在重新生成依赖关系并且可能未更改的情况下,不需要重新编译目标文件。
同样,我们使用 eval
函数在所有目标文件的 foreach
循环中生成依赖关系。请注意,我们在依赖文件中的目标文件之间创建了一个映射,展开 dep
一次会产生目标文件名,再次展开它会产生它所依赖的目标文件。
使用 make
构建项目应该会给出类似于
./gen-deps.awk src/csv_utilities.f90 > src/csv_utilities.f90.d
./gen-deps.awk src/csv_parameters.f90 > src/csv_parameters.f90.d
./gen-deps.awk src/csv_module.F90 > src/csv_module.F90.d
./gen-deps.awk src/csv_kinds.f90 > src/csv_kinds.f90.d
gfortran -c -o src/csv_kinds.f90.o src/csv_kinds.f90
gfortran -c -o src/csv_parameters.f90.o src/csv_parameters.f90
gfortran -c -o src/csv_utilities.f90.o src/csv_utilities.f90
gfortran -c -o src/csv_module.F90.o src/csv_module.F90
ar rcs libcsv.a src/csv_kinds.f90.o src/csv_module.F90.o src/csv_parameters.f90.o src/csv_utilities.f90.o
./gen-deps.awk src/tests/csv_read_test.f90 > src/tests/csv_read_test.f90.d
gfortran -c -o src/tests/csv_read_test.f90.o src/tests/csv_read_test.f90
gfortran -o src/tests/csv_read_test.exe src/tests/csv_read_test.f90.o libcsv.a
./gen-deps.awk src/tests/csv_test.f90 > src/tests/csv_test.f90.d
gfortran -c -o src/tests/csv_test.f90.o src/tests/csv_test.f90
gfortran -o src/tests/csv_test.exe src/tests/csv_test.f90.o libcsv.a
./gen-deps.awk src/tests/csv_write_test.f90 > src/tests/csv_write_test.f90.d
gfortran -c -o src/tests/csv_write_test.f90.o src/tests/csv_write_test.f90
gfortran -o src/tests/csv_write_test.exe src/tests/csv_write_test.f90.o libcsv.a
一旦生成了依赖文件,make
只会在源代码更改时更新它们,并且不需要为每次调用再次重建它们。
小技巧
通过正确的依赖关系,你可以利用 Makefile
的并行执行,只需使用 -j
标志来创建多个 make
进程。
由于现在可以自动生成依赖项,因此无需显式指定源文件,可以使用 wildcard
函数动态确定它们:
# List of all source files
SRCS := $(wildcard src/*.f90) \
$(wildcard src/*.F90)
TEST_SRCS := $(wildcard src/tests/*.f90)