Makefile
这篇文章我们围绕Go项⽬
中常⽤的Makefile语法
和规则
,来盘一盘Makefile
工具的使用。
Makefile有很多语法,但不是所有语法都需要熟练掌握的,部分语法在Go项⽬中是很少⽤到。要编写⼀个⾼质量
的Makefile,⾸先应该掌握⼀些核⼼的
、最常⽤的
语法知识。
话不多说,开始进入今天的主题吧~
Makefile是什么?
Makefile
本质是包含一系列规则
和shell命令
的文本文件,用于指导
如何编译
源代码,构建
可执行文件。
Makefile⽂件由3部分
组成:
-
Makefile规则
描述构建过程的结构,用于指定如何从源文件生成目标文件。
-
Makefile语法
Makefile文件的编写规范和格式,包含一系列的语法规则。
-
Makefile命令
可以是
Linux 命令
,也可以是可执⾏的脚本⽂件
。
文章也会按照这个顺序依次展开,并辅以案例。让我们正式开始吧~
规则
规则
是Makefile中的重要概念,也是我们学习Makefile的核心
。它⼀般由⽬标
、依赖
和命令
组成,⽤来指定源⽂件编译的先后顺序
。Makefile之所以受欢迎,核⼼原因就是Makefile规则,因为 Makefile规则可以⾃动判断是否需要重新编译某个⽬标,从⽽确保⽬标仅在需要时编译。
规则语法
规则的语法部分
主要包括target
、prerequisites
和command
,⽰例如下:
|
|
-
target
target
是指规则所要构建的目标文件
或目标名称
,描述了构建的最终结果。 -
prerequisites
⽣成target所需的
依赖项
。当有多个依赖项时,依赖项之间⽤空格分隔。 -
command
生成target要执⾏的
命令
,可以是任意的shell命令
。- 在执⾏command之前,默认会打印出该命令,然后再输出命令的结果;如果不想打印出命令,可在各个command前加上
@
。 - command可以为
多条
,也可以分⾏写,但每⾏都要以tab键开始
。另外,如果后⼀条命令依赖前⼀条命令,则这两条命令需要写在同⼀⾏,并⽤分号进⾏分隔。 - 如果要忽略命令的出错,可以在各个command之前加上
减号-
。
- 在执⾏command之前,默认会打印出该命令,然后再输出命令的结果;如果不想打印出命令,可在各个command前加上
只要targets不存在,或者prerequisites文件有更新,那么command所定义的命令就会被执⾏,重新生成targets文件。
我们来看个栗子,串联一下规则语法
:
|
|
在这个栗子中,my_program
就是target
。这个规则表明,如果main.c
或者helper.c
中的任何一个文件发生了变化,那么my_program
将被重新构建。构建的命令是gcc -o my_program main.c helper.c
。
伪⽬标
Makefile的管理能⼒
基本上都是通过伪⽬标
来实现的。
伪⽬标不是⽂件,make ⽆法⽣成它的依赖关系,也⽆法决定是否要执⾏它。 因此,无法为伪目标生成任何文件。
通常情况下,我们需要显式地标识某个⽬标为伪⽬标。在Makefile中可以使⽤.PHONY
来标识⼀个⽬标为伪⽬标
:
|
|
伪⽬标可以有依赖⽂件,也可以作为“默认⽬标”,例如:
|
|
因为伪⽬标总是会被执⾏,所以其依赖总是会被决议。通过这种⽅式,可以达到同时执⾏所有依赖项的⽬的。
order-only依赖
在上⾯介绍的规则中,只要prerequisites中有任何⽂件发⽣改变,就会重新构造target。但是有时候,我们希望只有当prerequisites中的部分⽂件改变时,才重新构造target。这时,你可以通过order-only prerequisites实现。
order-only prerequisites的形式如下:
|
|
在上⾯的规则中,只有第⼀次
构造targets时,才会使⽤order-only-prerequisites。后⾯即使order-onlyprerequisites发⽣改变,也不会重新构造targets。
只有normal-prerequisites中的⽂件发⽣改变时,才会重新构造targets。这⾥,符号“ | ”后⾯的 prerequisites就是order-only-prerequisites。
语法
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种变量赋值
⽅法。
-
=
最基本的赋值⽅法。例如:1
BASE_IMAGE = alpine:3.10
注意,
=右边为变量时,值取最终值
。1 2 3 4
A = a # B最后的值为 c b,⽽不是a b B = $(A) b A = c
-
:=
直接赋值,赋予当前值
,可以避免=
赋值带来的潜在的不⼀致。例如:1 2 3 4
A = a # B最后的值为 a b B := $(A) b A = c
-
?=
表⽰如果该变量没有被赋值,则赋予等号后的值。例如:1 2 3
# 如果之前已经定义了 PLATFORMS 变量 # 这个赋值语句就会被忽略,不会覆盖先前的定义 PLATFORMS ?= linux_amd64 linux_arm64
-
+=
表⽰将等号后⾯的值添加到前⾯的变量上。 例如:1
MAKEFLAGS += --no-print-directory
多行变量
Makefile中,可以通过define-endef关键字
定义多⾏变量
,变量中允许换⾏:
|
|
变量内容
被赋值给define
定义的变量名
,可以包含函数
、命令
、⽂字
或是其他变量。
看个栗子:
|
|
环境变量
Makefile中有两种环境变量
,分别是Makefile预定义
的环境变量和⾃定义
的环境变量。
其中,⾃定义的环境变量可以覆盖Makefile预定义的环境变量
。默认情况下,Makefile中定义的环境变量只在当前Makefile有效,如果想向下层传递(Makefile中调⽤另⼀个Makefile),需要使⽤export关键字来声明。
下⾯的例⼦声明了⼀个环境变量,并可以在下层Makefile中使⽤:
|
|
内置变量
Makefile支持两种内置变量:特殊变量
和⾃动化变量
。
特殊变量
特殊变量
是make提前定义好的,可以在makefile中直接引⽤。特殊变量列表如下:
变量 | 含义 |
---|---|
MAKE | 当前make解释器的文件名 |
MAKECMDGOALS | 命令行中指定的目标名(make的命令行参数) |
CURDIR | 当前make解释器的工作目录 |
MAKE_VERSION | 当前make解释器的版本 |
MAKEFILE_LIST | make所需处理的makefile文件列表,当前makefile的文件名总是位于列表的最后,文件名之间以空格进行分隔 |
.DEFAULT_GOAL | 指定如果在命令行中未指定目标,应该构建哪个目标,即使这个目标不是在第一行 |
.VARIABLES | 所有已经定义的变量名列表(预定义变量和自定义变量) |
.FEATURES | 列出本版本支持的功能,以空格隔开 |
.INCLUDE_DIRS | make查询makefile的路径,以空格隔开 |
自动化变量
⾃动化变量
可以提⾼我们编写Makefile的效率
和质量
。
在Makefile的模式规则中,⽬标
和依赖⽂件
都是⼀系列的⽂件,那么我们如何书写⼀个命令,来完成从不同的依赖⽂件⽣成相对应的⽬标呢?
这时就可以⽤到⾃动化变量。所谓⾃动化变量,就是这种变量会把模式中所定义的⼀系列的⽂件⾃动地挨个取出,⼀直到所有符合模式的⽂件都取完为⽌。这种⾃动化变量只应出现在规则的命令中。Makefile中⽀持的⾃动化变量⻅下表。
变量 | 含义 |
---|---|
$@ | 表示规则中的目标文件集。在模式规则中,如果有多个目标,那么$@就是匹配于目标中模式定义的集合 |
$% | 仅当目标是函数库文件中,表示规则中的目标成员名。例如,如果一个目标是foo.a(bar.o),那么$%就是bar.o,$@就是foo.a。如果目标不是函数库文件(Unix下是.a,Windows下是.lib),那么其值为空 |
$< | 依赖目标中的第一个目标名字。如果依赖目标是以模式(即%)定义的,那么$<将是符合模式的一系列的文件名 |
$? | 所有比目标新的依赖目标的集合,以空格分隔 |
$^ | 所有以来的目标的集合,以空格分隔。如果在依赖目标中有多个重复的,那么这个变量会去除重复的依赖目标,只保留一份 |
$+ | 这个变量很像$^,也是所有依赖目标的集合,只是它不去除重复的依赖目标 |
$| | 所有的order-only依赖目标的集合,以空格分隔 |
$* | 这个变量表示目标模式中%及其之前的部分。如果目标是dir/a.foo.b,并且目标的模式是a.%.b,那么,$*的值就是dir/a.foo |
上⾯这些⾃动化变量中,$*
是⽤得最多的。$*
对于构造有关联的⽂件名是⽐较有效的。如果⽬标中没有模式的定义,那么$*
也就不能被推导出。但是,如果⽬标⽂件的后缀是make所识别的,那么 $*
就是除了后缀的那⼀部分。例如:如果⽬标是foo.c ,因为.c是make所能识别的后缀名,所以 $* 的值就是foo。
条件语句
Makefile也⽀持条件语句。这⾥先看⼀个⽰例。 下⾯的例⼦判断变量ROOT_PACKAGE是否为空,如果为空,则输出错误信息,不为空则打印变量值:
|
|
条件语句的语法为:
|
|
例如,判断两个值是否相等:
|
|
- ifeq表⽰条件语句的开始,并指定⼀个条件表达式。表达式包含两个参数,参数之间⽤逗号分隔,并且表 达式⽤圆括号括起来。
- else表⽰条件表达式为假的情况。
- endif表⽰⼀个条件语句的结束,任何⼀个条件表达式都应该以endif结束。
- 表⽰条件关键字,有4个关键字:ifeq、ifneq、ifdef、ifndef。
为了加深你的理解,我们分别来看下这4个关键字的例⼦。
-
ifeq:条件判断,判断是否相等。 例如:
1 2 3 4 5
ifeq (<arg1>, <arg2>) ifeq '<arg1>' '<arg2>' ifeq "<arg1>" "<arg2>" ifeq "<arg1>" '<arg2>' ifeq '<arg1>' "<arg2>"
⽐较arg1和arg2的值是否相同,如果相同则为真。也可以⽤make函数/变量替代arg1或arg2,例如 ifeq ($(origin ROOT_DIR),undefined) 或 ifeq ($(ROOT_PACKAGE),) 。origin函数会在之后专⻔讲 函数的⼀讲中介绍到。
-
ifneq:条件判断,判断是否不相等。
|
|
⽐较arg1和arg2的值是否不同,如果不同则为真。
-
ifdef:条件判断,判断变量是否已定义。
1
ifdef <variable-name>
如果值⾮空,则表达式为真,否则为假。也可以是函数的返回值。
-
ifndef:条件判断,判断变量是否未定义。
1
ifndef <variable-name>
如果值为空,则表达式为真,否则为假。也可以是函数的返回值。
函数
Makefile同样也⽀持函数,函数语法包括定义语法
和调⽤语法
。
我们先来看下⾃定义函数。 make解释器提供了⼀系列的函数供Makefile调⽤,这些函数是Makefile的预定义函数。我们可以通过define关键字来⾃定义⼀个函数。⾃定义函数的语法为:
|
|
例如,下⾯这个⾃定义函数:
|
|
define本质上是定义⼀个多⾏变量,可以在call的作⽤下当作函数来使⽤,在其他位置使⽤只能作为多⾏变 量来使⽤,例如:
|
|
⾃定义函数是⼀种过程调⽤,没有任何的返回值。可以使⽤⾃定义函数来定义命令的集合,并应⽤在规则 中。
再来看下预定义函数。 刚才提到,make编译器也定义了很多函数,这些函数叫作预定义函数,调⽤语法和 变量类似,语法为:
|
|
或者
|
|
是函数名,是函数参数,参数间⽤逗号分割。函数的参数也可以是变量。 我们来看⼀个例⼦:
|
|
上⾯的例⼦⽤到了两个函数:word和subst。word函数有两个参数,1和subst函数的输出。subst函数将 PLATFORM变量值中的_替换成空格(替换后的PLATFORM值为linux amd64)。word函数取linux amd64 字符串中的第⼀个单词。所以最后GOOS的值为linux。
Makefile预定义函数能够帮助我们实现很多强⼤的功能,在编写Makefile的过程中,如果有功能需求,可以 优先使⽤这些函数。如果你想使⽤这些函数,那就需要知道有哪些函数,以及它们实现的功能。
常⽤的函数包括下⾯这些,你需要先有个印象,以后⽤到时再来查看。
工作流
实际使⽤过程中,Makefile工作流
分为如下两步:
编写Makefile⽂件
,指定整个项⽬的编译规则
;- 执行Linux
make命令
解析该Makefile⽂件,⾃动化编译
和管理
项目。
默认情况
下,make命令会在当前⽬录下,按照GNUmakefile
、makefile
、Makefile
(具体文件名取决于系统)⽂件的顺序查找 Makefile⽂件,⼀旦找到,就开始读取这个⽂件并执⾏。
⼤多数的make都⽀持makefile
和Makefile
两种⽂件名,建议使⽤Makefile
,容易辨别。
make也⽀持 -f
和 --file
参数来指定其他⽂件名
,⽐如 make -f golang.mk 或者 make –file golang.mk 。
第⼀步
,先编写⼀个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、⾃动⽣成依赖关系、⽂件搜索等等。这里重点解释一下如何引入其他Makefile
,以及这么做带来的收益。
Makefile要结构化
、层次化
,这⼀点可以通过在项⽬根⽬录下的Makefile中引⼊其他Makefile来实现。
在Makefile中,我们可以通过关键字include,把别的makefile包含进来,类似于C语⾔的#include,被包含的⽂件会插⼊在当前的位置。例如:
|
|
include也可以包含通配符include scripts/make-rules/*。make命令会按下⾯的顺序查找makefile ⽂件:
- 如果是绝对或相对路径,就直接根据路径include进来。
- 如果make执⾏时,有-I或–include-dir参数,那么make就会在这个参数所指定的⽬录下去找。
- 如果⽬录/include(⼀般是/usr/local/bin或/usr/include)存在的话,make也会去 找。
如果有⽂件没有找到,make会⽣成⼀条警告信息,但不会⻢上出现致命错误,⽽是继续载⼊其他的⽂件。 ⼀旦完成makefile的读取,make会再重试这些没有找到或是不能读取的⽂件。如果还是不⾏,make才会出现⼀条致命错误信息。如果你想让make忽略那些⽆法读取的⽂件继续执⾏,可以在include前加⼀个减 号-,如-include 。