写Recipe(四)

来源:互联网 发布:全聚合网络电视电脑版 编辑:程序博客网 时间:2024/06/01 19:57

5 写Recipe

规则(rule)中通常包含多条recipes,这些recipes用来被shell按顺序依次执行。这些命令用来更新规则(Rule)中的目标文件(target)。

用户使用许多不同种类的shell程序,但是默认情况下makefile中的recipe总是使用/bin/sh进行解释(除非指定了别的shell)。

5.1 Recipe语法

Makefile中包含两种语法,其中大部分使用make语法,但是recipe需要被shell解释,所以其使用shell语法。make不需要理解shell语法,只是在将recipe提交给shell时做简单的转化。

recipe中的每一行必须以tab开始,除非recipe与target和prerequisite放在一行,并用;隔开。makefile中,在规则语境(rule context)下的,任意以tab开始的一行,被视为rule的一部分。所谓规则语境,是指从一条规则开始,到下一条规则或变量定义为止的范围。空行或注释行会被忽略。

这些规则包含以下结论:

  • 以tab开始的空行不是空行,被视为空recipe。

  • recipe中的注释不是make中的注释,它会原封不动的传给shell,是否被视为注释,由shell决定。

  • 在规则语境下,以tab缩进定义的变量被视为recipe的变量,会被传给shell,而不是被视为一个make变量定义。

  • 在规则语境下,以tab缩进的条件表达式(ifdefifeq等)被视为recipe的一部分传给shell。

5.1.1 分割Recipe行

makefile的语法中,一个逻辑行可以被\分割成若干物理行,这样一列由\分开的行被视为一行recipe,并且shell也会把它当作一条语句执行。

与makefile其它地方的\相比,recipe中新行前的\不会被移除。\和新行字符都会被保留并传给shell,其解释取决于shell。如果下一行的第一个字符是recipe的前导符(默认是tab),那么这个\换行符会被移除,并且不会在该处增加空格。

例如,下面makefile中,对于all目标:

all :        @echo no\space        @echo no\        space        @echo one \        space        @echo one\         space

被视为四个隔开的shell命令,其输出如下:

nospacenospaceone spaceone space

更复杂的例子:

all : ; @echo 'hello \        world' ; echo "hello \    world"

会通过如下命令调用shell:

echo 'hello \world' ; echo "hello \    world"

根据shell的引号规则,会有如下输出:

hello \worldhello    world

注意到,新行字符\在双引号字符串中被移除,在单引号字符串中被保留。这是默认shell(/bin/sh)的处理方式,如果你指定了不同的shell,行为会有不同。

有时候你想要在单引号中将一个长行分割为多行,但是并不想在引号中使用新行字符\。通常这种情况出现在将一个脚本传递给一个语言时(如Perl),脚本中的额外的\会被解释称不同的意思,甚至导致语法错误。一种简单的处理方式是,将引号中的字符串或者整个命令放入一个make变量中,然后在recipe中使用这个变量。在这种情况下,makefile的新行引用规则就会生效,\会被移除。重写上面的例子:

HELLO = 'hello \world'all : ; @echo $(HELLO)

会得到如下输出:

hello world

你也可以使用target-specific变量,使变量和使用它的recipe关系更紧密(后面介绍)。

5.1.2 在recipe中使用变量

另一种处理recipe的方式是展开其中的变量引用。这发生在make读完了所有的makefile,并且确定target需要被重建之后。因此不需要重建的target的recipe并不会发生变量展开。

recipe中的变量和函数引用与makefile中其它地方的变量和函数引用有完全相同的语法和语义。它们有相同的引用规则:如果要在recipe中输入一个$符号,必须使用$$。在shell中使用$引用变量,因此你必须清楚你想要引用的变量是make变量(使用$)还是shell变量(使用$$)。例如:

LIST = one two threeall:        for i in $(LIST); do \            echo $$i; \        done

将会传递如下命令给shell:

for i in one two three; do \    echo $i; \done

其产生预期结果:

onetwothree

5.2 Recipe Echoing(回显)

通常,make在执行recipe之前先打印每一行recipe。我们称之为echoing(回显),因为这就像打印你的输入。

当一行以@开始时,这行就不再回显。在将这行传递给shell前,@被丢弃。通常这个语法用于那些只是打印信息的命令,例如用echo命令标识程序处理makefile的过程:

@echo About to make distribution files

当make使用-n--just-print选项时,只是回显将要执行的recipe,而并不真正执行它们。此时,即使以@开头的recipe也会被打印。当只是想查看make将要执行的recipe命令时很有用。

选项-s或者--silent将阻止make回显任何信息,相当于所有的recipe以@开头。makefile中没有prerequisites的特殊目标.SILENT具有类似功能,但是@更灵活。

5.3 执行Recipe

当执行recipe更新目标文件时,每一行recipe都会调用一个子shell,除非特殊目标.ONESHELL被指定。

请注意:这意味着shell中设置的变量,以及shell中执行的改变当前工作目录的程序(如cd)将不会影响到之后的recipe命令。如果想要cd影响下一个声明语句,就要把两个声明放到一个recipe行中。这样,make就会使用一个shell运行这一行,这个shell会依次执行这条recipe中的声明。例如:

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

这里我们使用shell的与(AND)操作符,因此当cd失败时,这个脚本就会退出,而不会在一个错误的目录中执行gobble命令。

5.3.1 使用One Shell

有时候,你想要把recipe中所有的命令行都发个一个shell。在两种情况下着非常有用:第一,当recipe包含很多命令行时,它可以在不需要额外处理的情况下优化makefile。第二,你可能想在recipe命令中包含新行字符(例如你使用的SHELL有不同的解释方式)。如果特殊目标.ONESHELL出现在makefile中的任何位置,那么这个makefile中的每个target的所有recipe行就使用一个shell。recipe行之间的新行字符会被保留。例如:

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

将会按照所期望的执行,即使recipe命令位于不同的行。

如果提供.ONESHELL,那么recipe中只有第一行会被检查特殊的前缀字符(如,@, -,+)。当SHELL被调用时,随后的行也会包含这个字符。如果想要让recipe以上面的特殊字符开始,不应该将它们放在recipe第一行的第一个字符(增加一条注释或者别的)。例如,下面对Perl来说是一个语法错误,因为make会移除第一个@,并且不会回显这个target的所有recipe:

.ONESHELL:SHELL = /usr/bin/perl.SHELLFLAGS = -eshow :        @f = qw(a b c);        print "@f\n";

下面的例子可以正常工作:

.ONESHELL:SHELL = /usr/bin/perl.SHELLFLAGS = -eshow :        # 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 = -eshow :        my @f = qw(a b c);        print "@f\n";

作为一种特性,如果SHELL是一个POSIX风格的shell,在recipe被处理之前,这个特殊字符会被移除。这个特性允许现有的makefiles增加.ONESHELL,并且可以在不进行太大改动的情况下正常运行。因为在POSIX shell脚本中,并没有以特殊字符开头的功能。下面的例子可以如期正常工作:

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

然而,即使拥有这个特性,有.ONESHELL的makefiles的行为差异依然很明显。例如,通常recipe中的一行执行失败时,剩余的其它recipe不会被处理。但在.ONESHELL下,如果不是最后一行的recipe执行失败,make是不会觉察到。你可以改变.SHELLFLAGS,为shell增加-e选项,这会让shell在命令行出现任何错误时退出,但是这可能导致你的recipe行为有所不同。最终你需要允许你的recipe在.ONESHELL下可以工作。

5.3.2 Shell的选择

程序使用的shell取决于变量SHELL。如果这个变量在makefile中没有被设置,shell默认使用/bin/sh。传给shell的参数取决于变量.SHELLFLAGS。变量.SHELLFLAGS的默认值是-c,如果在POSIX模式下是-ec

不像大多数变量,SHELL不能在环境中设置。这是因为SHELL环境变量用于指定交互模式下的私有shell程序。让私有shell影响makeflie是不明智的。

另外,当你在makefile中设置SHELL,它的值不会导出到make调用的recipe行。相反,从用户环境继承的值被导出。可以通过显式导出SHELL覆盖这种行为,迫使它传递给recipe行执行时环境。

然而,在MS-DOS和MS-Wnidows系统中,SHELL被使用,因为这些系统的用户通常不设置这个变量,因此它可以专门被make所使用。在MS-DOS系统,如果不能为make设置SHELL,可以给make使用的shell设置变量MAKESHELL。如果被设置,其将会被用作shell,而不是SHELL的值。

在DOS和Windows中选择Shell

在MS-DOS和MS-Windows中选择shell比在其它系统中复杂很多。

在MS-DOS中,如果没有设置SHELL,则会使用变量COMSPEC代替。

在MS-DOS系统中,在makefile的各行中设置变量SHELL会有不同的处理过程。系统默认的shell command.com 功能有限,许多make用户会安装别的shell。因此,在MS-DOS中,make检查SHELL的值,根据它所指向的Unix风格或DOS风格shell,而有不同的行为。

如果SHELL指向Unix风格的shell,MS-DOS中的make会额外检查该shell是否存在,如果不存在,就忽略设置SHELL的语句。在MS-DOS中GNU make搜索如下目录:

  1. 变量SHELL所指的精确位置。例如,如果SHELL = /bin/sh,make就会搜寻当前驱动器下的/bin目录。
  2. 当前目录。
  3. 变量PATH中的每个目录(按顺序查找)。

在它检查的每个目录,make首先查找特殊文件(上面的例子中是sh)。如果没找到,make还会在那个目录中查找带有已知的可执行文件后缀的sh文件。例如,.exe, .com, .bat, .btm, .sh等。

如果以上的任何尝试成功找到shell,那么SHELL的值会设置成该shell的完整路径。如果没有找到,SHELL的值不会改变,但是设置SHELL的行会被忽略。

注意,以上的扩展查找只限于设置了SHELL的makefile。如果SHELL设置在环境或命令行,需要提供shell的完整路径。

如果makefile中包含SHELL = /bin/sh(与许多Unix系统中的makefiles一样),只要在环境变量PATH目录下找到sh.exe,就可以使用。

5.4 并行执行

GNU make知道如何一次执行若干recipes。通常make一次只执行一条recipe,等到一条执行完,才会执行下一条recipe。然而,-j--jobs选项告诉make同时执行多个recipe。可以在makefile中设置伪目标.NOTPARALLEL来禁止并行执行。

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

如果-j后面跟一个整数,则指定了recipe一次执行的数量,这被称作工作槽(job slots)数量。如果-j后面什么也不跟,则表示对工作槽的数量没有限制。默认的工作槽数量是1,也就是串行执行。

make递归调用时,并行执行会产生问题,后面会解释。

如果一条recipe执行失败,并且这个错误不会被忽略,则构建当前target的其它recipes不会继续执行。如果recipe执行失败,且没有提供-k--keep-going选项,make会中断执行。如果make以任何原因终止,那么它会等待其子程序执行完以后再退出。

当系统负载很重时,你可能想要运行较少的工作槽。可以使用-l选项告诉make限制一次同时运行工作的数量,它会自动平衡负载。选项-l--max-load后面跟的是浮点数。例如:

-l 2.5

如果平均负载大于2.5,其会阻止make运行一个以上的工作。选项-l后不跟任何数字表示取消负载限制。

更准确的说,当make开始一个工作时,已经有至少一个工作正在运行,它检查当前平均负载。如果平均负载不低于-l提供的值,make就会等到平均负载低于这个值才开始新的工作,或者make会等到现有工作都完成才开始新工作。

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

5.4.1 并行执行的输出

当并行执行多个recipe时,每个recipe的结果都是即时输出的,因此不同的recipe产生的信息就会很分散,有时候甚至会将不同的recipe输出打印在同一行,这使得读取这些输出变得非常困难。

可以使用选项--output-sync-O选项避免这种情况发生。这个选项要求make保存被调用语句的执行结果,并在所有命令都执行结束以后一起打印。另外,对于多重递归调用的指令并行执行时,它们之间会达成协议,以保证同一时间只有一个输出。

如果可以打印工作目录,输入/可以将打印信息分组。如果不想看到这些信息,可以给MAKEFLAGS增加--no-print-directory选项。

同步输出有四个等级,通过给选项一个参数来指定。

等级 描述 none 默认值,所有输出产生时就会发送出去,没有同步。 line recipe的每个独立行的输出分为一组,当一行执行结束时打印。如果一条recipe有多行语句,那它们的输出分散在这一组中。 target 每个target的所有recipe的输出被分为一组,当这些recipe结束时一起打印。对应于选项–output-sync或-O没有参数。 recurse make的每个递归调用的输出分为一组,在递归调用结束时打印。

不管模式选哪个,总的创建时间是一致的,不同的只是输出信息的打印方式。

target和recurse模式都是收集一个target全部的recipe输出信息,并在这些recipe全部完成时连续打印输出。不同在于对make中包含递归调用的recipe处理方式。对于所有的recipes都没有递归调用的情况,target和recurse模式的行为完全相同。

如果recurse模式被选中,make执行包含递归调用的recipe的方式与其它targets相同:recipe的输出(包括递归make的输出)被保存,等到所有recipe执行完再打印。这保证了所有make递归实例中创建的targets的信息被放在一起,这使输出更容易被理解。然而这可能导致有很长的时间间隔内没有输出显示,而在之后又一下显示大量的输出。如果你不观看程序的创建过程,而是在创建好之后查看创建日志,这个选项就比较适合。

如果你一直看着输出,那么当程序创建时一长段的空白可能让人不快。target输出同步模式会在make递归调用时,不同步这些行的输出。

line模式在前端比较有用,它可以观察跟踪recipe开始和结束时的输出。

对于将输出显式到终端或者写入文件(通常描述为交互模式和非交互模式),make调用某些程序时的行为是不同的。例如,有些程序可以在终端中显示不同颜色的输出,但是在文件中无法显示颜色。如果你的makefile调用一个像这样的程序,此时使用输出同步选项会导致这个程序运行在非交互模式下,即使最终输出显示在终端。

5.4.2 并行执行的输入

两个处理过程不可能同时从相同的设备获取输入。为了确保只有一条recipe从终端获取输入,make只会让所有输入流中的一个生效用来运行recipe。如果其它的recipe试图读取标准输入,通常会产生一个致命错误(一个Broken pipe信号)。

哪个recipe将拥有有效的标准输入是不可预知的(这个标准输入可能来自终端,或者make中你重定向的标准输入)。第一个运行的recipe总是首先获得这个标准输入,该recipe执行完以后下一个recipe才能获得,以此类推。

在找到更好的解决方案之后,我们会改变make这一部分的工作方式,但现在只能这样。在这期间,如果你正在使用并行执行特性,那你不应该依赖任何使用标准输入的recipe。但是如果你没有使用并行特性,那么标准输入通常作用于所有的recipe。

5.5 Recipe中的错误

每个shell调用结束后,make会查看它的返回状态。如果shell成功退出(退出状态为0),下一行recipe会在一个新shell中执行。在最后一行执行完以后,这个规则就算完成了。

如果有错误产生,make会终止当前规则,甚至可能终止所有规则。

有时候特定的recipe行失败并不能说明出现了一个问题。例如,你可能使用mkdir命令确认一个目录存在。如果这个目录已经存在,makdir将产生一个错误,但是你可能希望make继续工作。

在一行开始(tab之后)输入一个-,可以忽略recipe行的错误。-会在将这行传给shell之前去除。

例如:

clean:        -rm -f *.o

此时,即使rm无法删除一个文件,make也会继续。

当你使用-i--ignore-errors选项运行make,所有recipe行中的错误都会被忽略。在makefile中指定特殊目标.IGNORE(并且没有prerequisites)具有相同的功能。这些方式已经被废弃,因为-有更好的灵活性。

当错误被忽略,make把错误的返回当作成功,但是会打印一条信息,告诉你shell的退出状态,以及这个错误已经被忽略。

当有错误发生,并且make没有被告知要忽略它,当前目标不能被成功重建,以及其它任何直接或间接依赖于它的目标也不能被重建。这些目标的其它recipe将不会被执行,因为它们的先决条件没有实现。

通常make在这种情况下立即停止,并返回一个非零的状态。然而,如果标识-k--keep-going被指定,make会继续检查这个target的prerequisite,并在必要时重建它们,完成以后才停止并返回非零值。例如,在编译一个object文件发生错误后,make -k会继续编译其他object文件,即使make知道不可能链接它们。

通常make假设你的目的是更新某个特定目标,一旦make知道这不可能达到,它会立即报告错误。选项-k告诉make尽可能多的重建已经改变的文件,过程中可能会发现一些没有依赖性的问题,并且假设你会在下次编译之前修改它们。这也是为什么Emaces的编译命令会把-k当作默认标志。

通常当一个recipe行失败时,如果其已经改变目标文件,那么这个文件已经被破坏,并且不能被使用——或者至少它没有被完全更新。然而该文件的时间戳表示它已经被更新,因此下次执行make时,将不会更新这个文件,这种情况类似于shell被一个信号突然中断。所以,通常正确的做法是在recipe更新一个文件失败后,将其删除。如果在makefile中.DELETE_ON_ERROR被当作一个目标,make就会这样执行。通常你想让make默认就这么做,但是为了向后兼容,你必须显式指定。

5.6 中断或终止make

当shell执行时,如果make获得一个执行错误信号,make可能会删除当前recipe正在更新的目标文件。当这个目标文件的最后修改时间与make第一次检查到它时的时间戳发生了变化,make就会这么做。

删除目标文件的目的是为保证在下次make运行时,从零开始重建该目标。为什么这样?假设在编译器运行时,你按下了Ctrl-C,这会终止编译器的运行,但是此时已经开始写一个名为foo.o的文件,其时间戳已经比源文件foo.c新。如果make不删除这个文件,在下次运行make时,不会再重建这个文件(因为时间戳已经更新),而是尝试将其跟其他object文件链接,这会产生一个奇怪的错误,因为它链接了一个只写了一半的文件。

你可以将一个目标文件依赖于特殊目标.PRECIOUS,这样就不会删除这个目标文件。在重建一个target时,make会检查是否其出现在.PRECIOUS的prerequisites中,以此来决定当错误信号产生时,是否需要删除这个目标文件。使用这种方式的一些原因可能是这些目标以一些原子方式(atomic fashion)更新,或者其只是为了记录修改时间(内容无所谓),或者这个文件必须始终存在以防其它问题产生。

5.7 make的递归调用

make的递归调用是指在makefile中使用make命令。这个技术在大型系统中很有用,你可以把各个子系统放在不同的makefile文件中。例如,假设有一个子目录subdir,它有自己的makefile,可以在自己的目录下运行make。你可以这样写:

subsystem:        cd subdir && $(MAKE)

或者:

subsystem:        $(MAKE) -C subdir

你可以把上面的例子复制到使用递归make命令的makeflie中,但是可能需要确定更多的细节,比如它们如何工作,以及子目录与上层目录如何关联。将调用递归make命令的目标声明为.PHONY有时候是很有必要的。

为了方便,GNU make开始时,会将变量CURDIR设置成当前工作目录。这个值不会再被更改,特别地在你包含了其它目录的文件时,这个值也不会改变。在makefile中为CURDIR变量设置的值同样不会改变工作目录(默认情况下环境变量CURDIR不会覆盖这个值)。注意,设置这个值对make的操作没有影响(并不会改变make的工作目录)。

5.7.1 变量MAKE如何工作

递归make命令通常使用变量MAKE,而不是显式的使用make命令:

subsystem:        cd subdir && $(MAKE)

这个变量的值是被调用的make的文件名。如果这个文件名是/bin/make,那么上面的recipe执行语句就是cd subdir && /bin/make。如果你使用一个特殊版本的make运行顶层的makefile,相同版本的make会被用在递归执行的命令中。

在一个规则的recipe中使用变量MAKE会对选项-t--touch),-n--just-print),或者-q--question)产生影响。使用MAKE同样也会影响recipe行开头使用的+字符。这个特性只有MAKE显式出现在recipe语句中时才会起作用,如果MAKE通过另一个变量扩展而来,则不会有什么用。在后一种情形中你必须使用+获得这种特性。

考虑在上面的例子中使用命令make -t(选项-t标识目标不需要运行任何recipe而进行更新)。make -t会创建一个名为subsystem的文件,而不做其它任何事。你想要真正运行的是cd subdir && make -t,但是这要求执行recipe,但是-t表示不执行recipe。

这个特性做了你想要做的:无论何时,recipe中包含变量MAKE,标志-t-n-q将不会应用于该行。包含MAKE的recipe可以正常执行,即使这些导致大多数recipe不能运行的标志存在。通常MAKEFLAGS机制将这些标志传递给子make,以便于将修改文件、打印recipe等请求传递给子系统。

5.7.2 向子make传递变量

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

向下传递或者向上导出变量,make将变量和它的值增加到运行每一行recipe的环境中。子make轮流使用这个环境初始化自己的变量值列表。

除非显式请求,make只有在变量定义在环境初始化或者定义在命令行,且变量名只由字母、数字和下划线组成时,才会导出这个变量。有些shell不能处理除字母数字下划线之外的其它字符组成的环境变量名。

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

特殊变量MAKEFLAGS总是被导出,如果你给它设置了任何值的话。

make自动将定义在命令行的变量通过变量MAKEFLAGS向下传递。

被make默认创建的变量通常不会向下传递,子make会定义自己的变量。

如果你想要向子make导出特定的变量,可以使用export指令:

export variable ...

如果你想要阻止一个变量被导出,可以使用unexport指令:

unexport variable ...

这两种情况中,exportunexport的参数被展开,以便变量或函数扩展出的变量可以被导出。

方便起见,你可以在定义一个变量的时候将它导出:

export variable = value

等同于:

variable = valueexport variable

export variable := value

等同于:

variable := valueexport variable

export variable += value

variable += valueexport variable

一样。

你可能已经注意到,exportunexport指令在make和shell sh中的工作方式相同。

如果你想让所有的变量都被导出,可以只使用export

export

这告诉make,那些没有用exportunexport指定的变量都会被导出。而其中被unexport指定的变量始终不会被导出。如果你使用这种方法导出变量,那些由变量名包含非字母数字下划线字符的变量不会被导出,除非是显式给它们指定export指令。

这种只使用export指令本身的导出行为是老式的make默认执行方式。如果你的makefile使用这种方式,并且想要兼容老版的make,可以使用特殊目标.EXPORT_ALL_VARIABLES代替export指令。这会被老式make忽略,而export指令则会导致语法错误。

同样,可以使用unexport本身告诉make默认不导出变量。因为这是默认行为,在之后如果想要导出变量时,要使用export专门指定。不能使用exportunexport指令本身指定某些变量导出,某些变量不导出,make会以最后检测到的exportunexport决定整个make运行中的行为。

作为一个特性,变量MAKELEVEL在一层一层向下传递的过程中不断改变。这个变量的值是一个字符串,表示十进制值的深度。对于顶层make是'0',子make是'1',以此类推。当make为recipe安装一个环境的时候,这个值就会增加。

变量MAKELEVEL的主要作用是测试条件指令,你可以写一个makefile,递归运行时以一种方式,直接运行时是另一种方式。

你可以使用变量MAKEFILES让所有的子make命令使用另外的makefiles。该变量的值是用空格分开的一列文件名。这个变量如果定义在其它层的makefile中,会通过环境向下传递。然后它会被当作子make的一个额外的makefiles文件列表,在子make读取自己特有的makefile之前读取。

5.7.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,并且你的系统支持它,那么父make和所有的子make将会保证在它们中,只有N个工作会被同时运行。注意,任何标记为递归的工作不计入这个N当中。

如果你的操作系统不支持上面的传递,那么-j不会加入到MAKEFLAGS中,因此这些子make运行在非并行模式。如果-j选项传递给子make,你可能获得比你所要求的更多的并行运行工作数量。

如果你不想要传递其它的标志下去,你必须改变MAKEFLAGS的值,像这样:

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

命令行变量定义实际会出现在变量MAKEOVERRIDES中,MAKEFLAGS包含这个变量的一个引用。如果你想传递标识下去,但不想传递命令行中的变量定义,可以将MAKEOVERRIDES置为空:

MAKEOVERRIDES = 

这并不总是有用。有些系统对环境的大小有一个小的限制,将太多的信息放在MAKEFLAGS中可能会溢出,这会提示Arg list too long的错误信息。(如果在makeflie中使用特殊目标.POSIX,会使make执行POSIX.2标准,其中MAKEOVERRIDES的改变不会影响到MAKEFLAGS。)

有一个相似的变量MFLAGS用于向后兼容。它和MAKEFLAGS有相同的值,除了它不包含命令行变量定义,并且它总是以连字符开始(除非为空)(MAKELAGS只会在以一个没有单字母版本的选项开始时,才会以连字符开始,例如,--warn-undefined-variables)。MFLAGS通常在递归make中显式使用,如下:

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

现在MAKEFLAGS使这个变量变得冗余。如果想让你的makefile向后兼容,可以使用这个变量,其在现代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之前被解释,与其有相同使用方式。当make构建MAKEFLAGS,将GNUMAKEFLAGS传给递归make,MAKEFLAGS将包含所有的标识,即使这些标志来自GNUMAKEFLAGS。因此,在解释了GNUMAKEFLAGS之后,GNU make会把这个变量的值设置为空字符串,以防递归时复制标志位。

最好在GUNMAKEFLAGS中只包含那些不改变你makefile行为的标识。如果你的makefile以任何方式调用GNU make,并且只是使用MAKEFLAGS,那么诸如--no-print-directory--output-sync等标识就适合GNUMAKEFLAGS

5.7.4 –print-directory选项

如果你使用了若干层的make递归调用, 那么选项-w或者--print-directory能够使输出信息更容易被理解。它在每个目录开始被处理,以及结束处理时,都会显示相关信息。例如,如果make -w在目录/u/gnu/make目录下运行,make会在开始处理时打印如下信息:

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

并在结束处理后打印:

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

通常,你不需要指定这个选项,因为make为你做了这件事:当你使用-C选项时,-w会自动打开,以及在子make中也会这样。如果你使用-s时不会自动打开-w-s表示不打印信息,或者使用--no-print-directory显式禁止。

5.8 封装(Canned)Recipes

当可以使用相同序列的命令创建若干targets时,可以使用define定义一个封装好的序列,recipe可以指向这个封装好的序列。这个序列其实是一个变量,因此名字不能与其它变量冲突。

下面是定义一个封装recipe的例子:

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

这里run-yacc是被定义的变量的名字,endef标识定义结束,之间的行都是命令。define指令不在封装序列中展开变量引用和函数调用,字符$,括号,变量名等,都是你定义的变量的一部分。

这个例子中的第一条命令运行yacc,其前提是规则使用封装序列。yacc的输出文件名为y.tab.c。第二条命令将输出移动到这条规则的目标文件名。

使用这个封装序列时,将这个变量替换到一条规则的recipe中,就像替换其它任何变量一样。因为被define定义的变量被递归的展开,所以在define中的变量引用现在被展开。例如:

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

变量$^会被foo.y替换,$@会被foo.c替换。

这是一个真实的例子,但是在实际当中并不会使用,因为根据隐含规则,make自己能够判断这些命令中的文件名。

在recipe执行时,封装序列中的每行都被当作包含它们的规则中实际的一行对待,会在它们前面加上tab。特别地,make为它们的每一行调用一个子shell。你可以在封装序列的每一行中使用影响命令行的前缀字符(如,@-+等)。例如,使用如下序列:

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

make将不会回显第一行中的echo命令。但是会回显之后的两行。

另一方面,指向封装序列的recipe行的前缀字符被应用于序列中的每一行。因此规则:

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

将不会回显任何recipe行。

5.9 使用空的Recipe

有时候定义空的Recipe是很有用的。只需要在recipe的位置用空格代替即可,例如:

target: ;

为target定义了一个空的recipe。也可以使用一个recipe的前缀字符(tab)来定义空recipe,但是这样表达不清楚,因为这看起来就是一个空行。

你可能会疑惑为什么需要定义一个什么都不做的recipe。一个原因是这样可以阻止一个target使用隐含的recipe(来自隐含规则或特殊目标.DEFAULT)。

空的recipe也可以用于避免target被其它的recipe语句的副作用创建的错误:如果target不使用空的recipe语句确保make不发生错误,当make不知道如何创建这个目标时,make会假设这个目标过期。

你可能倾向于为不是实际文件的target定义空的recipe语句,它的存在只是为了让它们的prerequisites能被重建。然而,这不是最好的方式,因为如果这个target文件确实存在时,这些prerequisites不会被重建。伪目标是更好的方式。