Makefile -- 二次展开(SECONDEXPANSION)

(翻译自 GNU make 3.1 What Makefiles Contain3.7 How make Reads a Makefile3.8 How Makefiles Are Parsed3.9 Secondary Expansion,总共四节)

1. Makefiles 构成

Makefiles 包含五种结构:显示规则(explicit rules),隐式规则(implicit rules),变量定义(variable definitions),指令(directives)和注释(comments)。后面的章节会详尽描述规则、变量和指令。

  • 显示规则说明何时以及如何重新生成一个或者多个文件,这些文件被称为规则的目标(targets)。显示规则会列出目标依赖的其他文件,这些文件称为目标的依赖(prerequisites)。同时,显示规则也会给出命令(recipe),用来创建或者更新目标(详见 4 Writing Rules)。

  • 隐式规则说明何时以及如何根据一类文件的名字重新生成这些文件。隐式规则描述了目标如何依赖和目标名字相似的文件,同时也会给出创建或更新这种目标的命令(recipe)(详见 10 Using Implicit Rules)。

  • 变量定义通过一行代码指定变量的文本字符串。之后的 makefile 解析过程中,变量会被替换成其对应的文本。2.4 Variables Make Makefiles Simpler 中的例子展示了如何定义变量,变量列出了所有的 object 文件。

  • 指令告诉 make 在读取 makefile 时,做一些特殊的处理,包括:

  • makfile# 开头的行为注释,注释行会被忽略。行尾添加未转义的反斜杠 \ 可以定义多行注释。 仅包含注释的行(或者开头包含一些空格)被当做空行,会被 make 忽略。如果想要使用 # ,需要通过反斜杠 \ 转义。注释可以出现在 makefile 的任意行,尽管在一些特定场景下会被特殊处理。

不能在变量引用和函数调用中使用注释:变量引用和函数调用中的 # 会按照字面意义处理(而不是注释的开始)。

命令(recipe)内部的注释和其他命令文本一样,会被传递给 shellshell 决定如何解释它:是否为注释取决于 shell

指令(directive)内部的注释不会被忽略,而是保持不变,当变量被展开时,根据变量求值上下文,被处理为 make 注释或者命令(recipe)文本。

2. make 如何读取 Makefile

GNU make 工作在两种不同的状态。第一种状态下,make 读取所有 makefiles(包括被包含的 makefiles),并内化所有变量、变量的值、隐式规则和显式规则。然后建立所有目标和对应依赖之间的依赖图。第二种状态下,make 通过内化的数据决定哪些目标需要更新,然后运行必要的命令更新这些目标。

理解两种状态的处理方式非常重要,因为这直接影响到变量和函数展开如何发生,这常常会给 makefiles 的编写带来困惑。下面是对 makefile 中不同结构的总结,说明了这些结构的不同部分在哪种状态下展开。

第一种状态下的展开称为立即展开(immediate):make 会在解析 makefile 时对这部分进行展开,其余展开称为延迟(deffered)展开。延迟展开部分被推迟到使用时展开:可以是在立即展开上下文环境中被引用时展开,或者是在第二种状态下需要时展开。

也许你还不熟悉下面的一些结构。熟悉后面章节后,可以回过头来查看这里。

变量赋值

变量定义按如下方式解析:

immediate = deferred
immediate ?= deferred
immediate := immediate
immediate ::= immediate
immediate += deferred or immediate
immediate != immediate

define immediate
	deferred
endef

define immediate =
	deferred
endef

define immediate ?=
	deferred
endef

define immediate :=
	immediate
endef

define immediate ::=
	immediate
endef

define immediate +=
	deferred or immediate
endef

define immediate !=
	immediate
endef

对于追加赋值操作 +=,如果变量之前已经通过 := 或者 ::= 设置为简单变量(simple variable),右侧会被立即展开,其他情况延迟展开。

对于 shell 赋值操作 !=,右侧会被立即展开,然后交给 shell 处理。结果保存在左侧的的变量中,左侧变量称为简单变量(simple varivable,会在每个引用中重新求值)。

条件指令

条件指令会立即展开。这意味条件指令中不能使用自动变量,因为自动变量直到规则的命令被调用时才会被设置。如果想要在条件指令中使用自动变量,应当将条件移到命令(recipe)中,然后使用 shell 条件语法替代。

规则定义

不管形式如何,规则总是按照相同方式展开:

immediate : immediate ; deferred
	deferred

目标(target)和依赖(prerequisites)会被立即展开,构建目标的命令(recipe)延迟展开。对于显示规则、模式规则、后缀规则、静态模式规则和简单的依赖定义都按这种方式展开。

3. 如何解析 Makefiles

GNU make 逐行解析 makefiles ,解析步骤如下:

  1. 读取所有的逻辑行,包括反斜杠转义行(请参阅 3.1.1 Splitting Long Lines)。
  2. 移除注释(请参阅 3.1 What Makefiles Contain)。
  3. 如果某行以命令(recipe)前缀字符开头,那么当前行处于规则上下中,make 将该行添加到当前规则的命令(recipe)中,然后继续解析下一行(请参阅 5.1 Recipe Syntax)。
  4. 扩展正在解析行中符合立即展开上下文的元素(请参阅 3.7 How make Reads a Makefile)。
  5. 扫描行中的分割符,如 : 或者 = 等,确定该行是宏赋值还是规则(请参阅 5.1 Recipe Syntax)。
  6. 内化操作的结果,继续读取下一行。

下面的宏赋值可以展开成完整的规则:

myrule = target : ; echo built
$(myrule)

下面的定义是无效的,因为展开后没有分割行:

define myrule
target:
	echo built
endef
$(myrule)

上述的 makefile 定义了目标 target,依赖为 echobuilt,效果和 target: echo built 一样,而不是带有命令的规则。展开后,换行符被当做空格处理,所有行都放置在同一行。

想要正确展开多行宏定义,需要使用 eval 函数:eval 函数会使 make 对已展开的宏再次进行解析(请参阅 8.9 The eval Function)。

4. 二次展开

通过前面的内容,我们知道 GNU make 工作在两种不同的状态:读取状态和目标更新状态(请参阅 3.7 How make Reads a Makefile)。GNU make 能够为某些或者所有定义在 makefile 中的目标(target)使能依赖(prerequisite仅适用于依赖)的二次展开(secondary expansion)。想要使能二次展开,需要在第一个需要使用该特性的依赖之前定义特殊目标 .SECONDEXPANSION

如果定义了特殊目标 .SECONDEXPANSION,在前面提到的两种工作状态之间,即在读取状态之后,所有在特殊目标 .SECONDEXPANSION 之后定义的依赖都会进行二次展开。所有变量和函数已经在 makefile 的第一次解析中展开,所以大部分情形下,二次展开没有任何作用。想要利用二次展开特性,需要转义变量或函数引用,即在第一次展开时转义引用,在二次展开时再展开变量或函数引用。例如:

.SECONDEXPANSION:
ONEVAR = onefile
TWOVAR = twofile
myfile: $(ONEVAR) $$(TWOVAR)

第一次展开之后,myfile 的依赖是 onefile$(TWOVAR):第一个依赖是对变量 ONEVAR 的引用,它被展开为 onefile*;而第二个变量引用被转义,没有被识别为变量引用(将 $$ 展开为 $)。第二次展开时,因为 onefile 既不是变量引用也不是函数引用,所以其值仍然是 onefile,而 $(TWOVAR) 是对变量 TWOVAR 的引用,被展开为 twofile。最终的结果是产生两个依赖:onefiletwofile

上述例子没有显示出二次展开的优势,在依赖列表里使用两个变量也能得到相同的结果,而且更简单。而在变量重置的情形下,不同之处更加明显,例如:

.SECONDEXPANSION:
AVAR = top
onefile: $(AVAR)
twofile: $$(AVAR)
AVAR = bottom

上例中,onefile* 的依赖会被立即展开,解析为 top,而 twofile 的依赖会在二次展开时被完全展开,其值为 bottom

这稍微更令人兴奋一点,当二次展开和目标的自动变量一起使用时,二次展开的真实威力才会真正显现。这意味着可以在二次展开中使用诸如 $@$* 等变量,就像在命令(recipe)中一样,他们会展开为你所期望的值。你所需要做的就是通过转义 $ 来延迟展开。显示规则和隐式规则中均可使用二次展开,这一点让二次展开的适用范围更广,例如:

.SECONDEXPANSION:
main_OBJS := main.o try.o test.o
lib_OBJS := lib.o api.o

main lib: $$($$@_OBJS)

这里,第一次展开后,mainlib 的依赖都是 $($@_OBJS)。第二次展开时,$@ 被设置为目标名,所以在 main 依赖的二次展开中,$($@_OBJS) 被设置成 $(main_OBJS),或者 main.o try.o test.o,而在 lib 依赖的二次展开中, $($@_OBJS) 被设置成 $(lib_OBJS),或者 lib.o api.o

只要能够合适的转义,也可以混合使用函数:

main_SRCS := main.c try.c test.c
lib_SRCS := lib.c api.c

.SECONDEXPANSION:
main lib: $$(patsubst %.c,%.o,$$($$@_SRCS))

上例中,允许指定源文件而不是目标(object)文件,这可以得到和前面的例子一样的结果。二次扩展状态下,自动的变量的取值,尤其是 $$@,和命令(recipes)中类似。不过,对于 make 能够理解的不同类型的规则中,存在一些微妙的不同之处和例外情况的。下面描述使用自动变量时的微妙差异。

显式规则的二次展开

显示规则的二次展开中,当目标是一个 archive 文件时,$$@$$% 分别被展开成目标文件名和目标文件成员名(例如:目标为 foo.a(bar.o) 时,$$@ 表示 foo.a$$% 表示 bar.o)。$$< 展开成目标的第一条规则中的第一个依赖(实际使用发现这里存在一些问题,详见第 5 节)。$$^$$+ 展开成目标的所有依赖(*$$^ 不包含重复项,$$+ 包含重复项)。下面的例子说明了这些行为:

.SECONDEXPANSION:
foo: foo.1 bar.1 $$< $$^ $$+ # line #1
foo: foo.2 bar.2 $$< $$^ $$+ # line #2
foo: foo.3 bar.3 $$< $$^ $$+ # line #3

第一个依赖列表中,三个自动变量($$<$$^$$+) 都展开成空字符串。第二个依赖列表中,他们的值分别是 foo.1foo.1 bar.1foo.1 bar.1。第三个依赖列表中,他们的值分别是 foo.1foo.1 bar.1 foo.2 bar.2foo.1 bar.1 foo.2 bar.2 foo.1 foo.1 bar.1 foo.1 bar.1

默认情况下,make 按照 makefile 中的定义顺序对规则进行二次展开,而同一目标的多个规则中,带有命令(recipe)的规则最后展开。
$$?$$* 不可用,会被扩展成空字符串。

静态模式规则的二次展开

静模式规则的二次展开和显式规则完全一样,有一个例外:对于静态模式规则,$$* 会被设置为主干(stem,例如:假设 %.o 匹配到 object 文件 bar.o$$* 展开成 bar)。和显式规则一样,$$? 不可用,会被扩展成空字符串。

隐式规则的二次展开

make 搜索隐式规则时,它会对每条匹配的目标模式的规则替换主干(stem),然后执行二次展开,自动变量求值方式和静态模式规则类似。例如:

.SECONDEXPANSION:
foo: bar
foo foz: fo%: bo%
%oo: $$< $$^ $$+ $$*

foo 的隐式规则工作时,$$< 展开成 bar$$^ 展开成 bar boo$$+ 也展开成 bar boo,而 $$* 展开成 f

注意:实际运行该规则时,发现最后一条模式匹配规则并未对 foo 生效。

注意到,二次展开后,目录前缀(D,请参阅10.8 Implicit Rule Search Algorithm)会被添加到依赖列表中的所有模式之后。例如:

.SECONDEXPANSION:
/tmp/foo.o:
%.o: $$(addsuffix /%.c,foo bar) foo.h
	@echo $^

在二次展开和目录前缀重构之后,依赖列表将是 /tmp/foo/foo.c /tmp/bar/foo.c foo.h。如果你不想这样重构依赖列表,可以使用 $$* 替换 %

5. 补充说明

前面第 4 节显示规则的二次扩展开中提到 $$< 会被展开为目标第一条规则第一个依赖(同一目标可以包含多条规则,最后一条规则的命令会覆盖前面的命令,而目标的依赖为所有规则依赖的集合,请参阅 4.11 Multiple Rules for One Target),在环境中运行,结果和描述不一致。
make 版本为 4.3,和文档描述版本一致。

.SECONDEXPANSION:
foo: foo.1 bar.1
foo: foo.2 bar.2 foo.1
        @echo [$^]
        @echo [$+]

上面的例子,输出为:

[foo.2 bar.2 foo.1 bar.1]
[foo.2 bar.2 foo.1 foo.1 bar.1]

如果 $$< 被替换为第一条规则的第一个依赖(即 foo.1),下面的 makefile 会应该输出相同的结果:

.SECONDEXPANSION:
foo: foo.1 bar.1
foo: foo.2 bar.2 $$<
   @echo [$^]
   @echo [$+]

但实际结果如下:

[foo.2 bar.2 foo.1 bar.1]
[foo.2 bar.2 foo.1 bar.1]

按照个人理解重新解释下面的例子:

.SECONDEXPANSION:
foo: foo.1 bar.1 $$< $$^ $$+ # line #1
foo: foo.2 bar.2 $$< $$^ $$+ # line #2
foo: foo.3 bar.3 $$< $$^ $$+ # line #3

个人理解是,当make 读取同一目标的所有规则后,会首先解析最后一条规则,然后再按 makefile 中规则定义的顺序开始依次解析构。上述例子中,首先解析第三条规则,然后是第一条规则,最后解析第二条规则。

  • 第三个依赖列表中,三个自动变量($$<$$^$$+) 都展开成空字符串,因为首先解析的是该规则。
  • 第一依赖列表中,他们的值分别是 foo.3foo.3 bar.3foo.3 bar.3
  • 第二个依赖列表中,他们的值分别是 foo.3foo.3 bar.3 foo.1 bar.1foo.3 bar.3 foo.1 bar.1 foo.3 foo.3 bar.3 foo.3 bar.3

再加上每条规则自带的 foo.n bar.nn 取 1,2,3,最终打印结果如下:

[foo.3 bar.3 foo.1 bar.1 foo.2 bar.2]
[foo.3 bar.3 foo.1 bar.1 foo.3 foo.3 bar.3 foo.3 bar.3 foo.2 bar.2 foo.3 foo.3 bar.3 foo.1 bar.1 foo.3 bar.3 foo.1 bar.1 foo.3 foo.3 bar.3 foo.3 bar.3]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值