三、写配方(Recipes)

前言

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。

  • 15
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值