写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
缩进的条件表达式(ifdef
,ifeq
等)被视为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搜索如下目录:
- 变量
SHELL
所指的精确位置。例如,如果SHELL = /bin/sh
,make就会搜寻当前驱动器下的/bin
目录。 - 当前目录。
- 变量
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
选项。
同步输出有四个等级,通过给选项一个参数来指定。
不管模式选哪个,总的创建时间是一致的,不同的只是输出信息的打印方式。
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 ...
这两种情况中,export
和unexport
的参数被展开,以便变量或函数扩展出的变量可以被导出。
方便起见,你可以在定义一个变量的时候将它导出:
export variable = value
等同于:
variable = valueexport variable
而
export variable := value
等同于:
variable := valueexport variable
像
export variable += value
与
variable += valueexport variable
一样。
你可能已经注意到,export
和unexport
指令在make和shell sh
中的工作方式相同。
如果你想让所有的变量都被导出,可以只使用export
:
export
这告诉make,那些没有用export
或unexport
指定的变量都会被导出。而其中被unexport
指定的变量始终不会被导出。如果你使用这种方法导出变量,那些由变量名包含非字母数字下划线字符的变量不会被导出,除非是显式给它们指定export
指令。
这种只使用export
指令本身的导出行为是老式的make默认执行方式。如果你的makefile使用这种方式,并且想要兼容老版的make,可以使用特殊目标.EXPORT_ALL_VARIABLES
代替export
指令。这会被老式make忽略,而export
指令则会导致语法错误。
同样,可以使用unexport
本身告诉make默认不导出变量。因为这是默认行为,在之后如果想要导出变量时,要使用export
专门指定。不能使用export
和unexport
指令本身指定某些变量导出,某些变量不导出,make会以最后检测到的export
或unexport
决定整个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不会被重建。伪目标是更好的方式。
- 写Recipe(四)
- bitbake之写一个recipe(1)
- bitbake之写一个recipe(2)
- 解释器的原则(Recipe)
- 从零开始写PHP(四)
- 自己动手写操作系统(四)
- 配置recipe
- 跟我一起写 Makefile(四)
- 跟我一起写 Makefile(四)
- 跟我一起写Makefile(四)
- 跟我一起写 Makefile(四)
- 跟我一起写 Makefile(四)
- 跟我一起写 Makefile(四)
- 跟我一起写 Makefile(四)
- 跟我一起写 Makefile(四)
- 跟我一起写 Makefile(四)
- 跟我一起写Makefile(四)
- 跟我一起写 Makefile(四)
- 设计模式六大原则
- 关于在cocos studio设置旋转度的bug
- 使用StretchBlt之前一定要用SetStretchBltMode(COLORONCOLOR)
- python中的try/except/else/finally语句
- RecyclerView自带bug,切换数据出现IndexOutOfBoundsException
- 写Recipe(四)
- studio+selenium+testNG+testReport
- 安卓学习日记(一):了解安卓架构(linux内核层、系统运行库层、应用框架层、应用层)
- Boost算法库——C++11算法(is_sorted)
- samohyes的心情笔记(5)
- Mybatis-分页插件(spring boot下)
- HenCoder UI 部分 2-1 布局基础
- windows下git代码管理初体验
- Python中的函数式编程(一)