写在前面
此系列是本人一个字一个字码出来的,包括示例和实验截图。该文章根据 GNU Make Manual 进行汉化处理并作出自己的整理,一是我对 Make 的学习记录,二是对大家学习 MakeFile 有更好的帮助。如对该博文有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。本篇博文可能十分冗长,请耐心阅读和学习。
make 概述
make
实用程序自动确认需要重新编译大型程序的哪些部分,并执行哪些命令来重新编译。本篇博文使用的示例是C
程序,但你可以将make
与任何编程语言结合使用,这些语言的编译器可以通过shell
命令运行。事实上,make
并不局限于程序。你可以用它来描述任何一项任务,当其他文件发生变化时,相关文件必须自动从其他文件中来进行更新。
准备和执行 make
要准备使用make
,必须编写一个名为makefile
的文件,该文件描述程序中文件之间的关系,并提供更新每个文件的命令。在程序中,可执行文件通常是从目标文件更新而来的,而目标文件又是通过编译源文件来实现的。
一旦存在合适的makefile
,每次更改一些源文件时,下面这个简单的shell
命令:
make
这足以执行所有必要的重新编译。make
程序使用makefile
数据库和文件的最后修改时间来决定哪些文件需要更新。对于这些当中的每一个文件,都会被记录在数据库中。
Makefiles 介绍
您需要一个名为makefile
的文件来告诉make
要做什么。大多数情况下,makefile
告诉make
如何编译和链接程序。但是功能不仅仅局限于此,它还可以告诉make
如何遇到让它执行某个操作的时候如何去做,比如删除某些文件作为清理操作。
在本篇博文,我们将写一个makefile
,来编译和链接一个简单的由C
编写的文本编辑器。如果你有能力访问GitHub
,你可以去 mazarf/editor 去下载克隆。当然作者已经把makefile
写好了,我建议你在学习的时候删掉它,去独立写一个,这对于你学习本篇博文有很大的帮助。
如果你访问有困难,我提供了一个没有makefile
版本的 源代码 。这是一个蓝奏云网盘分享,如果你要获取该文件,需要密码:haoj
。
当make
重新编译我们所谓上述的编辑器时,每个更改的C
源文件都必须重新编译。如果头文件已更改,则必须重新编译包含该头文件的每个C
源文件以确保安全。每次编译都会生成一个与源文件对应的目标文件。最后,如果任何源文件已被重新编译,则所有目标文件,无论是新创建的还是从以前的编译中保存的,都必须链接在一起以生成新的可执行的文本编辑器。
下面我们来开始学习makefile
的编写:
编写入门篇
概述
一个简单的makefile
包含着一系列的规则,它的大体模样如下:
[目标 (target)]:[条件 (prerequisites)]
[配置 (recipe)]
目标
通常是程序生成的文件的名称,例如可执行文件或对象 (object)文件。 目标也可以是要执行的操作的名称,例如clean
。
条件
是用作创建目标的输入的文件。一个目标
通常需要几个文件来制作。
配置
是执行的动作。 一个配置
可能有多个命令,或者在同一行上,或者每个在自己的行上,一定要注意的是你需要在每条配置
行的开头放置一个制表符,也就是你在键盘上按下一个Tab
。但如果您喜欢使用制表符以外的字符作为配置
的前缀(也就是除制表符以外的字符),则可以修改.RECIPEPREFIX
变量来设置成其他字符。
通常,配置
位于含有各种条件
的规则中,用于在任何条件
发生变化时创建目标文件。 但是,为目标指定配置
的规则不需要条件
。 例如,包含与目标clean
关联的删除命令的规则没有条件
。
一条规则解释了如何以及何时重新制作作为特定规则目标的某些文件。make
根据条件
执行配置
以创建或更新目标。规则还可以解释如何以及何时执行操作,这个东西之后再说。
makefile
可以包含除规则之外的其他文本,但简单的makefile
只需要包含规则。规则可能看起来比此示例看起来要复杂一些,但都或多或少都会有相似之处。
下面我们来写一个简单的makefile
,它描述了名为 text
的可执行文件依赖于八个目标文件的方式,而这些目标文件又依赖于对应的C源代码文件和头文件,如下所示:
text:line.o page.o prompt.o text.o
gcc -o text line.o page.o \
prompt.o text.o -lncurses
line.o: line.c line.h
gcc -c line.c
page.o: page.c page.h line.h
gcc -c page.c
prompt.o:prompt.c prompt.h
gcc -c prompt.c
text.o: text.c text.h prompt.h page.h line.h
gcc -c text.c
clean:
rm text line.o page.o prompt.o text.o
是不是看不太明白,我们来画一个示意图:
如上展示的就是所谓的依赖关系,如果有关编译器命令不会的话,建议自己查询。在一条生成语句中,我们使用反斜杠加换行符将一行分成两行,作用和一行是一样的,但增加的可读性。要使用此 makefile
创建名为text
的可执行文件,请转到该文件的当前目录下,输入:
make
效果如下:
wingsummer@wingsummer-PC editor → make
gcc -c line.c
gcc -c page.c
gcc -c prompt.c
gcc -c text.c
gcc -o text line.o page.o \
prompt.o text.o -lncurses
在你的当前文件夹中会有这些东西,如下图所示:
我们这个程序就可以拿到控制台运行了,是一个控制台的文本编辑器。如果要使用这个makefile
从目录中删除可执行文件和所有目标文件,输入:
make clean
所有的文件将会恢复到初始状态。
当目标
是一个文件时,如果它的条件
发生变化,则需要重新编译或重新链接。此外,应首先更新本身自动生成的任何条件
。配置
可以遵循包含目标和条件
的每一行。这些配置
说明了如何更新目标文件。制表符或.RECIPEPREFIX
变量指定的任何字符必须出现在配置
中每一行的开头,以将配置
与makefile
中的其他行区分开来。请记住,make
对配置
的工作原理一无所知,由你提供将正确更新目标文件的配置
。所有make
所做的只是在需要更新目标文件时执行您指定的配置
。
上面有一个特例,目标clean
不是一个文件,而仅仅是一个动作的名称。由于您通常不想执行此规则中的操作,因此clean
不是任何其他规则的条件
。因此,除非你明确告诉它,否则make
永远不会对它做任何事情。请注意,此规则不仅不是条件
,它也没有任何条件
,因此该规则的唯一目的是运行指定的配置
。这样不引用文件而只是动作的目标被称为假目标。
那么make
是如何处理makefile
的呢?
默认情况下,make
从第一个目标开始(不是名称以.
头的目标),这称为默认目标,但你可以使用.DEFAULT_GOAL
特殊变量修改。
在我们前面的简单的例子当中,我们的目标是重新编译一个text
可执行程序,因此我们首先得创建几个规则,然后在命令行调用make
。当你输入这条指令的时候,make
读取当前目录中的makefile
并从处理第一条规则开始。在示例中,此规则用于重新链接text
程序。但是在make
可以完全处理这个规则之前,它必须处理编辑所依赖的文件的规则,也就是所谓的目标
文件。这些文件中的每一个都根据自己的规则进行处理,这些规则通过编译其源文件来更新每个.o
文件。如果源文件或任何条件
的头文件比目标
文件更新,或者目标
文件不存在,则必须进行重新编译。
处理其他规则是因为它们的目标
需要目标的条件
。如果目标
不依赖其他规则或者依赖项,则不会处理该规则,除非告诉make
这样做,比如make clean
之类的命令。
在重新编译目标文件之前,make
会考虑更新其条件
、源文件和头文件。这个makefile
没有指定要为它们做的任何事情,.c
和.h
文件不是任何规则的目标,所以make
对这些文件什么都不做。但是此时make
会按照自己的规则更新自动生成的C
程序,例如Bison
或Yacc
制作的C
程序。
在重新编译任何需要的目标文件后,make
决定是否重新链接我们上面的编辑器text
。如果text
不存在,或者任何目标文件比它新,则必须这样做。如果一个目标文件刚刚被重新编译,它现在比text
新,所以text
被重新链接。因此,如果我们更改文件line.c
并运行make
,make
将编译该文件以更新line.o
,然后进行链接程序text
。
变量简化
在我们的示例中,我们必须在规则中列出所有目标文件两次以编译text
:
text:line.o page.o prompt.o text.o
gcc -o text line.o page.o \
prompt.o text.o -lncurses
这种重复很容易出错。如果一个新的目标文件被添加到我们的编译系统中,我们很可能会丢三落四导致错误。此时,我们可以通过使用变量来消除风险并简化生成文件。只需要定义一次,我们在之后就可以随意使用。
每个makefile
都有一个名为objects
、OBJECTS
、objs
、OBJS
、obj
或OBJ
的变量,这是所有对象文件名的列表,这是标准的做法。我们将在makefile
中用这样的一行定义这样的变量对象:
OBJS=text.o page.o line.o prompt.o
然后,每个我们想要放置目标文件名列表的地方,我们可以通过使用变量来替换变量的值,如下所示:
OBJS=text.o page.o line.o prompt.o
text:$(OBJS)
gcc -o text $(OBJS) -lncurses
line.o: line.c line.h
gcc -c line.c
page.o: page.c page.h line.h
gcc -c page.c
prompt.o:prompt.c prompt.h
gcc -c prompt.c
text.o: text.c text.h prompt.h page.h line.h
gcc -c text.c
clean:
rm text $(OBJS)
我们同样继续调用make
,发现效果是一样的。
让 make 简化 配置
没有必要详细说明编译单个C
源文件的方法,因为make
可以弄清楚它们。它有一个隐含的规则,用于使用cc
从相应命名的.c
文件更新.o
文件-c
命令。因此,我们可以从目标文件的规则中简化配置
。
当以这种方式自动使用.c
文件时,它也会自动添加到条件
列表中。因此,只要我们省略了配置
,我们就可以从条件
中省略.c
文件。
到目前的更改如下所示:
OBJS=text.o page.o line.o prompt.o
text:$(OBJS)
gcc -o text $(OBJS) -lncurses
line.o: line.c line.h
page.o: page.c page.h line.h
prompt.o:prompt.c prompt.h
text.o: text.c text.h prompt.h page.h line.h
.PHONY: clean
clean:
rm text $(OBJS)
效果如下:
wingsummer@wingsummer-PC editor → make
cc -c -o text.o text.c
cc -c -o page.o page.c
cc -c -o line.o line.c
cc -c -o prompt.o prompt.c
gcc -o text text.o page.o line.o prompt.o -lncurses
因为隐式规则非常方便,所以它们很重要。 你会看到它们经常被使用。
clean
编译程序并不是您可能想要为其编写规则的唯一事情。Makefiles
通常告诉除了编译程序之外如何做一些其他事情。例如,如何删除所有目标文件和可执行文件以使目录恢复到干净的状态。
以下是我们如何编写清理示例的make
规则:
clean:
rm text $(OBJS)
在实践中,我们可能希望以更复杂的方式编写规则来处理意料之外的情况。我们会这样做:
.PHONY: clean
clean:
-rm text $(OBJS)
这可以防止make
被一个名为clean
的实际文件混淆,并导致它继续运行。我们使用它的时候不应该将这样的规则放在makefile
的开头,因为我们不希望它默认运行。 因此,在示例makefile
中,我们希望重新编译text
的编辑规则保持默认目标。因为clean
不是text
的条件
,所以如果我们给出不带参数的命令make
,这条规则根本不会运行。为了使规则运行,我们必须输入make clean
。
编写高级篇
当你学会前面的入门的时候,想要看懂真正的makefile
还是有一定的差距,如下是我们示例自带的内容:
# makefile for text.c
CC=gcc
CFLAGS=-Wall -g
OBJS=text.o page.o line.o prompt.o
HEADERS=$(subst .o,.h,$(OBJS)) # text.h page.h ...
LIBS=-lncurses
text: $(OBJS)
$(CC) $(CFLAGS) -o text $(OBJS) $(LIBS)
text.o: text.c $(HEADERS)
$(CC) $(CFLAGS) -c text.c
page.o: page.c page.h line.h
$(CC) $(CFLAGS) -c page.c
# '$<' expands to first prerequisite file
# NOTE: this rule is already implicit
%.o: %.c %.h
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: cleanall clean cleantxt
cleanall: clean cleantxt
clean:
rm -f $(OBJS) text
cleantxt:
rm -f *.txt
虽然有一些部分我们已经能看懂了,但还有我们看不懂的地方,下面我们来开始介绍详细部分。
包含内容有什么
Makefile
包含五种内容:显式规则、隐式规则、变量定义、指令和注释。规则、变量和指令将在后面的章节中详细描述。
显式规则说明何时以及如何重新制作一个或多个文件,称为规则的目标。它列出了目标所依赖的其他文件,称为目标的条件
,还可能提供用于创建或更新目标的配置
。
隐式规则说明了何时以及如何根据文件名重新制作一类文件。它描述了目标如何依赖于名称与目标相似的文件,并提供了创建或更新此类目标的方法。
变量定义是为变量指定文本字符串值的行,该变量可以稍后替换到文本中。在我们前面的入门篇示例用到过,这里就不赘述了。
指令是make
在读取makefile
时执行某些特殊操作的指令。这些包括:读取另一个makefile
、决定(基于变量的值)是使用还是忽略makefile
的一部分、从包含多行的逐字字符串定义变量。
#
的作用是makefile
单行注释标志,作用和c
的//
作用是一样的。如果您想要#
作为文字,请使用反斜杠对其进行转义。注释可能会出现在makefile
的任何行上,尽管在某些情况下会特别处理它们。你不能在变量引用或函数调用中使用注释,任何#
实例都将在变量引用或函数调用中按字面意思(而不是作为注释的开头)处理。
配置
中的注释被传递到shell
,就像任何其他配置
文本一样,是由shell
决定如何解释它。
在定义指令中,在变量定义期间不会忽略注释,而是在变量值中保持原样。扩展变量时,它们将被视为注释或配置
文本,具体取决于评估变量的上下文。
多行分割
Makefile
使用基于行的语法,其中换行符是特殊的并标记语句的结尾。GNU make
对语句行的长度没有限制,最多不超过你计算机中的内存量。
但是,如果不换行,则很难阅读太长而无法显示的行。因此,可以通过在语句中间添加换行符来格式化makefile
以提高可读性。反斜杠\
字符转义内部换行符可以实现此功能。在需要区分的地方,我们将物理行
称为以换行符结尾的单行(不管它是否被转义),而逻辑行
是一个完整的语句,包括所有转义的换行符,直到第一个非转义换行符。
处理反斜杠/换行符
组合的方式取决于语句是配置
行还是非配置
行。在配置
行之外,反斜杠/换行符
被转换为单个空格字符。完成后,反斜杠/换行符
周围的所有空格都将压缩为一个空格,这包括反斜杠之前的所有空格、反斜杠/换行符
之后行首的所有空格以及任何连续的反斜杠/换行符
组合。如果定义了.POSIX
特殊目标,则反斜杠/换行符
处理会稍作修改以符合POSIX.2
:首先,不删除反斜杠之前的空格,其次,不压缩连续的反斜杠/换行符
。
如果您需要拆分一行但不希望添加任何空格,您可以利用一个巧妙的技巧:将反斜杠/换行符替换为三个字符美元符号/反斜杠/换行符:
var := one$\
word
在make
删除反斜杠/换行符
并将以下行压缩为一个空格之后,这相当于:
var := one$ word
然后make
会进行变量扩展。变量引用$
指的是一个不存在的具有单字符名称的变量,因此扩展为空字符串,给出最终赋值,相当于:
var := oneword
Makefile 命名
默认情况下,当make
查找makefile
时,它会依次尝试以下名称:GNUmakefile
、makefile
和Makefile
。如果您有一个特定于GNU make
的makefile
,并且不会被其他版本的make
理解,您应该使用这个名称。其他make
程序寻找makefile
和Makefile
,但不是GNUmakefile
。
如果make
没有找到这些名称,它就不会使用任何makefile
。然后您必须使用命令参数指定目标,make
将尝试找出如何仅使用其内置的隐式规则来重新制作它。如果你想为你的makefile
使用一个非标准的名字,你可以使用-f
或--file
选项来指定makefile
的名字。如果指定上面的参数,则不会自动检查默认的makefile
名称。
包含其他 Makefiles
include
指令告诉make
暂停读取当前的makefile
并在继续之前读取一个或多个其他makefile
。该指令是makefile
中的一行,如下所示
include 文件名
文件名
可以包含shell
模式文件名。如果为空,则不包含任何内容并且不打印错误。
行首允许并忽略多余的空格,但第一个字符不能是制表符或.RECIPEPREFIX
的值。如果该行以制表符开头,它将被视为配置
行。include
和文件名之间以及文件名之间需要空格,额外的空格会被忽略。允许在行尾添加以“#”开头的注释,如果文件名包含任何变量或函数引用,它们将被扩展。
例如,如果您有三个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
处理一个include
指令时,它会暂停读取包含的makefile
并依次从每个列出的文件中读取。完成后,make
继续读取指令出现的makefile
。
使用include
指令的一个场合是,由不同目录中的单个makefile
处理的多个程序需要使用一组通用的变量定义或模式规则。
另一个这样的场合是当您想从源文件自动生成条件
时,条件
可以放在主makefile
包含的文件中。这种做法通常比以某种方式将条件
附加到主makefile
末尾的做法更干净,就像传统上使用其他版本的make
所做的那样。
如果指定的名称不以斜杠开头,并且在当前目录中没有找到该文件,则会搜索其他几个目录。首先,搜索您使用-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 文件名
除了任何文件名
或任何文件名
的任何条件
不存在或无法重新制作时,这就像在所有方面都包含在内,但没有错误(甚至没有警告)。
为了与其他一些make
实现兼容,sinclude
是 -include
的另一个名称。
MAKEFILES
如果定义了环境变量MAKEFILES
,则make
将其值视为要在其他文件之前读取的其他makefile
的名称列表(由空格分隔)。这很像include
指令。此外,默认目标永远不会从这些makefile
中获取,如果没找到MAKEFILES
中列出的文件,这不是错误。
MAKEFILES
的主要用途是在make
的递归调用之间进行通信。通常不希望在顶级调用make
之前设置环境变量,因为通常最好不要弄乱外部的makefile
。但是,如果您在没有特定makefile
的情况下运行make
,则MAKEFILES
中的makefile
可以做一些有用的事情来帮助内置的隐式规则更好地工作,例如定义搜索路径。
一些用户很想在登录时自动在环境中设置MAKEFILES
,并且程序makefile
期望这样做。这是一个非常糟糕的主意,因为如果由其他人运行,这样的makefile
将无法工作,在makefile
中编写显式包含指令要好得多。
重写另一个 Makefile 的一部分
有时,有一个与另一个makefile
基本相同的makefile
是很有用的。你可以经常使用include
指令将其中一个包含到另一个中,并添加更多的目标或变量定义。但是,两个makefile
为同一个目标提供不同的配置
是无效的,不过还有另一种方法。
在包含的makefile
中(想要包含makefile
的另一者),你可以使用match-anything
模式规则来描述,表示要重新编译无法从包含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
搜索隐式规则来构建它,否则它将应用相同的match-anything
规则来强制自身并创建一个条件
循环。
make 是如何解析 Makefile
GNU make
在两个不同的阶段完成它的工作。在第一阶段,它读取所有makefile
、包含的makefile
等,并内化所有变量及其值以及隐式和显式规则,并构建所有目标及其先决条件的依赖关系图。在第二阶段,make
使用这些内部化数据来确定需要更新哪些目标并运行更新它们所需的配置
。
理解这种两阶段方法很重要,因为它直接影响变量和函数扩展的发生方式。在编写makefile
时,这通常是一些混乱的根源。下面是可以在makefile
中找到的不同构造的摘要,以及构造的每个部分发生扩展的阶段。
如果它发生在第一阶段,我们说扩展是立即的,make
将在解析makefile
时扩展构造的那部分。我们说,如果不是立即扩展,扩展就会被推迟。延迟构造部分的扩展会延迟到使用扩展之前,无论是在直接上下文中引用它时,还是在第二阶段需要它时。
变量赋值
变量定义解析如下:
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
对于附加操作符+=
,如果变量之前被设置为简单变量(:=
或'::=
),则认为右边是即时的,否则是延迟的。
对于shell
的赋值操作符!=
时,将立即计算右边的值并将其传递给shell
。结果存储在左侧命名的变量中,该变量成为一个简单变量(因此将在每次引用时重新计算)。
条件指令
条件指令立即被解析。这意味着,例如,自动变量不能在条件指令中使用,因为自动变量直到该规则的配方被调用时才会被设置。如果你需要在条件指令中使用自动变量,你必须将条件移动到配方中,并使用shell
条件语法。
规则定义
规则总是以同样的方式展开,无论其形式如何:
immediate : immediate ; deferred
deferred
也就是说,目标
和条件
部分将立即展开,而用于构建目标的配置
总是延迟。对于显式规则、模式规则、后缀规则、静态模式规则和简单的条件
定义来说是这样的。
自动变量
假设你正在编写一个模式规则,将一个.c
文件编译成一个.o
文件。那么如何编写cc
命令,以便它在正确的源文件名上操作?您不能在配置
中写入名称,因为每次应用隐式规则时名称都是不同的。
你要做的是使用一个特殊的特性,自动变量。这些变量具有根据规则的目标和先决条件重新计算的每个规则的值。在本例中,您将使用$@
作为对象文件名,使用$<
作为源文件名。
认识到自动变量值的可用范围是有限的,这一点非常重要。它们只在配置
中有值。特别是,你不能在规则的目标列表中使用它们,它们没有值,会扩展为空字符串。而且,不能在规则的条件
列表中直接访问它们。一个常见的错误是试图在先决条件列表中使用$@
,这行不通。然而,GNU make
有一个特殊的特性,即二次扩展,它将允许在先决条件列表中使用自动变量值,这将会在后面进行介绍。
下面是自动变量表:
当然,自动变量不仅仅是上面这些,还有有关D
和F
的变体,比如$(@D)
等,如果有需要请参考原文。
二次展开
前面我们了解到GNU
在两个不同的阶段中制作:读取阶段和目标更新阶段。GNU
也能够为Makefile
中定义的某些或所有目标启用条件
的第二次扩展。为了使第二次扩展发生,必须在使用此功能的第一个条件
列表之前定义特殊的目标。
如果定义了该特殊目标,则在上述两个阶段之间,就在读取阶段的末尾,第二次扩展了特殊目标后定义的目标的所有条件
。在大多数情况下,这种次要扩展将无效,因为在MakeFiles
的初始解析过程中,所有变量和功能参考都将进行扩展。为了利用解析器的二级扩展阶段,有必要逃避makefile
中的变量或函数参考。在这种情况下,第一个扩展仅取消参考,但并未扩展,并且扩展到次级扩展阶段。例如,考虑这个makefile
:
.SECONDEXPANSION:
ONEVAR = onefile
TWOVAR = twofile
myfile: $(ONEVAR) $$(TWOVAR)
在第一个扩展阶段之后,MyFile目标的先决条件列表将为单一和$(TWOVAR)
; 扩展了对ONEVAR
的第一个unescaped
变量引用,而第二个escaped
变量引用简单地保留,而未被识别为变量参考。现在,在次要扩展期间,第一个单词再次扩展,但是由于它不包含变量或函数引用,因此它仍然是一个值的值,而第二个单词现在是对变量TWOVAR
的正常引用,该引用将扩展到twofile
的值。最终的结果是有两个条件
,即onefile
和twofile
。
显然,这不是一个非常有趣的案例,因为仅通过在先决条件列表中显示两个变量,可以更轻松地实现相同的结果。如果变量重置,则会显而易见。考虑此示例:
.SECONDEXPANSION:
AVAR = top
onefile: $(AVAR)
twofile: $$(AVAR)
AVAR = bottom
在这里,onefile
的条件
将立即扩展,并解析到到值top
,而在二次扩展并产生底部值之前,twofile
的先决条件将不完整。
这更加令人兴奋,但是只有当您发现次要扩展始终发生在该目标的自动变量范围内时,此功能的真正力量才变得显而易见。这意味着您可以在第二个扩展过程中使用$@
,$*
等的变量,并且它们将具有预期值,就像在配置
中一样。 您要做的就是通过逃脱$
来推迟扩展。 同样,对于显式和隐式(模式)规则,都会发生次要扩展。 知道这一点,此功能的可能使用急剧增加。 例如:
.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))
有关二次扩展的内容就介绍这么多,由于其比较复杂,也无法在一次说明白,建议阅读官方文档。
假目标 (Phony Targets)
假目标是真正不是文件名的目标。 相反,当您提出明确请求时,它只是要执行的配置
的名称。使用虚假目标有两个原因:避免与同名文件发生冲突,并提高性能。
如果您编写了一条规则,该规则将无法创建目标文件,则每当目标出现进行重新制作时,将执行配置
。这是一个示例:
clean:
rm *.o temp
因为rm
命令没有创建名为clean
的文件,因此可能永远都不存在此类文件。因此,每次执行make clean
时,rm
命令将被执行。
在此示例中,如果在此目录中创建了名为clean
的文件,则将无法正常工作。 由于它没有条件
,因此将始终考虑clean
最新生成状态而不会执行其配置
。为了避免此问题,您可以通过使其成为特殊目标的条件
,来明确声明该目标为假。如下:
.PHONY: clean
clean:
rm *.o temp
完成此操作后,无论是否有名为clean
的文件,clean
就会被正确的执行。
假目标也与make
递归调用相结合。在这种情况下,MakeFile
通常会包含一个变量,该变量列出了要构建的许多子目录。处理此操作的一种简单方法是用循环在子目录上的配置
定义一个规则,例如:
SUBDIRS = foo bar baz
subdirs:
for dir in $(SUBDIRS); do \
$(MAKE) -C $$dir; \
done
但是,这种方法存在问题。 首先,该规则忽略了子make
中检测到的任何错误,因此即使在失败的情况下,它也会继续构建其余目录。可以通过添加shell
命令来注意错误和退出,但是即使使用-k
选项调用,这很不好。其次,也许更重要的是,您无法利用make
可以并行构建目标的能力,因为只有一个规则。
通过将子目录宣布为.PHONY
目标,这必须这样做,因为该子目录显然总是存在,否则不会构建。您可以消除这些问题:
SUBDIRS = foo bar baz
.PHONY: subdirs $(SUBDIRS)
subdirs: $(SUBDIRS)
$(SUBDIRS):
$(MAKE) -C $@
foo: baz
在这里,我们还声明,直到baz
子目录完成后才能构建foo
子目录。尝试并行构建时,这种关系声明尤其重要。隐式规则搜索被跳过。这就是为什么将目标宣布为.PHONY
对性能有益的原因,即使您不担心存在的实际文件。
假目标不应是真实目标文件的条件
。如果是这样,每次更新该文件时都会运行其配置
。只要假目标绝不是真正目标的条件
,只有当假目标是指定目标goal
时,假目标的配置
才会执行。
假目标可以有条件
。当一个目录包含多个程序时,最方便地将所有程序描述为一个makefile ./Makefile
。由于默认情况下的目标将是makefile
中的第一个,因此通常将其作为名为all
的假目标,并作为条件
将其授予所有单独的程序。例如:
all : prog1 prog2 prog3
.PHONY : all
prog1 : prog1.o utils.o
cc -o prog1 prog1.o utils.o
prog2 : prog2.o
cc -o prog2 prog2.o
prog3 : prog3.o sort.o utils.o
cc -o prog3 prog3.o sort.o utils.o
现在,您可以使用make
以重新编译这三个程序,也可以将其指定为重制的参数。假(Phoniness
)不是继承,除非明确宣布是这样的,否则假目标的条件
本身不是假的。
当一个假目标是另一个目标的条件
时,它将用作另一个子例程。例如,这里的meake cleanall
将删除对象文件,差异文件和文件程序:
.PHONY: cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
rm program
cleanobj :
rm *.o
cleandiff :
rm *.diff
小结
学会了上面的内容,我们来回去看自带的 makefile
:
# makefile for text.c
CC=gcc
CFLAGS=-Wall -g
OBJS=text.o page.o line.o prompt.o
HEADERS=$(subst .o,.h,$(OBJS)) # text.h page.h ...
LIBS=-lncurses
text: $(OBJS)
$(CC) $(CFLAGS) -o text $(OBJS) $(LIBS)
text.o: text.c $(HEADERS)
$(CC) $(CFLAGS) -c text.c
page.o: page.c page.h line.h
$(CC) $(CFLAGS) -c page.c
# '$<' expands to first prerequisite file
# NOTE: this rule is already implicit
%.o: %.c %.h
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: cleanall clean cleantxt
cleanall: clean cleantxt
clean:
rm -f $(OBJS) text
cleantxt:
rm -f *.txt
上面比较难理解的就下面的部分:
HEADERS=$(subst .o,.h,$(OBJS))
%.o: %.c %.h
$(CC) $(CFLAGS) -c $< -o $@
subst
是makefile
的里的函数,意为字符替换,但本篇并没有介绍,因为makefile
是在是太复杂了,要想彻底弄懂还是需要大量的时间的,具体查阅源文档的Functions for Transforming Text
部分,本篇仅仅起到抛砖引玉的作用。对于$(subst FROM, TO, TEXT)
,它的意思是将字符串TEXT
中的子串FROM
变为TO
。对于咱的示例就是将$(OBJS)
变量的值中的.o
字串替换为.h
。
%.o: %.c %.h
中%
就是一个匹配符号,使用它可以尝试编译当前文件下的所有对应.c
和.h
生成.o
文件。$<
用人话讲,意思就是构造所需文件列表的第一个文件的名字,$@
是目标的名字。我们可以make
一下看看这个被替换成了什么:
wingsummer@wingsummer-PC editor → make
gcc -Wall -g -c text.c
text.c: In function ‘load_file’:
text.c:234:5: warning: this ‘if’ clause does not guard... [-Wmisleading-indentation]
if(size < PAGE_SIZE)
^~
text.c:237:2: note: ...this statement, but the latter is misleadingly indented as if it were guarded by the ‘if’
init_page(p, filename, size);
^~~~~~~~~
text.c: In function ‘main’:
text.c:87:49: warning: ‘sprintf’ may write a terminating nul past the end of the destination [-Wformat-overflow=]
sprintf(status, "Saved as \'%s\'", page.filename);
^
text.c:87:17: note: ‘sprintf’ output between 12 and 267 bytes into a destination of size 266
sprintf(status, "Saved as \'%s\'", page.filename);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
text.c:81:49: warning: ‘sprintf’ may write a terminating nul past the end of the destination [-Wformat-overflow=]
sprintf(status, "Saved as \'%s\'", page.filename);
^
text.c:81:17: note: ‘sprintf’ output between 12 and 267 bytes into a destination of size 266
sprintf(status, "Saved as \'%s\'", page.filename);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
gcc -Wall -g -c page.c
gcc -Wall -g -c line.c -o line.o
gcc -Wall -g -c prompt.c -o prompt.o
gcc -Wall -g -o text text.o page.o line.o prompt.o -lncurses
重点注意的是下面几句:
gcc -Wall -g -c line.c -o line.o
gcc -Wall -g -c prompt.c -o prompt.o
这两句是makefile
并没有明确声明的,也就是我们%.o: %.c %.h
规则对应的执行。至此,本教程暂告一段落。
如果学会了上面的部分,如果有时间,如果有能力,建议把原文完完整整的看一遍,这样的话可能一天的时间就没了。对需要的重点部分看一看,剩下的如果用到就查。就算熟练使用makefile
维护项目,也未必能用到make
所有的功能,所以没必要为自己学不完makefile
而苦恼。