规则(rule)是写在 makefile 中的内容,用来指示何时以及如何重新生成某个特定的文件——该文件称为这条规则的目标(target)(通常一条规则对应一个目标)。规则中要写出作为目标的前置条件 (prerequisite,旧称:依赖关系)的其他文件清单,以及用来生成或更新目标的命令 (recipe,规则的执行内容)。
编写规则的顺序原则上没有意义。唯一的例外是在确定默认目标——也就是当你没有特别指定任何对象时 make 所要处理的目标——的时候。默认目标是第一个 makefile 中第一条规则里的第一个目标。这里还有两个例外:其一,以句点开头的目标,除非含有一个以上的斜杠「/」,否则不会成为默认目标;其二,定义模式规则的目标不会对默认目标产生任何影响。(请参阅模式规则的定义与重定义一节。)
因此,通常会把 makefile 写成让第一条规则成为「用于编译整个程序(或 makefile 所描述的所有程序)的规则」(这个目标常被命名为「all」)。请参阅指定目标的参数一节。
首先来看一个规则的例子:
foo.o : foo.c defs.h # 用于摆弄 frob 的模块
cc -c -g foo.c
这条规则的目标是 foo.o,前置条件是 foo.c 和 defs.h。命令中包含一条命令「cc -c -g foo.c」。命令行以制表符开头,以此标识它是命令。
这条规则陈述了以下两件事:
cc。命令中并未明确提到 defs.h,但因为认为 foo.c 应该会 #include 它,所以才把 defs.h 加入了前置条件。
一般而言,规则具有如下形式:
targets : prerequisites
recipe
…
或者如下形式:
targets : prerequisites ; recipe
recipe
…
targets(目标群)是文件名,以空格分隔。这里可以使用通配符(请参阅在文件名中使用通配符一节)。此外,「a(m)」这种形式的名字,表示归档文件 a 中的成员 m(请参阅作为目标的归档成员一节)。通常一条规则只有一个目标,但有时也有理由设置多个目标(请参阅一条规则有多个目标一节)。
recipe(命令)的行以制表符开头(或者以 .RECIPEPREFIX 变量值的首字符开头,请参阅其他特殊变量一节)。命令的第一行既可以放在前置条件的下一行并加上制表符,也可以放在同一行并加上分号。两种写法的效果相同。命令的语法还有其他一些区别,请参阅在规则中编写命令一节。
美元符号用于开始 make 的变量引用,所以当你想在目标或前置条件中写出真正的美元符号本身时,必须像「$$」那样重叠写两个(请参阅变量的用法一节)。此外,如果启用了二次展开(请参阅二次展开一节),并想在前置条件清单中写出美元符号本身,则实际上需要写四个美元符号(「$$$$」)。
长行可以通过在反斜杠后加换行来分割。不过 make 对 makefile 中单行的长度没有设限,所以这并不是必需的。
规则向 make 传达两件事:即目标何时过时(何时需要更新),以及需要时如何更新。
过时与否的判断标准是用 prerequisites(前置条件)来指定的。前置条件由以空格分隔的文件名组成。(这里同样可以使用通配符以及归档成员(请参阅用 make 更新归档文件一节)。)当目标不存在,或者比任意一个前置条件更旧(比较最后修改时间)时,该目标就被视为过时。其思路是:目标文件的内容是基于前置条件的信息计算出来的,因此只要任意前置条件发生变化,既有目标文件的内容就未必仍然正确了。
如何更新由 recipe(命令)来指定。它是由 shell(通常是「sh」)执行的一行或多行命令,但具备一些额外的功能(请参阅在规则中编写命令一节)。
GNU make 所理解的前置条件有两种。一种是上一节说明的普通前置条件,另一种是仅顺序 (order-only)前置条件。普通前置条件陈述两件事。第一,它规定命令被调用的顺序。目标的所有前置条件的命令完成之后,该目标的命令才会开始。第二,它规定依赖关系。如果任意前置条件比目标新,目标就被视为过时,必须重新生成。
通常,这正是你所期望的行为。因为目标的前置条件更新了,目标本身也应当更新。
但有时会出现这种情况:「想保证前置条件先于目标被构建,但又不想在前置条件更新时强制更新目标」。仅顺序前置条件就是用来建立这种关系的。仅顺序前置条件是通过在前置条件清单中放置竖线符号(|)来指定的。竖线符号左侧的前置条件是普通的,右侧的前置条件是仅顺序的:
targets : normal-prerequisites | order-only-prerequisites
当然,普通前置条件部分也可以为空。此外,同一目标的前置条件还可以分多行声明,它们会被恰当地连接起来(普通前置条件追加到普通前置条件清单,仅顺序前置条件追加到仅顺序前置条件清单)。需要注意的是,如果把同一个文件同时声明为普通前置条件和仅顺序前置条件,那么普通前置条件优先(因为普通前置条件完全涵盖了仅顺序前置条件的行为)。
在判断目标是否过时时,绝不会检查仅顺序前置条件。即使仅顺序前置条件被标记为伪目标(请参阅伪目标 (phony)一节),也不会因此导致目标被重新生成。
作为例子,考虑想把目标放到另一个目录的情况。在运行 make 之前,该目录可能并不存在。这种情形下,你希望在把目标放进去之前先创建好该目录,但由于目录的时间戳会随着文件的增删改名而改变,因此绝对要避免每当目录时间戳改变就重新生成所有目标。管理这一点的一种方法就是仅顺序前置条件。把该目录设为所有目标的仅顺序前置条件即可:
OBJDIR := objdir
OBJS := $(addprefix $(OBJDIR)/,foo.o bar.o baz.o)
$(OBJDIR)/%.o : %.c
$(COMPILE.c) $(OUTPUT_OPTION) $<
all: $(OBJS)
$(OBJS): | $(OBJDIR)
$(OBJDIR):
mkdir $(OBJDIR)
这样一来,创建 objdir 目录的规则在必要时会先于任何「.o」的构建运行。但仅仅因为 objdir 目录的时间戳改变了,并不会导致任何「.o」被构建。
用一个文件名,借助通配符就可以指定多个文件。make 的通配符是「*」「?」「[…]」,与 Bourne shell 的相同。例如 *.c 表示(工作目录内)所有名字以「.c」结尾的文件的清单。
当某个表达式匹配到多个文件时,其结果会被排序。2 不过,多个表达式并不会作为整体一起排序。例如 *.c *.h 会先列出所有名字以「.c」结尾的文件(已排序),再列出所有名字以「.h」结尾的文件(已排序)。
文件名开头的「~」字符也有特殊含义。单独使用,或后面接斜杠时,表示你的主目录。例如 ~/bin 会展开为 /home/you/bin。当「~」后面接着一个词时,该字符串表示这个词所指用户的主目录。例如 ~john/bin 会展开为 /home/john/bin。在没有按用户划分主目录的系统(如 MS-DOS 或 MS-Windows)上,可以通过设置环境变量 HOME 来模拟此功能。
在目标和前置条件中,通配符的展开由 make 自动进行。在命令中,通配符的展开由 shell 负责。在其他场合,只有用 wildcard 函数明确请求时,才会进行通配符展开。
通配符的特殊含义可以通过在其前面加反斜杠来消除。因此 foo\*bar 指的是由「foo」、星号和「bar」组成名字的某个特定文件。
通配符也可以用在规则的命令中。在那里它由 shell 展开。例如,下面是删除所有目标文件(object)的规则:
clean:
rm -f *.o
通配符在规则的前置条件中也很方便。如果在 makefile 中写下下面的规则,那么运行「make print」时,只会打印自上次打印以来被修改过的「.c」文件:
print: *.c
lpr -p $?
touch print
这条规则把 print 用作空目标文件。请参阅用于记录事件的空目标文件一节。(为了只打印被修改的文件,使用了自动变量「$?」。请参阅自动变量一节。)
在定义变量时,不会进行通配符展开。因此,如果像下面这样写:
objects = *.o
变量 objects 的值就是字面意义上的字符串「*.o」。但如果在目标或前置条件中使用 objects 的值,就会在那里进行通配符展开。如果在命令中使用 objects 的值,shell 可能会在命令执行时进行通配符展开。如果想让 objects 设置为展开后的结果,则改用下面的写法:
objects := $(wildcard *.o)
请参阅wildcard 函数一节。
这里来看一个因为草率地使用通配符展开,导致不按预期工作的例子。假设你想用目录内所有目标文件(object)来生成可执行文件 foo,于是写成下面这样:
objects = *.o
foo : $(objects)
cc -o foo $(CFLAGS) $(objects)
objects 的值是字面意义上的字符串「*.o」。由于通配符展开发生在 foo 的规则中,所以每个实际存在的「.o」文件都会成为 foo 的前置条件,并在需要时被重新编译。
但是,如果你把所有「.o」文件都删除了,会怎样呢?当通配符没有匹配到任何一个文件时,它会原样保留。于是 foo 就会依赖于一个名字奇怪的文件 *.o。这样的文件多半不存在,于是 make 会报出「不知道该如何生成 *.o」的错误。这并不是你期望的行为!
其实,用通配符展开得到期望的结果也是可能的,但这需要 wildcard 函数或字符串替换之类稍微高级一点的手法。请参阅wildcard 函数一节。
在 Microsoft 的操作系统(MS-DOS 和 MS-Windows)上,路径名的目录分隔使用反斜杠。如下所示:
c:\foo\bar\baz.c
这与 Unix 形式的 c:/foo/bar/baz.c 等价(c: 部分就是所谓的驱动器盘符)。在这些系统上运行 make 时,路径名中不仅支持 Unix 形式的斜杠,也支持反斜杠。但这种支持不及于通配符展开。因为在通配符展开中,反斜杠是引用字符。因此,在这类场合必须使用 Unix 形式的斜杠。
通配符的展开在规则中是自动进行的。但在设置变量时或函数的参数中,通配符的展开通常不会进行。想在那些地方进行通配符展开时,需要像下面这样使用 wildcard 函数:
$(wildcard pattern…)
无论在 makefile 的何处使用这个字符串,它都会被替换为以空格分隔的、匹配所给文件名模式中任意一个的、实际存在的文件名清单。如果某个模式没有匹配到任何实际存在的文件,那么该模式就会从 wildcard 函数的输出中被去掉。请注意,这与未匹配的通配符在规则中的行为不同。在规则中,未匹配的通配符不会被忽略,而是原样使用(请参阅使用通配符时的陷阱一节)。
与规则中的通配符展开一样,wildcard 函数的结果也会被排序。不过这同样是每个表达式分别排序。因此「$(wildcard *.c *.h)」会展开为匹配「.c」的所有文件(已排序),后接匹配「.h」的所有文件(已排序)。
wildcard 函数的一种用途是,像下面这样获取目录内所有 C 源文件的清单:
$(wildcard *.c)
像下面这样,把结果中「.c」这个后缀替换为「.o」,就能把 C 源文件的清单变成目标文件(object)的清单:
$(patsubst %.c,%.o,$(wildcard *.c))
(这里使用了另一个函数 patsubst。请参阅用于字符串替换与解析的函数一节。)
因此,把目录内所有 C 源文件编译并链接的 makefile 可以写成下面这样:
objects := $(patsubst %.c,%.o,$(wildcard *.c))
foo : $(objects)
cc -o foo $(objects)
(这利用了编译 C 程序的隐含规则,所以无需为各文件编写明确的编译规则。「:=」是「=」的变体,其说明请参阅变量的两种种类 (flavor)一节。)
在大型系统中,常常希望把源码与二进制文件放在不同的目录里。make 的目录查找(directory search)功能会为了找到前置条件而自动查找多个目录,从而让这变得容易。即便在目录之间重新摆放文件,也无需改写各条规则,只要改变搜索路径即可。
VPATH:所有前置条件的搜索路径
make 变量 VPATH 的值,指定了 make 应当查找的目录清单。多数情况下,会设想那些不在当前目录的前置条件文件存放在这些目录里,但 make 会把 VPATH 同时用作规则的前置条件与目标的查找列表。
因此,当被列为目标或前置条件的文件不存在于当前目录时,make 会从 VPATH 所列的目录中查找该名字的文件。如果在其中任意一个目录里找到了文件,那么该文件可能会成为前置条件(详见后述)。这样一来,就可以把前置条件清单中的文件名都当作仿佛存在于当前目录一样写进规则。请参阅顾及目录查找的命令写法一节。
在 VPATH 变量中,目录名以冒号或空白分隔。目录排列的顺序就是 make 查找的顺序。(在 MS-DOS 和 MS-Windows 上,冒号可以用在驱动器盘符之后的路径名本身中,因此 VPATH 的目录名分隔使用分号。)
例如,
VPATH = src:../headers
这样就指定了一条包含 src 和 ../headers 两个目录的路径,make 会按此顺序查找。
在这个 VPATH 值之下,下面的规则,
foo.o : foo.c
会被解释为仿佛写成了下面这样:
foo.o : src/foo.c
不过这是指 foo.c 不存在于当前目录、而在 src 目录中被找到的情形。
vpath 指令
与 VPATH 变量相似但更具选择性的,是 vpath 指令(注意是小写)。用它可以为特定类别的文件名——匹配某个模式的——指定搜索路径。借此,可以给某一类文件名指定特定的查找目录,而给另一类文件名指定别的目录(或不指定)。
vpath 指令有三种形式:
vpath pattern directories为匹配 pattern 的文件名指定 directories 作为搜索路径。
搜索路径 directories 是应当查找的目录清单,与 VPATH 变量所用的搜索路径一样,以冒号(在 MS-DOS 和 MS-Windows 上为分号)或空白分隔。
vpath pattern清除与 pattern 关联的搜索路径。
vpath
清除迄今为止用 vpath 指令指定的所有搜索路径。
vpath 的模式是含有「%」字符的字符串。这个字符串必须匹配正在被查找的前置条件的文件名。「%」字符匹配零个或多个任意字符的字符串(与模式规则相同。请参阅模式规则的定义与重定义一节)。例如 %.h 匹配以 .h 结尾的文件。(在没有「%」的情况下,模式必须与前置条件完全一致,但这很少有用。)
vpath 指令的模式中的「%」字符,可以在前面加反斜杠(「\」)来引用。引用「%」字符的反斜杠本身,可以再叠加反斜杠来引用。引用「%」字符或另一个反斜杠的反斜杠,会在模式与文件名比较之前被去掉。不会引用「%」字符的反斜杠则原样保留。
当前置条件不存在于当前目录时,如果某条 vpath 指令的 pattern 匹配了该前置条件文件的名字,那么该指令的 directories 会与 VPATH 变量的目录一样(并且先于它)被查找。
例如,
vpath %.h ../headers
这会指示 make:当名字以 .h 结尾的前置条件在当前目录中找不到时,到 ../headers 目录中查找。
当多个 vpath 模式匹配某个前置条件文件的名字时,make 会逐一处理匹配的 vpath 指令,查找各指令中所列的全部目录。make 按 makefile 中出现的顺序处理多条 vpath 指令。具有相同模式的多条指令彼此独立。
因此,
vpath %.c foo vpath % blish vpath %.c bar
会按 foo、再 blish、再 bar 的顺序查找以「.c」结尾的文件,而另一方面,
vpath %.c foo:bar vpath % blish
会按 foo、再 bar、再 blish 的顺序查找以「.c」结尾的文件。
当通过目录查找(无论是一般的还是选择性的)找到前置条件时,找到的路径名未必就是 make 实际在前置条件清单中提供给你的那个。目录查找发现的路径有时也会被丢弃。
make 用来决定保留还是丢弃目录查找所找到路径的算法如下:
make 不需要重新生成目标,就使用目录查找所找到的路径。
make 不得不重新生成时,目标就会在本地重新生成,而不是在目录查找所找到的目录中。
这个算法看起来或许复杂,但实际上,这往往恰恰正是你所期望的。
其他版本的 make 使用更简单的算法:即只要文件不存在、而通过目录查找找到了它,那么无论目标是否需要重新生成,都一律使用该路径名。因此,当目标被重新生成时,它会被生成在目录查找所发现的路径名处。
其实,如果你希望对部分或全部目录采用这种行为,可以用 GPATH 变量把它告诉 make。
GPATH 具有与 VPATH 相同的语法和形式(即以空白或冒号分隔的路径名清单)。当某个过时的目标在同样出现于 GPATH 中的目录里通过目录查找被找到时,其路径名不会被丢弃。目标会使用展开后的路径重新生成。
即便通过目录查找在别的目录中找到了前置条件,规则的命令也不会因此改变。命令会按写好的样子执行。因此,编写命令时必须留意,让它在 make 找到前置条件的那个目录里查找该前置条件。
这要借助「$^」之类的自动变量来实现(请参阅自动变量一节)。例如「$^」的值是规则所有前置条件的清单,也包含它们被找到的目录名。而「$@」的值是目标。因此:
foo.o : foo.c
cc -c $(CFLAGS) $^ -o $@
(变量 CFLAGS 之所以存在,是为了让你能指定通过隐含规则进行 C 编译时所传递的标志。这里以统一的语义使用它,使其对所有 C 编译一律生效。请参阅隐含规则所用的变量一节。)
前置条件中常常也包含头文件,但那些是不想在命令中提及的。自动变量「$<」只表示第一个前置条件:
VPATH = src:../headers
foo.o : foo.c defs.h hack.h
cc -c $(CFLAGS) $< -o $@
用 VPATH 或 vpath 指定的目录的查找,在考虑隐含规则时也会进行(请参阅隐含规则的用法一节)。
例如,当文件 foo.o 没有明确的规则时,make 会考虑隐含规则。例如,当 foo.c 存在时就编译它的内置规则等。如果在当前目录里找不到这个文件,就会查找适当的目录。当 foo.c 存在于那些目录中的任意一个(或在 makefile 中被提及)时,就会应用 C 编译的隐含规则。
隐含规则的命令出于需要通常会使用自动变量。其结果是,无需特别费力,目录查找所找到的文件名就会被原样使用。
目录查找对链接器所用的库有一种特殊的应用方式。当你写下名字形如「-lname」的前置条件时,这种特殊机制就会发挥作用。(这里发生了某种不寻常的事,可以从这一点看出:前置条件通常是文件的名字,而库的文件名一般不是「-lname」,而是形如 libname.a 的样子。)
当前置条件的名字形如「-lname」时,make 会特殊对待它,先查找一个名为 libname.so 的文件,找不到的话再查找名为 libname.a 的文件,按当前目录、匹配的 vpath 搜索路径与 VPATH 搜索路径所指定的目录、然后是 /lib、/usr/lib、prefix/lib(通常是 /usr/local/lib,但 MS-DOS/MS-Windows 版的 make 会表现得仿佛 prefix 被定义为 DJGPP 安装树的根)各目录的顺序查找。
例如,系统上有 /usr/lib/libcurses.a 库(且没有 /usr/lib/libcurses.so 文件)时,
foo : foo.c -lcurses
cc $^ -o $@
那么当 foo 比 foo.c 或 /usr/lib/libcurses.a 旧时,就会执行「cc foo.c /usr/lib/libcurses.a -o foo」这条命令。
被查找的文件的默认集合是 libname.so 和 libname.a,但这可以用 .LIBPATTERNS 变量来自定义。该变量值中的每个词都是模式字符串。当出现像「-lname」这样的前置条件时,make 会把清单中各模式内的百分号替换为 name,并用得到的各个库文件名进行上述目录查找。
.LIBPATTERNS 的默认值是「lib%.so lib%.a」,这给出了上面所说明的默认行为。
把这个变量设为空值,就可以完全禁用链接库的展开。
伪目标(phony target)是指实际上并非某个文件名字的目标。它更像是你为某段在明确请求时才执行的命令所起的一个名字而已。使用伪目标有两个理由:避免与同名文件冲突,以及提升性能。
如果写了一条带有「不生成目标文件」命令的规则,那么每当该目标被纳入重新生成的考量时,命令就会执行。下面是一个例子:
clean:
rm *.o temp
由于 rm 命令不会生成名为 clean 的文件,所以这样的文件大概永远不会存在。因此,每当输入「make clean」时,rm 命令都会执行。
在这个例子中,如果在这个目录里恰好生成了一个名为 clean 的文件,clean 目标就会无法正常工作。由于它没有前置条件,clean 会总是被视为最新,其命令也就不再执行。为避免这个问题,可以把该目标设为特殊目标 .PHONY 的前置条件,从而明确声明它是伪目标(请参阅特殊的内置目标名一节)。如下所示:
.PHONY: clean
clean:
rm *.o temp
这样一来,无论是否存在名为 clean 的文件,「make clean」都会执行命令。
.PHONY 的前置条件总是被解释为字面意义上的目标名(即便其中含有「%」字符),不会当作模式处理。如果想让模式规则总是被重新生成,请考虑使用「force 目标(强制目标)」(请参阅既无命令也无前置条件的规则一节)。
伪目标与 make 的递归调用(请参阅make 的递归用法一节)组合起来也很方便。在这种情形下,makefile 常常包含一个列举要构建的子目录数量的变量。处理它的一种省事方法,是定义一条带有「在循环中遍历子目录」命令的规则。如下所示:
SUBDIRS = foo bar baz
subdirs:
for dir in $(SUBDIRS); do \
$(MAKE) -C $$dir; \
done
但是,这种方法有几个问题。第一,子 make 中检测到的错误会被这条规则忽略,因此就算其中一个失败,也会继续构建其余目录。这可以通过追加一条「记录错误并退出」的 shell 命令来克服,但那样一来,即便 make 是带 -k 选项启动的也会退出,那是不合适的。第二,并且或许更重要的是,由于只有一条规则,无法充分发挥 make 并行构建目标的能力(请参阅并行执行一节)。各个 makefile 的目标会被并行构建,但子目录却只能一次构建一个。
如果把子目录声明为 .PHONY 目标(由于子目录显然总是存在,这是必须做的,否则它们不会被构建),就能消除这些问题:
SUBDIRS = foo bar baz
.PHONY: subdirs $(SUBDIRS)
subdirs: $(SUBDIRS)
$(SUBDIRS):
$(MAKE) -C $@
foo: baz
这里还进一步声明了:foo 子目录在 baz 子目录完成之前无法构建。这种关系的声明在尝试并行构建时尤为重要。
隐含规则的查找(请参阅隐含规则的用法一节)会对 .PHONY 目标省略。这就是为什么即便你并不在意实际文件是否存在,把目标声明为 .PHONY 在性能上也有好处的原因。
伪目标不应成为实际存在的目标文件的前置条件。如果那样做了,每当 make 考量该文件时,伪目标的命令就会执行。只要伪目标没有成为实际目标的前置条件,伪目标的命令就只会在该伪目标是被指定的目标(goal)时才执行(请参阅指定目标的参数一节)。
不要把被 include 的 makefile 声明为伪目标。伪目标并不意图表示实际文件,而且由于目标总是被视为过时,make 会总是把它重新生成、然后重新运行自身(请参阅makefile 是如何被重新生成的一节)。为避免这一点,即便被标记为伪的 include 文件被重新生成,make 也不会重新运行自身。
伪目标可以拥有前置条件。当一个目录包含多个程序时,把它们全部写进一个 makefile ./Makefile 中是最方便的。由于默认被重新生成的目标是 makefile 中的第一个,所以一般会把它设为一个名为「all」的伪目标,并在其前置条件中列出所有的各个程序。例如:
all : prog1 prog2 prog3
.PHONY : all
prog1 : prog1.o utils.o
cc -o prog1 prog1.o utils.o
prog2 : prog2.o
cc -o prog2 prog2.o
prog3 : prog3.o sort.o utils.o
cc -o prog3 prog3.o sort.o utils.o
这样一来,只需输入「make」就能重新生成全部三个程序,也可以把想重新生成的指定为参数(如「make prog1 prog3」)。「伪」这一性质不会被继承。伪目标的前置条件,除非被明确声明,否则其本身并不会变成伪目标。
当某个伪目标成为另一个伪目标的前置条件时,它就扮演了后者的子例程的角色。例如,在下面的例子中,「make cleanall」会删除目标文件(object)、差分文件以及 program 文件:
.PHONY: cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
rm program
cleanobj :
rm *.o
cleandiff :
rm *.diff
当某条规则既无前置条件也无命令,且这条规则的目标是一个不存在的文件时,make 会在每次执行这条规则时,把这个目标视为已被更新。这意味着,所有依赖于这个目标的目标都会总是执行其命令。
看个例子就明白了:
clean: FORCE
rm $(objects)
FORCE:
这里目标「FORCE」满足上述特殊条件,所以依赖于它的目标 clean 被强制执行命令。「FORCE」这个名字本身没有特殊含义,但它是这一用途中常用的名字之一。
如你所见,这样使用「FORCE」与使用「.PHONY: clean」会得到相同的结果。
使用「.PHONY」更明确也更高效。但其他版本的 make 并不支持「.PHONY」。因此「FORCE」会出现在许多 makefile 中。请参阅伪目标 (phony)一节。
空目标(empty target)是伪目标的一种,用来保存供偶尔明确请求的处理所用的命令。与伪目标不同,这种目标文件实际存在也无妨。只是文件的内容并不重要,通常是空的。
空目标文件的目的,是通过其最后修改时间来记录规则的命令最后一次执行的时刻。之所以能做到这一点,是因为命令中的某条命令是更新目标文件的 touch 命令。
空目标文件应当有某种前置条件(否则就没有意义了)。当请求重新生成空目标时,如果任意前置条件比目标新——换言之,自上次重新生成目标以来任意前置条件发生了变化——命令就会执行。下面是一个例子:
print: foo.c bar.c
lpr -p $?
touch print
有了这条规则,当自上次「make print」以来两个源文件中任意一个发生变化时,「make print」就会执行 lpr 命令。为了只打印被修改的文件,使用了自动变量「$?」(请参阅自动变量一节)。
特定的名字作为目标出现时,会具有特殊含义。
.PHONY
特殊目标 .PHONY 的前置条件被视为伪目标。当这样的目标被纳入考量时,make 会无条件地执行其命令,而不管该名字的文件是否存在、其最后修改时间如何。请参阅伪目标 (phony)一节。
.SUFFIXES
特殊目标 .SUFFIXES 的前置条件,是在考察后缀规则时所用的后缀清单。请参阅旧式的后缀规则一节。
.DEFAULT
为 .DEFAULT 指定的命令,会被用于所有找不到规则(无论是显式规则还是隐含规则)的目标。请参阅定义作为最后手段的默认规则一节。当指定了 .DEFAULT 的命令时,对于在规则中作为前置条件被列出、但未作为目标被列出的所有文件,都会改为执行该命令。请参阅隐含规则的查找算法一节。
.PRECIOUS
被列为 .PRECIOUS 前置条件的目标会获得下面的特殊待遇。即,即便在命令执行过程中 make 被强制终止或被中断,该目标也不会被删除。请参阅make 的中断与强制终止一节。此外,当目标是中间文件时,通常会在不再需要后被删除,而此时也不会执行这种删除。请参阅隐含规则的串接一节。在后一点上,它与 .SECONDARY 特殊目标的作用有所重叠。
也可以把隐含规则的目标模式(如「%.o」)列为 .PRECIOUS 特殊目标的前置条件文件。这样一来,凡是目标名匹配该模式的规则所生成的中间文件都会被保留。
.INTERMEDIATE
.INTERMEDIATE 所依赖的目标会被当作中间文件处理。请参阅隐含规则的串接一节。没有前置条件的 .INTERMEDIATE 不产生任何效果。
.NOTINTERMEDIATE
特殊目标 .NOTINTERMEDIATE 的前置条件,绝不会被视为中间文件。请参阅隐含规则的串接一节。没有前置条件的 .NOTINTERMEDIATE 会使所有目标都被当作非中间文件处理。
当前置条件是目标模式时,使用该模式规则构建的目标就不再被视为中间文件。
.SECONDARY
.SECONDARY 所依赖的目标会被当作中间文件处理。但是,绝不会被自动删除。请参阅隐含规则的串接一节。
.SECONDARY 在一些少见的情形下可用于避免多余的重新构建。例如:
hello.bin: hello.o bye.o
$(CC) -o $@ $^
%.o: %.c
$(CC) -c -o $@ $<
.SECONDARY: hello.o bye.o
考虑这样的情形:hello.bin 相对于源文件而言是最新的,但是目标文件 hello.o 不见了。如果没有 .SECONDARY,make 会在源文件并未变化的情况下重新生成 hello.o,接着也会重新生成 hello.bin。把 hello.o 声明为 .SECONDARY,make 就不再需要重新生成它,hello.bin 也不再需要重新生成。当然,如果任意源文件被更新了,为使 hello.bin 的生成得以成功,所有目标文件都会被重新生成。
没有前置条件的 .SECONDARY 会使所有目标都被当作 secondary 处理(即不再有任何目标因为被视为中间文件而被删除)。
.SECONDEXPANSION
当 .SECONDEXPANSION 在 makefile 中的某处作为目标被提及时,所有在它出现之后定义的前置条件清单,都会在读取完所有 makefile 之后再被展开一次。请参阅二次展开一节。
.DELETE_ON_ERROR
当 .DELETE_ON_ERROR 在 makefile 中的某处作为目标被提及时,如果某条规则的目标被修改过、且其命令以非零退出状态结束,make 会像收到信号时那样删除该目标。请参阅命令中的错误一节。
.IGNORE
为 .IGNORE 指定前置条件时,make 会忽略那些特定文件的命令执行时的错误。.IGNORE 的命令(如果有的话)会被忽略。
作为没有前置条件的目标被提及时,.IGNORE 指示忽略所有文件的命令执行时的错误。这种「.IGNORE」的用法仅出于历史兼容性而被支持。由于它影响 makefile 内的所有命令,所以并不太方便。建议使用更具选择性的方法来忽略特定命令的错误。请参阅命令中的错误一节。
.LOW_RESOLUTION_TIME
为 .LOW_RESOLUTION_TIME 指定前置条件时,make 会把那些文件视为由生成低分辨率时间戳的命令所创建的。.LOW_RESOLUTION_TIME 目标的命令会被忽略。
许多现代文件系统所具备的高分辨率文件时间戳,会减少 make 错误判断文件为最新的可能性。但遗憾的是,某些主机并不提供设置高分辨率文件时间戳的手段,因此像「cp -p」这种明确设置文件时间戳的命令,不得不舍弃其亚秒部分。由这类命令生成的文件,应当被列为 .LOW_RESOLUTION_TIME 的前置条件,以免 make 错误地把该文件判断为过时。例如:
.LOW_RESOLUTION_TIME: dst
dst: src
cp -p src dst
由于「cp -p」会舍弃 src 时间戳的亚秒部分,所以 dst 即便是最新的,通常也会比 src 稍旧一点。有了 .LOW_RESOLUTION_TIME 这一行,当 dst 的时间戳与 src 的时间戳处于同一秒的开头时,make 就会把 dst 视为最新。
由于归档格式的限制,归档成员的时间戳总是低分辨率的。无需把归档成员列为 .LOW_RESOLUTION_TIME 的前置条件,因为 make 会自动这样做。
.SILENT
为 .SILENT 指定前置条件时,make 在执行之前不再显示用于重新生成那些特定文件的命令。.SILENT 的命令会被忽略。
作为没有前置条件的目标被提及时,.SILENT 指示在执行命令之前一律不显示。也可以使用更具选择性的方法,只让特定命令的命令行变静默。请参阅命令的回显一节。如果想让 make 的某次特定运行中的全部命令都静默,请使用「-s」或「--silent」选项(请参阅选项一览一节)。
.EXPORT_ALL_VARIABLES
仅仅作为目标被提及,就会指示 make 默认把所有变量导出给子进程。这是使用不带参数的 export 的方法的一种替代。请参阅向子 make 传递变量一节。
.NOTPARALLEL
当 .NOTPARALLEL 作为没有前置条件的目标被提及时,本次 make 调用中的所有目标都会被串行执行,即便给出了「-j」选项。被递归调用的 make 命令仍会并行执行命令(除非其 makefile 也包含这个目标)。
当 .NOTPARALLEL 以目标作为前置条件时,那些目标的所有前置条件都会被串行执行。这相当于在所列各目标的每个前置条件之间隐式地加入 .WAIT。请参阅禁用并行执行一节。
.ONESHELL
当 .ONESHELL 作为目标被提及时,在构建目标时,命令的所有行会被传给一次 shell 调用,而不是各行分别被调用。请参阅命令的执行一节。
.POSIX
当 .POSIX 作为目标被提及时,makefile 会以 POSIX 兼容模式被解析和执行。这并不意味着只接受符合 POSIX 的 makefile。GNU make 的所有高级功能依然可用。这个目标的作用,是让 make 在其默认行为与 POSIX 有所不同的地方,按 POSIX 所要求的那样表现。
特别是,提及这个目标后,命令会仿佛向 shell 传了 -e 标志那样被调用。即,命令内最先失败的命令会使命令立即失败。
预定义的隐含规则的后缀,作为目标出现时也会被视为特殊目标。把两个后缀连接起来的「.c.o」之类同样如此。这些目标是后缀规则,是定义隐含规则的旧式方法(尽管至今仍被广泛使用)。原理上,任何目标名都可以把它分成两部分、并把两个片段都加入后缀列表,从而像这样赋予特殊含义。实际上,由于后缀通常以「.」开头,这些特殊目标名也以「.」开头。请参阅旧式的后缀规则一节。
当一条显式规则有多个目标时,它们会以两种方式之一被处理。即,被当作独立的目标处理,或被当作分组目标处理。采用哪种处理方式,取决于出现在目标清单之后的分隔符。
使用标准目标分隔符「:」的规则,定义的是独立的目标。这等同于让前置条件与命令重复出现,为每个目标各写一遍相同的规则。命令中通常会用「$@」之类的自动变量来指定正在构建的是哪个目标。
具有独立目标的规则在以下两种情况下很方便:
kbd.o command.o files.o: command.h
为所列的三个目标文件(object)各自添加一个额外的前置条件。这等同于写成下面这样:
kbd.o: command.h command.o: command.h files.o: command.h
bigoutput littleoutput : text.g
generate text.g -$(subst output,,$@) > $@
等同于下面这样。
bigoutput : text.g
generate text.g -big > bigoutput
littleoutput : text.g
generate text.g -little > littleoutput
这里假设了虚构的程序 generate 在被给予「-big」时与被给予「-little」时,会生成两种不同的输出。关于 subst 函数的说明,请参阅用于字符串替换与解析的函数一节。
顺带一提,正如变量「$@」可以改变命令那样,或许你也想根据目标来改变前置条件。这用普通规则的多个目标是做不到的,但用静态模式规则就能做到。请参阅静态模式规则一节。
当有一段命令不是构建独立目标、而是用一次调用生成多个文件时,可以通过声明规则使用分组目标(grouped targets)来表达这种关系。分组目标的规则使用分隔符「&:」(这里「&」用来暗示「全部」)。
当 make 构建分组目标中的任意一个时,make 明白:作为命令调用的结果,组内其他所有目标也会被更新。此外,即便只有分组目标中的一部分过时或缺失,make 也会认识到只要执行命令所有目标就都会更新。最后,只要分组目标中的任意一个过时,所有分组目标都会被视为过时。
作为例子,下面的规则定义了分组目标:
foo bar biz &: baz boz
echo $^ > foo
echo $^ > bar
echo $^ > biz
在分组目标的命令执行过程中,自动变量「$@」会被设为组内触发该规则的那个特定目标的名字。在分组目标的规则的命令中依赖这个变量时需要小心。
与独立目标不同,分组目标的规则必须包含命令。不过,作为分组目标成员的目标,也可以出现在不带命令的独立目标的规则定义中。
每个目标只能关联一条命令。当某个分组目标出现在独立目标的规则中,或出现在另一条带命令的分组目标的规则中时,会发出警告,并且后者的命令会替换前者的命令。此外,该目标会被从之前的组中移除,只出现在新的组中。
如果想让某个目标出现在多个组中,那么在声明所有包含该目标的组时,必须使用双冒号的分组目标分隔符「&::」。分组的双冒号目标各自独立处理,每条分组双冒号规则的命令,会在其多个目标中至少有一个需要更新时,最多执行一次。
一个文件可以成为多条规则的目标。所有规则中提到的所有前置条件,会被汇总进该目标的一个前置条件清单。只要目标比任意一条规则的任意前置条件更旧,命令就会执行。
对于一个文件,只能拥有一条会被执行的命令。当两条或更多规则给同一文件提供命令时,make 会使用最后给出的那条,并显示一条错误消息。(作为特殊情况,当文件的名字以点开头时,不会显示错误消息。这种奇怪的行为只是为了与其他 make 实现兼容,所以……请避免使用。)有时会想让同一个目标触发在 makefile 不同位置定义的多条命令。为此可以使用双冒号规则(请参阅双冒号规则一节)。
使用只带前置条件的额外规则,就能一次性为众多文件添加若干额外的前置条件。例如,makefile 中常常有像 objects 这样的变量,持有要构建的系统内所有编译器输出文件的清单。要简便地表达「只要 config.h 变了,它们就都必须重新编译」,可以写成下面这样:
objects = foo.o bar.o foo.o : defs.h bar.o : defs.h test.h $(objects) : config.h
这可以在不改动实际指定目标文件(object)生成方式的规则的情况下插入或移除。在想要时断时续地添加额外前置条件时,这是很方便的形式。
另一种巧妙做法是,把额外的前置条件用一个由 make 的命令行参数设置的变量来指定(请参阅变量的覆盖一节)。例如,
extradeps= $(objects) : $(extradeps)
这样一来,「make extradeps=foo.h」这条命令会把 foo.h 视为各目标文件(object)的前置条件,而单纯的「make」则不会。
当某个目标的所有显式规则都没有命令时,make 会查找可适用的隐含规则并试图找出一条(请参阅隐含规则的用法一节)。
静态模式规则(static pattern rules)是指定多个目标,并根据目标名构成各目标前置条件名的规则。它比有多个目标的普通规则更通用。因为各目标之间无需拥有相同的前置条件。它们的前置条件需要相似,但不一定相同。
静态模式规则的语法如下:
targets …: target-pattern: prereq-patterns …
recipe
…
targets 清单指定这条规则所适用的目标。目标中可以像普通规则的目标一样包含通配符(请参阅在文件名中使用通配符一节)。
target-pattern 和 prereq-patterns 陈述如何计算各目标的前置条件。每个目标与 target-pattern 比对,从中抽取目标名的一部分——称为词干(stem)。这个词干被代入各 prereq-pattern,从而构成前置条件名(从各 prereq-pattern 各构成一个)。
每个模式通常恰好含有一个「%」字符。当 target-pattern 匹配某个目标时,「%」可以匹配目标名的任意部分,这部分就称为词干。模式的其余部分必须完全一致。例如,目标 foo.o 匹配模式「%.o」,「foo」成为词干。目标 foo.c 或 foo.out 不匹配这个模式。
各目标的前置条件名,是把各前置条件模式中的「%」替换为词干而构成的。例如,某个前置条件模式是 %.c 时,代入词干「foo」就得到前置条件名 foo.c。也允许写不含「%」的前置条件模式。这种情况下,该前置条件对所有目标都相同。
模式规则的「%」字符,可以在前面加反斜杠(「\」)来引用。引用「%」字符的反斜杠本身,可以再叠加反斜杠来引用。引用「%」字符或另一个反斜杠的反斜杠,会在模式与文件名比较、或代入词干之前被去掉。不会引用「%」字符的反斜杠则原样保留。例如,在模式 the\%weird\\%pattern\\ 中,有效的「%」字符前面是「the%weird\」,其后接着「pattern\\」。末尾的两个反斜杠由于不影响任何「%」字符,所以原样保留。
下面是把 foo.o 和 bar.o 各自从对应的 .c 文件编译的例子:
objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
这里「$<」是保存前置条件名字的自动变量,「$@」是保存目标名字的自动变量。请参阅自动变量一节。
所指定的每个目标都必须匹配目标模式。对每个不匹配的目标都会发出警告。如果有一份只有部分匹配模式的文件清单,可以使用 filter 函数去掉不匹配的文件名(请参阅用于字符串替换与解析的函数一节):
files = foo.elc bar.o lose.o
$(filter %.o,$(files)): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
$(filter %.elc,$(files)): %.elc: %.el
emacs -f batch-byte-compile $<
在这个例子中,「$(filter %.o,$(files))」的结果是 bar.o lose.o,第一条静态模式规则会通过编译各自对应的 C 源文件来更新这些目标文件(object)。「$(filter %.elc,$(files))」的结果是 foo.elc,所以那个文件会从 foo.el 生成。
另一个例子展示了在静态模式规则中如何使用 $*:
bigoutput littleoutput : %output : text.g
generate text.g -$* > $@
当 generate 命令执行时,$* 会展开为词干——「big」或「little」。
静态模式规则与作为模式规则定义的隐含规则(请参阅模式规则的定义与重定义一节)有许多共同点。两者都有目标的模式,以及构成前置条件名字的模式。区别在于 make 如何决定规则何时适用。
隐含规则可以适用于匹配其模式的任何目标,但它被适用仅限于该目标在别处没有指定命令、且前置条件能被找到的情况。当多条隐含规则看起来都可适用时,其中只有一条会被适用。哪一条被选中取决于规则的顺序。
与之相对,静态模式规则适用于该规则中所指定的确切目标清单。它无法适用于此外的目标,而对所指定的每个目标必定适用。当两条都带命令、相互冲突的规则都适用时,那就是一个错误。
之所以说静态模式规则优于隐含规则,是出于以下理由:
make 使用了错误的隐含规则。哪一条被选中有时取决于隐含规则查找进行的顺序。用静态模式规则就没有不确定性。每条规则恰好适用于所指定的目标本身。
双冒号(double-colon)规则是在目标名之后加「::」而非「:」来书写的显式规则。当同一目标出现在多条规则中时,它会受到与普通规则不同的对待。带双冒号的模式规则具有完全不同的含义(请参阅匹配任意内容的模式规则一节)。
当某个目标出现在多条规则中时,那些规则必须全部为同一种类——全部是普通规则,或全部是双冒号。在双冒号的情况下,每一条都彼此独立。当目标比该规则的任意前置条件更旧时,每条双冒号规则的命令就会执行。当该规则没有前置条件时,其命令总会执行(即便目标已经存在)。其结果是,双冒号规则中可能一条都不执行,也可能只执行一部分,或者全部执行。
具有同一目标的双冒号规则,其实彼此完全分离。每条双冒号规则都会被单独处理,就像处理具有不同目标的规则那样。
某个目标的双冒号规则会按 makefile 中出现的顺序执行。话虽如此,双冒号规则真正有意义,是在执行命令的顺序无关紧要的情况下。
双冒号规则有点难懂,也很少有用。它为「更新目标的方式因哪个前置条件文件成为更新起因而不同」这种情况提供了机制,但这样的情况很少见。
每条双冒号规则都应当指定命令。不指定的话,如果有可适用的隐含规则就会使用它。请参阅隐含规则的用法一节。
在程序的 makefile 中,需要编写的规则中有很多只是单纯陈述「某个目标文件(object)依赖于某个头文件」而已,这种情况很常见。例如,当 main.c 经由 #include 使用 defs.h 时,会写成下面这样:
main.o: defs.h
这条规则是必要的,用来告诉 make:每当 defs.h 变化时就必须重新生成 main.o。可以想见,对于大型程序,要在 makefile 中写下几十条这样的规则。而且,每当增删一个 #include 时,都必须时刻格外留心,别忘了更新 makefile。
为避免这种麻烦,现代的 C 编译器大多会查看源文件内的 #include 行,替你写好这些规则。通常用编译器的「-M」选项来做到这一点。例如,下面这条命令:
cc -M main.c
会生成下面的输出:
main.o : main.c defs.h
这样一来,就不必再自己写下那些规则了。编译器会替你做。
需要注意的是,由于这样的规则会在 makefile 中提及 main.o,所以隐含规则的查找绝不会把它视为中间文件。这意味着,make 在使用那个文件之后绝不会删除它。请参阅隐含规则的串接一节。
在旧的 make 程序中,使用这一编译器功能、用「make depend」之类的命令随时生成前置条件,是传统的做法。那条命令会创建一个名为 depend 的文件,包含所有自动生成的前置条件,makefile 只要用 include 把它读进来即可(请参阅引入其他 makefile一节)。
在 GNU make 中,由于有重新生成 makefile 的功能,这种做法已经过时了——make 总会重新生成过时的 makefile,所以再也不必明确告诉 make 去重新生成前置条件了。请参阅makefile 是如何被重新生成的一节。
关于自动生成前置条件,我们推荐的做法是让每个源文件各对应一个 makefile。对每个源文件 name.c,准备一个 makefile name.d,其中列举目标文件 name.o 依赖于哪些文件。这样一来,就只需重新扫描被修改过的源文件,重新生成新的前置条件。
下面是从 C 源文件 name.c 生成名为 name.d 的前置条件文件(即 makefile)的模式规则:
%.d: %.c
@set -e; rm -f $@; \
$(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$
关于模式规则的定义,请参阅模式规则的定义与重定义一节。向 shell 传的「-e」标志,会在 $(CC) 命令(或其他任何命令)失败(以非零状态结束)时,让 shell 立即退出。
在 GNU C 编译器中,或许你会想用「-MM」标志代替「-M」。它会省略关于系统头文件的前置条件。详情请参阅 Using GNU CC 的控制预处理器的选项。
sed 命令的目的,是把(例如)下面的:
main.o : main.c defs.h
转换成下面这样:
main.o main.d : main.c defs.h
借此,让每个「.d」文件都依赖于对应的「.o」文件所依赖的所有源文件和头文件。于是 make 就明白:只要任意源文件或头文件发生变化,就必须重新生成前置条件。
定义好重新生成「.d」文件的规则之后,接下来用 include 指令把它们全部读进来。请参阅引入其他 makefile一节。例如:
sources = foo.c bar.c include $(sources:.c=.d)
(这个例子使用替换引用,把源文件的清单「foo.c bar.c」转换成前置条件 makefile 的清单「foo.d bar.d」。关于替换引用的详细信息,请参阅替换引用一节。)「.d」文件也和其他文件一样是 makefile,所以即便你什么都不做,make 也会按需重新生成它们。请参阅makefile 是如何被重新生成的一节。
需要注意的是,由于「.d」文件中包含目标定义,所以 include 指令务必放在 makefile 中第一个默认目标之后。否则就有把某个目标文件变成默认目标的风险。请参阅make 处理 makefile 的机制一节。
[译注] 关于正文中的脚注 (2)。原典的脚注记载:「某些旧版本的 GNU make 不会对通配符展开的结果排序」。在现今的 GNU make 中,每个通配符表达式的结果都会被排序。