makefile 学习笔记 五:编写 makefile

详细内容将 《GNU make》 3 Writing Makefiles 章节。

核心:告诉 make 如何重新编译系统的信息来自于读取名为 makefile 的数据库

一、makefile包含什么

Makefile 包含五种内容显式规则隐式规则变量定义指令注释

1、一个明确的规则说明何时以及如何重新制作一个或多个文件,称为规则的目标。它列出了目标所依赖的其他文件,称为目标的先决条件,并且还可以提供用于创建更新目标的方法。

2、规则说明何时以及如何根据名称重新制作一类文件。它描述了目标如何依赖于名称与目标相似的文件,并提供了创建更新此类目标的方法。

3、变量定义为变量指定文本字符串值的行,该值可以在以后替换到文本中。

4、指令是使 make 在读取 makefile 时执行某些特殊操作的指令。这些包括:

  • 读取另一个 makefile。

  • 决定(基于变量的值)是使用还是忽略 makefile 的一部分。

  • 从包含多行的逐字字符串定义变量

5、’#’ 在 makefile 的一行中开始注释它和该行的其余部分将被忽略。

  • 不能变量引用函数调用使用注释:在变量引用或函数调用中,# 的任何实例都将按字面意思处理(而不是作为注释的开始)。

  • 方法中的注释被传递到 shell,就像任何其他配方文本一样。 shell 决定如何解释它:这是否是一个注释取决于 shell

  • 在定义指令中,在变量定义期间不会忽略注释,而是在变量值中保持完整。当变量被扩展时,它们将被视为注释或配方文本,具体取决于评估变量的上下文。

1、拆分行

Makefile 使用“基于行”的语法,其中换行符特殊并标记语句结束。 GNU make 对语句行的长度没有限制,最多不超过您计算机中的内存量。

但是,如果不换行或滚动就很难阅读太长而无法显示的行。因此,您可以通过在语句中间添加换行符来格式化您的 makefile 以提高可读性:您可以通过使用反斜杠 ( \ ) 字符转义内部换行符来实现这一点。在需要区分的地方,我们将“物理行”称为以换行符结尾的单行(无论是否转义),“逻辑行”是一个完整的语句,包括所有转义的换行符,直到第一个非转义换行符。

特别说明:

1、反斜杠/换行符被转换单个空格字符。

2、反斜杠/换行符周围的所有空格都会压缩一个空格。

2、拆分行技巧

如果您需要拆分一行不想添加任何空格,您可以使用一个巧妙的技巧:用三个字符美元符号/反斜杠/换行符 替换您的反斜杠/换行符对:

 var := one$\
        word

在 make 删除反斜杠/换行符并将以下行压缩为一个空格后,这等效于:

 var := one$ word

然后 make 将执行变量扩展。变量引用**‘$’引用了一个不存在的单字符名称“ ”(空格)的变量**,因此扩展为空字符串,给出一个最终赋值,相当于:

var := oneword

3、测试

makefile 文件内容:

OBJECTS = main.o test.o

var := one$\
        word

main: $(OBJECTS)
	gcc main.o test.o -o main

main.o: main.c test.h
test.o: test.c

.PHONY: clean test
clean:	
	rm main $(OBJECTS)

test:
	echo $(var)

测试:

onlylove@ubuntu:~/My/makefile/01$ ls
main.c  makefile  test.c  test.h
onlylove@ubuntu:~/My/makefile/01$ make test
echo oneword
oneword
onlylove@ubuntu:~/My/makefile/01$

二、makefile 命名

默认情况下,当 make 查找 makefile 时,它会按顺序尝试以下名称:GNUmakefile、makefile 和 Makefile。

通常,您应该将您的 makefile 命名为 makefile 或 Makefile。 (我们推荐 Makefile,因为它出现在目录列表开头的显眼位置,紧邻其他重要文件,例如 README。)大多数 makefile 不推荐使用第一个名称 GNUmakefile。如果您有一个特定于 GNU make 的 makefile,并且不会被其他版本的 make 理解,那么您应该使用这个名称。其他 make 程序查找 makefile 和 Makefile,但不查找 GNUmakefile。

如果 make 没有找到这些名称,它就不会使用任何 makefile。然后你必须用命令参数指定一个目标,make 将尝试找出如何仅使用其内置隐式规则重新制作它

如果要为 makefile 使用非标准名称,可以使用“-f”或“–file”选项指定 makefile 名称。参数‘-f name’或‘–file=name’告诉 make 读取文件名作为makefile。如果您使用多个‘-f’或‘–file’选项,您可以指定多个 makefile。所有的 makefile 都按照指定的顺序有效地连接起来。如果指定“-f”或“–file”,则不会自动检查默认的 makefile 名称 GNUmakefile、makefile 和 Makefile。

三、include 其他 makefile

include 指令告诉 make 在继续之前暂停读取当前的 makefile 并读取一个或多个其他 makefile。该指令是 makefile 中的一行,如下所示:

include filenames…

文件名可以包含 shell 文件名模式。如果 filenames 为空,则不包含任何内容且不打印错误。

行的开头允许并忽略额外的空格,但第一个字符不能是制表符(或 .RECIPEPREFIX 的值)——如果该行以制表符开头,它将被视为方法行包含文件名之间以及文件名之间需要空格多余的空格在那里和指令末尾被忽略允许行尾添加以“#”开头的注释。如果文件名包含任何变量函数引用,它们将被扩展。

例如,如果您有三个 .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”选项指定的任何目录。然后按以下顺序搜索以下目录(如果存在):prefix/include(通常为 /usr/local/include)/usr/gnu/include、/usr/local/include、/usr/include。

如果在这些目录中的任何一个目录中都找不到包含的 makefile,则会生成警告消息,但这不是立即致命的错误;包含的 makefile 的处理继续进行。一旦它读完 makefile,make 将尝试重新制作任何过时或不存在的文件。只有在它尝试找到重新制作 makefile 的方法并失败后,才会将丢失的 makefile 诊断为致命错误。

如果您希望 make 简单忽略不存在或无法重新制作的 makefile,并且没有错误消息,请使用 -include 指令而不是 include,如下所示:

-include filenames…

这在任何方面都类似于 include ,只是如果任何文件名(或任何文件名的任何先决条件)不存在无法重新制作,则不会出现错误(甚至没有警告)。

为了与其他一些 make 实现兼容,sinclude 是 -include 的另一个名称。

四、MAKEFILES 环境变量

如果定义了环境变量 MAKEFILES,则 make 会将其值视为一个列表名字(用空格分割)在其他makefile文件之前读取。这与 include 指令非常相似:在各个目录中搜索这些文件。此外,默认目标不会从这些makefiles中的一个(或其中包含的任何makefiles)中获取,如果没有找到makefiles中列出的文件,则不会出现错误。

MAKEFILES的主要用途是进行递归调用之间的通信通常不希望在顶级调用 make 之前设置环境变量,因为通常最好不要从外部弄乱 makefile。但是,如果运行的是没有特定 makefile 的 makefile,则 MAKEFILES 中的 makefile 可以执行一些有用的操作来帮助内置的隐式规则更好地工作,例如定义搜索路径。

有些用户倾向于在登录时在环境中自动设置MAKEFILES,并编写MAKEFILES程序来期望这样做。这是一个非常糟糕的想法,因为这样的makefile将无法工作,如果由其他人运行。最好在makefile中写入显式的include指令。

五、如何重新生成makefile

有时可以从其他文件(例如 RCS 或 SCCS 文件)重新制作 makefile。如果可以从其他文件重新制作 makefile,您可能希望 make 获取最新版本的 makefile 并进行读取。

为此,在读入所有 makefile 后,make 会将每个文件视为目标尝试更新它。如果一个 makefile 有一个说明如何更新它的规则(在那个 makefile 或另一个 makefile 中找到),或者如果一个隐式规则适用于它,它会在必要时更新。在检查完所有的 makefile 之后,如果确实有任何更改,make 会从头开始并重新读取所有的 makefile每次重新启动都将导致特殊变量MAKE_RESTARTS更新

如果您知道您的一个或多个 makefile 无法重新制作,并且您希望 make 不对它们执行隐式规则搜索,也许是出于效率原因,您可以使用任何防止隐式规则查找的常规方法来执行此操作。例如,您可以使用 makefile 作为目标和空配方编写显式规则。

如果makefiles指定了一个双冒号规则来重制一个带有命令没有先决条件的文件,那么该文件将始终被重制。对于 makefile,每次运行 makefile 时,都会重新生成具有配方的双冒号规则但没有先决条件的 makefile,然后在 make 重新开始并再次读取生成文件后再次生成。这将导致无限循环:make会不断重制makefile,并且永远不会做任何其他事情。因此,为了避免这种情况,make 不会尝试重新制作被指定为具有配方但没有先决条件的双冒号规则目标的 makefile。

如果未指定要使用"-f"或"–file"选项读取的任何 makefile 文件,则 make 将尝试默认的 makefile 文件名;与使用"-f"或"–file"选项显式请求的 makefile 文件不同,make 不确定这些 makefile 文件是否存在。但是,如果默认 makefile 文件不存在,但可以通过运行 make 规则创建,则您可能希望运行这些规则,以便可以使用 makefile 文件。

因此,如果不存在任何默认的 makefile 文件,make 将尝试以搜索它们的相同顺序创建每个 makefile 文件,直到成功创建一个,或者用完了要尝试的名称。请注意,如果 make 找不到或创建任何 makefile 文件,则不是错误;makefile 文件并不总是必需的。

当您使用"-t"或"–touch"选项时,您不希望使用过时的 makefile 来决定相关目标。因此,"-t"选项对更新生成文件没有影响;即使指定了"-t",它们也确实更新了。同样,"-q"和"-n"不会阻止生成文件的更新,因为过期的生成文件会导致其他目标的错误输出。因此,‘make -f mfile -n foo’ 将更新 mfile,将其读入,然后打印命令以更新 foo 及其先决条件,而无需运行它。为 foo 打印的命令将是 mfile 更新内容中指定的命令。

然而,有时您可能确实希望阻止makefile的更新。您可以通过在命令行中将makefile指定为目标,或者将它们指定为makefile来实现这一点。当将makefile名称明确指定为目标时,选项-t等确实会应用于它们。

因此,‘make -f mfile -n mfile foo’ 将读取 makefile mfile,打印更新它所需的命令,而无需实际运行它,然后打印更新 foo 所需的命令而不运行它。foo 的命令将是 mfile 现有内容指定的命令。

六、重写另一个Makefile的部分

有时,拥有一个与另一个 makefile 几乎一样的 makefile 很有用。您通常可以使用 ‘include’ 指令将一个包含在另一个中,并添加更多目标或变量定义。但是,两个 makefile 为同一目标提供不同的配方是无效的。但还有另一种方式。

在包含 makefile 中,您可以使用匹配任何模式规则来表示,要重新生成无法从包含 makefile 中的信息创建的任何目标,make 应该在另一个生成文件中查找。

例如,如果您有一个名为 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没有说明如何制作的任何其他目标也是如此。

其工作方式是模式规则的模式仅为"%",因此它与任何目标匹配。该规则指定了一个先决条件,以确保即使目标文件已经存在,命令也会运行。我们为强制目标提供一个空配方,以防止 make 搜索隐式规则来构建它,否则它将应用相同的匹配规则来强制自身并创建先决条件循环!

七、make 如何读取 makefile

GNU make 的工作分为两个不同的阶段。在第一阶段,它读取所有 makefile 文件包含的 makefile 文件等,并将所有变量及其值以及隐式显式规则内化,并构建所有目标及其先决条件的依赖关系图。在第二阶段,make 使用此内部化数据确定需要更新哪些目标运行更新它们所需的配方

理解这种两阶段方法很重要,因为它直接影响变量和函数扩展的发生方式;在编写 makefile 时,这通常是一些混乱的根源。下面是可以在makefile中找到的不同构造的摘要,以及构造的每个部分展开的阶段。

我们说,如果扩展发生在第一阶段,则扩展是立即的:make将在解析 makefile 时扩展构造的该部分。如果扩展不是立即的,我们说扩展****被推迟。延迟构造部件的扩展将延迟到使用扩展之前:无论是在直接上下文中引用扩展时,还是在第二阶段需要扩展时。

1、变量赋值

1、Makefile 定义变量基本语法

变量的名称=值列表
  • 变量的名称可以由大小写字母、阿拉伯数字和下划线构成。

  • 等号左右的空白符没有明确的要求,因为在执行 make 的时候多余的空白符会被自动的删除。

  • 至于值列表,既可以是零项,又可以是一项或者是多项。

  • 变量使用可以用 $( ) 或 ${ }

2、Makefile 变量的四种基本赋值方式

  • 简单赋值 ( := ) 编程语言中常规理解的赋值方式,只对当前语句的变量有效。

  • 递归赋值 ( = ) 赋值语句可能影响多个变量,所有目标变量相关的其他变量都受影响。

  • 条件赋值 ( ?= ) 如果变量未定义,则使用符号中的值定义变量。如果该变量已经赋值,则该赋值语句无效。

  • 追加赋值 ( += ) 原变量用空格隔开的方式追加一个新值。

特别说明:

  • =、?= 为延时变量(延时变量:使用该变量的时候,才展开该变量,并确定该变量的值)
  • :=、+=为立即变量(立即变量:定义的时候就已经确定了该变量的值)

3、总结

符号含义变量类型
=递归赋值,当变量展开时,优先从后面展开延时变量
:=该变量立即赋值(会覆盖前面的值),当变量展开时,优先从前面展开立即变量
?=若前面没有定义该变量,则此处赋值,如果前面已经定义了,则此处不再赋值延时变量
+=追加赋值立即变量

4、测试

makefile 文件内容:

x = before
y = $(x)_AAAA		# 立即展开,当展开$(x)时,x值为before
x = later			# x 值会被覆盖

xx := before
yy := $(xx)_AAAA	# 延时展开,当展开$(xx)时,xx值为later
xx := later			# xx 值会被覆盖

xxx ?= before
xxx ?= later		# 条件不满足,未执行

xxxx += before
xxxx += later		# 追加

.PHONY:all
all:
	@echo "\$$(x)  = $(x)"
	@echo "\$$(xx) = $(xx)"
	@echo "y  = $(y)"
	@echo "yy = $(yy)"
	@echo "xxx = ${xxx}"
	@echo "xxxx = ${xxxx}"

测试结果:

$(x)  = later
$(xx) = later
y  = later_AAAA
yy = before_AAAA
xxx = before
xxxx = before later

2、条件指令

条件指令被立即解析。这意味着,例如,不能在条件指令中使用自动变量,因为在调用该规则的配方之前不会设置自动变量。如果您需要在条件指令中使用自动变量,您必须将条件移动到配方中并改用 shell 条件语法。

3、规则定义

无论采用何种形式,规则始终以相同的方式展开:

immediate : immediate ; deferred
        deferred

也就是说,目标先决条件部分会立即展开,而用于构建目标的配方始终会延迟。这适用于显式规则模式规则后缀规则静态模式规则简单的先决条件定义

八、如何解析makefile

GNU make 逐行解析 makefile。使用以下步骤进行解析:

1、读取完整的逻辑行,包括反斜杠转义的行。

2、删除注释

3、如果该行以配方前缀字符开头,并且我们处于规则上下文中,请将该行添加到当前配方并阅读下一行。

4、展开出现在直接展开上下文中的行元素。

5、扫描行中的分隔符(如":"“或”="),以确定该行是宏赋值还是规则。

6、内部化生成的操作并读取下一行。

这样做的一个重要后果是,如果宏是一行长,则可以扩展到整个规则。这将起作用:

myrule = target : ; echo built

$(myrule)

但是,这将不起作用,因为 make 在展开行后不会重新拆分行:

define myrule
target:
        echo built
endef

$(myrule)

上述makefile的结果是定义一个具有echo和built先决条件的目标目标,就像makefile包含了target: echo built一样,而不是一个带有配方的规则。在展开完成后,行中仍然存在的换行符将作为普通的空白符被忽略。

为了正确展开多行宏,必须使用 eval 函数:这会导致 make 解析器在展开的宏的结果上运行。

九、二次扩展

之前我们了解到,GNU 使作品分为两个不同的阶段:读入阶段和目标更新阶段。GNU make 还能够(仅)为 makefile 中定义的部分或全部目标启用先决条件的第二次扩展。为了进行第二次扩展,必须在使用此功能的第一个先决条件列表之前定义特殊目标 .SECONDEXPANSION。

如果定义了这个特殊目标,那么在上面提到的两个阶段之间,即读入阶段的末尾,在这个特殊目标之后定义的所有目标的先决条件将被第二次扩展。在大多数情况下,这种二次展开不会有任何效果,因为所有变量和函数引用都会在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。

这有点令人兴奋,但是只有当您发现第二次扩展总是发生在该目标的自动变量范围内时,该特性的真正威力才会显现出来。这意味着您可以使用诸如 @ 、 @、 @等变量。在第二次扩展中,它们将有它们的期望值,就像配方中一样。您所要做的就是通过转义$来推迟扩展。此外,显式和隐式(模式)规则都会发生二次扩展。了解了这一点后,此特性的可能用途将显著增加。例如:

.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))

这个版本允许用户指定源文件而不是目标文件,但给出了与前面示例相同的先决条件列表。

在第二个展开阶段对自动变量的求值,特别是对目标名称变量$$@的求值,其行为与在配方中求值类似。然而,对于不同类型的规则定义,存在一些细微的差异和特殊情况。下面描述了使用不同自动变量的微妙之处。

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, foo.1 bar.1 foo.2 bar.2。

规则按照makefile顺序进行二次展开,除了带有配方的规则总是最后计算。

变量 $$?$$* 不可用,并展开为空字符串。

2、静态模式规则的二次扩展

静态模式规则的辅助扩展规则与上面的显式规则的规则相同,但有一个例外:对于静态模式规则,$$* 变量设置为模式stem。与显式规则一样,$$?不可用,并展开为空字符串。

3、隐式规则的二次扩展

当 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。如果您对这种重构不感兴趣,可以在先决条件列表中使用 $$* 代替 %。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值