2-编写Makefile

        上一篇: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-写规则

Make命令   在make命令后不仅可以出现宏定义,还可以跟其他命令行参数,这些参数指定了需要编译的目标文件。其标准形式为:   target1 [target2 …]:[:][dependent1 …][;commands][#…]   [(tab) commands][#…]   方括号中间的部分表示可选项。Targets和dependents当中可以包含字符、数字、句点和"/"符号。除了引用,commands中不能含有"#",也不允许换行。   在通常的情况下命令行参数中只含有一个冒号":",此时command序列通常和makefile文件中某些定义文件间依赖关系的描述行有关。如果与目标相关连的那些描述行指定了相关的command序列,那么就执行这些相关的command命令,即使在分号和(tab)后面的aommand字段甚至有可能是NULL。如果那些与目标相关连的行没有指定command,那么将调用系统默认的目标文件生成规则。   如果命令行参数中含有两个冒号"::",则此时的command序列也许会和makefile中所有描述文件依赖关系的行有关。此时将执行那些与目标相关连的描述行所指向的相关命令。同时还将执行build-in规则。   如果在执行command命令时返回了一个非"0"的出错信号,例如makefile文件中出现了错误的目标文件名或者出现了以连字符打头的命令字符串,make操作一般会就此终止,但如果make后带有"-i"参数,则make将忽略此类出错信号。   Make命本身可带有四种参数:标志、宏定义、描述文件名和目标文件名。其标准形式为:   Make [flags] [macro definitions] [targets]   Unix系统下标志位flags选项及其含义为:   -f file  指定file文件为描述文件,如果file参数为"-"符,那么描述文件指向标准输入。如果没有"-f"参数,则系统将默认当前目录下名为makefile或者名为Makefile的文件为描述文件。在linux中, GNU make 工具在当前工作目录中按照GNUmakefilemakefileMakefile的顺序搜索 makefile文件。   -i   忽略命令执行返回的出错信息。   -s   沉默模式,在执行之前不输出相应的命令行信息。   -r   禁止使用build-in规则。   -n   非执行模式,输出所有执行命令,但并不执行。   -t   更新目标文件。   -q   make操作将根据目标文件是否已经更新返回"0"或非"0"的状态信息。   -p   输出所有宏定义和目标文件描述。   -d   Debug模式,输出有关文件和检测时间的详细信息。   linux下make标志位的常用选项与Unix系统中稍有不同,下面我们只列出了不同部分:   -c dir   在读取 makefile 之前改变到指定的目录dir。   -I dir   当包含其他 makefile文件时,利用该选项指定搜索目录。   -h   help文挡,显示所有的make选项。   -w   在处理 makefile 之前和之后,都显示工作目录。   通过命令行参数中的target ,可指定make要编译的目标,并且允许同时定义编译多个目标,操作时按照从左向右的顺序依次编译target选项中指定的目标文件。如果命令行中没有指定目标,则系统默认target指向描述文件中第一个目标文件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值