makefile规则
前一章中,我们编写了若干规则,用来编译与链接我们的单词计数(word counting)程序。我们为每一个规则定义了一个工作目标,也就是一个需要更新的文件。每个工作目标依存于一组必要条件,这组必要条件也都是文件。当你要求更新某个工作目标时,如果必要条件中存在时间戳在工作目标的时间戳之后的文件,make就会执行相应规则里的命令脚本。因为某个规则的工作目标可以是另一个规则的必要条件,所以这样的工作目标和必要条件将会形成依存图(dependency graph)。建立及处理依存图,并据此更新特定的工作目标,就是make所要做的事情。
规则对make而言十分重要,make允许你使用各种类型的规则。具体规则(explicit rule),就像我们在上一章所编写的规则,用来指定需要更新的工作目标:如果必要条件中存在时间戳在此工作目标的时间戳之后的文件,make就会对它进行更新的动作。这将会是你最常用的规则类型。模式规则(pattern rule)中所使用的是通配符(wildcard)而不是明确的文件名称,这让make得以对与模式相符的工作目标应用该规则,进行必要的更新动作。隐含规则(implicit rule)可以是模式规则,也可以是内置于make的后缀规则(suffix rule)。有了这些内置于make的规则可以让makefile的编写变得更加容易,因为对于工作目标的更新,make已经知道许多常见文件格式、后缀以及更新工作目标的程序。至于静态模式规则(static pattern rule),它就像正规模式规则一样,只不过它们只能应用在一串特定的工作目标文件中。
GNU make可作为许多其他版本的make的替代品,它特别针对兼容性提供了若干功能。后缀规则最初是make用来编写通则(general rule)的方法,尽管GNU make也支持后缀规则,不过为了更完整更一般化,它考虑以模式规则来替换。
具体规则
你编写的规则多半会是具体规则,以特定的文件作为工作目标和必要条件。每个规则都可以有多个工作目标。这意味着,每个工作目标所具备的必要条件可以跟其他工作目标的一样。如果这些工作目标尚未被更新,则make将会为它们执行同一组更新动作。例如:
- vpath.o variable.o: make.h config.h getopt.h gettext.h dep.h
这代表vpath.o和variable.o与同一组C文件具有依存关系。这一行等效于:
- vpath.o: make.h config.h getopt.h gettext.h dep.h
- variable.o: make.h config.h getopt.h gettext.h dep.h
这两个工作目标将会被分开处理。只要有任何一个目标文件尚未被更新(也就是说,任何一个头文件的时间戳在该目标文件的之后),则make将会执行规则中所指定的命令以便更新改目标文件。
你不必将规则一次定义完全(all at once)。每当make看到一个工作目标,就会将改工作目标与其必要条件加入依存图。如果make所看到的工作目标已经存在于依存图中,则任何额外的必要条件都会被附加到改工作目标在依存图中的项目里。对较简单的应用来说,这个特性可用来断开太长的规则以增进makefile的可读性:
- vpath.o: vpath.c make.h config.h getopt.h gettext.h dep.h
- vpath.o: filedef.h hash.h job.h commands.h variable.h vpath.h
对较复杂的应用来说,必要条件可以组成自看似无关的文件:
- # 确定 vpath.c 被编译之前 lexer.c 已经创建好了
- vpath.o: lexer.c
- # 以特殊的标记来编译 vpath.c
- vpath.o: vpath.c
- $(COMPILE.c) $(RULE_FLAGS) $(OUTPUT_OPTION) $<
- # 引入另一个程序所产生的依存关系
- include auto-generated-dependencies.d
第一个规则指出,每当lexer.c被更新后,vpath.o就必须被更新(这或许是因为产生lexer.c的过程中会有其他副作用)。这个规则还可用来确保必要条件的更新动作总是在工作目标之前被实施(注意规则的双向作用。就其正向作用而言,此规则指出,若lexer.c已经被更新,则需要对vpath.o执行更新的动作;就其反向作用而言,此规则指出,如果我们需要建立或使用vpath.o,首先必须确定lexer.c已经更新)。这个规则应该就近放在lexer.c处理规则的旁边,好让开发人员能够注意到这个微妙的关系。稍后,vpath.o的编译规则会被放到其他编译规则之中。此规则的命令用到了三个make变量。你将会看到更多的make变量,不过现在你只需要知道,一个变量可以是一个美元符号后面跟着单一字符(character),也可以是一个美元符号后面跟着一个加圆括号的单词。最后,.o文件和.h文件的依存关系是从另一个文件(这个文件产生自外部程序)引入到makefile的。
通配符
当你有一长串文件要指定时,为了简化此过程,make提供了通配符(wildcard),此功能也被称为文件名模式匹配(globbing)。make的通配符如同Bourne shell的~、*、?、[...]和[^...]。举例来说,*.*会被扩展(expand)成文件名中包含点号的所有文件。一个问好代表任何单一字符,而[...]代表一个字符集(character class)。若要取得字符集的“补集”则可以使用[^...]。
此外,“~”符号可以用来代表当前用户的主目录(home directory)。一个“~”符号之后若跟着用户的名称则代表改用户的主目录。
每当make在工作目标、必要条件或命令脚本等语境(context)中看到通配符,就会自动扩展通配符。在其他语境中,你可以通过函数的调用手动扩展通配符。如果你想创建适应能力较强的makefile,通配符非常有用。举例来说,如果不想手动列出一个程序里的所有文件,你就可以使用通配符:
- prog: *.c
- $(CC) -o $@ $^
不过通配符的使用务必谨慎为之,因为一不小心就会有误用的危险。比如:
- *.o: constants.h
这个规则的意图很明显:所有的目标文件皆依存于头文件constants.h。不过,如果工作目录中当前并未包含任何目标文件,则通配符扩展后变成下面这样:
- : constants.h
这是一个合法的make表达式,而且它本身并不会产生错误信息。实现此规则的正确方法,就是针对源文件使用通配符(因为它们总是存在的)以及将之转换成一串目标文件。
最后,值得注意的是,当模式出现现在工作目标或必要条件中时,是由make进行通配符的扩展。然而,当模式出现在命令中时,是由subshell进行扩展的动作。区别这两种情况有时会变得很重要,因为make会在读取makefile的时候立即扩展通配符,但是shell会在执行命令的时候扩展通配符。当有许多复杂的文件操作需要进行时,这两种扩展动作将会有很大的差别。
假想工作目标
到目前为止,我们所提到的工作目标以及必要条件都会进行文件的创建和更新的动作。尽管这是典型的用法,但是以工作目标充当标签(label)来代表命令脚本,通常会有些用处。举例来说,稍早我们曾提到许多makefile中,默认的首先要处理的标准工作目标称为all。任何不代表文件的工作目标就叫做假想工作目标(phony target)。另一个标准的假想工作目标称为clean:
- clean:
- rm -f *.o lexer.c
通常,make总是会执行假想工作目标,因为对应于改规则的命令并不会创建以改工作目标为名称的文件。
切记,make无法区分文件形式的工作目标与假想工作目标。如果当前目录中刚好出现与假想工作目标同名的文件,make将会在它的依存图中建立改文件与假想工作目标的关系。举例来说,如果你运行make clean时,工作目录中刚好存在clean这个文件,那么将会产生令人困惑的信息:
- $ make clean
- make: `clean` is up to date.
因为大多数的假想工作目标并未指定必要条件,clean工作目标总是会被视为已经更新,所以相应的命令永远不会被执行。
为了避免这个问题,GNU make提供了一个特殊的工作目标 —— .PHONY ,用来告诉make,改工作目标不是一个真正的文件。当你要声明假想工作目标时,只要将改工作目标指定为 .PHONY 的一个必要条件即可:
- .PHONY: clean
- clean:
- rm -f *.o lexer.c
现在,即使当前目录中存在名为clean的文件,make还是会执行对应于clean的命令。除了总是将工作目标标记为尚未更新、将一个工作目标声明为“假”之外,还会让make知道,不应该像处理一般规则那样,从源文件来建立以工作目标为名的文件。因此,make可以优化它的一般规则搜索程序来提高性能。
以“假想工作目标”作为“实际文件”的一个“必要条件”似乎不太有意义,因为假想工作目标总是尚未更新,这总会使得改实际文件(工作目标)被重新建立。然而,以“假想工作目标”作为“假想工作目标”的必要条件通常会有些用处。举例来说,all工作目标常会被用来指定所要编译的一串程序:
- .PHONY: all
- all: bash bashbug
其中,all工作目标将会创建bash以及bashbug。
你还可以将假想工作目标作为内置在makefile里的shell脚本来用。以假想工作目标作为另一个工作目标的必要条件,可让make在进行实际工作之前调用假想工作目标所代表的脚本。假设我们很在意磁盘空间的使用情况,因而在进行磁盘密集的工作之前,我们会想要显示磁盘尚有多少空间可供使用,我们可能会这么做:
- .PHONY: make-documentation
- make-documentation:
- df -k . | awk 'NR == 2 { printf("%d available\n", $$4) }'
- javadoc
这么做的问题是,我们最后可能会在不同的工作目标下多次指定df和awk命令,这会造成一个维护上的问题,因为如果我们在另一个系统上遇到了输出格式不同的df命令,那么我们必须到指定df和awk命令的每一处进行修改。此时,我们可以把df那一行放在它自己的假想工作目标里:
- .PHONY: make-documentation
- make-documentation: df
- javadoc
- .PHONY: df
- df:
- df -k . | awk 'NR == 2 { printf("%d available\n", $$4) }'
以df作为make-documentation的一个必要条件,可让make在产生文件之前先调用我们的df工作目标。可以这么做是因为make-documentation也是一个假想工作目标。现在,即使我们在其他工作目标中重复使用df,也不会造成什么维护上的问题。
假想工作目标还有许多其他的好处。
make的输出常会把想要进行阅读以及调试的人搞糊涂。这是因为:尽管makefile的编写是采用从上而下(top-down)的形式,不过make执行命令的方式却是采用从下而上(button-up)的形式;此外,你根本无法判断当前正在执行哪一个规则。如果能够在make的输出中为主要工作目标加上注释,那么make的输出就会变得容易阅读。这就是假想工作目标可以派上用场的地方。如下所示的例子摘录自bash的makefile:
- $(Program): build_msg $(OBJECTS) $(BUILTINGS_DEP) $(LIBDEP)
- $(RM) $@
- $(CC) $(LDFLAGS) -o $(Program) $(OBJECTS) $(LIBS)
- ls -l $(Program)
- size $(Program)
- .PHONY: build_msg
- build_msg:
- @printf "#\n# Building $(Program)\n#\n"
因为printf位于假想工作目标之中,所以在任何必要条件被更新之前会立即输出信息。如果以build_msg作为$(Program)命令脚本的第一个命令,那么在所有编译结果和依存关系都产生之后才会执行该命令。切记,因为假想工作目标总是尚未更新,所以假想工作目标build_msg会导致$(Program)被重建——即使它已经被更新。这么做似乎是明智的抉择,所有的计算工作在编译目标文件的时候大多已经完成,因此只有最后的链接工作一定会被执行。
假想工作目标还可以用来改善makefile的“用户接口”。工作目标通常包含目录路径元素、额外文件名成分(比如版本编号)以及标准扩展名的复合字符串,这使得“在命令行指定工作目标的文件名”成为一种挑战。你只要加入一个简单的假想工作目标,并以实际文件作为它的必要条件,就可以避免这个问题。
许多makefile多少都会包含一组标准的假想工作目标。表2-1列出了这些标准的假想工作目标。
工作目标 | 功能 |
---|---|
all | 执行编译应用程序的所有工作 |
install | 从已编译的二进制文件进行应用程序的安装 |
clean | 将产生自源代码的二进制文件删除 |
distclean | 删除编译过程中所产生的任何文件(除了二进制文件,也包含configure所产生的Makefile) |
TAGS | 建立可供编辑器使用的标记表 |
info | 从Texinfo源代码来创建GNU info文件 |
check | 执行与应用程序相关的任何测试 |
工作目标TAGS实际上不是一个假想工作目标,因为ctags和etags程序的输出就是名为TAGS的文件。此外之所以提到它。是因为就我们所知,它是绝无仅有的、标准的非假想工作目标(nonphony target)。
空工作目录
空工作目录(empty target)如同假想工作目标一样,可用来发挥make的潜在能力。假想工作目标总是尚未更新,所以它们总是会被执行,并且总是会使得它们的“依存对象”(工作目标所关联到的必要条件)被重载。但假设我们有若干命令,它们不会输出任何文件,只是偶尔需要被执行一下,而且我们并不想让我们的依存对象(dependent)被更新,改怎么办?此时,我们可以建立一个规则,它的工作目标是一个空文件(有时称为cookie):
- prog: size prog.o
- $(CC) $(LDFLAGS) -o $@ $^
- size: prog.o
- size $^
- touch size
请注意,size规则在执行完之后,会使用touch创建一个名为size的空文件。这个空文件可作为它的时间戳,因此make只在prog.o被更新之后才会执行size规则。此外,prog的必要条件size将不会导致prog的更新,除非它的目标文件的时间戳也在工作目标(的时间戳)之后。
与自动变量$?并用时,空文件特别有用。我们将会在“自动变量”一节探讨自动变量,不过事先了解一下这个变量应该不会有问题。对每个规则的命令脚本部分来说,make会将变量$?替换成一组必要条件,这组必要条件的时间戳在工作目标的时间戳之后。例如,下面的规则将会输出自从上次执行make print之后,变更过的所有文件:
- print: *.[hc]
- lpr $?
- touch $@
通常,空文件可用来标明最近发生了一个特殊的事件。