makefile 学习笔记 七:makefile 在规则中编写配方(命令)

详细内容见 《GNU make》 5 Writing Recipes in Rules 章节。

规则的配方由一个或多个要执行的shell命令行组成,一次执行一个,按它们出现的顺序执行。通常,执行这些命令的结果是更新规则的目标。

用户使用许多不同的 shell 程序,但是 makefile 中的配方总是由 /bin/sh 解释,除非 makefile 另有指定。

一、配方语法

makefile有一个不寻常的属性,即在一个文件中实际上有两种不同的语法大多数 makefile 使用 make 语法。但是,配方是由 shell 解释的,因此它们是使用 shell 语法编写的。make 程序不试图理解 shell 语法:在将配方交给 shell 之前,它只对配方的内容执行很少的特定翻译。

配方中的每一行都必须以一个制表符(或 .RECIPEPREFIX 变量值中的第一个字符)开始,除非配方的第一行可以附加到目标 - 先决条件行,中间有一个分号。makefile中任何以选项卡开始并出现在 “rule context” 中的行(即,在一个规则启动之后,直到另一个规则或变量定义之后)都将被视为该规则的配方的一部分。配方行中可能会出现空白行和注释行;他们将被忽略。

这些规则的一些后果包括:

  • 以制表符开头的空行不是空的:它是一个空的配方

  • 配方中的注释不是 make 注释;它将按原样传递给 shell。shell 是否将其视为注释取决于您的 shell。

  • “rule context” 中的变量定义(如果由制表符缩进作为行上的第一个字符)将被视为配方的一部分,而不是 make 变量定义,并传递给 shell。

  • “rule context” 中的一个条件表达式(ifdef、ifeq等),在行的第一个字符被制表符缩进,将被认为是配方的一部分,并传递给 shell。

1、拆分配方行

make确实解释配方的少数几种方法之一是检查换行符前的反斜杠。和普通的makefile语法一样,在makefile中,一个逻辑配方行可以通过在每个换行符前放置一个反斜杠将其拆分为多个物理行。像这样的一系列行被认为是一个单独的配方行,将调用shell的一个实例来运行它。

然而,与makefile中其他地方的处理方式不同,反斜杠换行对不会从配方中删除。反斜杠和换行符都被保留并传递给shell。反斜杠/换行符的解释方式取决于您的 shell。如果反斜杠/换行符之后下一行的第一个字符是配方前缀字符(默认情况下为制表符),则将删除该字符(并且仅该字符)。空格永远不会添加到配方中。

例如,此makefile中所有目标的配方:

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

请注意:反斜杠/换行符对是如何在用双引号("…")引号括起来的字符串中删除的,而不是从用单引号(’…’)括起来的字符串中删除的。

有时您希望将一个长行分隔在单引号中,但您不希望在引用的内容中出现反斜杠/换行符。在将脚本传递给Perl等语言时经常会出现这种情况,在这种情况下,脚本中多余的反斜杠可能会改变其含义,甚至会导致语法错误。处理此问题的一种简单方法是将带引号的字符串甚至整个命令放入 make 变量中,然后在配方中使用该变量。在此情况下,将使用 makefile 的换行符引用规则,并且反斜杠/换行符将被删除。如果我们用这个方法重写上面的例子:

HELLO = 'hello \
world'

all : ; @echo $(HELLO)

我们将得到如下输出:

hello world

如果您愿意,还可以使用特定于目标的变量来获得变量与使用它的配方之间更紧密的对应关系。

2、在配方中使用变量

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

二、配方回显

通常,在执行配方之前,请打印配方的每一行。我们称此为回显,因为它给人的感觉是您正在亲自输入这些行

当一行以 ‘@’ 开头时,该行的回显被抑制。在将行传递到 shell 之前,将丢弃 ‘@’。通常情况下,您会将该命令用于唯一的效果是打印内容的命令,例如用于指示 makefile 进程的 echo 命令:

@echo About to make distribution files

当 make 被赋予标志 ‘-n’ 或 ‘–just-print’ 时,它只回显大多数菜谱,而不执行它们。在这种情况下,甚至打印以 ‘@’ 开头的配方行。这个标志对于发现哪些配方认为是必要的而实际上没有做它们是有用的。

要使的 ‘-s’ 或 ‘–silent’ 标志防止所有的重复,就好像所有的食谱都以 ‘@’ 开头一样。对于特殊目标 .SILENT, makefile中的规则也具有相同的效果。

三、配方执行

当需要执行配方来更新目标时,它们是通过为配方的每一行调用一个新的子 shell 来执行的,除非特殊目标 .ONESHELL 生效(实际上,make 可能采用不影响结果的快捷方式)

请注意:这意味着设置 shell 变量和调用 shell 命令(如cd),这些命令为每个进程设置了本地上下文,不会影响配方中的下面几行。如果您想使用cd来影响下一条语句,请将这两条语句放在一个配方行中。然后 make 将调用一个 shell 来运行整行,shell 将按顺序执行语句。例如:

foo : bar/lose
        cd $(<D) && gobble $(<F) > ../$@

在这里,我们使用shell AND 操作符(&&),这样如果cd命令失败,脚本就会失败,而不必尝试在错误的目录中调用gobble命令,这可能会导致问题(在本例中,它肯定会导致 …/foo 至少要被截断)。

1、使用 shell

有时,您更希望将配方中的所有行传递给 shell 的单个调用。通常在两种情况下这是有用的:首先,它可以通过避免额外的进程来提高 makefile 中的性能,其中配方包含许多命令行。其次,您可能希望在配方命令中包含换行符(例如,您可能使用一个非常不同的解释器作为 SHELL)。如果 .ONESHELL 特殊目标出现在makefile的任何地方,那么每个目标的所有配方行都将提供给shell的单个调用。配方行之间的换行符将被保留。例如:

.ONESHELL:
foo : bar/lose
        cd $(@D)
        gobble $(@F) > ../$@

即使命令在不同的配方行上,现在也可以按预期工作。

如果提供了 .ONESHELL,那么只会检查配方的第一行是否有特殊的前缀字符(’@’、’-’ 和 ‘+’)。当调用SHELL时,后续的行将包括配方行中的特殊字符。如果您想要您的配方从这些特殊字符之一开始,您需要安排它们不在第一行的第一个字符,可能通过添加注释或类似的方法。例如,这将是 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 :
        # Make sure "@" is not the first character on the first line
        @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,那么在处理配方之前,“内部” 配方行中的特殊前缀字符将被删除。此特性的目的是允许现有的 makefile 添加特殊目标 .ONESHELL,并且在不进行大量修改的情况下仍能正常运行。因为在POSIX shell脚本中,行开头的特殊前缀字符是不合法的,所以这并不是功能上的损失。例如,这按预期方式工作:

.ONESHELL:
foo : bar/lose
        @cd $(@D)
        @gobble $(@F) > ../$@

然而,即使有了这个特殊的特性,带有 .ONESHELL 的makefile也会以不同的方式表现出来。例如,通常情况下,如果配方中的任何一行失败,就会导致规则失败,并且不再处理配方行。在 .ONESHELL 下,除了最终配方线以外的任何失败都不会被 make 注意到。您可以修改 .SHELLFLAGS 以将 -e 选项添加到 shell,这将导致命令行中任何位置的任何故障导致 shell 失败,但这本身可能会导致配方的行为不同。最终,您可能需要强化配方行,以允许它们与 .ONESHELL 配合使用。

2、选择 shell

用作 shell 的程序取自变量 SHELL。如果未在 makefile 中设置此变量,则程序 /bin/sh 将用作 shell。传递给 shell 的参数取自变量 .SHELLFLAGS。 .SHELLFLAGS 的缺省值通常为 -c,在符合 POSIX 模式下为 -ec。

与大多数变量不同,变量SHELL从不从环境中设置。这是因为 SHELL 环境变量用于指定您个人选择的交互式 shell 程序。像这样的个人选择会影响 makefile 的功能,这是非常糟糕的。

此外,当您在makefile中设置SHELL时,该值不会在环境中导出到生成调用的配方行。相反,将导出从用户环境(如果有的话)继承的值。您可以通过显式导出SHELL来重写此行为,强制将其在环境中传递给配方行。

但是,在 MS-DOS 和 MS-Windows 上,使用了 SHELL 在环境中的值,因为在这些系统上,大多数用户不设置此变量,因此最有可能专门设置为由 make 使用。在 MS-DOS 上,如果 SHELL 的设置不适合 make,则可以将变量 MAKESHELL 设置为 make 应该使用的 shell;如果设置,它将用作 shell 而不是 SHELL 的值。

1、在DOS和Windows中选择一个Shell

在MS-DOS和MS-Windows中选择一个shell比在其他系统上要复杂得多。

在MS-DOS中,如果没有设置SHELL,则使用变量COMSPEC的值(总是设置)。

在 Makefiles 中设置变量SHELL的行处理在MS-DOS中是不同的。自带 shell,command.com,其功能非常有限,许多 make 用户倾向于安装替换 shell。因此,在 MS-DOS 上,make 会检查 SHELL 的值,并根据它指向 Unix 样式还是 DOS 样式的 shell 来更改其行为。即使SHELL指向command.com,这也允许合理的功能。

如果SHELL指向一个unix风格的SHELL,在MS-DOS上做额外的检查是否可以找到该SHELL;如果不是,则忽略设置SHELL的行。在MS-DOS中,GNU在以下地方搜索shell:

1、在SHELL的值所指向的精确位置。例如,如果 makefile 指定 ‘SHELL = /bin/sh’,make 将在当前驱动器上的目录 ‘/bin’ 中查找。

2、在当前目录中。

3、在 PATH 变量的每个目录中,按顺序排列。

在它检查的每个目录中,make 将首先查找特定的文件(在上面的示例中为 sh)。如果没有找到,它也会在该目录中查找具有识别可执行文件的已知扩展名之一的文件。例如 .exe、.com、.bat、.btm、.sh 和 其他。

如果其中任何一个尝试成功,SHELL 的值将被设置为所找到的 SHELL 的完整路径名。但是,如果这些都没有找到,SHELL 的值将不会被更改,因此设置它的行将被有效地忽略。因此,如果 Make 实际安装在 make 运行的系统上,则 make 将仅支持特定于 Unix 样式 shell 的功能。

注意,这种对shell的扩展搜索仅限于从Makefile设置shell的情况;如果它是在环境或命令行中设置的,你需要将它设置为 shell 的完整路径名,就像在 Unix 上一样。

上述特定于DOS的处理的效果是,包含 ‘SHELL = /bin/sh’ 的Makefile(就像许多Unix makefile一样),如果你在PATH的某个目录中安装了例如 sh.exe,则可以在MS-DOS上保持不变。

四、并行执行

GNU make知道如何同时执行多个配方通常,make 一次只执行一个配方,等待它完成,然后再执行下一个配方。但是,’-j’ 或 ‘–jobs’ 选项告诉 make 同时执行多个配方。您可以使用 .NOTPARALLEL 伪目标在特定的makefile中禁止并行性。

在 MS-DOS 上,’-j’ 选项没有效果,因为该系统不支持多处理。

如果 ‘-j’ 选项后面跟着一个整数,这是一次执行的配方数;这称为作业槽数。如果 ‘-j’ 选项后面没有任何整数,则对作业槽的数量没有限制。作业槽的默认数量为 1,这意味着串行执行(一次执行一件事)。

处理递归 make 调用会引发并行执行的问题。

如果配方失败(被信号杀死或以非零状态退出),且该配方的错误不会被忽略,则将不运行重做相同目标的其余配方行。如果配方失败且未提供 ‘-k’ 或 ‘–keep-going’ 选项,请中止执行。如果 make 因任何原因(包括信号)终止,并且子进程正在运行,则它会等待它们完成,然后再实际退出。

当系统负载过重时,您可能希望运行的作业比轻负载时少。可以使用 ‘-l’ 选项告诉 make 根据负载平均值限制一次运行的作业数。选项 ‘-l’ 或 ‘–max-load’ 后面跟着一个浮点数。例如:

-l 2.5

如果负载平均值高于 2.5,则不会让开始多个作业。果前面有一个 ‘-l’ 选项,后面没有数字的 ‘-l’ 选项会删除负载限制。

更准确地说,当 make 去启动一个作业,并且它已经运行了至少一个作业时,它会检查当前的负载平均值;如果它不低于 ‘-l’ 给出的限制,则等待直到平均负载低于该限制,或直到所有其他作业完成。

默认情况下,没有负载限制。

1、并行执行时的输出

当并行运行多个配方时,每个配方的输出将在生成后立即显示,结果是来自不同配方的消息可能会分散出现,有时甚至出现在同一行上。这使得阅读输出非常困难。

为了避免这种情况,您可以使用 ‘–output-sync’(’-O’) 选项。此选项指示 make 保存它调用的命令的输出,并在命令完成后将其全部打印出来。此外,如果有多个递归 make 调用并行运行,它们将进行通信,以便一次只有一个递归生成输出。

如果启用了工作目录打印,则在每个输出分组周围打印 enter/leave 消息。如果不希望看到这些消息,可以向 MAKEFLAGS 添加 ‘–no-print-directory’ 选项。

当同步输出时,有四个级别的粒度,通过给选项提供一个参数来指定(例如:’-Oline’ 或 ‘–output-sync=recurse’)。

  • none:这是默认值:所有输出在生成时直接发送,并且不执行同步。

  • line:配方中每行的输出都会在该行完成后立即进行分组和打印。如果配方由多行组成,则它们可能会穿插其他配方中的行。

  • target:每个目标的整个配方的输出将在目标完成后进行分组和打印。如果给出了 --output-sync 或 -O 选项而不提供任何参数,则这是默认设置。

  • recurse:一旦递归调用完成,每个 make 的递归调用的输出都会被分组和打印。

无论选择何种模式,总构建时间都是相同的。唯一的区别在于输出的显示方式。

“target” 和 “recurse” 模式都收集目标的整个配方的输出,并在配方完成时不间断地显示出来。它们之间的区别在于如何处理包含递归调用的配方。对于所有没有递归行的配方,“target” 和 “recurse” 模式的行为相同。

如果选择 “recurse” 模式,则包含递归 make 调用的配方将被视为与其他目标相同:在整个配方完成后,将保存并打印配方的输出(包括递归 make 的输出)。这可确保将给定递归 make 实例构建的所有目标的输出组合在一起,从而使输出更易于理解。然而,这也会导致在构建期间很长一段时间内看不到输出,然后是大量的输出。如果您没有在构建过程中查看构建,而是在事后查看构建的日志,那么这可能是您的最佳选择。

如果您正在查看输出,那么构建期间的长时间安静间隔可能会令人沮丧。“target” 输出同步模式使用标准方法检测何时以递归方式调用 make,并且不会同步这些行的输出。递归 make 将对其目标执行同步,每个目标的输出将在完成后立即显示。请注意,配方递归行的输出不同步(例如,如果递归行在运行 make 之前打印消息,则该消息将不会同步)。

‘line’ 模式对于正在监视 make 输出以跟踪配方何时开始和完成时的前端非常有用。

make 调用的某些程序如果确定要将输出写入终端而不是文件(通常描述为 ‘“interactive”’ 模式与 ‘non-interactive’ 模式),则其行为可能会有所不同。例如,许多可以显示彩色输出的程序,如果它们确定没有写入终端,就不会这样做。如果您的 makefile 调用了这样的程序,那么使用输出同步选项将导致程序认为它正在 ‘non-interactive’ 模式下运行,即使输出最终会转到终端。

2、并行执行时的输入

两个进程不能同时从同一设备接受输入。为了确保只有一个配方一次尝试从终端获取输入,make 将使除一个正在运行的配方之外的所有配方的标准输入流失效。如果另一个配方尝试从标准输入读取数据,它通常会导致致命错误('Broken pipe’信号)。

无法预测哪个配方将具有有效的标准输入流(该流将来自终端,或者您重定向make的标准输入的任何地方)。第一个配方运行将始终首先获得它,并且在完成之后开始的第一个配方将得到它,依此类推。

如果我们找到更好的替代方案,我们将改变这方面的工作方式。与此同时,如果您正在使用并行执行特性,则不应该依赖任何使用标准输入的配方;但是如果您没有使用此特性,那么标准输入在所有配方中都正常工作。

五、配方中的错误

在每次 shell 调用返回后,查看其退出状态。如果 shell 成功完成(退出状态为零),则配方中的下一行将在新 shell 中执行;最后一行完成后,规则就完成了。

如果出现错误(退出状态为非零),请放弃当前规则,也许放弃所有规则。

有时,某个配方行的失败并不表示有问题。例如,可以使用 mkdir 命令来确保目录存在。如果目录已经存在,mkdir 将报告一个错误,但您可能希望继续执行。

要忽略配方行中的错误,请在该行文本的开头(在初始选项卡之后)写一个 ‘-’。在将行传递给 shell 执行之前,将丢弃 ‘-’。

例如:

clean:
        -rm -f *.o

即使 rm 无法删除文件,make 也会继续执行。

当您使用 ‘-i’ 或 ‘–ignore-errors’ 标志运行 make 时,所有规则的所有配方中的错误都会被忽略。如果没有先决条件,那么makefile中针对特殊目标 .IGNORE 的规则也具有相同的效果。这种方法不太灵活,但有时很有用。

当由于 A 或 B 标志而忽略错误时,make 将错误视为与成功一样返回,只是它会打印出一条消息,告诉您 shell 退出时使用的状态代码,并说错误已被忽略。

当错误发生时,nake 没有被告知忽略,这意味着当前目标不能被正确地重做,任何其他直接或间接依赖于它的目标也不能被正确地重做。由于这些目标的先决条件尚未实现,因此将不再为这些目标执行其他方案。

通常情况下,make 在这种情况下立即放弃,返回非零状态。但是,如果指定了 ‘-k’ 或 ‘–keep-going’ 标志,则 make 将继续考虑挂起目标的其他先决条件,必要时在放弃并返回非零状态之前重新创建它们。例如,在编译一个目标文件出错后,‘make -k’ 将继续编译其他目标文件,即使它已经知道链接它们是不可能的。

通常的行为假设您的目的是使指定的目标保持最新状态;一旦 make 知道这是不可能的,它不妨立即报告故障。’-k’ 选项表示,真正的目的是测试尽可能多的程序中所做的更改,也许是为了找到几个独立的问题,以便您可以在下次尝试编译之前将它们全部更正。这就是为什么Emacs的编译命令默认传递 ‘-k’ 标志的原因。

通常,当配方行失败时,如果它完全更改了目标文件,则该文件已损坏且无法使用,或者至少没有完全更新。然而,该文件的时间戳表示它现在是最新的,因此下次运行时,它将不会尝试更新该文件。这种情况就像炮弹被一个信号杀死一样;因此,通常正确的做法是,如果配方在开始更改文件后失败,则删除目标文件。如果 .DELETE_ON_ERROR 作为目标出现,make 就会这样做。这几乎总是你想做的,但这不是历史惯例;因此,为了兼容性,您必须显式地请求它。

六、中断或杀死 make

如果 make 在 shell 执行时收到致命信号,它可能会删除配方应该更新的目标文件。如果目标文件的上次修改时间自首次检查以来已更改,则会执行此操作。

删除目标的目的是确保在下次运行 make 时从头开始重新制作目标。这是为什么呢?假设你在编译器运行时输入 Ctrl-c,然后它开始写目标文件 foo.c。Ctrl-c 终止编译器,导致文件不完整,其上次修改时间比源文件 foo.c 新。如果 make 未执行此操作,则下次调用 make 时会认为 foo.o 不需要更新,从而导致链接器在尝试链接对象文件时出现奇怪的错误消息,其中一半丢失。

通过使特殊目标 .PRECIOUS 依赖于目标文件,可以通过这种方式防止删除目标文件。在重做目标之前,make 会检查目标是否出现在 .PRECIOUS 的前提条件上,如果有信号发生,就决定是否删除目标。可能这样做的一些原因是目标以某种原子方式更新,或者目标的存在只是为了记录修改时间(其内容无关紧要),或者必须一直存在以防止其他类型的麻烦。

尽管 make 尽最大努力进行清理,但在某些情况下,清理是不可能的。例如,make 可能被一个无法捕获的信号杀死。或者,其中一个程序调用可能被终止或崩溃,留下一个最新但损坏的目标文件:make 不会意识到这个失败需要清理目标。或者 make 本身可能会遇到错误并崩溃。

出于这些原因,最好编写防御性配方,即使它们失败也不会留下损坏的目标。最常见的是,这些配方创建临时文件,而不是直接更新目标,然后将临时文件重命名为最终的目标名称。有些编译器已经以这种方式运行了,因此您不需要编写防御配方。

七、make 的递归使用

递归使用 make 意味着在 makefile 中使用 make 作为命令当您需要为组成更大系统的各种子系统分别创建makefile时,这种技术非常有用。例如,假设您有一个子目录 subdir,它有自己的 makefile,并且您希望包含目录的 makefile 在子目录上运行 make。你可以这样写:

subsystem:
        cd subdir && $(MAKE)

或者,等价于:

subsystem:
        $(MAKE) -C subdir

您可以通过复制这个例子来编写递归 make 命令,但是关于它们的工作方式和原因,以及子 make 与顶级 make 之间的关系,还有很多事情需要了解。您还可能发现将调用递归 make 命令的目标声明为 .PHONY 是很有用的。

为了方便起见,当GNU make启动时(在它处理了任何-C选项之后),它将变量 CURDIR 设置为当前工作目录的路径名。这个值不会再被 make 修改:特别要注意的是,如果你包含了来自其他目录的文件,CURDIR 的值不会改变。如果在makefile中设置了该值,则该值具有相同的优先级(默认情况下,环境变量CURDIR不会覆盖该值)。请注意,设置这个变量对 make 的操作没有影响(例如,它不会导致 make 更改其工作目录)。

1、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 变量的效果与在配方行开头使用 ‘+’ 字符的效果相同。此特殊特性仅在 MAKE 变量直接出现在配方中时才启用:如果 MAKE 变量通过另一个变量的扩展被引用,则此特性不适用。在后一种情况下,您必须使用 ‘+’ 令牌来获得这些特殊效果。

考虑上面例子中的命令 ‘make -t’。’-t’选项将目标标记为最新,而无需实际运行任何配方;按照示例中 ‘-t’,‘make -t’ 命令的通常定义将创建一个名为子系统的文件,而不执行任何其他操作。你真正想让它做的是运行 ‘cd subdir && make -t’;但这需要执行配方,而 ‘-t’ 说不要执行配方。

特殊功能使其可以执行您想要的操作:每当规则的配方行包含变量 MAKE 时,标志 ‘-t’、’-n’和’-q’ 就不适用于该行。包含 MAKE 的配方行会正常执行,尽管存在一个导致大多数配方无法运行的标志。通常的 MAKEFLAGS 机制将标志传递给子 make,因此您访问文件或打印配方的请求将被传递到子系统。

2、将变量传达给子 make

顶级 make 的变量值可以通过环境显式请求传递给子 make。这些变量在 B 中定义为默认值,但它们不会覆盖子 make 使用的 makefile 中定义的变量,除非您使用 ‘-e’ 开关。

要传递或导出变量,make 会将变量及其值添加到环境中,以运行配方的每一行。反过来,子 make 使用环境来初始化其变量值表。

除非通过显式请求,否则仅当变量最初在环境中定义或在命令行上设置变量,并且其名称仅由字母、数字和下划线组成时,make 才会导出变量。有些 shell 不能处理由字母、数字和下划线以外的字符组成的环境变量名。

不导出 make 变量 SHELL 的值。相反,调用环境中 SHELL 变量的值将传递给子 make。您可以使用 export 指令强制 make 导出 SHELL 的值,如下所述。

特殊变量 MAKEFLAGS 始终被导出(除非您取消导出它)。如果您将 MAKEFLAGS 设置为任何值,那么它将被导出。

make 将在命令行中定义的变量值放到 MAKEFLAGS 变量中,从而自动向下传递。

如果变量是默认情况下由 make 创建的,则通常不会向下传递变量。子 make 将为自己定义这些内容。

如果你想导出特定的变量到子 make,使用 export 指令,像这样:

export variable …

如果你想阻止一个变量被导出,使用 unexport 指令,像这样:

unexport variable …

在这两种形式中,export 和 unexport 的参数都是展开的,变量或函数也可以展开为要导出的(un)变量名(列表)。

为了方便起见,您可以在定义变量的同时导出它:

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 单独导出变量,除非在 export 指令中特别提到,否则名称中包含字母、数字和下划线以外的字符的变量将不会被导出。

由 export 指令本身引发的行为在 GNU make 的旧版本中是默认的。如果你的makefile依赖于这个行为,并且你想要与旧版本的 make 兼容,你可以为特殊目标 .EXPORT_ALL_VARIABLES 编写一个规则,而不是使用 export 指令。这将被旧的 makes 忽略,而 export 指令将导致语法错误。

同样,您可以使用 unexport 本身来告诉 make 在默认情况下不要导出变量。因为这是默认行为,所以只有在A之前被自己使用过的时候(可能是在一个包含的makefile中),才需要这样做。您不能单独使用 export 和 unexport 为某些配方导出变量,而不能为其他配方导出变量。单独出现的最后一个 export 或 unexport 指令决定了整个 make 运行期间的行为。

作为一项特殊功能,变量 MAKELEVEL 在从一个级别传递到另一个级别时会发生变化。这个变量的值是一个字符串,以十进制形式表示级别的深度。顶级 make 的值是 ‘0’,次级 make 的值是 ‘1’,次次级 make 的值是 ‘2’,以此类推。当 make 为配方设置环境时,会发生增量。

MAKELEVEL 的主要用途是在条件指令中测试它;通过这种方式,你可以编写一个makefile,当你递归运行时,它会以一种方式运行,而当你直接运行时,它会以另一种方式运行。

您可以使用变量 MAKEFILES 使所有子 make 命令使用额外的 makefile。MAKEFILES 的值是一个以空格分隔的文件名列表。如果在外部makefile中定义这个变量,则通过环境传递;然后它作为一个额外的makefiles列表,让子 make 在通常的或指定的makefiles之前读取。

3、将选项传递给子 make

像 ‘-s’ 和 ‘-k’ 这样的标志会通过变量 MAKEFLAGS 自动传递给子 make。这个变量由 make 自动设置,以包含 make 收到的标志字母。因此,如果你执行 ‘make -ks’,那么 MAKEFLAGS 就会得到 ‘ks’ 的值。

因此,每个子 make 在它的环境中都得到了 MAKEFLAGS 的值。作为响应,它从该值中获取标志并处理它们,就像它们作为参数给出一样。

同样,在命令行上定义的变量也通过 MAKEFLAGS传递给子 make。MAKEFLAGS 中包含 ‘=’、make 的值中的单词被视为变量定义,就像它们出现在命令行一样。

选项 ‘-C’、’-f’、’-o’ 和 ‘-W’ 没有放入选项 MAKEFLAGS 中;这些选项不会被传递下去。

‘-j’ 选项是一种特殊情况。如果您将它设置为某个数值 ‘N’,并且您的操作系统支持它(大多数UNIX系统都支持;其他的通常不会),父类 make 和所有的子 make 将通信,以确保在它们之间只有 ‘N’ 作业同时运行。请注意,任何标记为递归的作业都不会计入总作业中(否则,我们可能会让’N’ 子 make 运行,并没有为任何实际工作留下任何槽位)。

如果您的操作系统不支持上述通信,那么没有 ‘-j’ 被添加到 MAKEFLAGS 中,因此子 make 以非并行模式运行。如果 ‘-j’ 选项被传递给子 make,你会得到比你要求的更多的并行作业。如果你给出一个不带数字参数的 ‘-j’,意思是并行运行尽可能多的作业,这是传递的,因为多个无穷大不大于1。

如果不想传递其他标志,则必须更改 MAKEFLAGS 的值,如下所示:

subsystem:
        cd subdir && $(MAKE) MAKEFLAGS=

命令行变量定义实际上出现在变量 MAKEOVERRIDES 中,而变量 MAKEFLAGS 包含对该变量的引用。如果你想正常传递标志,但是不想传递命令行变量定义,你可以重置 MAKEOVERRIDES 为空,就像这样:

MAKEOVERRIDES =

这通常是没用的。然而,有些系统对环境的大小有一个很小的固定限制,将如此多的信息输入到 MAKEFLAGS 的值中可能会超过它。如果您看到错误消息A,这可能就是问题所在(为了严格遵守POSIX.2,如果特殊目标C出现在makefile中,更改A不会影响B。你可能不关心这个)。

为了历史兼容性,还存在一个类似的变量 MFLAGS。它具有与 A 相同的值,只是它不包含命令行变量定义,并且始终以连字符开头,除非它是空的(仅当 MAKEFLAGS 以没有单字母版本的选项开头时,A 才以连字符开头,例如 ‘–warn-undefined-variables’)。传统上,A 在递归 make 命令中显式使用,如下所示:

subsystem:
        cd subdir && $(MAKE) $(MFLAGS)

但是现在A使这个用法变得多余。如果您希望您的 makefile 与旧的 make 程序兼容,请使用此技术;它也可以在更现代的 make 版本中工作。

如果您希望在每次运行 make 时设置某些选项(如 ‘-k’),那么 MAKEFLAGS 变量也很有用。您只需在环境中为 MAKEFLAGS 设置一个值。您还可以在makefile中设置 MAKEFLAGS ,以指定应该对该makefile有效的其他标志(注意,不能这样使用 MFLAGS。该变量仅用于兼容性;make 不会以任何方式解释您为其设置的值。)。

当 make 解释 MAKEFLAGS 的值时(无论是来自环境还是来自makefile),如果值不是以1开头,它首先会在前面加一个连字符。然后它将值分割成由空格分隔的单词,并解析这些单词,就像它们是命令行上给出的选项一样(除了 ‘-C’、’-f’、’-h’、’-o’、’-W’ 和它们的长命名版本被忽略;无效选项没有错误)。

如果你确实把 MAKEFLAGS 放在你的环境中,你应该确保不要包含任何会严重影响 make 的行为和破坏 makefiles 和 make 本身目的的选项。例如,如果将 ‘-t’、’-n’ 和 ‘-q’ 选项放入这些变量之一,可能会产生灾难性的后果,并且肯定会产生至少令人惊讶且可能令人讨厌的效果。

如果你想在 GNU make 之外运行 make 的其他实现,因此不想将 GNU make-specific 标志添加到 MAKEFLAGS 变量中,你可以将它们添加到 GNUMAKEFLAGS 变量中。此变量在 MAKEFLAGS 之前解析,方式与 MAKEFLAGS 相同。当 make 构造 MAKEFLAGS 传递给递归时,make 它将包含所有标志,甚至是从 GNUMAKEFLAGS 中获取的标志。因此,解析 GNUMAKEFLAGS 后,GNU make 将该变量设置为空字符串,以避免递归期间重复标记。

最好只使用带有标志的 GNUMAKEFLAGS,它不会实质性地改变你的makefile的行为。如果你的 makefile 无论如何都需要GNU make,那么只需使用 MAKEFLAGS。像 ‘–no-print-directory’ 或 ‘–output-sync’ 这样的标志可能适合于 GNUMAKEFLAGS。

4、–print-directory 选项

如果使用多个级别的递归 make 调用,则 ‘-w’ 或 ‘–print-directory’ 选项可以将输出显示为 make 开始处理它,并在 make 完成处理时显示每个目录,从而使输出更易于理解。例如,如果在目录 /u/gnu/make 中运行 ‘make -w’,make 将打印以下格式的一行:

make: Entering directory `/u/gnu/make'.

在执行任何其他操作之前,以及表单的一行:

make: Leaving directory `/u/gnu/make'.

处理完成时。

通常,您不需要指定此选项,因为 ‘make’ 会为您执行此操作:当您使用 ‘-C’ 选项时,’-w’ 会自动打开,而在子 make 中则会自动打开。如果您还使用 ‘-s’(表示静默),或者如果您使用 ‘–no-print-directory’ 显式禁用它,则 make 不会自动打开 ‘-w’。

八、定义封装配方

当相同的命令序列用于创建各种目标时,您可以使用define指令将其定义为一个屏蔽序列,并从那些目标的菜谱中引用该屏蔽序列。该封闭序列实际上是一个变量,因此名称不能与其他变量名称冲突。

以下是定义 canned 配方的示例:

define run-yacc =
yacc $(firstword $^)
mv y.tab.c $@
endef

这里 run-yacc 是被定义变量的名称;endef 表示定义的结束中间的行是命令。中间的行是命令。define 指令不扩展被屏蔽序列中的变量引用和函数调用;’$’ 字符、括号、变量名等等,都成为您定义的变量值的一部分。

本例中的第一个命令在使用屏蔽序列的规则的第一个前提条件下运行 Yacc。Yacc的输出文件总是命名为 y.tab.c。第二个命令将输出移动到规则的目标文件名。

要使用封闭序列,请将变量替换为规则的配方。你可以像替换其他变量一样替换它。因为 defined 定义的变量是递归扩展的变量,所以在 defined 中编写的所有变量引用现在都是扩展的。例如:

foo.c : foo.y
        $(run-yacc)

当变量 ‘$^’ 出现在run-yacc的值中时,‘foo.y’ 将被替换,而 ‘foo.c’ 将被替换为 ‘$@’。

这是一个现实的例子,但在实践中不需要这个特定的示例,因为 make 有一个隐式规则,可以根据所涉及的文件名找出这些命令。

在recipe执行中,被封装序列的每一行都被视为在规则中单独出现的行,前面有一个制表符。特别是,make 为每行调用一个单独的子 shell。您可以在被屏蔽序列的每一行上使用影响命令行的特殊前缀字符(’@’、’-‘和’+’)。例如,使用这个固定的序列:

define frobnicate =
@echo "frobnicating target $@"
frob-step-1 $< -o $@-step-1
frob-step-2 $@-step-1 -o $@
endef

make 不会回显第一行 echo 命令。但它将与以下两条配方线相呼应。

另一方面,配方行上引用封闭序列的前缀字符应用于序列中的每一行。因此,规则:

frob.out: frob.in
        @$(frobnicate)

不回显任何配方行。

九、使用空配方

定义什么都不做的配方有时是有用的这只需给出一个只包含空格的配方即可完成。例如:

target: ;

为 target 定义一个空配方。您也可以使用以配方前缀字符开头的行来定义一个空的配方,但是这可能会令人困惑,因为这样的行看起来是空的。

您可能想知道为什么要定义一个什么都不做的配方。这很有用的一个原因是可以防止目标获得隐式配方(来自隐含规则或 .DEFAULT 特殊目标)。

空配方还可用于避免将创建为另一个配方的副作用的目标的错误:如果目标不存在,空配方可确保 make 不会抱怨它不知道如何构建目标,并且 make 将假定目标已过期。

您可能倾向于为不是实际文件的目标定义空配方,而只是为了它们的先决条件可以被重做而存在。但是,这并不是最好的方法,因为如果目标文件确实存在,先决条件可能不会被正确地重新设置。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值