GNU Make 项目管理 第四章 函数

创建时间 2021-04-18
更新时间 2021-04-22

GNU make 支持内置函数和自定义函数。函数调用看起来很像变量引用,但是包含用一个或多个用逗号分隔的参数。大多数内置函数扩展为某个值,然后将该值赋给变量或传递给子 shell。自定义的函数存储在变量或宏中,并期望调用者传递一个或多个参数。

自定义函数

将命令序列存储在变量中为广泛的应用打开了大门。例如,这里有一个用来终止进程的宏:

你可能会问:“为什么要在 makefile 中做这个?”,好吧,在 Windows 上,打开一个文件会锁定它,防止其他进程写入。在我写这本书的时候,PDF 文件经常会被 Acrobat 阅读器锁定,从而阻止我的 makefile 更新 PDF。因此,我在几个目标中添加了这个命令,以便在尝试更新锁定的文件之前终止 Acrobat 阅读器。

AWK := awk
KILL := kill

# $(kill-acroread)
define kill-acroread
    @ ps -W |                                       \
    $(AWK) 'BEGIN { FIELDWIDTHS = "9 47 100" }      \
            /AcroRd32/ {                            \
                        print "Killing " $$3;       \
                        system( "$(KILL) -f " $$1 ) \
                        }'
endef

(这个宏被显式地编写来使用 Cygwin 工具,所以我们搜索的程序名以及 ps 和 kill 选项不是标准的 Unix。) 为了终止一个进程,我们将 ps 的输出通过管道输送到awk。awk 脚本根据其 Windows 程序名查找 Acrobat 阅读器,并在进程运行时杀死该进程。我们使用 FIELDWIDTHS 特性将程序名及其所有参数视为一个字段。这将正确打印出完整的程序名称和参数,即使它包含空格。awk 中的字段引用被写成 $1$2 等形式。如果我们不以某种方式引用它们,它们将被视为 make 变量。我们可以告诉 make 将 $n 的引用传递给 awk,而不是扩展它自己,方法是将 $n 中的 $ 符号转义为额外的 $$n。make 将看到双美元符号,将其折叠为单个美元符号并将其传递给子 shell。

Cygwin 工具是许多标准 GNU 和 Linux 程序到 Windows 的一个移植。它包括编译器套件、X11R6、ssh,甚至 inetd。该端口依赖于一个兼容库,该库根据Win32 API 函数实现 Unix 系统调用。这是一个令人难以置信的工程壮举,我强烈推荐它。请从 http://www.cygwin.com 下载。

真漂亮。如果我们想要经常使用 define 指令,它可以让我们避免重复代码。但它并不完美。如果我们想要终止除 Acrobat 阅读器之外的进程,该怎么办?我们需要定义另一个宏并复制脚本吗?不!

变量和宏可以被传递参数,这样每个展开都可以是不同的。宏的参数在宏定义体中用 $1$2 等引用。要参数化 kill-acroread 函数,只需要添加一个搜索参数:

AWK := awk
KILL := kill
KILL_FLAGS := -f
PS := ps
PS_FLAGS := -W
PS_FIELDS := "9 47 100"

# $(call kill-program,awk-pattern)
define kill-program
    @ $(PS) $(PS_FLAGS) |                           \
    $(AWK) 'BEGIN { FIELDWIDTHS = $(PS_FIELDS) }    \
            /$1/ { \
                print "Killing " $$3; \
                system( "$(KILL) $(KILL_FLAGS) " $$1 ) \
                }'
endef

我们用参数引用 $1 替换了 awk 搜索模式 /AcroRd32/。注意宏参数 $1awk 字段引用 $$1 之间的细微区别。记住哪个程序是变量引用的目标接受者是非常重要的。只要我们在改进这个函数,我们还对它进行了适当的重命名,并用变量替换了特定 Cygwin 的硬编码值。现在我们有了一个用于终止进程的可移植宏。

让我们来看看它的实际应用:

FOP := org.apache.fop.apps.Fop
FOP_FLAGS := -q
FOP_OUTPUT := > /dev/null
%.pdf: %.fo
    $(call kill-program,AcroRd32)
    $(JAVA) $(FOP) $(FOP_FLAGS) $< $@ $(FOP_OUTPUT)

该模式规则杀死正在运行的 Acrobat 进程,然后通过调用 Fop 处理器 (http://xml.apache.org/fop) 将 fo (格式化对象) 文件转换为 pdf 文件。展开变量或宏的语法是:

$(call macro-name[, param1...])

call是一个内置的 make 函数,它将展开它的第一个参数,并用它给出的其余参数替换出现的 $1$2等。(事实上,在控制权转移的意义上,它根本没有真正 “调用” 它的宏参数,而是执行一种特殊的宏扩展。) 宏名称是任何宏或变量的名称 (记住,宏只是允许嵌入换行符的变量)。宏或变量值甚至不必包含 $n 引用,但是使用 call 也没有什么意义。宏名称后面的宏的参数用逗号分隔。

注意,要调用的第一个参数是一个未展开的变量名(也就是说,它不是以 $ 开头)。这是相当不寻常的。只有另一个内置函数 origin 接受未展开的变量。如果将要调用的第一个参数用 $() 括起来,则该参数将作为变量展开,并将其值传递给 call。

使用 call 进行参数检查的方式非常少。可以调用任意数量的参数。如果一个宏引用了一个参数 $n,而在调用实例中没有相应的参数,那么这个变量就会压缩为空。如果在调用实例中有比 $n 引用更多的参数,那么额外的参数永远不会在宏中展开。

如果你从一个宏中调用另一个宏,你应该注意 make 3.80 中有一些奇怪的行为。在展开过程中,call 函数将参数定义为普通 make 变量。因此,如果一个宏调用另一个宏,父宏的参数可能会在子宏的扩展中可见:

define parent
    echo "parent has two parameters: $1, $2"
    $(call child,$1)
endef

define child
    echo "child has one parameter: $1"
    echo "but child can also see parent's second parameter: $2!"
endef

scoping_issue:
    @$(call parent,one,two)

当运行时,我们看到宏实现有一个范围问题。

$ make
parent has two parameters: one, two
child has one parameter: one
but child can also see parent's second parameter: two!

这个问题已经在 3.81 中解决了,因此 $2 在 子宏中压缩为空。

在本书的其余部分中,我们将花更多的时间讨论自定义函数,但在进入真正有趣的内容之前,我们需要更多的背景知识!

内建函数

一旦您开始使用 make 变量而不仅仅是简单的常量,您会发现您想要以越来越复杂的方式操作变量及其内容。嗯,你可以。GNU make 有几十个内置函数用于处理变量及其内容。这些函数可以分为几个大类:字符串操作、文件名操作、流程控制、自定义函数和一些(重要的)杂项函数。

但首先,我们来了解一下函数语法。所有函数的形式为:

$(function-name arg1[, argn])

$( 后面跟着内置函数名,然后是函数的参数。前导空格将会在第一个参数中删除,但是所有后续参数都包括前导(当然还有内嵌的和后面的)空格。

函数参数用逗号分隔,只有一个参数的函数可以不用逗号,有两个参数的函数用一个逗号,等等。许多函数接受单个参数,将其视为由空格分隔的单词组成的列表。对于这些函数,单词之间的空格被视为单个单词的分隔符,否则将被忽略。

我喜欢空格。它使代码更易于阅读和维护。所以我将使用空格,只要我可以用。然而,有时候,参数列表或变量定义中的空格会干扰代码的正常运行。当这种情况发生时,您别无选择,只能删除有问题的空格。在本章前面的一个例子中,我们已经看到了在 grep 命令的搜索模式中意外插入了尾随空格。随着更多示例的继续,我们将指出出现空格问题的地方。

许多函数接受模式作为参数。该模式使用的语法与模式规则中使用的语法相同(参见第二章的“模式规则”一节)。一个模式包含单个 % 和开头或结尾字符(或两者都包含)。字符 % 表示零个或多个任意类型的字符。要匹配目标字符串,模式必须匹配整个字符串,而不仅仅是字符串中的一个子集。我们将用一个示例来说明这一点。% 字符在模式中是可选的,通常在适当的时候被省略。

字符串函数

大多数 make 的内置函数以两种形式操作文本,但某些函数对字符串操作的功能特别强,这里将讨论这些函数。

make 中常见的字符串操作是从列表中选择一组文件。这就是 grep 在 shell 脚本中通常使用的功能。在 make 中,我们有 filterfilter-outfindstring 函数。


$(filter pattern...,text)

filter 函数将文本视为由空格分隔的单词组成的序列,并返回匹配 pattern 的这些单词的列表。例如,要构建用户界面代码的存档,我们可能希望只选择 ui 子目录中的目标文件。在下面的例子中,我们从文件名列表中提取了以 ui/ 开头、以 .o 结尾的文件名。% 字符匹配任意数量的字符:

$(ui_library): $(filter ui/%.o,$(objects))
    $(AR) $(ARFLAGS) $@ $^

filter 也可以接受用空格分隔的多个模式。如上所述,模式必须匹配要包含在输出列表中的单词的整个单词。所以,例如:

words := he the hen other the%
get-the:
    @echo he matches: $(filter he, $(words))
    @echo %he matches: $(filter %he, $(words))
    @echo he% matches: $(filter he%, $(words))
    @echo %he% matches: $(filter %he%, $(words))

当执行时,makefile 生成输出:

$ make
he matches: he
%he matches: he the
he% matches: he hen
%he% matches: the%

如您所见,第一个模式只匹配单词 he,因为模式必须匹配整个单词,而不仅仅是它的一部分。其他模式匹配恰当位置的 he 加上包含 he 的单词。

一个模式只能包含一个 %。如果模式中包含额外的 % 字符,那么除第一个字符外的所有字符都被视为文字字符。

filter 不能匹配单词内的子字符串或接受多个通配符,这看起来有点奇怪。您会发现有些时候非常想念这个功能。但是,您可以使用循环和条件测试来实现类似的功能。我们稍后会告诉你怎么做。


$(filter-out pattern...,text)

filter-out 函数的功能与 filter 相反,它选择与模式不匹配的每个单词。这里我们选择了所有非 C 头文件。

all_source := count_words.c counter.c lexer.l counter.h lexer.h
to_compile := $(filter-out %.h, $(all_source))

$(findstring string,text)

这个函数在文本中查找字符串。如果找到了字符串,函数将返回字符串,否则,它将不返回任何内容。

起初,这个函数可能看起来像我们认为 filter 可能是的子字符串搜索 grep 函数,但事实并非如此。首先,也是最重要的一点,这个函数只返回搜索字符串,而不是它找到的包含搜索字符串的单词。其次,搜索字符串不包含通配符(换句话说,搜索字符串中的 % 字符按字面意思匹配)。

这个函数与后面讨论的 if 函数一起非常有用。但是,在一种情况下,我发现 findstring 本身就很有用。

假设您有几个具有并行结构的树,例如参考源文件、沙箱源文件、调试二进制文件和优化二进制文件。您希望能够从当前目录(没有来自根的当前相对路径)找到您所在的树。以下是一些框架代码来确定这一点:

find-tree:
    # PWD = $(PWD)
    # $(findstring /test/book/admin,$(PWD))
    # $(findstring /test/book/bin,$(PWD))
    # $(findstring /test/book/dblite_0.5,$(PWD))
    # $(findstring /test/book/examples,$(PWD))
    # $(findstring /test/book/out,$(PWD))
    # $(findstring /test/book/text,$(PWD))

(每一行都以一个制表符和 shell 注释字符开始,所以每一行都像其他命令一样在自己的子 shell中“执行”。Bourne Again Shellbash 和许多其他类似 Bourne 的 Shell 简单地忽略了这些行。这是一种比键入 @echo 更方便的打印简单 make 结构的扩展的方法。您可以使用更可移植的 : shell 操作符来实现几乎相同的效果,但 : 操作符执行重定向。因此,包含 > word 的字符串会创建文件 word,这是一个副作用。)当运行时,它会产生:

$ make
# PWD = /test/book/out/ch03-findstring-1
#
#
#
#
# /test/book/out
#

可以看到,在测试父目录之前,每个针对 $(PWD) 的测试都返回 null。然后返回父目录本身。如图所示,代码只是作为 findstring 的演示。这可以用来编写一个返回当前树的根目录的函数。


有两个搜索和替换函数:

$(subst search-string,replace-string,text)

这是一个简单的、非通配符的搜索和替换。它最常见的用途之一是在文件名列表中用另一个后缀替换:

sources := count_words.c counter.c lexer.c
objects := $(subst .c,.o,$(sources))

这将把所有出现的 .c 替换为 .o,在 $(sources) 中的任何位置,或者更普遍地说,包含替换字符串的所有搜索字符串。

这个例子是一个常见的例子,说明了在函数调用参数中空格的重要性。注意,逗号后面没有空格。如果我们写成:

sources := count_words.c counter.c lexer.c
objects := $(subst .c, .o, $(sources))

(注意每个逗号后面的空格),$(objects) 的值应该是:

count_words .o counter .o lexer .o

根本不是我们想要的。问题是 .o 参数之前的空格是替换文本的一部分,并被插入到输出字符串中。在 .c 之前的空格是可以的,因为在第一个参数之前的所有空格都会被 make 删除。事实上,$(source) 前面的空格可能也是良性的,因为 $(objects) 很可能被用作一个简单的命令行参数,其中前面的空格不是问题。然而,我永远不会在一个函数调用中在逗号后面添加空格,即使它产生了正确的结果:

# Yech, the spacing in this call is too subtle.
objects := $(subst .c,.o, $(source))

注意 subst 不理解文件名或文件后缀,只理解字符串。如果我的一个源文件内部包含一个 .c,它也将被替换。例如,文件名 car.cdr.c 将被转换为 car.odr.o。可能不是我们想要的。

在第二章的“自动依赖生成”一节中,我们讨论了依赖生成。该部分的最后一个 makefile 示例是这样使用 subst 的:

VPATH = src include
CPPFLAGS = -I include
SOURCES =   count_words.c   \
            lexer.c         \
            counter.c

count_words: counter.o lexer.o -lfl
count_words.o: counter.h
counter.o: counter.h lexer.h
lexer.o: lexer.h

include $(subst .c,.d,$(SOURCES))

%.d: %.c
    $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
    sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
    rm -f $@.$$$$

subst 函数用于将源文件列表转换为依赖文件列表。由于依赖文件作为要 include 的参数出现,因此它们被认为是依赖,并使用规则 %.d 进行更新。


$(patsubst search-pattern,replace-pattern,text)

这是搜索和替换的通配符版本。通常,模式可以包含单个 %。替换模式中的 % 将由匹配的文本展开。重要的是要记住搜索模式必须匹配文本的全部。例如,下面将删除文本中的尾斜杠,而不是所有的斜杠:

strip-trailing-slash = $(patsubst %/,%,$(directory-path))

替换引用是执行相同替换的可移植方法。替换引用的语法是:

$(variable:search=replace)

search 的文本可以是一个简单的字符串,在这种情况下,只要字符串出现在单词末尾,它就会被替换。也就是说,只要它后面有空格或变量值的末尾。此外,search 可以包含一个表示通配符的 %,在这种情况下,搜索和替换遵循 patsubst 的规则。与 patsubst 相比,我发现这个语法晦涩难懂,难以阅读。


正如我们所看到的,变量通常包含单词列表。下面是一些函数,用于从列表中选择单词,计算列表的长度等。与所有 make 函数一样,单词由空格分隔。

$(words text)

这个函数返回文本中的单词数。

CURRENT_PATH := $(subst /, ,$(HOME))
words:
    @echo My HOME path has $(words $(CURRENT_PATH)) directories.

这个函数有很多用途,我们很快就会看到,但是为了有效地使用它,我们还需要介绍更多的函数。


$(word n,text)

返回文本中的第 n 个单词。第一个单词编号为 1。如果 n 大于文本中的单词数,则函数返回值为空。

version_list := $(subst ., ,$(MAKE_VERSION))
minor_version := $(word 2, $(version_list))

变量 MAKE_VERSION 是一个内置变量。(见第3章“标准制作变量”一节。)

你总是可以用下面的语句得到列表中的最后一个单词:

current := $(word $(words $(MAKEFILE_LIST)), $(MAKEFILE_LIST))

这将返回最近读取的 makefile 的名称。


$(firstword text)

这将返回文本中的第一个单词。这相当于$(word 1,text)。

version_list := $(subst ., ,$(MAKE_VERSION))
major_version := $(firstword $(version_list))

$(wordlist start,end,text)

这将返回从开始到结束的文本中的单词,包括。与 word 函数一样,第一个单词编号为 1。如果 start 大于单词数,则该值为空。如果 start 大于 end,则为空。如果 end 大于单词数,则返回从 start 开始的所有单词。

# $(call uid_gid, user-name)
uid_gid = $(wordlist 3, 4,  \
            $(subst :, ,    \
            $(shell grep "^$1:" /etc/passwd)))

一些重要的函数

在继续讨论管理文件名的函数之前,让我们先介绍两个非常有用的函数 sort 和shell


$(sort list)

sort 函数对其列表参数进行排序并删除重复项。结果列表包含按字典顺序排列的所有唯一单词,每个单词用一个空格隔开。此外,排序带前后空格。

$ make -f- <<< 'x:;@echo =$(sort d b s d t )='
=b d s t=

当然,sort 函数是由 make 直接实现的,因此它不支持 sort 程序的任何选项。这个函数由它的参数操纵,通常是一个变量或另一个 make 函数的返回值。


$(shell command)

shell 函数接受单个参数,该参数被展开(像所有参数一样)并传递给子 shell 执行。然后读取命令的标准输出,并作为函数的值返回。输出中的换行序列被折叠成一个空格。任何末尾的换行符都会被删除。不会返回标准错误,也不会返回任何程序退出状态。

stdout := $(shell echo normal message)
stderr := $(shell echo error message 1>&2)
shell-value:
    # $(stdout)
    # $(stderr)

如你所见,发送到 stderr 的消息像往常一样被发送到终端,因此不包含在 shell 函数的输出中:

$ make
error message
# normal message
#

下面是一个创建目录集的循环:

REQUIRED_DIRS = ...
_MKDIRS := $(shell for d in $(REQUIRED_DIRS); \
do \
[[ -d $$d ]] || mkdir -p $$d; \
done)

通常,如果需要保证在执行任何命令脚本之前,存在必要的输出目录,这用 makefile 更容易实现。这个变量通过使用 bash shell 的 for 循环来创建必要的目录,以确保存在一组目录存在。双方括号是与测试程序类似的 bash 测试语法,只是不执行单词拆分和路径名展开。因此,如果变量包含一个有空格的文件名,测试仍然可以正常工作(并且没有引号)。通过将这个 make 变量在 makefile 的早期赋值,我们可以确保在命令脚本或其他变量使用输出目录之前执行它。_MKDIRS 的实际值是无关紧要的,而且 _MKDIRS 本身永远不会被使用。


由于 shell 函数可以用来调用任何外部程序,所以您应该注意如何使用它。特别地,您应该考虑简单变量和递归变量之间的区别。

START_TIME := $(shell date)
CURRENT_TIME = $(shell date)

定义变量时,START_TIME 变量会导致 date 命令执行一次。每次在 makefile 中使用变量时,CURRENT_TIME 变量都将重新执行 date。

我们的工具箱现在已经足够用来编写一些相当有趣的函数了。下面是一个用于测试一个值是否包含重复值的函数:

# $(call has-duplicates, word-list)
has-duplicates = $(filter   \
                $(words $1) \
                $(words $(sort $1))))

我们计算列表和唯一列表中的单词,然后“比较”这两个数字。没有理解数字的 make 函数,只有字符串。要比较两个数字,必须将它们作为字符串进行比较。最简单的方法是使用 filter。我们在另一个数字中搜索一个数字。如果存在重复项,has-duplicates 函数将是非空的。

下面是一种生成带有时间戳的文件名的简单方法:

RELEASE_TAR := mpwm-$(shell date +%F).tar.gz

这产生:

mpwm-2003-11-11.tar.gz

我们可以产生相同的文件名,但 date 得做更多的工作:

RELEASE_TAR := $(shell date +mpwm-%F.tar.gz)

下一个函数可用于将相对路径(可能是从com目录)转换为完全合格的 Java 类名:

# $(call file-to-class-name, file-name)
file-to-class-name := $(subst /,.,$(patsubst %.java,%,$1))

这个特定的模式也可以用两个 subst 来完成:

# $(call file-to-class-name, file-name)
file-to-class-name := $(subst /,.,$(subst .java,,$1))

然后,我们可以像这样使用这个函数来调用 Java 类:

CALIBRATE_ELEVATOR := com/wonka/CalibrateElevator.java
calibrate:
    $(JAVA) $(call file-to-class-name,$(CALIBRATE_ELEVATOR))

如果在 com 上面的 $(sources) 中有更多的父目录组件,它们可以通过下面的函数来删除,通过传递目录树的根作为第一个参数:

在 Java中,建议所有的类都在包含开发者完整的 Internet 域名的包中声明。此外,目录结构通常反映包结构。因此,许多源树看起来像 root-dir/com/company-name/dir

# $(call file-to-class-name, root-dir, file-name)
file-to-class-name := $(subst /,.,      \
                        $(subst .java,, \
                        $(subst $1/,,$2)))

当阅读这样的函数时,最简单的方法就是由内向外地理解它们。从最内部的 subst 开始,该函数删除字符串 $1/,然后删除字符串 .java,最后将所有斜杠转换为句点。

文件名函数

Makefile 作者花费大量时间处理文件。所以有很多 make 函数来帮助完成这个任务也就不足为奇了。


$(wildcard pattern...)

通配符已在第二章介绍目标、依赖和命令脚本的上下文中介绍过。但如果我们想在另一个上下文中使用这个功能,比如变量定义?有了 shell 函数,我们可以简单地使用子 shell 来扩展模式,但如果我们需要经常这样做,那将非常慢。相反,我们可以使用 wildcard 函数:

sources := $(wildcard *.c *.h)

wildcard 函数接受模式列表,并对每个模式执行展开。如果模式不匹配任何文件,则返回空字符串。与目标和依赖中的通配符扩展一样,支持普通的 shell 通配符: ~, *, ?, [...], 和 [^...].

make 3.80 的手册中没有提到允许一个以上的模式

通配符的另一种用法是在条件语句中测试文件是否存在。当与 if 函数(稍后将介绍)一起使用时,您经常会看到 wildcard 函数调用,其参数根本不包含通配符字符。例如,

dot-emacs-exists := $(wildcard ~/.emacs)

如果用户的主目录不包含 .emacs 文件,则返回空字符串。


$(dir list...)

dir 函数返回列表中每个单词的目录部分。下面是一个返回包含 C 文件的所有子目录的表达式:

source-dirs := $(sort \
                $(dir \
                  $(shell find . -name '*.c')))

find 函数返回所有源文件,然后 dir 函数删除离开目录的文件部分,sort 删除重复的目录。请注意,这个变量定义使用了一个简单的变量,以避免每次使用变量时重新执行find(因为我们假定在执行 makefile 期间源文件不会自动出现或消失)。下面是一个需要递归变量的函数实现:

# $(call source-dirs, dir-list)
source-dirs = $(sort \
                $(dir \
                  $(shell find $1 -name '*.c'))))

该版本接受以空格分隔的目录列表作为第一个参数进行搜索。要查找的第一个参数是要搜索的一个或多个目录。目录列表的末尾由第一个 - 参数识别。(一个我几十年来都不知道的查找功能!)


$(notdir name...)

notdir 函数返回文件路径的文件名部分。下面是一个从 Java 源文件返回 Java 类名的表达式:

# $(call get-java-class-name, file-name)
get-java-class-name = $(notdir $(subst .java,,$1))

在许多实例中,可以同时使用 dirnotdir 来产生所需的输出。例如,假设一个自定义 shell 脚本必须在它生成输出文件的同一个目录中执行。

$(OUT)/myfile.out: $(SRC)/source1.in $(SRC)/source2.in
    cd $(dir $@); \
    generate-myfile $^ > $(notdir $@)

自动变量 $@ 表示目标,可以将目标目录和文件分解为单独的值。实际上,如果 OUT 是一个绝对路径,就没有必要在这里使用 notdir函数,但是这样做会使输出更具可读性。

在命令脚本中,分解文件名的另一种方法是使用 $(@D)$(@F),在第二章的“自动变量”一节中提到过。


下面是添加和删除文件后缀的函数。


$(suffix name...)

suffix 函数返回参数中每个单词的后缀。下面是一个测试列表中所有单词后缀是否相同的函数:

# $(call same-suffix, file-list)
same-suffix = $(filter 1 $(words $(sort $(suffix $1))))

suffix 函数更常见的用法是在条件语句中与 findstring 结合使用。


$(basename name...)

basename 函数是 suffix 的补充。它返回没有后缀的文件名。调用 basename 后,任何前置路径组件都保持不变。下面是用 basename 重写的 file-to-class-nameget-java-class-name 函数:

# $(call file-to-class-name, root-directory, file-name)
file-to-class-name := $(subst /,., \
                        $(basename \
                          $(subst $1/,,$2)))
# $(call get-java-class-name, file-name)
get-java-class-name = $(notdir $(basename $1))

$(addsuffix suffix,name...)

addsuffix 函数将给定的后缀文本附加到名称中的每个单词。后缀文本可以是任何内容。下面是用来查找 PATH 中所有匹配表达式的文件的函数:

# $(call find-program, filter-pattern)
find-program = $(filter $1,                   \
                $(wildcard                    \
                  $(addsuffix /*,             \
                    $(sort                    \
                      $(subst :, ,            \
                        $(subst ::,:.:,       \
                          $(patsubst :%,.:%,  \
                            $(patsubst %:,%:.,$(PATH)))))))))
find:
    @echo $(words $(call find-program, %))

最内层的三个替换说明了 shell 语法中的一种特殊情况。空路径表示当前目录。为了规范化这个特殊的语法,我们按此顺序搜索空的后面的路径、空的前面的路径和空的内部路径。任何匹配的组件都被替换为 “.” 接下来,路径分隔符被替换为一个空格,以创建单独的单词。sort 函数用于删除重复的路径组件。然后将后缀 /* 附加到每个单词,并调用 wildcard 来扩展表达式。最后,通过 filter 提取所需的模式。

虽然这看起来是一个运行非常慢的函数(在许多系统上也可能如此),但在我的 1.9 GHz 512 MB 的 P4 上,这个函数只需要 0.20 秒就能执行,并找到 4335 个程序。可以通过将调用中的 $1 参数移动到 wildcard 中来提高性能。下面的版本去掉了 filter 的调用,并更改 addsuffix 以使用调用者的参数。

# $(call find-program,wildcard-pattern)
find-program = $(wildcard \
                $(addsuffix /$1, \
                  $(sort \
                    $(subst :, , \
                      $(subst ::,:.:, \
                        $(patsubst :%,.:%, \
                          $(patsubst %:,%:.,$(PATH))))))))
find:
    @echo $(words $(call find-program,*))

该版本运行时间为 0.17 秒。它运行得更快,因为 wildcard 不再返回所有文件,然后使用函数 filter 去掉它们。GNU make 手册中也有类似的例子。还请注意,第一个版本使用过滤器样式的通配符样式 (~, *, ?, [...], 和 [^...]).


$(addprefix prefix,name...)

addprefix 函数是 addsuffix 的补充。下面是用于测试一组文件是否存在且非空的表达式:

# $(call valid-files, file-list)
valid-files = test -s . $(addprefix -a -s ,$1)

这个函数与前面的大多数示例不同,它是在命令脚本中执行的。它使用 shell 的测试程序和 -s 选项(“true 如果文件存在且不是空的”)来执行测试。因为 test 命令在多个文件名之间需要一个-a (and) 选项,所以 addprefix 会在每个文件名之前加上 -a。用来启动“and”链的第一个文件是 .,它总是返回 true。


$(join prefix-list,suffix-list)

join 函数是 dirnotdir 的补充。它接受两个列表,并将 prefix-list 中的第一个元素与 suffix-list 中的第一个元素连接起来,然后将 prefix-list 中的第二个元素与 suffix-list 中的第二个元素连接起来,以此类推。它可以用来重建用 dirnotdir 分解的列表。


流程控制

因为到目前为止,我们所看到的许多函数都实现了在列表上执行它们的操作,所以即使没有循环结构,它们也能很好地工作。但是如果没有真正的循环操作符和某种条件处理,make 宏语言就会非常有限。幸运的是,make 同时提供了这两种语言特性。我把 fatalerror 的功能扔进了这一节,显然是一种非常极端的流程控制形式!


$(if condition,then-part,else-part)

if 函数(不要与第三章讨论过的条件指令 ifeq, ifneq, ifdefifndef 混淆) 根据条件表达式的值选择两个宏展开的其中之一。如果它的展开包含任何字符(甚至空格),该条件为真。在这种情况下,将展开 then-part。否则,如果展开条件为空,则为false,则展开 else-part

在第三章中,我对宏语言和其他编程语言进行了区分。宏语言的工作原理是通过定义和展开宏,将源文本转换为输出文本。当我们看到 if 函数如何工作时,这个区别就变得更清楚了。

下面是一种测试 makefile 是否在 Windows 上运行的简单方法。仅在 Windows 上查找定义的 COMSPEC 环境变量:

PATH_SEP := $(if $(COMSPEC),;,:)

make 首先删除前导和尾随的空格,然后展开表达式,从而计算条件。如果展开产生任何字符(包括空格),则表达式为 true。现在 PATH_SEP 包含在路径中使用的适当字符,无论生成文件是在 Windows 还是 Unix 上运行。在上一章中,我们提到了如果你使用了一些最新的特性(比如 eval),来检查 make 的版本。if 函数和 filter 函数通常一起用于测试字符串的值:

$(if $(filter $(MAKE_VERSION),3.80),,\
    $(error This makefile requires GNU make version 3.80.))

现在,随着 make 的后续版本的发布,该表达式可以被扩展为更可接受的版本:

$(if $(filter $(MAKE_VERSION),3.80 3.81 3.90 3.92),,\
    $(error This makefile requires one of GNU make version.))

这种技术的缺点是,在安装新版本的 make 时,必须更新代码。但这并不经常发生。(例如,3.80 是 2002 年 10 月以来的发布版本。)上面的测试可以作为顶级表达式添加到 makefile 中,因为如果为真,则 if 压缩为空,否则错误将终止 make。


$(error text)

error 函数用于打印致命错误消息。在函数打印出消息后,make 以退出状态 2 结束。输出的前缀是当前 makefile 的名称、当前行号和消息文本。下面是 make 的公共断言编程构造的实现:

# $(call assert,condition,message)
define assert
    $(if $1,,$(error Assertion failed: $2))
endef

# $(call assert-file-exists,wildcard-pattern)
define assert-file-exists
    $(call assert,$(wildcard $1),$1 does not exist)
endef

# $(call assert-not-null,make-variable)
define assert-not-null
    $(call assert,$($1),The variable "$1" is null)
endef
error-exit:
    $(call assert-not-null,NON_EXISTENT)

第一个函数 assert 只是测试它的第一个参数,如果参数为空,则输出用户的错误消息。第二个函数建立在第一个函数的基础上,并用通配符模式测试一个文件是否存在。注意,参数可以包含任意数量的通配符模式。

第三个函数是一个非常有用的断言,它依赖于计算变量。make 变量可以包含任何内容,包括其他 make 变量的名称。但是,如果一个变量包含另一个变量的名称,你如何访问另一个变量的值呢?很简单,将变量展开两次:

NO_SPACE_MSG := No space left on device.
NO_FILE_MSG := File not found.
…;
STATUS_MSG := NO_SPACE_MSG
$(error $($(STATUS_MSG)))

这个示例稍微人为了一些,以保持简单,但是在这里 STATUS_MSG 通过存储错误消息变量名,被设置为几个错误消息之一。在打印消息时,首先展开 STATUS_MSG 以访问错误消息变量名 $(STATUS_MSG),然后再次展开以访问消息文本 $($(STATUS_MSG))。在非空断言函数中,我们假定函数的参数是 make 变量的名称。我们首先展开参数 $1,以访问变量名,然后再次展开 $($1),以确定它是否有值。如果为空,则在错误消息中使用 $1 中的变量名。

$ make
Makefile:14: *** Assertion failed: The variable "NON_EXISTENT" is null. Stop.

还有一个 warning 函数(请参阅本章后面的“不太重要的函数”一节),它与 error 相同的格式打印消息,但不终止 make。


$(foreach variable,list,body)

foreach 函数提供了一种方法来重复展开文本,在每次展开中替换为不同的值。请注意,这与重复执行带有不同参数的函数不同(尽管它也可以这样做)。例如:

letters := $(foreach letter,a b c d,$(letter))
show-words:
    # letters has $(words $(letters)) words: '$(letters)'
$ make
# letters has 4 words: 'a b c d'

当执行这个 foreach 时,它将循环控制变量 letter 设置为 a b c d 中个值,并展开循环体 $(letter),为每个值展开一次。展开的文本用空格分隔连接起来。下面是一个用来测试是否设置了一组变量的函数:

VARIABLE_LIST := SOURCES OBJECTS HOME
$(foreach i,$(VARIABLE_LIST), \
$(if $($i),, \
$(shell echo $i has no value > /dev/stderr)))

(shell 函数中的伪文件 /dev/stderr 需要将 shell 设置为 bash),这个循环将 i 设置为 VARIABLE_LIST 中的单词。if 中的测试表达式首先计算 $i 以获得变量名,然后在计算过的表达式 $($i) 中再次计算该变量名,以查看它是否为非空。如果表达式有一个值,那么 then 部分什么也不做;否则,else 部分将打印一个警告。注意,如果我们的 echo 省略了重定向,shell 命令的输出将被替换到 makefile 中,产生一个语法错误。如图所示,整个 foreach 循环扩展为空。

如前所述,下面是一个从列表中收集所有包含子字符串的单词的函数:

# $(call grep-string, search-string, word-list)
define grep-string
  $(strip                        \
    $(foreach w, $2,             \
      $(if $(findstring $1, $w), \
        $w)))
endef
words := count_words.c counter.c lexer.l lexer.h counter.h
find-words:
    @echo $(call grep-string,un,$(words))

不幸的是,这个函数不接受模式,但它找到了简单的子字符串:

$ make
count_words.c counter.c counter.h

关于变量和括号的样式说明

如前所述,一个字符的 make 变量不需要括号。例如,所有基本的自动变量都是一个字符。即使在 GNU make 手册中,自动变量也是普遍地不带括号的。然而,make 手册几乎对所有其他变量都使用了括号,甚至是单个字符变量,并强烈建议用户效仿。这突出了 make 变量的特殊性,因为几乎所有其他具有“$变量”(例如shell、perl、awk、yacc等) 的程序都不需要圆括号。编程中最常见的错误之一是忘记括号。下面是 foreach 包含错误的常用用法:

INCLUDE_DIRS :=INCLUDES := $(foreach i,$INCLUDE_DIRS,-I $i)
# INCLUDES now has the value "-I NCLUDE_DIRS"

但是,我发现,通过聪明地使用单字符变量和省略不必要的括号,阅读宏会容易得多。例如,我认为 has-duplicates 函数没有完整的括号更容易阅读:

# $(call has-duplicates, word-list)
has-duplicates = $(filter \
                    $(words $1) \
                    $(words $(sort $1))))

与:

# $(call has-duplicates, word-list)
has-duplicates = $(filter \
                    $(words $(1)) \
                    $(words $(sort $(1)))))

然而,带有完整圆括号的 kill-program 函数可能更具可读性,因为它有助于将 make 变量与 shell 变量或其他程序中使用的变量区分开来:

define kill-program
    @ $(PS) $(PS_FLAGS) |                           \
    $(AWK) 'BEGIN { FIELDWIDTHS = $(PS_FIELDS) }    \
        /$(1)/{                                     \
            print "Killing " $$3;                   \
            system( "$(KILL) $(KILLFLAGS) " $$1 )   \
        }'
endef

搜索字符串包含宏的第一个参数 $(1)$$3$$1 表示 awk 变量。

我使用单字符变量,只有在看起来使代码更具可读性时才省略括号。我通常对宏的参数和 foreach 循环中的控制变量这样做。你应该遵循适合你的情况的风格。如果您对 makefile 的可维护性有任何疑问,请遵循 make 手册的建议并使用完整的括号。请记住,make 程序主要是为了缓解与维护软件相关的问题。如果您在编写 makefile 时遵循这一点,就很可能避免一些麻烦。

一些稍微不重要的函数

最后,我们有一些杂项(但很重要)的字符串函数。虽然与 foreachcall 相比微不足道,但您会发现自己经常使用它们。


$(strip text)

strip 函数从文本中删除所有前导和尾随的空白,并将所有内部空白替换为单个空格。该函数的一个常见用途是清除条件表达式中使用的变量。我最经常使用这个函数从变量和宏定义中删除不必要的空白,已经跨多行格式化的代码。但是,如果函数对前导空格敏感,那么将函数参数$1$2 等用 strip 包装也是一个好主意。程序员通常不会意识到 make 的微妙之处,他们会在调用参数列表的逗号后添加一个空格。

$(origin variable)

origin 函数返回一个描述变量起源的字符串。这在决定如何使用变量值时非常有用。例如,如果一个变量的值来自环境,您可能希望忽略它,但如果它是从命令行设置的,则不希望忽略它。对于一个更具体的示例,下面是一个新的 assert 函数,用于测试是否定义了一个变量:

# $(call assert-defined,variable-name)
define assert-defined
  $(call assert,                            \
    $(filter-out undefined,$(origin $1)),   \
    '$1' is undefined)
endef

origin的可能返回值是:

undefined

从未定义过该变量。

default

变量的定义来自 make 的内置数据库。如果改变内置变量的值,origin 将返回最近的定义。

environment

变量的定义来自环境(--environment-overrides 选项没有打开)。

environment override

变量的定义来自环境(并且打开了 --environment-overrides 选项)。

file

变量的定义来自 makefile。

command line

变量的定义来自命令行。

override

变量的定义来自于 override 指令。

automatic

该变量是由 make 定义的自动变量。


$(warning text)

warning 函数类似于 error 函数,不同之处是它不会导致 make 退出。与 error 函数一样,输出的前缀是当前 makefile 的名称和当前行号,后面是消息文本。warning 函数扩展为空字符串,因此几乎可以在任何地方使用。

$(if $(wildcard $(JAVAC)),,                                 \
    $(warning The java compiler variable, JAVAC ($(JAVAC)), \
        is not properly set.))

高级自定义函数

我们将花很多时间编写宏函数。不幸的是,在 make 中没有很多特性可以帮助调试它们。让我们首先尝试编写一个简单的调试跟踪函数来帮助我们。

如前所述,call 将其每个参数绑定到编号变量 $1$2 等。可以给出任意数量的参数来调用。作为一种特殊情况,当前执行的函数名(即变量名)可以通过 $0 访问。使用此信息,我们可以编写一对调试函数,用于跟踪宏展开的过程:

# $(debug-enter)
debug-enter = $(if $(debug_trace),\
                $(warning Entering $0($(echo-args))))
# $(debug-leave)
debug-leave = $(if $(debug_trace),$(warning Leaving $0))

comma := ,
echo-args = $(subst ' ','$(comma) ',\
            $(foreach a,1 2 3 4 5 6 7 8 9,'$($a)'))

如果我们想观察函数 a 和函数 b 是如何被调用的,我们可以像这样使用这些跟踪函数:

debug_trace = 1

define a
    $(debug-enter)
    @echo $1 $2 $3
    $(debug-leave)
endef

define b
    $(debug-enter)
    $(call a,$1,$2,hi)
    $(debug-leave)
endef

trace-macro:
    $(call b,5,$(MAKE))

通过将 debug-enterdebug-leave 变量放置在函数的开头和结尾,您可以跟踪自己函数的展开。这些功能远非完美。echo-args 函数只回显前 9 个参数,更糟糕的是,它不能确定调用中实际参数的数量(当然,两者都不能)。尽管如此,我还是在自己的调试中“原样”使用了这些宏。当执行时,makefile 生成以下跟踪输出:

$ make
makefile:14: Entering b( '5', 'make', '', '', '', '', '', '', '')
makefile:14: Entering a( '5', 'make', 'hi', '', '', '', '', '', '')
makefile:14: Leaving a
makefile:14: Leaving b
5 make hi

正如一位朋友最近对我说的那样,“我以前从未想过 make 也会是一种编程语言。” GNU make 不是你奶奶的 make! (GNU make isn’t your grandmother’s make!)

eval 和 value

eval 函数与其他内置函数完全不同。它的目的是将文本直接提供给 make 解析器。例如,

$(eval sources := foo.c bar.c)

eval 的参数首先扫描变量并展开(就像所有函数的参数一样),然后解析和计算文本,就像它来自输入文件一样。这个示例非常简单,您可能想知道为什么要使用这个函数。让我们尝试一个更有趣的例子。假设您有一个 makefile 来编译十几个程序,并且您想为每个程序定义几个变量,比如源、头和对象。而不是对每组变量重复这些变量赋值:

ls_sources := ls.c glob.c
ls_headers := ls.h glob.h
ls_objects := ls.o glob.o

我们可以尝试定义一个宏来完成这项工作:

# $(call program-variables, variable-prefix, file-list)
define program-variables
    $1_sources = $(filter %.c,$2)
    $1_headers = $(filter %.h,$2)
    $1_objects = $(subst .c,.o,$(filter %.c,$2))
endef

$(call program-variables, ls, ls.c ls.h glob.c glob.h)

show-variables:
    # $(ls_sources)
    # $(ls_headers)
    # $(ls_objects)

program-variables 宏接受两个参数,三个变量的前缀和一个文件列表,该宏从中选择要在每个变量中设置的文件。但是,当我们尝试使用这个宏时,我们得到了错误:

$ make
Makefile:7: *** missing separator. Stop.

由于 make 解析器的工作方式,这并不像预期的那样工作。扩展为多行的宏(在顶层解析层)是非法的,会导致语法错误。在本例中,解析器认为这一行是一条规则或命令脚本的一部分,但缺少分隔符标记。相当令人困惑的错误信息。引入 eval 函数来处理这个问题。如果我们把调用线路改为:

$(eval $(call program-variables, ls, ls.c ls.h glob.c glob.h))

我们得到了我们想要的:

$ make
# ls.c glob.c
# ls.h glob.h
# ls.o glob.o

使用 eval 可以解决解析问题,因为 eval 处理多行宏展开,本身展开为零行。

现在我们有了一个非常简洁地定义三个变量的宏。请注意宏中的赋值是如何将传入函数的前缀和固定后缀 $1_sources 中的变量名组成的。这些并不是前面描述的精确计算的变量,但它们具有相同的风格。

继续这个例子,我们意识到我们也可以在宏中包含规则:

# $(call program-variables,variable-prefix,file-list)
define program-variables
    $1_sources = $(filter %.c,$2)
    $1_headers = $(filter %.h,$2)
    $1_objects = $(subst .c,.o,$(filter %.c,$2))
    $($1_objects): $($1_headers)
endef

ls: $(ls_objects)

$(eval $(call program-variables,ls,ls.c ls.h glob.c glob.h))

请注意,这两个版本的程序变量说明了函数参数中空格的问题。在前一个版本中,两个函数形参的简单使用不受参数上的前导空格的影响。也就是说,无论 $1$2 中的任何前导空格,代码的行为都是相同的。但是,新版本引入了计算变量 $($1_objects)$($1_headers)。现在,向函数 (ls) 的第一个参数添加一个前导空格,将导致计算的变量以前导空格开始,然后展开为空,因为我们定义的变量都没有以前导空格开始。这应该是一个非常隐蔽的问题。

当我们运行这个 makefile 时,我们发现 make 以某种方式忽略了 .h 依赖。为了查这个问题,我们通过运行 make 的 --print- database-base 选项来检查 make 的内部数据库,我们发现了一些奇怪的东西:

$ make --print-database | grep ^ls
ls_headers = ls.h glob.h
ls_sources = ls.c glob.c
ls_objects = ls.o glob.o
ls.c:
ls.o: ls.c
ls: ls.o

ls.o.h 丢了,使用计算变量的规则有问题。

当 make 解析 eval 函数调用时,它首先展开用户定义的函数、程序变量。宏的第一行展开为:

ls_sources = ls.c glob.c

请注意,宏的每一行都按照预期立即展开。其他变量赋值的处理方法类似。然后我们来看看规则:

$($1_objects): $($1_headers)

计算的变量首先展开其变量名:

$(ls_objects): $(ls_headers)

然后进行外部变量展开,得到:

:

等等!我们的变量去哪了?答案是,前面三个赋值语句是展开的,但没有通过 make 进行计算。让我们继续看看它是如何工作的。一旦对程序变量的调用被展开,make 将看到如下内容:

$(eval ls_sources = ls.c glob.c
ls_headers = ls.h glob.h
ls_objects = ls.o glob.o

:)

eval 函数然后执行并定义这三个变量。所以,答案是规则中的变量在被定义之前就被展开了。

我们可以通过显式延迟计算变量的展开,直到定义了这三个变量,来解决这个问题。我们可以通过在计算变量前引用 $ 符号来做到这一点:

$$($1_objects): $$($1_headers)

这一次,make 数据库显示了我们所期望的依赖:

$ make -p | grep ^ls
ls_headers = ls.h glob.h
ls_sources = ls.c glob.c
ls_objects = ls.o glob.o
ls.c:
ls.o: ls.c ls.h glob.h
ls: ls.o

总的来说,eval 的参数被展开了两次: 第一次是 make 为 eval 准备参数列表,第二次是 eval

我们通过推迟计算变量的计算来解决最后一个问题。处理这个问题的另一种方法是强制对变量赋值进行早期计算,方法是用 eval 包装每个赋值:

# $(call program-variables,variable-prefix,file-list)
define program-variables
    $(eval $1_sources = $(filter %.c,$2))
    $(eval $1_headers = $(filter %.h,$2))
    $(eval $1_objects = $(subst .c,.o,$(filter %.c,$2)))

    $($1_objects): $($1_headers)
endef

ls: $(ls_objects)

$(eval $(call program-variables,ls,ls.c ls.h glob.c glob.h))

通过将变量赋值封装在它们自己的 eval 调用中,当程序变量宏被展开时,我们通过 make 将它们内部化。然后可以立即在宏中使用它们。

当我们增强 makefile 时,我们意识到我们可以将另一条规则添加到宏中。程序本身取决于它的对象。因此,为了完成我们的参数化makefile,我们添加了一个顶级的 all 目标,并需要一个变量来处理 makefile 可以管理的所有程序:

#$(call program-variables,variable-prefix,file-list)
define program-variables
    $(eval $1_sources = $(filter %.c,$2))
    $(eval $1_headers = $(filter %.h,$2))
    $(eval $1_objects = $(subst .c,.o,$(filter %.c,$2)))

    programs += $1

    $1: $($1_objects)

    $($1_objects): $($1_headers)
endef

# Place all target here, so it is the default goal.
all:

$(eval $(call program-variables,ls,ls.c ls.h glob.c glob.h))
$(eval $(call program-variables,cp,...))
$(eval $(call program-variables,mv,...))
$(eval $(call program-variables,ln,...))
$(eval $(call program-variables,rm,...))

# Place the programs prerequisite here where it is defined.
all: $(programs)

注意 all 目标及其依赖的位置。直到5次 eval 调用之后,程序变量才被正确地定义,但是我们想把 all 目标放在 makefile 的第一个位置,所以 all 是默认的目标。我们可以通过先考虑所有的约束,然后再添加依赖来满足所有的约束。

函数 program-variables 存在问题,因为一些变量的计算太早了。make 实际上提供了一个 value 函数来帮助解决这种情况。value 函数返回未展开的变量参数的值。然后可以将此未展开的值传递给 eval 进行处理。通过返回一个未展开的值,我们可以避免在宏中引用一些变量引用的问题。不幸的是,这个函数不能与 program-variables 宏一起使用。这是因为 value 是一个全有或全无的函数。如果使用,value 将不会展开宏中的任何变量。此外,value 不接受参数(如果它接受了,也不会对它们做任何事情),所以我们的程序名和文件列表参数不会被展开。

由于这些限制,您在本书中不会经常看到 value 的使用。

钩子函数

用户定义函数只是包含文本的变量。调用函数将在变量文本中展开 $1$2 等引用(如果它们存在的话)。如果该函数不包含这些变量引用中的任何一个,则调用无关紧要。实际上,如果变量不包含任何文本,则调用无关紧要。没有错误或警告发生。如果您碰巧拼错了一个函数名,这可能会非常令人沮丧。但这也可能非常有用。

函数都是关于可重用代码的。函数重用得越频繁,就越值得把它写好。可以通过向函数中添加钩子来提高函数的可重用性。钩子是一种函数引用,用户可以重新定义它,以便在正常执行期间执行自己的自定义任务。

假设您正在 makefile 中构建许多库。在某些系统上,您可能希望运行 ranlib,而在其他系统上,您可能希望运行 chmod。你可以选择编写一个函数并添加一个钩子,而不是为这些操作编写显式的命令:

# $(call build-library, object-files)
define build-library
    $(AR) $(ARFLAGS) $@ $1
    $(call build-library-hook,$@)
endef

为了使用该钩子,定义函数 build-library-hook:

$(foo_lib): build-library-hook = $(RANLIB) $1
$(foo_lib): $(foo_objects)
    $(call build-library,$^)

$(bar_lib): build-library-hook = $(CHMOD) 444 $1
$(bar_lib): $(bar_objects)
    $(call build-library,$^)

参数传递

函数可以从四个“源”获取数据: 通过调用传入的参数、全局变量、自动变量和特定目标的变量。其中,依赖于参数是最模块化的选择,因为它们的使用使函数免受对全局数据的任何变化,但有时这并不是最重要的原则。

假设我们有几个项目使用一个公共的 make 函数集。每个项目都可以用一个变量前缀来标识,比如 PROJECT1_,项目的关键变量都使用前缀加上跨项目后缀。前面的示例 PROJECT_SRC 可能看起来像 PROJECT1_SRCPROJECT1_BINPROJECT1_LIB。与其编写一个需要这三个变量的函数,不如使用计算变量并传递一个参数,即前缀:

# $(call process-xml,project-prefix,file-name)
define process-xml
    $($1_LIB)/xmlto -o $($1_BIN)/xml/$2 $($1_SRC)/xml/$2
endef

传递参数的另一种方法是使用特定目标的变量。当大多数调用使用默认值,但少数调用需要特殊处理时,这特别有用。当规则在 include 文件中定义,但在变量定义在 makefile 中调用时,特定目标的变量也提供了灵活性。

release: MAKING_RELEASE = 1
release: libraries executables


$(foo_lib):
$(call build-library,$^)



# $(call build-library, file-list)
define build-library
    $(AR) $(ARFLAGS) $@                 \
        $(if $(MAKING_RELEASE),         \
            $(filter-out debug/%,$1),   \
        $1)
endef

此代码设置一个特定目标的变量,以指示何时对发布执行构建。在这种情况下,库 build-library 函数将从库中过滤出所有 debug 模块。

参考资料