Makefile之配方规则

Makefile之配方规则


下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

温故知新

规则的配方由一个或多个要执行的 shell 命令行组成,一次一个,按照它们出现的顺序。通常,执行这些命令的结果是使规则的目标更新。用户使用许多不同的 shell 程序,但 Makefile 中的配方始终由/bin/bash除非 Makefile 另有指定。


下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

配方语法

Makefile包含两种不同的语法:大部分Makefile使用make语法,而配方则使用shell语法。make程序不尝试理解shell语法:它只对配方的内容执行极少的特定翻译,然后将其交给shell。

配方中的每一行必须以制表符(或.RECIPEPREFIX变量的值的第一个字符)开头,除了第一行配方可以通过分号与目标和先决条件行连接。Makefile中的任何以制表符开头的行,出现在规则上下文中(也就是,在启动规则后,直到另一个规则或变量定义之前),都将被视为该规则的配方的一部分。空白行和纯注释行可以出现在配方行之间,它们会被忽略。

这些规则的一些情况包括:

  1. 以制表符开头的空白行不是空白行:它们是空配方。
  2. 配方中的注释不是make注释;它将按原样传递给shell。shell是否将其视为注释取决于你的shell。
  3. 规则上下文中以制表符作为行首字符缩进的变量定义将被视为配方的一部分,而不是make变量定义,并传递给shell。
  4. 规则上下文中以制表符作为行首字符缩进的条件表达式将被视为配方的一部分,并传递给shell。将配方行拆分

下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

分割配方行

在Makefile中,make解释配方的方式之一是检查换行符前是否有反斜杠。与正常的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

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


下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

在配方中使用变量

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

下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

配方回显

通常,make在执行之前会打印每行配方。我们称这个过程为echo,因为它看起来就像是你自己在键入这些命令。

当一行以@开头时,该行的输出会被抑制。@在传递给shell之前被丢弃。通常,你可以将这个特性用于只用于打印信息的命令,比如使用echo命令来指示Makefile的进度:

@echo About to make distribution files

当使用标志-n--just-print运行make时,它只会回显大多数配方,而不会执行它们。在这种情况下,甚至以@开头的配方行也会被打印出来。这个标志对于查找make认为哪些配方是必要的,而不实际执行它们很有用。

使用make的-s--silent标志可以阻止所有回显,就好像所有的配方都以@开头一样。Makefile中特殊目标.SILENT的规则,没有前提条件,也具有相同的效果。


下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

配方执行

当需要执行配方以更新目标时,通常会为配方的每一行调用一个新的子shell,除非已启用.ONESHELL特殊目标。请注意:这意味着设置shell变量和调用诸如cd之类设置每个进程局部上下文的shell命令不会影响配方中的后续行。如果要使用cd来影响下一条语句,将两条语句放在一个单独的配方行中。然后make将调用一个shell来运行整个行,shell将按顺序执行这些语句。例如:

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

这里我们使用shell的AND操作符&&,这样如果cd命令失败,脚本将会在尝试在错误的目录中调用gobble命令之前失败,这可能会引发问题(在这种情况下,至少会导致…/foo被截断)。


下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

使用一个shell

有时,您可能希望将配方中的所有行传递给单个shell的一次调用。

通常有两种情况下会这样使用:首先,它可以提高性能,特别是在配方包含许多命令行的Makefile中,避免了额外的进程。其次,您可能希望在您的配方命令中包括换行符(例如,也许您正在将一个非常不同的解释器用作您的SHELL)。如果Makefile的任何地方出现.ONESHELL特殊目标,那么每个目标的所有配方行将提供给单个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 :
    # 确保“@”不是第一行的第一个字符
    @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以向shell添加-e选项,这将导致命令行中的任何位置的任何失败都将导致shell失败,但这可能会使您的配方行的行为不同。最终,您可能需要加固您的配方行,以使它们能够与.ONESHELL一起正常工作。


下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

选择shell

在Makefile中,用作shell的程序由SHELL变量确定。默认情况下,如果您没有在Makefile中设置SHELL变量,make将默认使用/bin/bash作为shell。

以下是关于Makefile中SHELL变量的一些关键要点:

  1. 默认Shell:如果您没有在Makefile中指定SHELL变量,make将默认使用/bin/sh shell。

  2. 环境变量SHELL环境变量用于指定用户交互使用的shell程序的选择。通常情况下,它在系统环境中设置,除非您在Makefile中明确设置它,否则make不会使用它。

  3. .SHELLFLAGS:传递给shell的参数取自.SHELLFLAGS变量。.SHELLFLAGS的默认值通常是-c,或者在符合POSIX标准的模式下是-ec

  4. 不受环境变量影响:与大多数变量不同,SHELL变量从不从环境中设置。这是因为SHELL环境变量用于指定用户的个人交互式shell选择。如果个人选择影响Makefile的功能,那将是非常糟糕的。

  5. 未导出到recipe行:如果在Makefile中设置了SHELL变量,那么该值不会导出到make调用的recipe行的环境中。相反,导出的是从用户环境继承的值。您可以通过显式导出SHELL来覆盖此行为,从而强制将其传递给recipe行的环境。

  6. 在MS-DOS和MS-Windows中的特殊处理:在MS-DOS和MS-Windows上,SHELL变量的值取自环境中的SHELL,因为在这些系统上,大多数用户不设置此变量,因此它很可能是专门用于make的。在MS-DOS上,如果SHELL的设置不适用于make,您可以设置MAKESHELL变量,以指定make应该使用的shell。如果设置了MAKESHELL,它将用作shell,而不是SHELL的值。

  7. 在DOS和Windows中选择shell:在DOS和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,make在MS-DOS上还会检查该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的值将不会更改,因此设置SHELL的行将被忽略。这样,只有在实际安装了Unix风格的shell的系统上,make才会支持特定于Unix风格的shell的功能。

需要注意的是,对于在Makefile中设置SHELL的行的上述DOS特定处理,仅适用于Makefile中设置SHELL的情况;如果它在环境或命令行中设置,您应该将其设置为shell的完整路径,就像在Unix上一样。

总之,上述DOS特定处理的效果是,包含SHELL = /bin/sh(正如许多Unix Makefile所做的那样)的Makefile将在MS-DOS上不经修改地工作,只要您在PATH中的某个目录中安装了例如sh.exe

如果您在MS-DOS或Windows上使用make,可以根据您的系统设置SHELL变量,以指定make应使用的shell。这对确保make在不同环境中正常工作非常重要,尤其是在DOS和Windows系统上。

您可以在Makefile中将SHELL设置为所需的shell,以确保make在不同系统上都能正常工作。如果您使用的是Unix风格的shell,只需设置SHELL,并确保安装了相应的shell程序。如果您在MS-DOS上使用make,还可以设置MAKESHELL变量以指定替代shell程序,从而确保make可以找到适当的shell。


下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

并行执行

make支持并行执行配方,可以使用-j--jobs选项来启用。这个选项告诉make同时执行多个配方,这可以显著加快构建过程,特别是在多核系统上。以下是关于make并行执行的一些关键要点:

  1. -j--jobs选项:你可以使用-j选项后跟整数来指定同时执行的作业槽或配方的数量。例如,-j 4将同时运行最多四个配方。如果在-j后面没有指定整数,那么配方的数量没有限制,实际上启用了并行执行。

  2. 默认作业数:默认情况下,make的作业数为1,这意味着串行执行(一次执行一个任务)。

  3. 并行执行与递归make:处理递归make调用时,会引发一些与并行执行相关的问题。

  4. 错误处理:如果某个配方失败(被信号终止或以非零状态退出),并且不忽略该配方的错误,则不会运行重新构建相同目标的其余配方行。如果配方失败,而未提供-k--keep-going选项,make会中止执行。如果make以任何原因终止(包括信号),同时仍然有子进程在运行,则它会等待它们完成后才真正退出。

  5. 控制并行性:使用-l--max-load选项可以限制同时运行的作业数,根据系统的负载平均值。-l--max-load选项后跟浮点数。例如:

-l 2.5

这将不允许make在负载平均值高于2.5时启动多于一个作业。如果在-l选项后没有指定数字,将删除负载限制,如果之前的-l选项指定了限制。

  1. 禁用并行执行:在某些情况下,你可能希望在Makefile中禁用某些或所有目标的并行执行。

  2. 并行执行期间的输出和输入:并行执行期间,输出和输入可能会导致问题。

请注意,对于MS-DOS系统,-j选项无效,因为该系统不支持多进程。


下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

禁用并行执行

在GNU make中,你可以使用不同的方法来禁用并行执行,特别是当某些或所有目标无法并行执行,或者添加必要的前提条件以通知make不可行时。以下是一些方法:

  1. 使用.NOTPARALLEL特殊目标:如果在Makefile中的任何位置指定.NOTPARALLEL特殊目标而且没有前提条件,那么整个make实例都将以串行方式运行,而不考虑并行设置。例如:
all: one two three
one two three: ; @sleep 1; echo $@

.NOTPARALLEL:

不管如何调用make,目标one、two和three都将串行运行。

  1. 使用.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将它们列为前提条件且没有串行化。

  1. 使用.WAIT特殊目标:.WAIT特殊目标允许你以更精细的方式控制特定前提条件的串行化。当.WAIT特殊目标出现在前提条件列表中,且启用了并行执行,make将在构建.WAIT左侧的所有前提条件完成之前,不会构建.WAIT右侧的任何前提条件。例如:
all: one two .WAIT three
one two three: ; @sleep 1; echo $@

如果启用了并行执行,make将尝试并行构建one和two,但不会尝试构建three,直到one和two都完成。

请注意,与.NOTPARALLEL和目标提供的.WAIT类似,.WAIT只在构建包含它的目标时生效。如果相同的前提条件在其他目标中存在,而没有.WAIT,则它们可能仍然并行执行。因此,与定义前提关系相比,.NOTPARALLEL.WAIT不够可靠,但它们易于使用,在较不复杂的情况下可能已经足够。

.WAIT前提条件不会出现在规则的自动变量中。

你可以在Makefile中创建一个实际的.WAIT目标以实现可移植性,但不是必需的。如果创建了.WAIT目标,它不应具有前提条件或命令。

.WAIT功能还在其他版本的make中实现,并在POSIX标准中为make指定。


下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

并行执行期间的输出

在并行运行多个recipe时,每个recipe生成的输出会立即显示出来,导致来自不同recipe的消息可能会交错,有时甚至出现在同一行上。这可能使输出的阅读非常困难。

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

如果启用工作目录打印,则在每个输出组中打印进入/离开消息。如果你不想看到这些消息,可以将’–no-print-directory’选项添加到MAKEFLAGS。

输出同步有四个粒度级别,可以通过给选项传递参数来指定(例如,‘-Oline’或’–output-sync=recurse’)。

  • none:这是默认设置,所有输出会直接发送,没有执行同步。
  • line:来自每个recipe的每一行输出都会分组,并在该行完成后立即打印。如果一个recipe由多个行组成,它们可能与其他recipe的行交错。
  • target:对于每个目标的整个recipe的输出被分组,并在目标完成后打印。如果没有给出–output-sync或-O选项的参数,这是默认设置。
  • recurse:来自每个递归调用make的输出都会分组,并在递归调用完成后打印。

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

“target”和“recurse”模式都会收集每个目标的整个recipe的输出,并在recipe完成后显示它,这种模式下所有没有递归行的recipe,“target”和“recurse”模式的行为相同。

如果选择了“recurse”模式,包含递归make调用的recipe将与其他目标一样处理:整个recipe的输出,包括递归make的输出,都会保存并在整个recipe完成后打印出来。这确保了由给定的递归make实例构建的所有目标的输出被分组在一起,这可能会使输出更容易理解。但这也会导致构建期间出现长时间的没有输出的时间段,然后是大量输出。如果你不是在进行构建时观看输出,而是在事后查看构建日志,这可能是最适合你的选项。

如果你正在观看输出,构建过程中的长时间的安静期间可能会让人沮丧。输出同步模式"target"检测到make将要递归调用的情况,使用标准方法,它将不同步这些行的输出。递归make将执行其目标的同步,每个目标的输出将在完成时立即显示。请注意,recipe的递归行的输出不会被同步(例如,如果递归行在运行make之前打印消息,该消息将不会被同步)。

"line"模式可用于监视make输出以跟踪何时启动和完成recipe的前端。

如果由make调用的某些程序在确定它们是写入终端还是文件时会有不同行为(通常被描述为“交互”与“非交互”模式)。例如,许多可以显示带颜色的输出的程序如果确定它们未写入终端则不会显示颜色。如果你的Makefile调用了这样的程序,那么使用输出同步选项将导致程序认为它在“非交互”模式下运行,即使最终的输出将进入终端。


下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

并行执行时的输入

在同一时间内,两个进程无法同时从相同设备获取输入。为了确保只有一个recipe尝试从终端获取输入,make将使除一个正在运行的recipe之外的所有其他recipe的标准输入流无效。如果另一个recipe尝试从标准输入读取数据,通常会导致致命错误("Broken pipe"信号)。

无法预测哪个recipe将具有有效的标准输入流(该流将来自终端,或者你在make的标准输入上重定向到的地方)。第一个运行的recipe将首先获得它,然后在第一个recipe完成后启动的第一个recipe将获得它,依此类推。

如果我们找到更好的替代方法,我们将更改make工作方式的这个方面。与此同时,如果你正在使用并行执行功能,则不应依赖于任何recipe使用标准输入;但如果你不使用此功能,那么标准输入在所有recipe中都可以正常使用。


下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

配方中的错误

在每个shell调用返回后,make会查看其退出状态。如果shell成功完成(退出状态为零),则将在新的shell中执行recipe的下一行;在最后一行完成后,规则也完成。

如果发生错误(退出状态为非零),make会放弃当前规则,甚至可能放弃所有规则。

有时候某个recipe行的失败并不表示问题。例如,你可以使用mkdir命令来确保目录存在。如果目录已经存在,mkdir将报告错误,但你可能希望无论如何让make继续执行。

要忽略recipe行中的错误,请在行文本的开头(在初始制表符之后)写一个“-”。这个“-”在传递给shell执行之前被丢弃。

例如,

clean:
    -rm -f *.o

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

当使用-i--ignore-errors标志运行make时,所有规则的所有recipe中的错误都将被忽略。如果没有前提条件,Makefile中的特殊目标.IGNORE的规则具有相同的效果,这种方法不太灵活,但有时会很有用。

当要忽略错误时,由于--i标志,make将错误返回视为成功,但会打印一条消息,告诉你shell退出时的状态代码,并表示已忽略错误。

当发生make未被告知要忽略的错误时,这意味着当前目标不能正确重建,任何直接或间接依赖于它的目标也不能正确重建。不会为这些目标执行进一步的recipe,因为它们的前提条件尚未实现。

通常,在这种情况下,make会立即放弃,并返回非零状态。但是,如果指定了-k--keep-going标志,make将继续考虑待处理目标的其他前提条件,必要时重新构建它们,然后放弃并返回非零状态。例如,在编译一个对象文件时发生错误,make -k将继续编译其他对象文件,尽管它已经知道链接它们将是不可能的。

通常,当recipe行失败时,如果它已经更改了目标文件,那么文件将受到破坏,无法使用,或者至少没有完全更新。但文件的时间戳表示它现在是最新的,因此下次运行make时,它将不会尝试更新该文件。情况与通过信号杀死shell时完全相同;所以一般来说,如果recipe在开始更改文件后失败,通常应该删除目标文件。如果.DELETE_ON_ERROR出现为一个目标,make将执行此操作。这几乎总是你希望make执行的操作,但这并不是历史惯例,因此出于兼容性考虑,你必须显式请求它。


下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

打断或杀死make

如果make在shell执行时收到致命信号,它可能会删除recipe应该更新的目标文件。这是在目标文件的上次修改时间自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本身可能遇到错误并崩溃。

因此,最好编写防御性的recipe,即使它们失败也不会留下损坏的目标。最常见的情况是,这些recipe创建临时文件而不是直接更新目标,然后将临时文件重命名为最终目标名称。一些编译器已经采用这种方式,因此你不需要编写防御性的recipe。


下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

递归使用make

递归使用make是指在Makefile中使用make作为一个命令的技术。这个技术在你想为组成一个较大系统的各个子系统创建单独的Makefile时非常有用。例如,假设你有一个子目录subdir,它有自己的Makefile,并且你希望包含该目录的Makefile在子目录上运行make。你可以通过编写以下方式实现:

subsystem:
    cd subdir && $(MAKE)

或者等效地,可以这样写:

subsystem:
    $(MAKE) -C subdir

你可以通过复制这个示例来编写递归make命令,但有很多关于它们的工作原理以及为什么以及子make与顶层make的关系的事情需要了解。你可能还会发现,将调用递归make命令的目标声明为.PHONY是有用的。

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


下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

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命令将创建一个名为subsystem的文件,然后什么也不做。你实际上希望它运行cd subdir && make -t;但这将需要执行该配方,而-t表示不执行配方。

这个特殊功能使得它做到了你想要的:每当一个规则的配方行包含变量MAKE时,标志-t-n-q不适用于该行。包含MAKE的配方行将正常执行,尽管存在一个导致大多数配方不运行的标志。通常的MAKEFLAGS机制会将这些标志传递给子make,因此你的对于触摸文件或打印配方的请求会传播到子系统。


下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

将变量传递给子系统make

在递归make命令中,顶层make的变量值可以通过显式请求传递给子make的环境。这些变量在子make中被定义为默认值,但它们不会覆盖子make使用的Makefile中定义的变量,除非你使用-e开关。

要传递或导出一个变量,make将变量及其值添加到每行配方的运行环境中。子make依次使用环境来初始化其变量值表。有关来自环境的变量的更多信息,

除非明确请求,make只会导出变量,如果它们最初在环境中定义,或者如果在命令行上设置了它们并且它们的名称只包含字母、数字和下划线。

make变量SHELL的值不会被导出。相反,调用环境中的SHELL变量值将传递给子make。你可以通过使用export指令来强制make导出其SHELL的值。

特殊变量MAKEFLAGS始终被导出(除非你取消导出它)。如果将MAKEFILES设置为任何内容,它也将被导出。

make会自动传递在命令行上定义的变量值,将它们放在MAKEFLAGS变量中。

通常情况下,如果变量是由make默认创建的,它们不会被传递。子make会为自己定义这些变量。

如果你想将特定的变量导出到子make,可以使用export指令,如下所示:

export variable …

如果要防止导出某个变量,可以使用unexport指令,如下所示:

unexport variable …

在这两种形式中,export和unexport的参数都会被扩展,因此可以是扩展到变量名(列表)的变量或函数。

作为一种便捷方式,你可以同时定义一个变量并导出它,方法是:

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未明确提到的变量应该被导出。通过unexport指令给出的任何变量仍然不会被导出。

export指令本身在旧版本的GNU make中是默认行为。如果你的Makefile依赖于这种行为,并且你希望与旧版本的make兼容,可以在Makefile中添加特殊目标.EXPORT_ALL_VARIABLES,而不是使用export指令。这将被旧版本的make忽略,而使用export指令将导致语法错误。

当使用export本身或.EXPORT_ALL_VARIABLES导出默认的变量时,只有名称完全由字母、数字和下划线组成的变量将被导出。要导出其他变量,必须在export指令中明确列出它们。

将变量的值添加到环境需要对它进行扩展。如果扩展一个变量具有副作用(比如info、eval或类似函数),那么每次调用命令时都会看到这些副作用。你可以通过确保这些变量具有默认情况下不可导出的名称来避免这种情况。然而,更好的解决方案是根本不使用这个“默认导出”功能,而是通过名称明确导出相关的变量。

你可以使用unexport本身告诉make不要默认导出变量。由于这是默认行为,你只需要在之前使用了export的地方(可能在包含的Makefile中)使用它。你不能使用export和unexport本身来使变量对某些配方而言导出,对其他配方而言不导出。出现在导出或unexport指令中的最后一个指令本身将决定整个make运行的行为。

作为一个特殊特性,变量MAKELEVEL在从一个级别传递到另一个级别时发生了变化。该变量的值是一个十进制数字,表示级别的深度。顶层make的值为‘0’;子make为‘1’;子子make为‘2’,依此类推。增量发生在make为配方设置环境时。

MAKELEVEL的主要用途是在条件指令中对它进行测试;这样,你可以编写一个Makefile,如果被递归运行,则会以一种方式运行,如果被直接运行,则会以另一种方式运行。

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


下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

向子节点传达变量make

variables,这个变量,如果在外部Makefile中定义,将被通过环境传递,然后作为子make在读取通常或特定的make文件之前读取的额外的make文件列表。

递归make的主要用途是在大型项目中使用不同子系统的单独make文件。例如,假设你有一个名为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更改其工作目录,例如)。

递归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’命令将创建一个名为subsystem的文件,并不执行其他操作。你实际想要的是运行‘cd subdir && make -t’;但这需要执行配方,而“-t”表示不要执行配方。

这个特殺使得它实现了你想要的效果:每当一个规则的配方行包含MAKE变量时,“-t”、“-n”和“-q”标志都不适用于该行。包含MAKE的配方行将被正常执行,尽管存在一个标志,该标志会导致大多数配方不被运行。通常的MAKEFLAGS机制将这些标志传递给子make,因此你的请求将传播到子系统,以触及文件、打印配方。


下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

向子节点传达选项

这段文本详细解释了如何将选项传递给递归的 make 命令。以下是关键要点的总结:

  1. MAKEFLAGS 变量:诸如 -s(静默模式)和 -k(继续执行,即使出现错误)等选项会通过 MAKEFLAGS 变量自动传递给子 make。这个变量包含了顶层 make 收到的选项字母。例如,如果你运行 make -ks,那么 MAKEFLAGS 的值将是 ‘ks’。

  2. MAKEFLAGS 变量的值MAKEFLAGS 的值包括可能为空的字符组,代表不需要参数的单字母选项,后面跟着一个空格和需要参数或者具有长选项名称的选项。如果一个选项既有单字母选项又有长选项,那么单字母选项会优先使用。如果命令行上没有单字母选项,那么 MAKEFLAGS 的值将以空格开始。

  3. 命令行变量的传递:通过 MAKEFLAGS,命令行上定义的变量也会传递给子 makeMAKEFLAGS 中包含的单词,如果包含 ‘=’,那么 make 会将其解释为变量定义,就好像它们出现在命令行上一样。

  4. 未传递的选项-C-f-o-W 这些选项不会放入 MAKEFLAGS 中,因此它们不会传递给子 make

  5. -j 选项的特殊情况-j 选项是一个特殊情况,它控制并行执行。如果你设置它为一个数字值 ‘N’,并且你的操作系统支持它,那么顶层 make 和所有子 make 将协调以确保在它们之间同时运行不超过 ‘N’ 个作业。如果你不在命令行上提供 make 支持并行的选项,那么 MAKEFLAGS 中不会包含 -j 选项,子 make 将以非并行模式运行。如果你提供 -j 而没有指定数字参数,意味着尽可能并行运行,那么这个设置将传递下去,因为多个无限只是一个。

  6. 不传递其他选项:如果你不希望传递其他选项,你可以通过改变 MAKEFLAGS 的值来实现,比如:

    subsystem:
        cd subdir && $(MAKE) MAKEFLAGS=
    
  7. MAKEOVERRIDES 和 MFLAGSMAKEOVERRIDES 变量用于存储命令行上的变量定义。MFLAGS 也用于传递选项,但不包含命令行上的变量定义。它通常用于旧版本的 make,但现代版本已经使用 MAKEFLAGS 实现了同样的功能。

  8. 指定默认的选项:如果你希望每次运行 make 时都设置某些选项,你可以在环境中设置 MAKEFLAGS 的值。你也可以在 makefile 中设置 MAKEFLAGS,以指定在该 Makefile 中也生效的额外选项。

  9. 注意事项:要确保 MAKEFLAGS 中不包含会显著改变 make 和 Makefile 行为的选项,例如 -t(模拟运行,不执行命令)、-n(仅显示要执行的命令,不执行)和 -q(查询模式,仅检查是否需要构建目标)。将这些选项放入 MAKEFLAGS 可能会产生灾难性的后果,至少会产生令人惊讶且可能令人讨厌的效果。

  10. GNUMAKEFLAGS 变量:如果你希望运行 GNU Make 以外的其他 make 实现,并且不希望向 MAKEFLAGS 变量添加 GNU Make 特定的选项,你可以将它们添加到 GNUMAKEFLAGS 变量中。这个变量会在 MAKEFLAGS 前解析,类似于 MAKEFLAGS。然后,make 会将 MAKEFLAGS 构建为传递给递归 make 的所有选项,甚至包括从 GNUMAKEFLAGS 中获取的选项。因此,在解析 GNUMAKEFLAGS 后,GNU Make 将将其值设置为空字符串,以避免在递归期间重复选项。

通常情况下,最好只使用 MAKEFLAGS,但是如果你希望运行 GNU Make 以外的其他 make 实现,同时不想向 MAKEFLAGS 变量添加 GNU Make 特定的选项,那么你可以将这些选项添加到 GNUMAKEFLAGS 变量中。GNUMAKEFLAGS 会在 MAKEFLAGS 之前解析,就像 MAKEFLAGS 一样。然后,make 在构建要传递给递归 makeMAKEFLAGS 时会包括所有选项,即使它们来自于 GNUMAKEFLAGS。因此,在解析 GNUMAKEFLAGS 后,GNU Make 会将其值设置为空字符串,以避免在递归期间重复选项。

总之,MAKEFLAGSGNUMAKEFLAGS 变量非常有用,因为它们允许你在不修改 Makefile 的情况下设置和传递选项。然而,你应该小心使用它们,确保不会引入会显著改变 make 行为的选项。这些变量可以让你方便地在不同级别的递归 make 中传递选项和变量,同时保持灵活性和兼容性。


下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

打印目录选项

如果你在多个级别的递归make调用中使用,-w--print-directory选项可以使输出更容易理解,它会显示make开始处理每个目录和完成处理时的信息。例如,如果在目录/u/gnu/make中运行make -w,你将看到以下形式的输出:

make: 进入目录 `/u/gnu/make'

这行显示在make开始处理该目录之前。当处理完成后,你将看到以下形式的行:

make: 离开目录 `/u/gnu/make'

通常情况下,你不需要显式指定这个选项,因为make会自动为你执行这个操作:当你使用-C选项或在子级的make中时,-w会自动启用。但如果你同时使用-s(表示安静模式)或使用--no-print-directory来显式禁用它,make将不会自动启用-w


下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

定义重复配方

你可以使用define指令来定义一个可重复使用的命令序列,然后在多个目标的命令中引用这个定义。这个定义实际上是一个变量,所以名称不能与其他变量名冲突。

下面是一个定义可重复使用命令序列的示例:

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

在这个示例中,run-yacc是正在定义的变量名称;endef标志着定义的结束;中间的行是命令。define指令不会扩展定义中的变量引用和函数调用;其中的$字符、括号、变量名等等都成为了你正在定义的变量的值。

这个示例中的第一个命令对使用该可重复使用命令序列的任何规则的第一个前提条件运行Yacc。Yacc的输出文件总是命名为y.tab.c。第二个命令将输出文件移动到规则的目标文件名。

要使用可重复使用命令序列,将该变量插入到规则的命令中。你可以像其他变量一样插入它。因为使用define定义的变量是递归扩展变量,现在将扩展在define中写的所有变量引用。例如:

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

当它出现在run-yacc的值中时,$^将被替换为foo.y$@将被替换为foo.c

这是一个实际的示例,但实际上不需要这个特定的示例,因为make有一个隐式规则,可以根据涉及的文件名来确定这些命令。

在执行命令时,可重复使用命令序列的每一行都会像在规则中单独出现的一样处理,前面加上一个制表符。特别是,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)

不会显示任何命令行。


下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

使用空配方

有时定义什么都不做的命令是有用的。这可以通过给出只包含空白的命令来完成。例如:

target: ;

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

你可能想知道为什么要定义一个什么都不做的命令。这样做的一个原因是防止目标获取隐式命令(来自隐式规则或.DEFAULT特殊目标)。

空命令也可以用于避免为将作为另一个命令的副作用而创建的目标引发错误:如果目标不存在,空命令确保make不会抱怨不知道如何构建目标,而且make会假定目标已经过时。

你可能倾向于为不是实际文件的目标定义空命令,而只是存在以便它们的前提条件可以被重建。然而,这不是这样做的最佳方法,因为如果目标文件实际上存在,前提条件可能无法正确地被重新创建。


下一篇:练习6 Makefile之使用变量,上一篇:练习4 Makefile之显式规则目录首页

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
详细易懂的Linux makefile教程 一、概述 什么是makefile?或许很多Winodws的程序员都不知道这个东西,因为那些Windows的IDE都为你做了这个工作,但我觉得要作一个好的和professional的程序员,makefile还是要懂。这就好像现在有这么多的HTML的编辑器,但如果你想成为一个专业人士,你还是要了解HTML的标识的含义。特别在Unix下的软件编译,你就不能不自己写makefile了,会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力。 因为,makefile关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。 makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一种在工程方面的编译方法。 现在讲述如何写makefile的文章比较少,这是我想写这篇文章的原因。当然,不同产商的make各不相同,也有不同的语法,但其本质都是在“文件依赖性”上做文章,这里,我仅对GNU的make进行讲述,我的环境是RedHat Linux 8.0,make的版本是3.80。必竟,这个make是应用最为广泛的,也是用得最多的。而且其还是最遵循于IEEE 1003.2-1992 标准的(POSIX.2)。 在这篇文档中,将以C/C++的源码作为我们基础,所以必然涉及一些关于C/C++的编译的知识,相关于这方面的内容,还请各位查看相关的编译器的文档。这里所默认的编译器是UNIX下的GCC和CC。 二、关于程序的编译和链接 —————————— 在此,我想多说关于程序编译的一些规范和方法,一般来说,无论是C、C++、还是pas,首先要把源文件编译成中间代码文件,在Windows下也就是 .obj 文件,UNIX下是 .o 文件,即 Object File,这个动作叫做编译(compile)。然后再把大量的Object File合成执行文件,这个动作叫作链接(link)。 ...

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值