规则的命令 (recipe,规则要执行的内容)由一行或多行待执行的 shell 命令组成。这些命令按照书写的顺序逐条执行。通常,执行这些命令的结果是使该规则的目标变为最新状态。
用户使用的 shell 程序各不相同,但除非 makefile 另有指定,makefile 中的命令始终由 /bin/sh 解释执行。参见命令的执行。
makefile 有一个不太寻常的性质:在同一个文件中,其实混杂着两种不同的语法。makefile 的大部分使用 make 的语法(参见如何编写 Makefile)。然而命令部分是要交给 shell 解释的,因此用 shell 的语法书写。make 程序并不试图理解 shell 的语法。在把命令内容交给 shell 之前,它只对其做极少数特定的转换。
命令的每一行都必须以制表符(Tab)开头(或者以 .RECIPEPREFIX 变量中设置的字符开头;参见其他特殊变量)。但有一个例外:命令的第一行可以用分号紧跟在写有目标和前置条件 (prerequisite) 的行之后。只要某一行以制表符开头,并且出现在「规则的上下文」中(也就是从某条规则开始,直到出现另一条规则或变量定义为止),它就被视为该规则命令的一部分。命令各行之间可以出现空行或仅含注释的行,它们会被忽略。
由上述规则可以得出如下推论:
make 的注释;它会被原样传给 shell。shell 是否将其当作注释,取决于你所用的 shell。
make 的变量定义,并被传给 shell。
ifdef、ifeq 等;参见条件语句的语法),如果行首第一个字符是用制表符缩进的,就会被视为命令的一部分并被传给 shell。
make 解释命令的少数场景之一,就是检查换行符之前是否有反斜杠。与 makefile 的常规语法一样,通过在每个换行符前放置反斜杠,逻辑上的一行命令可以在 makefile 中拆分成多个物理行书写。这样连续的若干行被视为一行命令,执行它时只会启动一次 shell。
不过,与 makefile 中其他位置的处理方式(参见拆分长行)不同,在命令内部,反斜杠和换行符的组合不会被去除。反斜杠和换行符都会被原样保留并传给 shell。这个反斜杠/换行符如何被解释,取决于你所用的 shell。如果反斜杠/换行符之后下一行的第一个字符是命令前缀字符(默认是制表符;参见其他特殊变量),那么这个字符(且仅有这一个字符)会被去除。命令中绝不会被添加空白。
例如,看看下面这个 makefile 中 all 目标的命令:
all :
@echo no\
space
@echo no\
space
@echo one \
space
@echo one\
space
它由四条独立的 shell 命令组成,其输出如下:
nospace nospace one space one space
再看一个稍微复杂的例子,这个 makefile:
all : ; @echo 'hello \
world' ; echo "hello \
world"
它会用下面这条命令启动一次 shell:
echo 'hello \
world' ; echo "hello \
world"
然后,按照 shell 的引用规则,得到如下输出:
hello \ world hello world
请注意,在用双引号("…")括起来的字符串中,反斜杠/换行符的组合被去除了,而在用单引号('…')括起来的字符串中却没有被去除。这就是默认 shell(/bin/sh)处理反斜杠/换行符的方式。如果你在 makefile 中指定了别的 shell,处理方式可能会有所不同。
有时你想在单引号内部拆分长行,但又不希望反斜杠/换行符出现在被引用的内容里。在把脚本传给诸如 Perl 这类语言时,这种情况很常见,因为在这些语言中,脚本里多出来的反斜杠可能会改变其含义,甚至导致语法错误。应对这一点的一个简单办法,是先把被引用的字符串、乃至整条命令放进一个 make 变量,然后在命令中使用这个变量。这样就会套用 makefile 的换行引用规则,反斜杠/换行符会被去除。把前面的例子用这种方法重写:
HELLO = 'hello \ world' all : ; @echo $(HELLO)
就能得到如下输出:
hello world
如果你愿意,也可以使用针对目标的变量(参见针对目标的变量值),从而让变量与使用它的命令之间形成更紧密的对应关系。
make 对命令进行的另一项处理,是展开命令中的变量引用(参见变量引用基础)。这发生在 make 读完所有 makefile、并判定某个目标已过时(需更新)之后。因此,不会被重新构建的目标,其命令永远不会被展开。
命令中的变量引用和函数引用,与 makefile 其他位置的引用具有完全相同的语法和语义。引用规则也一样。如果你想在命令中输出一个美元符号本身,就必须把美元符号写成两个(「$$」)。对于像默认 shell 那样用美元符号引入变量的 shell,务必在心里清楚地区分:你想引用的究竟是 make 的变量(用一个美元符号),还是 shell 的变量(用两个美元符号)。例如:
LIST = one two three
all:
for i in $(LIST); do \
echo $$i; \
done
这会导致下面这条命令被传给 shell:
for i in one two three; do \
echo $i; \
done
并产生符合预期的结果:
one two three
通常,make 会在执行命令的每一行之前先把它显示在屏幕上。我们称之为回显(echo),因为这看起来就像是你自己在输入这些行一样。
如果某一行以「@」开头,该行的回显就会被抑制。「@」会在该行传给 shell 之前被去除。这通常用于那些唯一目的就是显示信息的命令,例如用来通报 makefile 进展情况的 echo 命令:
@echo About to make distribution files
给 make 加上「-n」或「--just-print」标志时,它只回显大多数命令而不执行它们(参见选项汇总)。这种情况下,即使是以「@」开头的命令(recipe)行也会被显示出来。当你想知道 make 认为有哪些命令是必要的,而又不真正执行它们时,这个标志很有用。
给 make 加上「-s」或「--silent」标志,会使其完全不进行回显,就好像所有命令都以「@」开头一样。在 makefile 中为没有前置条件的特殊目标 .SILENT 写一条规则,也能得到同样的效果(参见特殊的内置目标名)。
当需要执行命令来更新某个目标时,除非特殊目标 .ONESHELL 生效(参见在单个 shell 中执行),否则会为命令的每一行启动一个新的子 shell 来执行。(实际上,make 可能会在不影响结果的范围内走一些捷径。)
请注意:这意味着,设置 shell 变量,或者启动像 cd 这样在各进程内部设定局部上下文的 shell 命令,都不会影响命令的后续各行。(译注:cd 等在每个子 shell 中各自独立,因此不会传递到下一行。)如果你想让 cd 影响下一条语句,请把两条语句放进同一行命令里。这样 make 就会用一个 shell 来执行整行,shell 会依次执行各条语句。例如:
foo : bar/lose
cd $(<D) && gobble $(<F) > ../$@
这里我们使用了 shell 的 AND 运算符(&&)。这样,一旦 cd 命令失败,脚本就会直接失败,而不会试图在错误的目录中启动 gobble 命令。在错误的目录中执行可能会引发问题(在这种情况下,至少可以肯定 ../foo 会丢失内容而变成空文件)。
有时,你会希望把命令的所有行都一次性传给一次 shell 调用。这大致在两种情况下有用。其一,在命令由许多条命令(recipe)行组成的 makefile 中,通过避免多余的进程来改善性能。其二,你想让命令中包含换行符的时候(例如,你把一个完全不同的解释器用作 SHELL)。只要在 makefile 中的任何位置出现特殊目标 .ONESHELL,那么对所有目标来说,命令的全部各行都会被一次性传给一次 shell 调用。命令各行之间的换行符会被保留。例如:
.ONESHELL:
foo : bar/lose
cd $(<D)
gobble $(<F) > ../$@
这样,即使各命令写在不同的命令(recipe)行上,也能如预期那样工作了。
指定了 .ONESHELL 时,只有命令的第一行会被检查特殊前缀字符(「@」「-」「+」)。从第二行起,当 SHELL 被启动时,这些特殊字符仍会保留在命令(recipe)行中。如果你想让命令以这些特殊字符之一开头,就需要设法让它不要成为第一行的行首字符(加一行注释之类即可)。例如,下面这个例子在 Perl 中会产生语法错误,因为开头的「@」被 make 去除了:
.ONESHELL:
SHELL = /usr/bin/perl
.SHELLFLAGS = -e
show :
@f = qw(a b c);
print "@f\n";
不过,下面这两种写法都能正确工作:
.ONESHELL:
SHELL = /usr/bin/perl
.SHELLFLAGS = -e
show :
# 确保第一行的行首字符不是 "@"
@f = qw(a b c);
print "@f\n";
或者
.ONESHELL:
SHELL = /usr/bin/perl
.SHELLFLAGS = -e
show :
my @f = qw(a b c);
print "@f\n";
作为一项特殊功能,如果 SHELL 被判定为 POSIX 风格的 shell,那么「内部的」命令(recipe)行(第二行及以后)中的特殊前缀字符,会在命令被处理之前去除。这项功能的用意,是让现有的 makefile 即使添加了特殊目标 .ONESHELL,也无需大幅修改就能正确运行。由于这些特殊前缀字符在 POSIX shell 脚本中不能放在行首,因此这并不会损失任何功能。例如,下面这个能如预期般工作:
.ONESHELL:
foo : bar/lose
@cd $(@D)
@gobble $(@F) > ../$@
不过,即便有这项特殊功能,使用了 .ONESHELL 的 makefile 在行为上仍可能发生足以被察觉的变化。例如,通常情况下命令中任何一行失败都会导致规则失败,后续的命令(recipe)行不会被处理。然而在 .ONESHELL 之下,除最后一行命令以外的任何地方发生失败,make 都不会察觉。你可以修改 .SHELLFLAGS,给 shell 加上 -e 选项,这样命令行中任何地方发生失败都会使 shell 失败;但这本身也可能改变命令的行为。最终,你或许需要把命令(recipe)行写得更健壮,使其能在 .ONESHELL 下正常工作。
用作 shell 的程序取自变量 SHELL。如果这个变量没有在你的 makefile 中设置,就用程序 /bin/sh 作为 shell。传给 shell 的参数取自变量 .SHELLFLAGS。.SHELLFLAGS 的默认值通常是 -c,在 POSIX 兼容模式下则是 -ec。
与大多数变量不同,变量 SHELL 绝不会从环境中设置。这是因为 SHELL 环境变量被用来指定你个人偏好的、供交互式使用的 shell 程序。如果这种个人选择影响到 makefile 的运作,那将非常糟糕。参见来自环境的变量。
此外,即使你在 makefile 中设置了 SHELL,该值也不会作为环境变量导出给 make 启动的命令(recipe)行。取而代之,导出的是从用户环境继承而来的值(如果有的话)。你可以通过显式地导出 SHELL 来改变这一行为(参见向子 make 传递变量),从而强制它作为环境变量传给命令(recipe)行。
不过,在 MS-DOS 和 MS-Windows 上,环境中 SHELL 的值是会被使用的,因为在这些系统上大多数用户不会设置这个变量,所以一旦设置了,很可能就是专门为 make 而设的。在 MS-DOS 上,如果 SHELL 的设置不适合 make,你可以把变量 MAKESHELL 设为你想让 make 使用的 shell;一旦设置,它就会代替 SHELL 的值被用作 shell。
在 MS-DOS 和 MS-Windows 中选择 shell,要比在其他系统上复杂得多。
在 MS-DOS 上,如果没有设置 SHELL,就会改用(始终会被设置的)变量 COMSPEC 的值。
在 makefile 中设置变量 SHELL 的那些行,其处理在 MS-DOS 上有所不同。标准的 shell——command.com——其功能贫弱得可笑,许多 make 用户往往会安装一个替代的 shell。因此在 MS-DOS 上,make 会检查 SHELL 的值,并根据它指向的是 Unix 风格还是 DOS 风格的 shell 来改变自身行为。这样一来,即使 SHELL 指向 command.com,也能获得还算说得过去的功能。
如果 SHELL 指向 Unix 风格的 shell,MS-DOS 上的 make 还会进一步确认该 shell 是否确实能找到;如果找不到,就忽略设置 SHELL 的那一行。在 MS-DOS 中,GNU make 会从以下位置查找 shell:
SHELL 的值所指向的那个确切位置。例如,如果 makefile 指定了「SHELL = /bin/sh」,make 就会在当前驱动器的 /bin 目录中查找。
PATH 变量中包含的各个目录,按顺序查找。
在它检查的每个目录中,make 会先查找该名字本身的文件(在上面的例子中是 sh)。如果找不到,它还会在该目录中查找带有已知可执行文件扩展名的同名文件。例如 .exe、.com、.bat、.btm、.sh 等等。
只要其中任何一次尝试成功,SHELL 的值就会被设为所找到 shell 的完整路径名。然而,如果一个都找不到,SHELL 的值就不会改变,结果设置它的那一行实际上被忽略。这样做是为了让 make 只在确实安装了 Unix 风格 shell 的系统上,才支持该 shell 特有的功能。
需要注意的是,这种对 shell 的扩展查找仅限于 SHELL 从 makefile 中设置的情形。如果它是在环境或命令行中设置的,那么就像在 Unix 上一样,会期望你自己设好 shell 的完整路径名。
上述 DOS 特有处理的结果是:一个包含「SHELL = /bin/sh」的 makefile(许多 Unix 的 makefile 都是这样),只要你在 PATH 上某个目录中安装了诸如 sh.exe 之类的程序,在 MS-DOS 上也能原样运行。
GNU make 懂得如何同时执行多条命令。通常,make 一次只执行一条命令,等它完成后再执行下一条。不过,使用「-j」或「--jobs」选项,可以指示 make 同时执行许多条命令。你也可以从 makefile 内部,对部分或全部目标抑制并行执行(参见禁用并行执行)。
在 MS-DOS 上,由于该系统不支持多进程,「-j」选项没有效果。
「-j」选项后面跟一个整数时,它就是一次执行的命令数。我们称之为作业槽(job slot)的数量。如果「-j」选项后面没有跟着像整数的东西,作业槽的数量就不受限制。作业槽的默认数量是 1,这意味着串行执行(一次一个)。
在处理递归的 make 调用时,会引出与并行执行相关的问题。关于这一点的更多信息,参见向子 make 传递选项。
如果某条命令失败(被信号杀死,或以非零状态退出),而该命令的错误未被忽略(参见命令中的错误),那么用于重新生成同一目标的其余命令(recipe)行将不会运行。如果命令失败时没有给出「-k」或「--keep-going」选项(参见选项汇总),make 就会中止执行。当 make 因任何原因(包括信号)退出而仍有子进程在运行时,它会先等待这些子进程结束,然后才真正退出。
当系统负载较高时,你大概会希望以比负载较低时更少的作业数来运行。可以使用「-l」选项,指示 make 根据平均负载来限制一次运行的作业数。「-l」或「--max-load」选项后面跟一个浮点数。例如,
-l 2.5
这样设置后,只要平均负载超过 2.5,make 就不会启动第二个及以后的作业。如果指定「-l」选项却不跟数字,就会解除之前由「-l」选项设置的负载限制。
更确切地说,当 make 准备启动一个作业时,如果已经至少有一个作业在运行,它就会检查当前的平均负载;如果该负载不低于用「-l」给出的限制值,make 就会一直等待,直到平均负载降到该限制以下,或者其他作业全部结束为止。
默认情况下,没有负载限制。
如果 makefile 完整而准确地定义了所有目标之间的依赖关系,那么无论是否启用并行执行,make 都会正确地构建各个目标。这是编写 makefile 的理想方式。
然而,有时 makefile 中的部分或全部目标无法并行执行,而又难以添加用来告知 make 的前置条件。这种情况下,makefile 可以用多种方法来禁用并行执行。
如果在任何位置指定了没有前置条件的特殊目标 .NOTPARALLEL,那么无论并行设置如何,整个 make 实例都会串行运行。例如:
all: one two three one two three: ; @sleep 1; echo $@ .NOTPARALLEL:
这种情况下,无论怎样启动 make,目标 one、two、three 都会串行执行。
如果特殊目标 .NOTPARALLEL 带有前置条件,那么这些前置条件中的每一个都会被视为一个目标,这些目标的所有前置条件都会串行执行。注意,只有在构建这个目标时,前置条件才会串行执行:如果另一个目标列出了相同的前置条件,而它不在 .NOTPARALLEL 的管辖范围内,那么这些前置条件仍可能并行执行。例如:
all: base notparallel base: one two three notparallel: one two three one two three: ; @sleep 1; echo $@ .NOTPARALLEL: notparallel
这里,「make -j base」会并行执行目标 one、two、three,而「make -j notparallel」会串行执行它们。如果运行「make -j all」,它们会并行执行,因为 base 把它们列为前置条件,并且没有被串行化。
.NOTPARALLEL 目标不应附带命令。
最后,使用特殊目标 .WAIT,可以精细地控制特定前置条件的串行化。当这个目标出现在前置条件列表中、且启用了并行执行时,make 在 .WAIT 左侧的所有前置条件全部完成之前,绝不会构建 .WAIT 右侧的任何前置条件。例如:
all: one two .WAIT three one two three: ; @sleep 1; echo $@
如果启用了并行执行,make 会尝试并行构建 one 和 two,但在两者都完成之前不会着手构建 three。
与给 .NOTPARALLEL 指定的目标一样,.WAIT 只有在构建其前置条件列表中含有它的那个目标时才会生效。如果相同的前置条件还出现在其他不带 .WAIT 的目标中,它们仍可能并行执行。正因如此,无论是带目标的 .NOTPARALLEL 还是 .WAIT,作为控制并行执行的手段,都不如定义前置条件关系来得可靠。不过,它们用起来简单,在不太复杂的情形下应该够用了。
.WAIT 前置条件不会出现在该规则的任何自动变量中。
为了可移植性,你可以在 makefile 中创建一个实体的 .WAIT 目标,但使用这项功能并不要求这么做。如果创建 .WAIT 目标,就不应让它带有前置条件或命令。
.WAIT 功能在其他版本的 make 中也有实现,并且在 make 的 POSIX 标准中也有规定。
并行运行多条命令时,每条命令的输出都会在生成的同时立即出现。其结果是,来自不同命令的消息可能会交织在一起,有时甚至重叠出现在同一行上。这会使输出非常难以阅读。
为避免这种情况,可以使用「--output-sync」(「-O」)选项。这个选项指示 make 先把它所启动命令的输出保存起来,等命令完成后再一次性显示。此外,如果有多个并行运行的递归 make 调用,它们会相互协调,使同一时间只有一个在产生输出。
如果启用了工作目录显示(参见「--print-directory」选项),则会在每个输出分组的前后显示「进入/离开目录」的消息。如果你不想看到这些消息,请把「--no-print-directory」选项加到 MAKEFLAGS 中。
同步输出的粒度有四个级别,通过给选项加参数来指定(例如「-Oline」或「--output-sync=recurse」)。
none这是默认值。所有输出都在生成时直接送出,不进行任何同步。
line命令中每一行的输出会被汇集起来,在该行完成后立即显示。如果一条命令由多行组成,它们可能会与其他命令的行交织在一起。
target每个目标的整条命令的输出会被汇集起来,在该目标完成后显示。如果给出 --output-sync 或 -O 选项而不带参数,这就是默认值。
recursemake 每次递归调用的输出会被汇集起来,在该次递归调用完成后显示。
无论选择哪种模式,整个构建所需的总时间都不变。不同的只是输出呈现的方式。
「target」模式和「recurse」模式都会汇集某个目标整条命令的输出,在命令完成时不间断地显示出来。两者的区别在于对含有 make 递归调用的命令的处理方式(参见递归地使用 make)。对于不含递归行的命令,「target」模式和「recurse」模式的行为完全相同。
如果选择「recurse」模式,含有递归 make 调用的命令会被当作和其他目标一样对待。也就是说,命令的输出——包括来自递归 make 的输出——都会被保存,等整条命令完成后再显示。这能确保某个递归 make 实例所构建的所有目标的输出汇聚成一整块,有时会让输出更易于理解。但另一方面,这也会导致构建过程中出现长时间完全看不到输出、随后大量输出一下子涌现的情况。如果你不是实时观察构建的进行,而是事后查看构建日志,那么这或许是最合适的选择。
在实时观察输出时,构建过程中长时间的沉默会让人烦躁。「target」输出同步模式会检测 make 即将以标准方式被递归调用,并且不同步那一行的输出。递归的 make 会为它自己的目标进行同步,各自的输出在完成时立即显示。请注意,命令中递归行的输出不会被同步(例如,如果递归行在运行 make 之前显示了一条消息,那条消息不会被同步)。
「line」模式对于那些监视 make 输出、以追踪命令何时开始和完成的前端程序很有用。
make 所启动的某些程序,会根据自己判断输出目标是终端还是文件而改变行为(常被称为「交互式(interactive)」模式与「非交互式(non-interactive)」模式)。例如,许多能给输出着色的程序,在判断自己不是在向终端写入时就不会着色。如果 makefile 启动了这类程序,那么使用输出同步选项会让该程序误以为自己运行在「非交互式」模式下,尽管输出最终还是会送到终端。
两个进程不能同时从同一设备获取输入。为了确保同一时间只有一条命令从终端获取输入,make 会使正在运行的命令中除一条以外的所有标准输入流失效。如果另一条命令试图从标准输入读取,通常会引发致命错误(「Broken pipe」信号)。
哪条命令能获得有效的标准输入流(它来自终端,或来自你为 make 重定向标准输入的去处)是无法预测的。最先运行的命令总是最先获得它,在那条命令结束之后最先启动的命令接着获得它,以此类推。
如果我们找到更好的替代方案,就会改变 make 这部分的行为。在此之前,如果你在使用并行执行功能,就不应让任何命令使用标准输入;反过来,如果你没有使用这项功能,那么标准输入在所有命令中都能正常工作。
每次 shell 调用返回后,make 都会检查它的退出状态。如果 shell 正常完成(退出状态为零),命令的下一行就会在一个新的 shell 中执行。最后一行结束后,该规则就完成了。
如果出现了错误(退出状态为非零),make 就会放弃当前规则,有时甚至放弃所有规则。
某条命令(recipe)行失败,并不一定意味着出了问题。例如,你可能会用 mkdir 命令来确保某个目录存在。如果该目录已经存在,mkdir 会报告一个错误,但你大概仍希望 make 继续执行。
要忽略某条命令(recipe)行中的错误,请在该行文本的开头(在最初的制表符之后)写一个「-」。「-」会在该行被传给 shell 执行之前去除。
例如,
clean:
-rm -f *.o
这样,即使 rm 无法删除某个文件,make 也会继续执行。
以「-i」或「--ignore-errors」标志运行 make 时,所有规则的所有命令中的错误都会被忽略。在 makefile 中为特殊目标 .IGNORE 写一条没有前置条件的规则,也有同样的效果。这种方式灵活性较差,但有时很有用。
当因为「-」或「-i」标志而忽略错误时,make 会把出错的返回当作成功一样对待,只不过它会显示一条消息,告诉你 shell 退出时的状态码,并说明该错误已被忽略。
当发生了一个 make 未被告知要忽略的错误时,这意味着当前目标无法被正确地重新生成,直接或间接依赖它的其他目标也都无法重新生成。由于这些目标的前提条件没有达成,不会再为它们执行任何命令。
通常,在这种情况下 make 会立即放弃,并返回非零状态。不过,如果指定了「-k」或「--keep-going」标志,make 在放弃并返回非零状态之前,会继续考虑那些待处理目标的其他前置条件,必要时把它们重新生成。例如,在某个目标文件的编译出错之后,「make -k」仍会继续编译其他目标文件——即便它已经知道把它们链接起来已经不可能了。参见选项汇总。
通常的行为假定:你的目的是让指定的目标变为最新状态。一旦 make 得知这已不可能,它索性立即报告失败。「-k」选项则告诉它:真正的目的是尽可能多地测试你对程序所做的改动——或许是想找出若干个相互独立的问题,以便在下次尝试编译之前把它们一并修正。这正是 Emacs 的 compile 命令默认传递「-k」标志的原因。
通常,当一条命令(recipe)行失败时,如果它对目标文件做出了哪怕一点改动,那么这个文件就是损坏的、无法使用的——至少没有被完整地更新。但文件的时间戳却表明「现在是最新的」,所以下次运行 make 时,它不会再试图更新这个文件。这和 shell 被信号杀死时的情况完全相同。参见中断或杀死 make。因此,一般正确的做法是:如果命令在开始改动文件之后失败了,就把这个目标文件删除。只要 .DELETE_ON_ERROR 作为目标出现,make 就会这么做。这在大多数情况下都是你希望 make 做的事,但它并非历史上的惯例。所以为了兼容性,你必须显式地提出要求。
如果 make 在 shell 运行期间收到致命信号,它可能会删除该命令本应更新的目标文件。当目标文件的最后修改时间相比 make 最初检查时已发生变化时,就会这么做。
删除目标的目的,是为了让下次运行 make 时它能从头重新生成。为什么呢?假设你在编译器运行期间按下 Ctrl-c,而它恰好刚开始写目标文件 foo.o。Ctrl-c 杀死了编译器,结果留下一个不完整的文件,其最后修改时间比源文件 foo.c 还要新。不过 make 也收到了 Ctrl-c 信号,并删除这个不完整的文件。要是 make 不这么做,下次启动 make 时就会判定 foo.o 无需更新——其结果是,链接器试图链接一个缺了一半的目标文件,并给出奇怪的错误消息。
要防止这样删除目标文件,可以让特殊目标 .PRECIOUS 依赖该文件。make 在重新生成目标之前,会确认它是否出现在 .PRECIOUS 的前置条件中,并据此决定在信号发生时是否应当删除该目标。你之所以想这么做,可能的理由包括:该目标以某种原子的(不可分割的)方式更新;或者它只是为了记录更新时间而存在(内容无关紧要);又或者它必须始终存在,以防止其他种类的问题。
make 会尽力做好善后,但也有无法善后的情形。例如,make 可能被它无法捕获的信号杀死。又或者,make 所启动的某个程序被杀死或崩溃,留下一个时间戳上看似最新、实则损坏的目标文件。这种情况下,make 不会察觉到这次失败需要对目标进行善后。再或者,make 自身遇到 bug 而崩溃。
出于这些原因,最好编写防御性的命令(defensive recipe)。防御性的命令即使失败,也不会留下损坏的目标。最常见的做法是:不直接更新目标,而是先创建一个临时文件,再把临时文件重命名为最终的目标名。有些编译器本来就这样工作,这种情况下你就无需编写防御性的命令了。
make 的递归使用,是指在 makefile 中把 make 当作一条命令来用。这种手法在以下场景中很有用:你想为构成某个更大系统的各个子系统分别准备各自的 makefile。例如,有一个带有自己 makefile 的子目录 subdir,而你想从包含它的目录的 makefile 中,对该子目录运行 make。这可以这样写:
subsystem:
cd subdir && $(MAKE)
或者用下面这种等价的写法也行(参见选项汇总):
subsystem:
$(MAKE) -C subdir
你可以照搬这个例子来编写递归的 make 命令,但关于它们如何、以及为何这样工作,还有子 make 与顶层 make 之间的关系,有许多需要了解的地方。把启动递归 make 命令的目标声明为「.PHONY」有时会很方便(关于这在什么场景下有用的更多说明,参见伪目标)。
为方便起见,GNU make 在启动时(处理完 -C 选项之后),会把当前工作目录的路径名设置到变量 CURDIR 中。此后 make 不会再去触碰这个值。特别要注意,即使 include 其他目录中的文件,CURDIR 的值也不会改变。这个值具有与在 makefile 中设置时相同的优先级(默认情况下,环境变量 CURDIR 不会覆盖这个值)。另外请注意,设置这个变量并不会影响 make 的行为(例如,make 并不会因此改变工作目录)。
在递归的 make 命令中,应该始终使用变量 MAKE,而不是显式的命令名「make」。写法如下:
subsystem:
cd subdir && $(MAKE)
这个变量的值,是 make 被启动时所用的文件名。如果这个文件名是 /bin/make,那么实际执行的命令就是「cd subdir && /bin/make」。如果你用某个特别版本的 make 来执行顶层 makefile,那么递归调用时也会执行同一个特别版本。
作为一项特殊功能,在规则的命令中使用变量 MAKE 时,会改变「-t」(「--touch」)、「-n」(「--just-print」)、「-q」(「--question」)选项的效果。使用变量 MAKE,与在命令(recipe)行开头放一个「+」字符具有相同的效果。参见不执行命令的替代操作。这项特殊功能只有在 MAKE 变量直接出现在命令中时才生效。如果 MAKE 变量是通过另一个变量的展开间接引用的,它就不适用。那种情况下,要获得这些特殊效果,就必须使用「+」记号。
考虑上面例子中的「make -t」命令。(「-t」选项实际上并不执行任何命令,只是把目标标记为最新状态。参见不执行命令的替代操作。)按照「-t」的通常定义,在这个例子里「make -t」命令本应只创建一个名为 subsystem 的文件,别的什么都不做。可你真正想要的是执行「cd subdir && make -t」,而这需要执行命令,「-t」却说不要执行命令。
这项特殊功能正好能让它按你的期望行事。只要规则的命令(recipe)行中含有变量 MAKE,「-t」「-n」「-q」这些标志就不会作用于该行。即便存在使大多数命令不被执行的标志,含有 MAKE 的命令(recipe)行仍会照常执行。而通常的 MAKEFLAGS 机制会把这些标志传给子 make(参见向子 make 传递选项),于是你提出的「触碰文件」或「显示命令」的要求,就会传播到各个子系统。
顶层 make 的变量值,只要明确要求,就能通过环境传给子 make。这些变量在子 make 中被定义为默认值,但除非使用「-e」开关(参见选项汇总),否则不会覆盖子 make 所用 makefile 中定义的变量。
为了把变量向下传递、也就是导出(export),make 会把该变量及其值添加到执行命令各行所用的环境中。子 make 接着用这个环境来初始化自己的变量值表。参见来自环境的变量。
除非有明确的要求,否则 make 只在以下情形下导出变量:该变量本来就在环境中有定义,或者它是在命令行中设置的、且其名称仅由字母、数字和下划线组成。
make 变量 SHELL 的值不会被导出。取而代之,启动时环境中 SHELL 变量的值会传给子 make。使用后文所述的 export 指令 (directive),可以让 make 导出 SHELL 的值。参见选择 shell。
特殊变量 MAKEFLAGS 总是会被导出(除非你对它 unexport)。MAKEFILES 只要设置了任何值,就会被导出。
make 会自动把命令行中定义的变量值向下传递。它通过把这些变量放进 MAKEFLAGS 变量来做到这一点。参见向子 make 传递选项。
如果某个变量是 make 默认创建的(参见隐含规则所用的变量),通常它们不会向下传递。子 make 会自行定义它们。
如果你想把特定的变量导出给子 make,可以像下面这样使用 export 指令:
export variable …
反过来,如果你想阻止某个变量被导出,可以像下面这样使用 unexport 指令:
unexport variable …
无论哪种形式,export 和 unexport 的参数都会被展开。因此,你也可以指定一些变量或函数,让它们展开成需要(un)export 的变量名(列表)。
很方便的是,你也可以同时进行变量的定义和导出:
export variable = value
这与下面的写法结果相同:
variable = value export variable
另外,
export variable := value
与下面的写法结果相同:
variable := value export variable
同样地,
export variable += value
与下面的写法完全相同:
variable += value export variable
参见向变量追加文本。
你可能已经注意到,export 和 unexport 指令在 make 中的工作方式,与它们在 shell sh 中的工作方式完全一样。
如果你想让所有变量默认都被导出,可以单独使用 export:
export
这是在告诉 make:即使是 export 或 unexport 指令没有明确提及的变量,也应当被导出。被 unexport 指令指定的变量,仍然不会被导出。
单独使用 export 所引起的行为,在旧版本的 GNU make 中曾是默认行为。如果你的 makefile 依赖这一行为,并且想与旧版本的 make 保持兼容,那么可以用特殊目标 .EXPORT_ALL_VARIABLES 添加到 makefile 中,来代替 export 指令。这个特殊目标会被旧版 make 忽略,而 export 指令则会在旧版 make 中引发语法错误。
当通过单独的 export 或 .EXPORT_ALL_VARIABLES 默认导出变量时,只有名称仅由字母数字和下划线组成的变量才会被导出。要导出其他变量,就必须在 export 指令中逐个列出它们的名称。
要把变量的值加入环境,就需要把这个值展开。如果变量的展开带有副作用(比如 info 或 eval 这类函数),那么每次启动命令时都会出现这种副作用。要避免这一点,可以把这类变量的名称取成默认情况下不会被导出的名字。不过,更好的解决办法是:完全不使用这项「默认导出」功能,而是改用逐一列名的方式,显式地 export 相关变量。
单独使用 unexport,是在告诉 make 默认不导出变量。由于这本来就是默认行为,所以只有在之前(大概是在某个 include 进来的 makefile 中)单独使用过 export 的情况下,才会需要它。你不能通过单独使用 export 和 unexport 来做到「在某些命令中导出变量、在另一些命令中不导出」。单独出现的最后一个 export 或 unexport 指令,决定了整个 make 运行过程的行为。
作为一项特殊功能,变量 MAKELEVEL 在逐级向下传递时会发生变化。这个变量的值,是用十进制表示的层级深度字符串。在顶层 make 中其值为「0」,在子 make 中为「1」,在子子 make 中为「2」,以此类推。这个递增发生在 make 为命令设置环境的时候。
MAKELEVEL 的主要用途,是在条件指令中对它进行测试(参见Makefile 的条件部分)。这样一来,你就能写出在被递归执行时与被你直接执行时行为不同的 makefile。
使用变量 MAKEFILES,可以让所有子 make 命令都使用额外的 makefile。MAKEFILES 的值,是用空白分隔的文件名列表。如果这个变量在外层的 makefile 中有定义,它就会通过环境向下传递。于是它会充当一份额外 makefile 的列表,供子 make 在读取其通常的或指定的 makefile 之前先行读入。参见变量 MAKEFILES。
像「-s」「-k」这样的标志,会通过变量 MAKEFLAGS 自动传给子 make。这个变量由 make 自动设置,使其包含 make 所接收到的标志字符。因此,运行「make -ks」时,MAKEFLAGS 的值就会变成「ks」。
其结果是,所有子 make 都会从环境中接收到 MAKEFLAGS 的值。相应地,子 make 会从这个值中取出标志,并像它们是作为参数给出的那样处理。参见选项汇总。这意味着,与其他环境变量不同,在环境中指定的 MAKEFLAGS 优先于在 makefile 中指定的 MAKEFLAGS。
MAKEFLAGS 的值,是先有一串(可以为空的)表示不带参数的单字符选项的字符,后面跟一个空格,再排列上带参数的选项和长名称选项。如果某个选项同时有单字符版和长名称版,总是优先使用单字符版。如果命令行上一个单字符选项也没有,MAKEFLAGS 的值就会以空格开头。
同样地,命令行中定义的变量也会通过 MAKEFLAGS 传给子 make。MAKEFLAGS 的值中含有「=」的单词,make 会把它当作仿佛出现在命令行上的变量定义来处理。参见覆盖变量。
选项「-C」「-f」「-o」「-W」不会被放进 MAKEFLAGS。这些选项不会向下传递。
「-j」选项是个特殊情况(参见并行执行)。如果给它设了某个数值「N」,并且你的操作系统支持(大多数 UNIX 系统支持,其他系统大多不支持),那么父 make 和所有子 make 会相互协调,使它们整体上同时运行的作业恰好只有「N」个。需要注意的是,被标记为递归的作业(参见不执行命令的替代操作)不计入总作业数(否则就会有「N」个子 make 在运行,而没有留下用于实际工作的槽位了!)。
如果你的操作系统不支持上述协调机制,「-j」就不会被加入 MAKEFLAGS。因此,子 make 会以非并行模式运行。要是「-j」选项被传给了子 make,那么并行运行的作业数就会远多于你所要求的数量。如果给「-j」不带数值参数,那意味着尽可能多地并行运行作业,这是会向下传递的——因为无穷个无穷,和一个无穷并无区别。
如果你不想把其他标志向下传递,就必须像下面这样修改 MAKEFLAGS 的值:
subsystem:
cd subdir && $(MAKE) MAKEFLAGS=
命令行的变量定义,实际上出现在变量 MAKEOVERRIDES 中,而 MAKEFLAGS 含有对这个变量的引用。如果你想让标志照常向下传递,却不想传递命令行的变量定义,那么可以像下面这样把 MAKEOVERRIDES 重置为空:
MAKEOVERRIDES =
这并不是你平时会做的事。但是,有些系统对环境的大小有一个很小的固定上限,如果在 MAKEFLAGS 的值中放入这么多信息,就可能超出这个上限。如果出现「Arg list too long」这样的错误消息,原因或许就在于此。(如果严格遵循 POSIX.2,当 makefile 中出现特殊目标「.POSIX」时,修改 MAKEOVERRIDES 不会影响 MAKEFLAGS。不过你大概不必为此操心。)
出于历史兼容性的考虑,还存在一个很相似的变量 MFLAGS。它与 MAKEFLAGS 的值相同,但有两点不同:它不含命令行的变量定义;并且只要它不为空,就总是以连字符开头(MAKEFLAGS 以连字符开头,只在它以没有单字符版的选项——如「--warn-undefined-variables」——开头时才会发生)。MFLAGS 传统上是像下面这样,在递归的 make 命令中显式使用的:
subsystem:
cd subdir && $(MAKE) $(MFLAGS)
但如今有了 MAKEFLAGS,就不需要这种用法了。如果你想让 makefile 与旧的 make 程序保持兼容,可以使用这种手法;它在较新版本的 make 中也能正常工作。
MAKEFLAGS 变量在另一种情况下也很有用:你希望每次运行 make 时都设置某些特定选项,比如「-k」(参见选项汇总)。只要把 MAKEFLAGS 的值放进环境就行了。你也可以在 makefile 中设置 MAKEFLAGS,以指定在该 makefile 中应当生效的额外标志。(注意,MFLAGS 不能用于此目的。那个变量只是为兼容性而设置的,你设的值 make 不会以任何方式去解释。)
当 make 解释 MAKEFLAGS 的值时(无论来自环境还是来自 makefile),它会先在值不以连字符开头时给它前面加上一个连字符。接着把值切分成用空白分隔的单词,并把这些单词当作仿佛在命令行上给出的选项来解析(不过「-C」「-f」「-h」「-o」「-W」以及它们的长名称版会被忽略,即使是非法选项也不会报错)。
把 MAKEFLAGS 放进环境时,务必小心,绝不要包含会剧烈改变 make 行为、从而毁掉 makefile 或 make 本身目的的选项。例如,把「-t」「-n」「-q」选项放进这些变量中的任何一个,都可能招致惨痛的后果,至少会带来令人意外、并且很可能令人厌烦的效果。
如果你还想使用 GNU make 以外的 make 实现,因而不想把 GNU make 特有的标志加进 MAKEFLAGS 变量,那么可以改为把它们加进 GNUMAKEFLAGS 变量。这个变量会在 MAKEFLAGS 之前、以与 MAKEFLAGS 相同的方式被解析。在组装要传给递归 make 的 MAKEFLAGS 时,make 会把所有标志——包括取自 GNUMAKEFLAGS 的——都包含进去。其结果是,在解析完 GNUMAKEFLAGS 之后,GNU make 会把这个变量设为空字符串,以避免在递归过程中标志重复。
最好只在 GNUMAKEFLAGS 中使用那些不会实质性改变 makefile 行为的标志。如果你的 makefile 反正都需要 GNU Make,那就直接用 MAKEFLAGS 好了。像「--no-print-directory」或「--output-sync」这样的标志,可能适合放进 GNUMAKEFLAGS。
在使用多层递归 make 调用时,使用「-w」或「--print-directory」选项会很有帮助:make 在开始和结束处理某个目录时,会分别把对应的目录显示出来,从而让输出更易于理解。例如,在目录 /u/gnu/make 中运行「make -w」时,make 会在做其他事情之前先显示一行如下形式的内容:
make: Entering directory `/u/gnu/make'.
并在处理完成时显示一行如下形式的内容:
make: Leaving directory `/u/gnu/make'.
通常,你无需指定这个选项,因为「make」会自动帮你处理。「-w」会在使用「-C」选项时,以及在子 make 中自动开启。不过,如果你同时还使用了表示静默的「-s」,或者使用了显式禁用的「--no-print-directory」,make 就不会自动开启「-w」。
如果同一串命令对生成各种各样的目标都有用处,你可以用 define 指令把它定义为一个固定序列(canned sequence),然后在这些目标的命令中引用这个固定序列。固定序列实际上是变量,所以它的名字不能与其他变量名冲突。
下面是一个定义固定命令的例子:
define run-yacc = yacc $(firstword $^) mv y.tab.c $@ endef
这里 run-yacc 是被定义的变量的名字。endef 标示定义的结束,其间的各行就是命令。define 指令不会展开固定序列中的变量引用或函数调用。「$」字符、括号、变量名等等,都会成为你正在定义的那个变量值的一部分。关于 define 的完整说明,参见定义多行变量。
这个例子中的第一条命令,会对使用该固定序列的规则的第一个前置条件运行 Yacc。Yacc 的输出文件名总是 y.tab.c。第二条命令则把这个输出移动到规则的目标文件名。
要使用固定序列,就把它的变量代入规则的命令中。你可以像对待其他变量那样代入它(参见变量引用基础)。用 define 定义的变量是递归展开变量,所以你写在 define 内部的所有变量引用,都会在此时此地展开。例如:
foo.c : foo.y
$(run-yacc)
当 run-yacc 的值中出现「$^」时,那里会代入「foo.y」,而「$@」会代入「foo.c」。
这是个现实的例子,不过这个特定的例子其实并不需要,因为 make 有一条隐含规则,能根据涉及的文件名推断出这些命令(参见使用隐含规则)。
在命令的执行上,固定序列的每一行都会被当作仿佛它单独出现在规则中、前面带着一个制表符那样来处理。特别是,make 会为每一行启动一个单独的子 shell。作用于命令(recipe)行的特殊前缀字符(「@」「-」「+」),可以用在固定序列的每一行上。参见如何编写规则中的命令 (recipe)。例如,使用下面这个固定序列时:
define frobnicate = @echo "frobnicating target $@" frob-step-1 $< -o $@-step-1 frob-step-2 $@-step-1 -o $@ endef
make 不会回显第一行,也就是 echo 命令。但随后的两条命令(recipe)行会被回显。
另一方面,加在引用固定序列的那条命令(recipe)行上的前缀字符,会作用于该序列的所有行。因此,下面这条规则:
frob.out: frob.in
@$(frobnicate)
任何命令(recipe)行都不会被回显。(关于「@」的完整说明,参见命令的回显。)
有时,定义一条什么都不做的命令会很方便。这只需给出一条仅由空白组成的命令即可。例如:
target: ;
这就为 target 定义了一条空命令。你也可以用以命令前缀字符开头的行来定义空命令,但这样的行看起来是空的,容易让人困惑。
你或许会奇怪,为什么会想定义一条什么都不做的命令。它有用的理由之一,是为了防止目标获得隐含的命令(来自隐含规则或 .DEFAULT 特殊目标;参见使用隐含规则和定义作为最后手段的默认规则)。
空命令还可以用来避免错误:当某个目标是作为另一条命令的副作用而创建的时候。如果目标不存在,只要有一条空命令,make 就不会抱怨自己不知道如何生成该目标,而会把该目标视为过时(需更新)。
对于并非实际文件、只是为了让前置条件得到重新生成而存在的目标,你或许会想为它定义一条空命令。但这并不是最好的做法,因为当目标文件确实存在时,前置条件有可能不会被正确地重新生成。关于实现这一点的更好方法,参见伪目标。