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 的主要缺点是缺少确定模块依赖项的功能。这通常通过手动添加或使用外部工具自动扫描源代码来解决。一些编译器(如 Intel 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 评估它们。

我们相应地调整了依赖项,因为我们现在可以通过源文件名定义对象文件的名称

# 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')进行测试。你应该看到如下输出

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)