上一篇:1-Make概述与简介
告诉 make 如何重新编译的信息来自读取一个名为 Makefile 的文件。
1. Makefile 包含哪些内容
Makefile 包含五种内容:显式规则、隐式规则、变量定义、指令和注释。
① 显式规则说明何时以及如何生成一个或多个文件(称为规则的目标)。它列出了目标所依赖的先决条件,还可能给出用于创建或更新目标的recipe;
② 隐式规则说明何时以及如何根据文件名生成一类文件。它描述了目标文件如何依赖于名称与目标文件相似的文件,并给出了创建或更新此类目标文件的方法。
③ 变量定义是为变量指定文本字符串值的一行,该值可以在以后替换到文本中。
④ 指令是让 make 在读取 Makefile 时执行某些特殊操作的指令。这些指令包括:
Ⅰ. 读取另一个 Makefile;
Ⅱ. 根据变量值决定使用还是忽略 Makefile 的某一部分;
Ⅲ. 用包含多行的逐字字符串定义变量;
⑤ 在 Makefile 的某一行中,"#"开始一个注释。它和该行的其余部分都会被忽略,但尾部的反斜杠如果没有被另一个反斜杠转义,则会在多行中继续注释。仅包含注释的一行(前面可能有空格)实际上是空白行,会被忽略。如果要使用字面意义上的 #,请用反斜线转义(例如:\#)。注释可以出现在 Makefile 的任何一行,但在某些情况下会被特殊处理。
不能在变量引用或函数调用中使用注释:在变量引用或函数调用中,任何 # 的实例都将按字面意思处理(而不是作为注释的开头)。
与其他recipe文本一样,recipe中的注释也会传递给 shell。shell 决定如何解释:这是否是注释取决于 shell。
在定义指令中,注释不会在变量定义过程中被忽略,而是保留在变量值中。当变量被展开时,这些注释将被视为make注释或recipe文本,具体取决于上下文。
1.1 换行
Makefile 使用 "基于行 "的语法,其中换行符是特殊符号,标志着语句的结束。make 对语句行的长度没有限制,最高可达计算机内存容量。
不过,如果行太长,在不换行或滚动的情况下很难阅读。因此,可以通过在语句中间添加换行符来格式化 Makefile,以提高可读性。方法是:用反斜线(\)字符转义内部换行符。在需要区分的地方,会将 "物理行 "称为以换行结束的单行(无论是否转义),而将 "逻辑行 "称为包括所有转义换行直至第一个未转义换行的完整语句。
1.1.1 换行时不添加空白
如果需要分行,但又不想添加任何空白,可以使用一个小技巧:用美元$符号、反斜杠和换行符这三个字符替换反斜杠/换行符对:
var := one$\
word
在 make 删除反斜线/新行并将下一行压缩为一个空格后,这相当于:
var := one$ word
然后 make 将执行变量扩展。变量引用"$"指的是一个不存在的单字符名""(空格)变量,因此会扩展为空字符串,最终赋值相当于:
var := oneword
2. Makefile如何命名
默认情况下,当 make 查找 Makefile 时,它会依次尝试以下名称:GNUmakefile、makefile 和 Makefile。通常,你应该把 makefile 称为 makefile 或 Makefile。
我们推荐使用 Makefile,因为它出现在目录列表开头的显著位置,紧挨着 README 等其他重要文件。
如果 make 找不到这些名字,它就不会使用任何 makefile。这时,必须用命令参数指定一个目标,make 会尝试只用内置的隐含规则来生成它。
如果想使用非标准的 makefile 名称,可以使用"-f "或"--file "选项指定 makefile 名称。参数"-f name "或"--file=name "告诉 make 将文件名作为 makefile 读取。如果使用多个"-f "或"--file "选项,就可以指定多个 makefile。所有 makefile 会按照指定的顺序有效连接。如果指定了"-f "或"--file",默认的 makefile 名称 GNUmakefile、makefile 和 Makefile 将不会被自动选中。
3. include其他Makefile
include 指令告诉 make 在继续之前暂停读取当前 Makefile,并读取一个或多个其他 Makefile。该指令是 Makefile 中的一行,如下所示:
include filenames...
filenames 可以包含 shell 文件名模式。如果文件名为空,则不包含任何内容,也不会打印错误信息。
允许在行首使用额外的空格,但第一个字符不能是制表符。如果行首使用制表符,则该行将被视为recipe行。在 include 和文件名之间以及文件名与文件名之间必须有空白;指令中和指令末尾的空白将被忽略。行尾允许使用以 "#"开头的注释。如果文件名中包含变量或函数引用,则会对其进行扩展。
例如,如果您有3个 .mk 文件,分别是 a.mk、b.mk 和 c.mk,而 $(bar) 扩展为 bish bash,那么表达式如下:
include foo *.mk $(bar)
相当于:
include foo a.mk b.mk c.mk bish bash
make 在处理包含指令时,会暂停读取包含该指令的 Makefile,然后依次读取每个列出的文件。完成后,make 会继续读取指令所在的 Makefile。
使用 include 指令的一种情况是,由不同目录下的 Makefile 处理的多个程序需要使用一组共同的变量定义或模式规则。
另一种情况是想从源文件中自动生成先决条件;先决条件可以放在主 Makefile 包含的文件中。这种做法通常比在其他版本的 make 中将先决条件附加到主 Makefile 末尾的做法更为简洁。
如果指定的文件名不是以斜线开头,而且在当前目录下找不到该文件,则会搜索其他几个目录。首先,将搜索使用"-I "或"--include-dir "选项指定的任何目录。
然后按以下顺序搜索下列目录(如果存在):前缀/包含(通常/usr/local/include)、/usr/gnu/include、/usr/local/include、/usr/include。
.INCLUDE_DIRS 变量将包含 make 搜索包含文件的当前目录列表。
通过在命令行中添加带有特殊值 - 的命令行选项 -I(例如 -I-),可以避免在这些默认目录中进行搜索。这会导致 make 忘记任何已设置的包含目录,包括默认目录。
如果在这些目录中的任何一个中找不到包含的 Makefile,这并不是一个立即致命的错误;包含该包含的 Makefile 的处理仍在继续。一旦读取完 Makefile,make 会尝试重编任何过时或不存在的 Makefile。只有在找不到重写 Makefile 的规则,或者找到了规则但recipt失败之后,make 才会将丢失的 makefile 诊断为致命错误。
如果想让 make 忽略不存在或无法重做的 Makefile,并且不给出错误信息,可以使用 -include 指令代替 include,如下所示:
-include filenames...
除了在任何文件名(或任何文件名的先决条件)不存在或无法生成的情况下不会出错(甚至不会发出警告)外,该功能在任何方面都与 include 类似。
4. 如何重制Makefile
有时,Makefile 可以从其他文件重制,比如 RCS 或 SCCS 文件。如果 Makefile 可以从其他文件重制,那么可能希望 make 能读入最新版本的 Makefile。
为此,读入所有 Makefile 后,make 会按处理顺序将每个文件视为目标,并尝试更新。如果启用了并行编译,那么 Makefile 也将并行重建。
如果 Makefile 中有说明如何更新的规则(可以在该 Makefile 或其他 Makefile 中找到),或者如果隐式规则适用于该 Makefile,那么在必要时就会更新该 Makefile。检查完所有 Makefile 后,如果有任何更改,make 会从头开始,重新读取所有 Makefile。(它还会尝试重新更新每一个文件,但通常不会再次更改,因为它们已经是最新的了)。每次重新启动都会导致特殊变量 MAKE_RESTARTS 的更新。
如果一个或多个 Makefile 不能被重制,并且想阻止 make 对它们执行隐式规则搜索,也许是出于效率的考虑,则可以使用任何防止隐式规则查找的正常方法来做到这一点。例如,可以编写一条以 Makefile 为目标的显式规则,并编写一个空recipe。
如果 Makefile 指定了双冒号规则来重制一个有recipe但没有先决条件的文件,那么该文件将始终被重制,当 make 重新启动并再次读入 Makefile 时又会重制一次。这会造成无限循环。因此,为了避免出现这种情况,make 不会尝试重制那些被指定为双冒号规则目标的 Makefile;
伪目标具有相同的效果:它们永远不会被认为是最新的,因此被标记为伪目标的包含文件会导致 make 不断重启。为了避免这种情况,make 不会尝试重制被标记为伪目标的Makefile。
可以利用这一点来优化启动时间:如果知道不需要重制 Makefile,可以通过添加以下任一选项来防止 make 重制 Makefile:
.PHONY: Makefile
或:
Makefile:: ;
如果没有使用"-f "或"--file "选项指定要读取的 Makefile,make 将尝试使用默认的 Makefile 名称;与使用"-f "或"--file "选项明确请求的 Makefile 不同,make 无法确定这些 Makefile 是否存在。不过,如果默认的 Makefile 不存在,但可以通过运行 make 规则来创建。因此,如果默认的 Makefile 都不存在,make 会尝试制作每一个 Makefile,直到成功制作一个,或者没有可尝试的文件名为止。请注意:如果 make 无法找到或制作任何 Makefile,这并不是错误;Makefile 并不总是必须的。
当使用"-t "或"--touch "选项时,不希望使用过时的 Makefile 来决定触及哪些目标。因此,"-t "选项对更新 Makefile 没有影响;即使指定了"-t",它们也会被更新。同样,"-q"(或"--question")和"-n"(或"--just-print")也不会阻止更新 Makefile,因为过时的 Makefile 会导致其他目标的错误输出。因此,"make -f mfile -n foo"将更新 mfile、读入它,然后打印更新 foo 及其先决条件的recipe,而无需运行它。为 foo 打印的recipe将是 mfile 更新内容中指定的recipe。
不过,有时可能真的希望连 Makefile 也不要更新。可以在命令行中将 Makefile 指定为目标,也可以将它们指定为 Makefile。当 Makefile 名称被明确指定为目标时,选项"-t "等就会应用于它们。因此,"make -f mfile -n mfile foo "将读取 Makefile mfile,在不实际运行它的情况下打印更新它所需的recipe,然后在不运行它的情况下打印更新 foo 所需的recipe。foo 的recipe将是 mfile 中现有内容指定的recipe。
5. 覆盖另一个 Makefile 的部分内容
有时,一个 Makefile 与另一个 Makefile 大同小异,这也是很有用的。你通常可以使用 "include "指令将一个包含在另一个中,并添加更多目标或变量定义。不过,两个 Makefile 为同一个目标提供不同的recipe是无效的。不过还有另一种方法。
在包含 Makefile 的 Makefile 中(外层的Makefile),可以使用 match-anything 模式规则来说明,要重制任何无法根据包含 Makefile 中的信息制作的目标,make 应该在另一个 Makefile 中查找。
例如,如果有一个名为 Makefile 的 makefile,其中说明了如何制作目标文件 "foo"(以及其他目标文件),那么您可以编写一个名为 GNUmakefile 的 makefile,其中包含:
foo:
frobnicate > foo
%: force
@$(MAKE) -f Makefile $@
force: ;
如果键入 "make foo",make 就会找到 GNUmakefile,读取它,并发现要制作 foo,需要运行配方 "frobnicate > foo"。如果你说 "make bar",make 在 GNUmakefile 中找不到制作 bar 的方法,因此它将使用模式规则中的配方:"make -f Makefile bar"。如果 Makefile 提供了更新 bar 的规则,make 就会应用该规则。同样,对于 GNUmakefile 未说明如何制作的其他目标,也是如此。
其工作原理是,模式规则的模式仅为"%",因此可以匹配任何目标文件。该规则指定了一个先决条件 force,以保证即使目标文件已经存在,recipe也能运行。我们给强制目标一个空recipe,以防止 make 搜索隐含规则来构建它,否则它会应用相同的匹配规则来强制自己,并创建一个先决条件循环!
6. make 如何读取 Makefile
GNU make 的工作分为两个不同的阶段。在第一阶段,它会读取所有的 Makefile、包含的 Makefile 等,并将所有变量及其值、隐式和显式规则内部化,同时建立所有目标及其先决条件的依赖关系图。在第二阶段,make 会使用这些内部化数据来确定哪些目标需要更新,并运行必要的recipe来更新它们。
理解这种两阶段方法非常重要,因为它直接影响到变量和函数的扩展方式;这也是编写 Makefile 时经常出现混淆的原因。下面总结了 Makefile 中的不同结构体,以及结构体各部分的扩展阶段。
如果扩展发生在第一阶段,我们称之为即时扩展:make 会在解析 Makefile 时扩展结构体的这一部分。如果扩展不是即时的,我们就说它是延迟扩展。延迟的构造部分的扩展会延迟到使用时才进行:要么是在即时上下文中被引用时,要么是在第二阶段需要时。
变量赋值:
immediate = deferred
immediate ?= deferred
immediate := immediate
immediate ::= immediate
immediate :::= immediate-with-escape
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 :::=
immediate-with-escape
endef
define immediate +=
deferred or immediate
endef
define immediate !=
immedi
对于运算符'+=',如果变量先前被设置为简单变量(':='或'::='),则右侧被认为是立即的,否则是延迟的。
对于 immediate-with-escape 运算符":::=",右侧的值会立即展开,然后转义(也就是说,展开结果中的所有 $ 实例都会被替换为 $$)。
对于 shell 赋值运算符"!=",右边的值会被立即评估并交给 shell。结果存储在左侧命名的变量中,该变量被视为递归扩展变量。
7. 条件指令
条件指令会被立即解析。例如,这意味着自动变量不能在条件指令中使用,因为在调用该规则的recipe之前,自动变量不会被设置。如果需要在条件指令中使用自动变量,必须将条件移入recipe中,并使用 shell 条件语法。
8. 规则定义
无论形式如何,规则总是以相同的方式展开:
immediate : immediate ; deferred
deferred
也就是说,目标和先决条件部分会立即展开,而用于构建目标的recipe总是被延迟。这适用于显式规则、模式规则、后缀规则、静态模式规则和简单的前提条件定义。
9. 如何解析Makefile
make 对 Makefile 进行逐行解析,解析工作按以下步骤进行:
①. 读入完整的逻辑行,包括反斜线转码行。
②. 删除注释。
③. 如果该行以recipe前缀字符开头,且我们处于规则上下文中,则将该行添加到当前recipe中并读取下一行。
④. 立即扩充上下文中出现的行元素。
⑤. 扫描行中的分隔符,如": "或"=",以确定该行是宏赋值还是规则。
⑥. 内化操作结果并读取下一行。
这样做的一个重要结果是,如果一条规则只有一行长,则宏可以扩展为整条规则。这样就可以了:
myrule = target : ; echo built
$(myrule)
但是,这样做是行不通的,因为 make 在展开行之后不会重新拆分行:
define myrule
target:
echo built
endef
$(myrule)
上述Makefile 的结果是定义了一个目标 "target",其先决条件为 "echo "和 "build",就好像 Makefile 包含了目标:echo built,而不是一个带有recipe的规则。扩充完成后,行中仍然存在的换行符将作为正常空白被忽略。
为了正确扩展多行宏,必须使用 eval 函数:这将导致 make 解析器在扩展宏的结果上运行。
10. 二次扩展
make 的工作分为两个不同的阶段:读入阶段和目标更新阶段。对于 Makefile 中定义的某些或所有目标,make 还可以对先决条件(仅)进行第二次扩展。为了实现第二次扩展,必须在使用此功能的第一个先决条件列表之前定义特殊目标: .SECONDEXPANSION
如果定义了 .SECONDEXPANSION,那么当make 需要检查目标的先决条件时,就会对先决条件进行第二次扩展。在大多数情况下,这种二次展开不会产生任何影响,因为在最初解析 Makefile 时,所有变量和函数的引用都已展开。因此,为了利用解析器的二次展开阶段,有必要在 Makefile 中转义变量或函数的引用。在这种情况下,第一次扩充只是取消转义,但并不扩充引用,扩充工作留给了二次扩充阶段。例如,请看这个 Makefile:
.SECONDEXPANSION:
ONEVAR = onefile
TWOVAR = twofile
myfile: $(ONEVAR) $$(TWOVAR)
在第一扩展阶段后,myfile 目标文件的先决条件列表将是 onefile 和 $(TWOVAR);第一个(未转义的)变量引用 ONEVAR 被扩展,而第二个(转义的)变量引用只是未转义,不被识别为变量引用。现在,在二次扩展过程中,第一个单词再次被扩展,但由于它不包含变量或函数引用,其值仍然是 onefile,而第二个单词现在是对变量 TWOVAR 的正常引用,它被扩展为值 twofile。最终的结果是有两个先决条件:onefile 和 twofile。
显然,这并不是一个非常有趣的例子,因为只要让这两个变量不加转义地出现在先决条件列表中,就可以更容易地得到同样的结果。如果对变量进行重置,其中的差别就会显现出来;请看这个例子:
.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)
在这里,初始扩展后,主目标和 lib 目标的先决条件都将是 $($@_OBJS)。在二次扩展过程中,$@ 变量将被设置为目标名称,因此主目标的扩展结果将是 $(main_OBJS),或 main.o try.o
test.o,而 lib 目标的二级扩展将产生 $(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))
该允许用户指定源文件而不是对象文件,但生成的先决条件列表与上一示例相同。
在二次扩展阶段对自动变量的评估,尤其是对目标名称变量 $$@ 的评估,与在recipe中的评估行为类似。不过,在不同类型的规则定义中,也会出现一些细微的差别。
10.1 显式规则的二次扩展
在显式规则的二次扩展过程中,$$@ 和 $$% 分别求值为目标的文件名,如果目标是存档成员,则求值为目标成员名。$$< 变量的计算结果是该目标的第一条规则中的第一个先决条件。$$^ 和 $$+ 的值为同一目标已出现规则的所有前提条件列表($$+ 有重复,$$^ 无重复)。下面的示例将有助于说明这些行为:
.SECONDEXPANSION:
foo: foo.1 bar.1 $$< $$^ $$+ # line #1
foo: foo.2 bar.2 $$< $$^ $$+ # line #2
foo: foo.3 bar.3 $$< $$^ $$+ # line #3
在第一个前提条件列表中,所有三个变量($$<、$$^ 和 $$+)都展开为空字符串。在第二个列表中,它们的值分别为 foo.1、foo.1 bar.1 和 foo.1 bar.1。在第三个变量中,它们的值分别为 foo.1、foo.1 bar.1 foo.2 bar.2 和 foo.1 bar.1 foo.2 bar.2 foo.1 foo.1 bar.1 foo.1 bar.1。
规则按 Makefile 顺序进行二次扩展,但有recipe的规则总是最后评估。
变量 $$? 和 $$* 不可用,会扩展为空字符串。
10.2 隐含规则的二次扩展
在 make 搜索隐式规则时,它会替换词干,然后对每条具有匹配目标模式的规则进行二次扩展。自动变量的取值方式与静态模式规则相同。例如:
.SECONDEXPANSION:
foo: bar
foo foz: fo%: bo%
%oo: $$< $$^ $$+ $$*
当对目标 foo 进行隐式规则时,$$< 会扩展为 bar,$$^ 会扩展为 bar boo,$$+ 也会扩展为 bar boo,而 $$* 则会扩展为 f。
请注意,目录前缀 (D) 将(在扩展后)附加到先决条件列表中的所有模式。例如:
.SECONDEXPANSION:
/tmp/foo.o:
%.o: $$(addsuffix /%.c,foo bar) foo.h
@echo $^
经过二级扩展和目录前缀重构后,打印出的先决条件列表将是 /tmp/foo/foo.c /tmp/bar/foo.c foo.h。
如果对这种重构不感兴趣,可以在先决条件列表中使用 $$* 代替 %。
下一篇: 3-写规则