羽夏 MakeFile 简明教程
阅读原文时间:2022年05月11日阅读:1

  此系列是本人一个字一个字码出来的,包括示例和实验截图。该文章根据 GNU Make Manual 进行汉化处理并作出自己的整理,一是我对 Make 的学习记录,二是对大家学习 MakeFile 有更好的帮助。如对该博文有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。本篇博文可能十分冗长,请耐心阅读和学习。

当你读到后面可能感觉名词有点怪怪的,那个就是我的译文。因为老外表述翻译到中文有些突兀,谷歌翻译也比较离谱,我语文不行也不好翻译,如果有能力看看原文吧。

  make实用程序自动确认需要重新编译大型程序的哪些部分,并执行哪些命令来重新编译。本篇博文使用的示例是C程序,但你可以将make与任何编程语言结合使用,这些语言的编译器可以通过shell命令运行。事实上,make并不局限于程序。你可以用它来描述任何一项任务,当其他文件发生变化时,相关文件必须自动从其他文件中来进行更新。

  要准备使用make,必须编写一个名为makefile的文件,该文件描述程序中文件之间的关系,并提供更新每个文件的命令。在程序中,可执行文件通常是从目标文件更新而来的,而目标文件又是通过编译源文件来实现的。

  一旦存在合适的makefile,每次更改一些源文件时,下面这个简单的shell命令:

make

  这足以执行所有必要的重新编译。make程序使用makefile数据库和文件的最后修改时间来决定哪些文件需要更新。对于这些当中的每一个文件,都会被记录在数据库中。

  您需要一个名为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

  是不是看不太明白,我们来画一个示意图:

graph TD
A(text);B(line.o);C(page.o);
D(prompt.o);E(text.o);
E---A;D---A;C---A;B---A;
1[line.c];2[line.h];3[page.c];4[page.h];
5[prompt.c];6[prompt.h];7[text.c];8[text.h];
1 ==>B; 2 ==>B;
3 -->C;4-->C;1-->C;
5 -…->D;6-…->D;
7-.->E;8-.->E;6-.->E;4-.->E;2-.->E;

  如上展示的就是所谓的依赖关系,如果有关编译器命令不会的话,建议自己查询。在一条生成语句中,我们使用反斜杠加换行符将一行分成两行,作用和一行是一样的,但增加的可读性。要使用此 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程序,例如BisonYacc制作的C程序。

  在重新编译任何需要的目标文件后,make决定是否重新链接我们上面的编辑器text。如果text不存在,或者任何目标文件比它新,则必须这样做。如果一个目标文件刚刚被重新编译,它现在比text新,所以text被重新链接。因此,如果我们更改文件line.c并运行makemake将编译该文件以更新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都有一个名为objectsOBJECTSobjsOBJSobjOBJ的变量,这是所有对象文件名的列表,这是标准的做法。我们将在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时,它​​会依次尝试以下名称:GNUmakefilemakefileMakefile。如果您有一个特定于GNU makemakefile,并且不会被其他版本的make理解,您应该使用这个名称。其他make程序寻找makefileMakefile,但不是GNUmakefile

  如果make没有找到这些名称,它就不会使用任何makefile。然后您必须使用命令参数指定目标,make将尝试找出如何仅使用其内置的隐式规则来重新制作它。如果你想为你的makefile使用一个非标准的名字,你可以使用-f--file选项来指定makefile的名字。如果指定上面的参数,则不会自动检查默认的makefile名称。

包含其他 Makefiles

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

include 文件名

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

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

  例如,如果您有三个mk文件,a.mkb.mkc.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(和其他目标),你可以写一个名为GNUmakefilemakefile,它内容如下:

foo:
        frobnicate > foo

%: force
        @$(MAKE) -f Makefile $@
force: ;

  如果你输入命令make foomake会找到 GNUmakefile并扫描,发现要生成foo,它需要运行frobnicate > foo。 如果你说make barmake将无法在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有一个特殊的特性,即二次扩展,它将允许在先决条件列表中使用自动变量值,这将会在后面进行介绍。

  下面是自动变量表:

自动变量符号

含义

$@

规则目标的文件名。如果目标是存档成员,则$@是存档文件的名称。在有多个目标的模式规则中,$@是导致规则配置运行的任何目标的名称。

$%

目标成员名。例如,如果目标是foo.abar.o),那么$%就是bar.o$@foo.a。当目标不是存档成员时,$%为空。

$<

第一个条件的名称。如果目标从隐式规则获得配置,这将是隐式规则添加的第一个条件

$?

比目标更新的所有条件的名称,名称之间用空格隔开。如果目标不存在,则将包括所有条件。对于作为归档成员的条件,只使用命名的成员。

$^

所有条件的名称,它们之间有空格。对于作为归档成员的条件,只使用命名的成员。目标对它所依赖的其他文件只有一个条件,无论每个文件作为条件列出多少次。因此,如果您多次为目标列出条件,那么$^的值只包含名称的一个副本。此列表不包含任何order-only条件

$+

这类似于$^,但是不止一次列出的先决条件会按照它们在makefile中列出的顺序重复出现。这在链接命令时非常有用,当需要以特定顺序重复库文件名时。

`$

`

$*

隐式规则匹配词干。如果目标为dir/a.foo.b,目标匹配模式是a.%.b,则词干为dir/foo

  当然,自动变量不仅仅是上面这些,还有有关DF的变体,比如$(@D)等,如果有需要请参考原文。

二次展开

  前面我们了解到GNU在两个不同的阶段中制作:读取阶段和目标更新阶段。GNU也能够为Makefile中定义的某些或所有目标启用条件的第二次扩展。为了使第二次扩展发生,必须在使用此功能的第一个条件列表之前定义特殊的目标。

  如果定义了该特殊目标,则在上述两个阶段之间,就在读取阶段的末尾,第二次扩展了特殊目标后定义的目标的所有条件。在大多数情况下,这种次要扩展将无效,因为在MakeFiles的初始解析过程中,所有变量和功能参考都将进行扩展。为了利用解析器的二级扩展阶段,有必要逃避makefile中的变量或函数参考。在这种情况下,第一个扩展仅取消参考,但并未扩展,并且扩展到次级扩展阶段。例如,考虑这个makefile

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

  在第一个扩展阶段之后,MyFile目标的先决条件列表将为单一和$(TWOVAR); 扩展了对ONEVAR的第一个unescaped变量引用,而第二个escaped变量引用简单地保留,而未被识别为变量参考。现在,在次要扩展期间,第一个单词再次扩展,但是由于它不包含变量或函数引用,因此它仍然是一个值的值,而第二个单词现在是对变量TWOVAR的正常引用,该引用将扩展到twofile的值。最终的结果是有两个条件,即onefiletwofile

  显然,这不是一个非常有趣的案例,因为仅通过在先决条件列表中显示两个变量,可以更轻松地实现相同的结果。如果变量重置,则会显而易见。考虑此示例:

.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 $@

  substmakefile的里的函数,意为字符替换,但本篇并没有介绍,因为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而苦恼。