前言
Recipes由一条到多条shell命令组成,它们按顺序执行,通常是为了更新规则里面定义的target文件。除非在makefile里额外指定,否则总是使用/bin/sh来解释这些shell命令。
声明Recipe
一个makefile里面通常有两种声明,大部分内容都是make声明(由make程序来解释),recipes部分使用的是shell声明(由shell程序来解释)。make程序不会尝试去理解recipes部分写的是什么,写得对不对,它只会做极少量的翻译处理(比如target-update阶段的变量展开)然后就直接交给shell去运行。
在Recipe里面使用变量
在进入target-update阶段时,make会对recipes部分的变量和函数引用进行一次展开,而shell里面也可以使用变量,并且make和shell都是使用$
来引用变量,所以你必须清楚你到底是想在recipe里面引用make变量还是shell变量。比如你想在recipe里面使用make变量name,则应写成$name
;如果你是想使用shell变量name,则应写成$$name
。再比如有下面的makefile片段:
LIST = one two three
all:
for i in $(LIST); do \
echo $$i; \
done
这里LIST是make变量,使用$
可以由make直接展开;而i是shell变量,应该使用$$
躲避make的展开,交由shell进行展开。
打印Recipes
默认情况下,make会打印交给shell去运行的recipes,可以在recipe的前面加一个[ @ ]符号来关闭该条recipe的打印(特别是对于那些本身就是用于打印信息的recipes)。
当我们给make传入‘-n’或‘–just-print’的参数时,make只会打印recipes(哪怕前面有[ @ ]符号),而不会实际运行它们,这通常可以用于调试。
当我们给make传入‘-s’或‘–silent’的参数时,会实现相同的效果:所有的recipes都不会被打印,就像所有的recipes前面都加了[ @ ]一样。在makefile里定义一个特殊的目标.SILENT
也可以实现相同的效果(不需要任何prerequisites)。
执行Recipes
通常来说,同一条规则里面的每行recipe都会开启一个新的shell进程来运行,所以如果在一行recipe里面设置了一些变量或者进入了某些目录,并不会影响到后续的recipes。对于那些具有依赖关系的recipes,应该写在同一行,让它们在同一个shell进程里面执行(可以使用shell里面的[ && ]连接各个shell命令)。
有时候我们可能希望能在同一个shell进程里面运行同一条规则的所有recipes,特别是当recipes数量比较多的时候,这样可以提高执行recipes的效率。这可以通过在makefile里面定义特殊的目标.ONESHELL
来实现。定义.ONESHELL
后,对于一个target的所有recipes都会在一个shell进程里面执行。
通常来说(没有使用.ONESHELL
的情况下),一条recipe执行失败会直接引起这条规则出错,make不再处理后续的recipes。但是在使用.ONESHELL
的时候,这些recipes一旦交给shell就会被依次执行完,make并不知道某条recipe是否执行失败(最后一条recipe除外),因此中间有recipes运行失败的时候make也不会停下来。当然我们可以通过给.SHELLFLAGS
添加-e的选项来让shell命令执行失败的时候退出shell,所以在使用.SHELLFLAGS
时要小心编写recipes来应对这种情况。
并行执行Recipes
通常情况下,规则里面的recipes都是串行处理的,make会等待当前这条recipe处理完后,再调用shell处理下一条。我们可以给make传入‘-j’或‘–jobs’参数,让其同时处理多条recipes;也可以在makefile里面针对某些targets关闭掉并行处理的功能。
‘-j’选项后面的整数表示可以同时处理的recipe条数(任务数,number of job slots)。如果没有指定整数,则表示任务数无限制。没有指定‘-j’选项的情况下,默认的任务数1。
在构建一个target的过程中出现一个不可忽略的recipe执行错误时,make会停止运行该target后续的recipes。如果没有为make指定‘-k’或者‘–keep-going’选项,make会终止运行。如果make因为一些原因终止运行(比如收到Ctrl+C的信号,或者出现recipe执行错误),但是还有子进程在运行,它会等待子进程返回后再真正停止运行并退出。
部分关闭并行处理
当makefile里面各个targets之间的依赖关系被完整而准确地定义时,并行构建不会有问题,当然这是理想的情况。
有时候,makefile里某些甚至所有的targets不能并行构建,并且没有办法通过使用prerequisites告知make哪些可以并行构建,哪些不行。那么,对于不能并行构建的targets,就只能手动关闭其并行构建。
可以在makefile里面定义.NOTPARALLEL
的特殊目标(不含recipes)来关闭并行构建的功能(不管make的时候是否传入-j)。如果.NOTPARALLEL
后面没有prerequisites列表,则整个make过程都串行处理。对于部分需要进行串行处理的targets,应该将它们作为prerequisites写在.NOTPARALLEL
后面,这样,make将串行构建这些targets的prerequisites。比如,有如下的makefile片段:
all: one two three
one two three: ; @sleep 1; echo $@
.NOTPARALLEL:
这里,关掉了整个make过程的并行构建,make -jx的时候,all目标的三个依赖one two three都将被串行构建。又比如下面的makefile片段:
all: base notparallel
base: one two three
notparallel: one two three
one two three: ; @sleep 1; echo $@
.NOTPARALLEL: notparallel
make -jx base的时候,base目标的依赖one two three将被并行构建。而make -jx notparallel的时候,因为notparallel作为.NOTPARALLEL
的依赖,所以其prerequisites将被串行构建,因此one two three将串行构建。
使用.NOTPARALLEL
是将target的整个prerequisites文件串行构建,我们还可以通过在prerequisites列表里面使用特殊目标.WAIT
来细粒度控制部分prerequisites文件的串行构建。使用.WAIT
将可并行构建和需要串行构建的prerequisites分开,左边的部分构建完后,才会串行构建右边的部分。比如有如下的makefile片段:
all: one two .WAIT three
one two three: ; @sleep 1; echo $
使用make -jx,one two会先被并行构建,然后才构建three。
并行构建的输出
并行构建目标的prerequisites时,因为每个prerequisite可能都会有打印输出,如果不进行同步的话,这些输出会混杂在一起,无法阅读。我们可以在make的时候传入–output-sync的选项,来控制构建的打印输出,–output-sync有如下四级控制粒度:
- none
不进行输出同步,没有使用output-sync时,make默认使用该粒度。 - line
按recipe行进行打印输出,这样不同的prerequisites输出仍然可能会混杂。 - target
output-sync选项默认使用的粒度,将target的整个recipe输出进行打印。 - recurse
将一次make构建的输出进行打印。
Recipes的错误处理
执行recipes的时候,make会检查每次shell调用的退出状态(对于非.ONESHELL
,一行recipe就会调用一次shell)。对于失败的退出状态(非0),make会放弃当前的规则,甚至是放弃所有的规则直接退出。
但是有时候,一条recipe执行出错并不意味着真的是有问题,比如说调用mkdir的命令来保证一个目录存在的时候,如果这个目录本身就存在那么命令就会出错,此时你还是希望继续构建。我们可以在recipe前面加上[ - ]来让make忽略这条recipe的错误退出状态(make将recipe传给shell前会去掉[ - ])。比如,有如下的makefile片段:
clean:
-rm -f *.o
就算rm失败,make也会继续进行构建。
当我们给make传入‘-i’或者‘–ignore-errors’参数的时候,make会忽略所有recipes的错误退出状态(虽然shell可能会提示发生了某些错误)。
当发生了未被忽略的错误时,通常make会直接停止构建,并返回错误的非0状态。但是如果make的时候指定了‘-k’或‘–keep-going’的参数,则make还是会接着运行,哪怕它知道最终的构建可能是无法完成的,它还是会尽可能构建出其它能构建出来的targets。通常来说,‘-k’是用于告诉make此次构建的真实目的是尽可能多地测试此次进行的更改,尽可能多地发现问题以便于后续的改进。
有时候一个target构建到一半,recipe运行出错,此时可能已经更改了target文件,造成target文件不完整,或者是错误的,不可使用。这时候,你可能希望make自动删除掉这个半成品的target文件,不然下次再make的时候,这个target就不会被更新了。在makefile里定义特殊目标.DELETE_ON_ERROR
可以实现这个功能。
递归调用make
递归调用是指在makefile里面调用make,这在由多个子系统构成的大型工程里面很常用,这些子系统通常都有自己的makefile。假如说你正在运行当前目录下的主makefile,当前目录下还有个subdir的目录,里面有个子makefile。你想在主makefile里面调用make去运行这个子makefile,可以这样写:
subsystem:
cd subdir && $(MAKE)
也可以这样写:
subsystem:
$(MAKE) -C subdir
那么,递归调用是怎么工作的?实际使用时有什么需要注意的呢?
- 递归调用时在recipe中应该使用
$(MAKE)
,不应该直接使用make命令。 - make开始运行,其处理完-C选项后,会设置CURDIR的变量为当前工作目录的绝对路径,后续不会再去改动这个变量。这个CURDIR变量是make变量,不是环境变量,并且就算设置了同名的环境变量对make的CURDIR变量也是没有影响的。比如在subdir目录里有如下的makefile:
.PHONY: all
all: sub
sub:
@echo "sub makefile"
@echo "make.CURDIR = ${CURDIR}"
@echo "shell.CURDIR = $${CURDIR}"
我们进入到subdir目录,运行make,打印如下内容:
sub makefile
make.CURDIR = /home/boluo/sambashare/make-test/subdir
shell.CURDIR =
我们进入到subdir的父目录,运行make -f subdir/Makefile
sub makefile
make.CURDIR = /home/buoluo/sambashare/make-test
shell.CURDIR =
可以看到,给CURDIR的始终是当前工作目录,而不是Makefile所在的目录。
- 递归调用会开启一个新的make进程(子make进程),子make进程的CURDIR跟父进程的CURDIR没有关系,比如有如下的主Makefile调用subdir目录下的子Makefile:
.PHONY: all sub1 sub2
all:
@echo "main makefile"
@echo "main.CURDIR = ${CURDIR}"
sub1:
@echo "main.CURDIR = ${CURDIR}"
$(MAKE) -C subdir
sub2:
@echo "main.CURDIR = ${CURDIR}"
@cd subdir && $(MAKE)
运行make sub1,打印如下结果:
main.CURDIR = /home/boluo/sambashare/make-test
make -C subdir
make[1]: Entering directory '/home/boluo/sambashare/make-test/subdir'
sub makefile
make.CURDIR = /home/boluo/sambashare/make-test/subdir
shell.CURDIR =
make[1]: Leaving directory '/home/boluo/sambashare/make-test/subdir'
运行make sub2,打印如下结果:
main.CURDIR = /home/boluo/sambashare/make-test
make[1]: Entering directory '/home/boluo/sambashare/make-test/subdir'
sub makefile
make.CURDIR = /home/boluo/sambashare/make-test/subdir
shell.CURDIR =
make[1]: Leaving directory '/home/boluo/sambashare/make-test/subdir'
可以看到无论是通过先进入subdir还是通过使用-C指定subdir,子make进程的CURDIR变量都是subdir目录的绝对路径,它们都会先把工作目录切换到subdir再设置CURDIR变量。
- 顶层的make想要将变量值传递给子make,需要借助环境变量。因为一个make进程是一个shell进程,shell子进程不会继承父进程里定义的全局变量,而父进程将全局变量导出为环境变量后,可以由子进程继承。
make中设置环境变量有两种方法,第一种方法是在makefile中直接使用export,跟shell中的EXPORT用法类似,使用方式如下:
export variable ...
也可在定义变量的时候直接导出:
export variable = value
export variable := value
export variable += value
如果想导出所有的变量,可以直接使用export,效果同在makefile中使用.EXPORT_ALL_VARIABLES
特殊目标。
export
make中的MAKEFLAGS变量总是会被导出,所以-k -s等选项总是会自动传递给子make进程。
第二种方法是在make的时候通过命令传入,如下:
$(MAKE) -C subdir NAME=VALUE
假如我们不想主make进程的MAKEFLAGS变量传递到子make进程,可以使用这种方式进行对其值进行重置。
- 递归调用make的时候,make默认会通过打印提示当前将要到哪个目录里面去运行,也即默认使用‘-w’或‘–print-directory’选项。可以通过使用‘–no-print-directory’关闭掉这个功能。
使用define定义集装配方(Canned Recipes)
当一些targets会用到相同序列的recipes时,可以使用define将这些recipes定义成一个集装配方,方便在这些targes的规则里面使用,这样可以省去重复编写recipes的工作,整体缩短makefile的篇幅。不过目前还没有见过这样用的,估计用得不多,先留在这里,以后有遇到的时候再进行补充。
使用空配方(Empty Recipes)
空配方不是指rule没有recipes,而是recipes部分是空白的(TAB+空格之类的)。如果某个target的规则只是没有recipes,make会尝试使用合适的隐式recipes来构造这个target。但是如果是空recipe,则make不会做任何事情。我们可以使用空recipe来防止make使用隐式recipes来构造target,特别是某些本来就不对应实际文件的targets,比如之前接触过的FORCE。