GNU Make 项目管理 第四章 函数
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/
。注意宏参数 $1
和 awk
字段引用 $$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 中,我们有 filter
、filter-out
和 findstring
函数。
$(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 Shell、bash 和许多其他类似 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))
在许多实例中,可以同时使用 dir
和 notdir
来产生所需的输出。例如,假设一个自定义 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-name
和 get-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
函数是 dir
和 notdir
的补充。它接受两个列表,并将 prefix-list
中的第一个元素与 suffix-list
中的第一个元素连接起来,然后将 prefix-list
中的第二个元素与 suffix-list
中的第二个元素连接起来,以此类推。它可以用来重建用 dir
和 notdir
分解的列表。
流程控制
因为到目前为止,我们所看到的许多函数都实现了在列表上执行它们的操作,所以即使没有循环结构,它们也能很好地工作。但是如果没有真正的循环操作符和某种条件处理,make 宏语言就会非常有限。幸运的是,make 同时提供了这两种语言特性。我把 fatal
和 error
的功能扔进了这一节,显然是一种非常极端的流程控制形式!
$(if condition,then-part,else-part)
if
函数(不要与第三章讨论过的条件指令 ifeq
, ifneq
, ifdef
和 ifndef
混淆) 根据条件表达式的值选择两个宏展开的其中之一。如果它的展开包含任何字符(甚至空格),该条件为真。在这种情况下,将展开 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 时遵循这一点,就很可能避免一些麻烦。
一些稍微不重要的函数
最后,我们有一些杂项(但很重要)的字符串函数。虽然与 foreach
或 call
相比微不足道,但您会发现自己经常使用它们。
$(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-enter
和 debug-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_SRC
、PROJECT1_BIN
和 PROJECT1_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 模块。
评论