特别放送丨给你一份Go项目中最常用的Makefile核心语法

思考并回答以下问题:

Makefile文件由三个部分组成,分别是Makefile规则、Makefile语法和Makefile命令(这些命令可以是Linux命令,也可以是可执行的脚本文件)。

Makefile的使用方法

在实际使用过程中,我们一般是先编写一个Makefile文件,指定整个项目的编译规则,然后通过make命令来解析该Makefile文件,实现项目编译、管理的自动化。

默认情况下,make命令会在当前目录下,按照GNUmakefile、makefile、Makefile文件的顺序查找Makefile文件,一旦找到,就开始读取这个文件并执行。

大多数的make都支持“makefile”和“Makefile”这两种文件名,但我建议使用“Makefile”。因为这个文件名第一个字符大写,会很明显,容易辨别。make也支持-f--file参数来指定其他文件名,比如make -f golang.mk或者make --file golang.mk

Makefile规则介绍

规则一般由目标、依赖和命令组成,用来指定源文件编译的先后顺序。Makefile之所以受欢迎,核心原因就是Makefile规则,因为Makefile规则可以自动判断是否需要重新编译某个目标,从而确保目标仅在需要时编译。

这一讲我们主要来看Makefile规则里的规则语法、伪目标和order-only依赖。

规则语法

Makefile的规则语法,主要包括target、prerequisites和command,示例如下:

1
2
3
4
target ...: prerequisites ...
command
...
...

target,可以是一个object file(目标文件),也可以是一个执行文件,还可以是一个标签(label)。target可使用通配符,当有多个目标时,目标之间用空格分隔。prerequisites,代表生成该target所需要的依赖项。当有多个依赖项时,依赖项之间用空格分隔。command,代表该target要执行的命令(可以是任意的shell命令)。在执行command之前,默认会先打印出该命令,然后再输出命令的结果;如果不想打印出命令,可在各个command前加上@。command可以为多条,也可以分行写,但每行都要以tab键开始。另外,如果后一条命令依赖前一条命令,则这两条命令需要写在同一行,并用分号进行分隔。如果要忽略命令的出错,需要在各个command之前加上减号-。只要targets不存在,或prerequisites中有一个以上的文件比targets文件新,那么command所定义的命令就会被执行,从而产生我们需要的文件,或执行我们期望的操作。

我们直接通过一个例子来理解下Makefile的规则吧。第一步,先编写一个hello.c文件。

第二步,在当前目录下,编写Makefile文件。

第三步,执行make,产生可执行文件。

上面的示例Makefile文件有两个target,分别是hello和hello.o,每个target都指定了构建command。当执行make命令时,发现hello、hello.o文件不存在,就会执行command命令生成target。第四步,不更新任何文件,再次执行make。

当target存在,并且prerequisites都不比target新时,不会执行对应的command。第五步,更新hello.c,并再次执行make。

当target存在,但prerequisites比target新时,会重新执行对应的command。第六步,清理编译中间文件。Makefile一般都会有一个clean伪目标,用来清理编译中间产物,或者对源码目录做一些定制化的清理:

我们可以在规则中使用通配符,make支持三个通配符:*,?和~,例如:

伪目标接下来我们介绍下Makefile中的伪目标。Makefile的管理能力基本上都是通过伪目标来实现的。在上面的Makefile示例中,我们定义了一个clean目标,这其实是一个伪目标,也就是说我们不会为该目标生成任何文件。因为伪目标不是文件,make无法生成它的依赖关系,也无法决定是否要执行它。通常情况下,我们需要显式地标识这个目标为伪目标。在Makefile中可以使用.PHONY来标识一个目标为伪目标:

伪目标可以有依赖文件,也可以作为“默认目标”,例如:

因为伪目标总是会被执行,所以其依赖总是会被决议。通过这种方式,可以达到同时执行所有依赖项的目的。order-only依赖在上面介绍的规则中,只要prerequisites中有任何文件发生改变,就会重新构造target。但是有时候,我们希望只有当prerequisites中的部分文件改变时,才重新构造target。这时,你可以通过order-onlyprerequisites实现。order-onlyprerequisites的形式如下:

在上面的规则中,只有第一次构造targets时,才会使用order-only-prerequisites。后面即使order-only-prerequisites发生改变,也不会重新构造targets。只有normal-prerequisites中的文件发生改变时,才会重新构造targets。这里,符号“|”后面的prerequisites就是order-only-prerequisites。到这里,我们就介绍了Makefile的规则。接下来,我们再来看下Makefile中的一些核心语法知识。Makefile语法概览因为Makefile的语法比较多,这一讲只介绍Makefile的核心语法,以及IAM项目的Makefile用到的语法,包括命令、变量、条件语句和函数。因为Makefile没有太多复杂的语法,你掌握了这些知识点之后,再在实践中多加运用,融会贯通,就可以写出非常复杂、功能强大的Makefile文件了。

命令Makefile支持Linux命令,调用方式跟在Linux系统下调用命令的方式基本一致。默认情况下,make会把正在执行的命令输出到当前屏幕上。但我们可以通过在命令前加@符号的方式,禁止make输出当前正在执行的命令。我们看一个例子。现在有这么一个Makefile:

执行make命令:

可以看到,make输出了执行的命令。很多时候,我们不需要这样的提示,因为我们更想看的是命令产生的日志,而不是执行的命令。这时就可以在命令行前加@,禁止make输出所执行的命令:

再次执行make命令:

可以看到,make只是执行了命令,而没有打印命令本身。这样make输出就清晰了很多。这里,我建议在命令前都加@符号,禁止打印命令本身,以保证你的Makefile输出易于阅读的、有用的信息。默认情况下,每条命令执行完make就会检查其返回码。如果返回成功(返回码为0),make就执行下一条指令;如果返回失败(返回码非0),make就会终止当前命令。很多时候,命令出错(比如删除了一个不存在的文件)时,我们并不想终止,这时就可以在命令行前加-符号,来让make忽略命令的出错,以继续执行下一条命令,比如:

变量变量,可能是Makefile中使用最频繁的语法了,Makefile支持变量赋值、多行变量和环境变量。另外,Makefile还内置了一些特殊变量和自动化变量。我们先来看下最基本的变量赋值功能。Makefile也可以像其他语言一样支持变量。在使用变量时,会像shell变量一样原地展开,然后再执行替换后的内容。Makefile可以通过变量声明来声明一个变量,变量在声明时需要赋予一个初值,比如ROOT_PACKAGE=github.com/marmotedu/iam。引用变量时可以通过$()或者${}方式引用。我的建议是,用$()方式引用变量,例如$(ROOT_PACKAGE),也建议整个makefile的变量引用方式保持一致。变量会像bash变量一样,在使用它的地方展开。比如:

展开后为:

接下来,我给你介绍下Makefile中的4种变量赋值方法。=最基本的赋值方法。例如:

使用=进行赋值时,要注意下面这样的情况:

B最后的值为cb,而不是ab。也就是说,在用变量给变量赋值时,右边变量的取值,取的是最终的变量值。:=直接赋值,赋予当前位置的值。例如:

B最后的值为ab。通过:=的赋值方式,可以避免=赋值带来的潜在的不一致。?=表示如果该变量没有被赋值,则赋予等号后的值。例如:

+=表示将等号后面的值添加到前面的变量上。例如:

Makefile还支持多行变量。可以通过define关键字设置多行变量,变量中允许换行。定义方式为:

变量的内容可以包含函数、命令、文字或是其他变量。例如,我们可以定义一个USAGE_OPTIONS变量:

Makefile还支持环境变量。在Makefile中,有两种环境变量,分别是Makefile预定义的环境变量和自定义的环境变量。其中,自定义的环境变量可以覆盖Makefile预定义的环境变量。默认情况下,Makefile中定义的环境变量只在当前Makefile有效,如果想向下层传递(Makefile中调用另一个Makefile),需要使用export关键字来声明。下面的例子声明了一个环境变量,并可以在下层Makefile中使用:

此外,Makefile还支持两种内置的变量:特殊变量和自动化变量。特殊变量是make提前定义好的,可以在makefile中直接引用。特殊变量列表如下:

Makefile还支持自动化变量。自动化变量可以提高我们编写Makefile的效率和质量。在Makefile的模式规则中,目标和依赖文件都是一系列的文件,那么我们如何书写一个命令,来完成从不同的依赖文件生成相对应的目标呢?这时就可以用到自动化变量。所谓自动化变量,就是这种变量会把模式中所定义的一系列的文件自动地挨个取出,一直到所有符合模式的文件都取完为止。这种自动化变量只应出现在规则的命令中。Makefile中支持的自动化变量见下表。

上面这些自动化变量中,$是用得最多的。$对于构造有关联的文件名是比较有效的。如果目标中没有模式的定义,那么$也就不能被推导出。但是,如果目标文件的后缀是make所识别的,那么$就是除了后缀的那一部分。例如:如果目标是foo.c,因为.c是make所能识别的后缀名,所以$*的值就是foo。条件语句Makefile也支持条件语句。这里先看一个示例。下面的例子判断变量ROOT_PACKAGE是否为空,如果为空,则输出错误信息,不为空则打印变量值:

条件语句的语法为:

例如,判断两个值是否相等:

ifeq表示条件语句的开始,并指定一个条件表达式。表达式包含两个参数,参数之间用逗号分隔,并且表达式用圆括号括起来。else表示条件表达式为假的情况。endif表示一个条件语句的结束,任何一个条件表达式都应该以endif结束。表示条件关键字,有4个关键字:ifeq、ifneq、ifdef、ifndef。为了加深你的理解,我们分别来看下这4个关键字的例子。ifeq:条件判断,判断是否相等。例如:

比较arg1和arg2的值是否相同,如果相同则为真。也可以用make函数/变量替代arg1或arg2,例如ifeq($(originROOT_DIR),undefined)或ifeq($(ROOT_PACKAGE),)。origin函数会在之后专门讲函数的一讲中介绍到。ifneq:条件判断,判断是否不相等。
比较arg1和arg2的值是否不同,如果不同则为真。ifdef:条件判断,判断变量是否已定义。

如果值非空,则表达式为真,否则为假。也可以是函数的返回值。ifndef:条件判断,判断变量是否未定义。

如果值为空,则表达式为真,否则为假。也可以是函数的返回值。函数Makefile同样也支持函数,函数语法包括定义语法和调用语法。我们先来看下自定义函数。make解释器提供了一系列的函数供Makefile调用,这些函数是Makefile的预定义函数。我们可以通过define关键字来自定义一个函数。自定义函数的语法为:

例如,下面这个自定义函数:

define本质上是定义一个多行变量,可以在call的作用下当作函数来使用,在其他位置使用只能作为多行变量来使用,例如:

自定义函数是一种过程调用,没有任何的返回值。可以使用自定义函数来定义命令的集合,并应用在规则中。再来看下预定义函数。刚才提到,make编译器也定义了很多函数,这些函数叫作预定义函数,调用语法和变量类似,语法为:

或者

是函数名,是函数参数,参数间用逗号分割。函数的参数也可以是变量。我们来看一个例子:

上面的例子用到了两个函数:word和subst。word函数有两个参数,1和subst函数的输出。subst函数将PLATFORM变量值中的_替换成空格(替换后的PLATFORM值为linuxamd64)。word函数取linuxamd64字符串中的第一个单词。所以最后GOOS的值为linux。Makefile预定义函数能够帮助我们实现很多强大的功能,在编写Makefile的过程中,如果有功能需求,可以优先使用这些函数。如果你想使用这些函数,那就需要知道有哪些函数,以及它们实现的功能。常用的函数包括下面这些,你需要先有个印象,以后用到时再来查看。

引入其他Makefile除了Makefile规则、Makefile语法之外,Makefile还有很多特性,比如可以引入其他Makefile、自动生成依赖关系、文件搜索等等。这里我再介绍一个IAM项目的Makefile用到的重点特性:引入其他Makefile。在14讲中,我们介绍过Makefile要结构化、层次化,这一点可以通过在项目根目录下的Makefile中引入其他Makefile来实现。在Makefile中,我们可以通过关键字include,把别的makefile包含进来,类似于C语言的#include,被包含的文件会插入在当前的位置。include用法为include,示例如下:

include也可以包含通配符includescripts/make-rules/*。make命令会按下面的顺序查找makefile文件:如果是绝对或相对路径,就直接根据路径include进来。如果make执行时,有-I或—include-dir参数,那么make就会在这个参数所指定的目录下去找。如果目录/include(一般是/usr/local/bin或/usr/include)存在的话,make也会去找。如果有文件没有找到,make会生成一条警告信息,但不会马上出现致命错误,而是继续载入其他的文件。一旦完成makefile的读取,make会再重试这些没有找到或是不能读取的文件。如果还是不行,make才会出现一条致命错误信息。如果你想让make忽略那些无法读取的文件继续执行,可以在include前加一个减号-,如-include。总结在这一讲里,为了帮助你编写一个高质量的Makefile,我重点介绍了Makefile规则和Makefile语法里的一些核心语法知识。在讲Makefile规则时,我们主要学习了规则语法、伪目标和order-only依赖。掌握了这些Makefile规则,你就掌握了Makefile中最核心的内容。在介绍Makefile的语法时,我只介绍了Makefile的核心语法,以及IAM项目的Makefile用到的语法,包括命令、变量、条件语句和函数。你可能会觉得这些语法学习起来比较枯燥,但还是那句话,工欲善其事,必先利其器。希望你能熟练掌握Makefile的核心语法,为编写高质量的Makefile打好基础。

0%