一个make的介绍#

我们简要讨论了 make 的基础知识。本章给出了为大型项目扩展 make 的想法和策略。

在详细讨论make之前,请考虑以下几点:

  1. make 是一个 Unix 工具,在移植到非 Unix 平台时可能会给你带来困难。也就是说,还有不同风格的 make 可用,并非所有都支持你想要使用的功能。

  2. 虽然 make 让你可以完全控制构建过程,但这也意味着你对整个构建过程负责,并且你必须为项目的每个细节指定规则。你可能会发现自己花费大量时间编写和维护 Makefile,而不是开发源代码。

  3. 你可以使用你的 Makefile,但请考虑你项目中可能不熟悉 make 的其他开发人员。你希望他们花多少时间学习你的 Makefile,他们是否能够调试或添加功能?

  4. 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 脚本定义了五个组,其中两个使用特殊模式 BEGINEND 分别在脚本开始之前和脚本结束之后运行。在脚本开始之前,我们使脚本不区分大小写,因为我们在这里处理 Fortran 源代码。我们还使用特殊变量 FILENAME 来确定我们当前正在解析哪个文件,并允许一次处理多个文件。

定义了三种模式后,我们正在寻找 moduleuseinclude 语句作为第一个以空格分隔的条目。使用使用的模式,并非所有有效的 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)