Makefile 的组成
- 显示规则:显示规则说明了如何生成目标文件
- 隐晦规则:make 有自动推导的功能,利用隐晦规则可以简写makefile
- 变量的定义:在 makefile 中可以定义一系列变量,变量一般为字符串,makefile 中的变量类似 C/C++中的宏,当 makefile 执行时,变量都会被其定义的字符串所替换
- 文件指示:包括三部分,一个是在一个 makefile 中引用另一个 makefile,类似 C/C++中的
#include
;另一个是根据某些情况指定 makefile 中的有效部分,类似 C/C++中的预编译#if
;最后一个是定义一个多行的命令 - 注释:makefile 中只有行注释,和 UNIX 的 Shell 脚本一样,注释也是使用
#
字符
Makefile 的文件名
默认情况下,make 命令会在当前目录下按顺序查找文件名为“GNUmakefile”、“makefile”、“Makefile”的文件,找到后解释执行。
三种命名方式中推荐使用“Makefile”来命名,此种命名方式更为醒目和通用。当然我们也可以使用其他的文件名来命名 Makefile 文件,使用其他命名方式时需要使用 make 命令的 -f
参数来指定要解释执行 Makefile 文件。
Makefile 书写规则
规则包含两个部分:一个是依赖关系,另一个是生成目标的方法。
1 | targets : prerequisites # 第一部分 |
- targets : 可以是目标文件,也可以是执行文件,还可以是标签,目标可以是一个文件也可以是多个文件,多个文件的话用空格分开
- prerequisites : 生成 target 所依赖的文件,同样可以是多个文件,多个文件的话用空格分开
- command :要执行的命令,必须以
Tab
键开头,如果命令过长则可以使用反斜杠\
作为换行符
这是一个文件的依赖关系,目标文件 targets 依赖于 prerequisites 文件,其生成规则定义在 command 中,也即:当 prerequisites 中存在比 targets 中更新的文件时(或者当 targets 文件不存在时),就会执行 command 命令来生成新的 targets 文件。
文件搜寻
在一些大的工程中,存在大量的源文件,通常的做法是把这许多的源文件进行分类并存放在不同的目录中。所以,当 make 指令需要去寻找文件的依赖关系时,我们需要在依赖关系中使用每个文件的完整路径,但最好的方法是将依赖关系中的文件可能存在的路径告诉 make,让 make 去自动查找。
告诉 make 文件查找路径的方法有两种。第一种是在 Makefile 文件中使用特殊变量 VPATH
,如果没有设置这个变量,则 make 只会在当前的目录中去寻找依赖文件和目标文件。如果定义了这个变量,则当 make 在当前目录找不到依赖文件和目标文件时,就会去该变量指定的路径下查找。
1 | VPATH = src:../headers |
通过 VPATH
变量可以设置多个查找目录,多个查找目录之前使用冒号分割,并按设置的顺序查找。上述示例指定了两个查找目录,分别是“src”目录和“../headers”目录。
另一种设置文件搜索路径的方法是使用 make 指令的“vpath”关键字。它可以指定不同的文件在不同的搜索目录中,它的使用方法有三种:
vpath <pattern> <directories>
:为符合模式<pattern>
的文件指定搜索目录为<directories>
(也即告诉 make 在指定的搜索目录中去搜索符合指定模式的文件)vpath <pattern>
:清除符合模式<pattern>
的文件的搜索目录vpath
:清除所有已被设置的文件搜索目录<pattern>
指定了需要搜索的文件集,而<directories>
则指定了要去哪里搜索需要的文件集。注意,vpath
使用方法中的<pattern>
需要包含%
字符,该字符的意思是「匹配零或多个字符」。例如,%.h
表示所有以.h
结尾的文件。
1 | vpath %.h ../headers |
上述示例表示如果在当前目录下没有找到所需的 .h
文件,则去 “../headers”目录下去查找。
伪目标文件
在 makefile 文件中通常存在一个名为「clean」的目标,这是一个伪目标。
1 | clean: |
如上所示,我们通常需要提高一个清除所有目标文件以便完整重新编译的「clean」目标,并通过「make clean」来使用该目标。
因为我们并不生成“clean”这个文件,所以说“伪目标”并不是一个文件,而只是一个标签。而由于“伪目标”并不是文件,所以 make 无法生成它的依赖关系和决定它是否要执行。我们只有通过显式地指明这个“目标”才能让其生效,这里也即使用 make clean
指令来使用「clean」目标。
当然,“伪目标”的取名不能和已有的文件名重名,不然就失去了“伪目标”的意义了。为了避免重名的情况,我们可以使用一个特殊的标记 .PHONY
来显式地指明一个目标是“伪目标”,以向 make 说明,不管是否有这个文件,这个目标就是一个“伪目标”。只要有这个声明,只有使用 make clean
才能运行这个目标。示例如下:
1 | .PHONY : clean |
自动生成依赖性
在 Makefile 中,我们的依赖关系常常包含一系列的头文件。特别是在一个比较大型的工程中,我们需要搞清楚哪些 C/C++文件包含了哪些头文件,并且在加入或者删除头文件时也需要小心修改 Makefile,这增加了维护 Makefile 的难度。
为了避免这种繁重而又容易出错的事情,我们可以使用 C/C++编译的一个功能来帮助我们梳理这些包含关系。大多数的 C/C++编译器都支持一个 -M
选项,来自动找寻源文件中包含的头文件,并生成一个依赖关系(如果使用 GNU 的 C/C++编译器,需要使用 -MM
参数,不然 -M
参数会把一些标准库的头文件也包含进来)。示例如下:
1 | // main.cc文件 |
执行 g++ -MM main.cc
的输出结果为:
1 | main.o: main.cc header/header.h |
GNU 组织建议把编译器为每一个源文件自动生成的依赖关系放到一个文件中,为每一个 name.cc
的文件都生成一个 name.d
的 Makefile 文件,.d
文件中就存放对应 .cc
文件的依赖关系。
于是,我们就可以写出 .cc
文件和 .d
文件的依赖关系,并让 make 自动更新或生成 .d
文件,并将其包含在我们的主 Makefile 中,这样我们就可以自动化地生成每个文件的依赖关系了。示例如下:
1 | SRCS = $(wildcard *.cc) # 源文件 |
Makefile 中的命令
在 Makefile 中,每条规则中的命令和操作系统 Shell 的命令行是一致的,make 会按顺序一条一条的执行命令,每条命令必须以 Tab
键开头(除非命令是紧跟在依赖规则后面的分号后的)。
我们在 UNIX 下可能会使用不同的 Shell,但是 Makefile 中的命令默认是被 /bin/sh
(也即 UNIX 的标准 Shell)执行的。
显示命令
默认情况下,make 会把其要执行的命令行在命令执行钱打印到屏幕上。当我们在命令行前加上一个 @
字符时,这个命令将会取消打印到屏幕。
命令执行
当依赖文件新于目标文件时,也就是依赖关系的目标文件需要被更新时,make 会一条一条的执行其后的命令。不过需要注意的是,如果我们希望让上一条命令的结果应用在下一条命令执行前时,则应该将这两条命令写在同一行中并使用「分号」来分割。
命令出错
每当一条命令执行完后,make 会检测每个命令的返回码,如果命令返回成功,那么 make 就会继续执行下一条命令。当一个规则中的所有命令都成功返回后,这个规则便成功完成了。如果一个规则中的某个命令出错了,那么 make 就会终止执行当前规则,默认情况下这会终止后续所有规则的执行。
但是,有些时候命令的出错并不表示就是错误的,我们需要忽略命令的出错。为了做到这一点我们可以在 Makefile 的命令行前加上一个减号 -
,标记为不管命令执行是否出错都认为是成功的。例如:
1 | clean: |
还有一种全局的方法,在执行 make 命令时加上 -i
或 -ignore-errors
参数,那么 Makefile 中的所有命令都会忽略错误。
make 命令的另一个参数 -k
或 -keep-going
会终止出错规则的执行,但会继续执行后续其他规则。
定义命令包
如果在 Makefile 中会重复执行一些相同的命令序列,则可以将这些重复执行的命令序列定义为一个命令包。定义语法和使用方法如下所示:
1 | # 定义,name为定义的命令包变量的名字 |
make 在执行命令包时,命令包中的每个命令会被依次独立执行。
Makefile 中的变量
在 Makefile 中定义的变量类似 C/C++中的「宏」,它代表了一个文本字符串,Makefile 在执行的时候会自动在使用变量的地方替换为其所代表的字符串。
变量名可以包含字符、数字和下划线,但是不能含有 :
、#
、=
或空字符(空格、Tab 和回车等)。且变量名是大小写敏感的。
变量在声明的时候就需要给予初值,而在使用时需要在变量名前加上 $
字符,且最好使用小括号 ()
或者大括号 {}
把变量名给包裹起来(在使用时给变量名加上括号只是为了更加安全地使用整个变量)。
用变量定义变量
在定义变量的值时,可以使用其他变量来构造变量的值。Makefile 提供了两种方式来用变量定义变量的值。
使用 =
定义
第一种方式就是简单的使用 =
来定义,等号的右侧是用来定义新变量值的变量。右侧的变量可以定义在文件的任何一处,也就是说在使用这个变量来定义另一个变量的值时,这个变量不一定是已经定义好的。例如:
1 | varA = $(varB) |
这个功能的好处是可以把在定义变量时使用的变量推迟到后面进行定义,坏处是 kennel 会导致递归定义,进入死循环。例如:
1 | varA = $(varB) |
使用 :=
定义
为了避免第一种定义方式可能导致的递归定义问题,我们可以使用第二种定义方式,也即使用操作符 :=
。例如:
1 | varA := Hello |
这种方式不会导致递归定义问题是因为这种方式不允许前面的变量使用后面的变量,也即一个变量只能使用在它之前已经定义好的变量。
变量高级用法
?=
操作符
示例:
1 | var ?= Hello world |
如上, ?=
操作符的意思是,如果变量 var
没有被定义的话,那么 var
将被定义为 Hello world
,如果 var
已被定义,则什么也不做。
+=
操作符
我们可以使用 +=
操作符来为一个变量追加值,例如:
1 | src = a.cc b.cc d.cc |
则变量 src
的值为 a.cc b.cc d.cc e.cc
。=
变量值的替换
我们可以替换变量中共有的部分,方法是使用 $(var:a=b)
或者 ${var:a=b}
,其意思是:把变量 var
中所有以字符 a
结尾的字符串替换为以 b
结尾的字符串。例如:
1 | src1 := a.o b.o |
在上面的例子中,第一行我们先定义了一个变量 src1
,第二行我们将变量 src1
中所有以 .o
结尾的字符串替换成以 .c
结尾,并赋值给变量 src2
,所以变量 src2
的值为 a.c b.c
。
还有一种变量替换的方式是以「静态模式」定义的,例如:
1 | src1 := a.o b.o |
这种方式依赖于被替换的字符串中具有相同的模式。
把变量的值再作为变量
我们可以将一个变量的值作为另一个变量的名字,例如:
1 | x = y |
在上面的例子中,$(x)
为 y
,那么 $($(x))
就等于 $(y)
,而变量 y
的值为 z
,所以变量 a 的值为 z
。
环境变量
系统环境变量可以在 make 运行时被载入到 Makefile 文件中。但是,如果 Makefile 中已经定义了同名的系统环境变量,或者这个变量由 make 命令行带入,那么系统环境变量的值将会被覆盖(如果 make 指定了 -e
参数,则系统环境变量将反过来覆盖 Makefile 中定义的同名变量)。
当 make 嵌套调用时,通过命令行设置的变量会以系统环境变量的方式传递到下层的 Makefile 中。而定义在文件中的变量,如果要向下层 Makefile 传递,则需要使用 export
关键字来声明。
override 指示符
如果通过 make 的命令行参数设置了一个变量的值,则 Makefile 中对这个变量的赋值会被忽略。如果我们需要在 Makefile 中设置这类参数的值,那么我们可以使用 override
关键字。其语法是:
1 | override <variable> = <value> |
目标变量
我们前面所提到的变量都是指的全局变量,在整个 Makefile 文件中我们都可以访问这些变量。
我们也可以为某个目标设置局部变量,这种变量被称为「Target-specific Variable」,它可以和「全局变量」同名,因为它的作用范围只在指定目标所在的规则及其连带规则中,而不会影响到规则链以外的全局变量的值。例如:
1 | # 语法 |
模式变量
GNU 的 make 还支持「模式变量」(Pattern-specific Variable)。前面我们提到,我们可以将变量定义在某个目标上,而通过模式变量,我们可以将变量定义在符合这种模式的所有目标上。
例如,我们可以按照如下方式给所有以 .o
的目标定义目标变量(也即模式变量):
1 | %.o : CFLAGS = -O |
Makefile 条件判断
使用条件判断,我们可以让 make 根据运行时的不同情况选择不同的执行分支。条件表达式可以是比较变量的值,也可以是比较变量和常量的值。条件表达式的语法为:
1 | <conditional-directive> |
其中 <conditional-directive>
表示条件关键字,在 make 中有四个条件关键字:ifeq
,ifneq
,ifdef
和 ifndef
。分别表示「是否相等」以及「是否定义」。用法及示例如下:
1 | # 用法 |
Makefile 中的函数
在 Makefile 中可以使用函数来处理变量,函数调用后的返回值也可以当作变量来使用。
在 Makefile 中,函数的调用类似变量的使用,也是用 $
来标识的,语法如下:
1 | $(<function> <arguments>) |
其中,<function>
代表函数名,<arguments>
代表函数调用需要的参数,多个参数之间使用逗号 ,
分隔,而函数名和参数之间用「空格」分隔。函数中的参数也可以使用变量。
字符串处理函数
subset
1 | $(subset <from>,<to>,<text>) |
- 名称:字符串替换函数
- 功能:把字符串
<text>
中的<from>
字符串替换为<to>
- 返回:返回被替换后的字符串
patsubst
1 | $(patsubst <pattern>,<replacement>,<text>) |
- 名称:模式字符串替换函数
- 功能:把字符串
<text>
中符合模式<pattern>
的字符串替换为<replacement>
- 返回:返回被替换后的字符串
- 示例:
$(patsubst %.c, %.o, main.c)
strip
1 | $(strip <string>) |
- 名称:去空格函数
- 功能:去掉
<string>
字符串中开头和结尾的空字符 - 返回:返回被去掉空格后的字符串值
findstring
1 | $(findstring <find>,<in>) |
- 名称:查找字符串函数
- 功能:在字符串
<in>
中查找<find>
字符串 - 返回:如果找到,则返回
<find>
否则返回空字符串
filter
1 | $(filter <pattern>, <text>) |
- 名称:过滤函数
- 功能:返回字符串
<text>
中符合模式<pattern>
的字符串
filter-out
1 | $(filter-out <pattern>, <text>) |
- 名称:反过滤函数
- 功能:返回字符串
<text>
中不符合模式<pattern>
的字符串
sort
1 | $(sort <list>) |
- 名称:排序函数
- 功能:对字符串
<list>
中的单词进行排序(升序),并返回排序后的字符串(该函数会去掉<list>
中重复的单词)
word
1 | $(word <n>, <text>) |
- 名称:取单词函数
- 功能:从 1 开始取单词
<text>
中的第n
个单词并返回,如果n
超过单词数量则返回空字符串
wordlist
1 | $(wordlist <n1>,<n2>,<text>) |
- 名称:取字符串函数
- 功能:从字符串
<text>
中取从第n1
到n2
的字符串并返回(也是从 1 开始计数)
words
1 | $(words <text>) |
- 名称:单词个数统计函数
- 功能:统计
<text>
中单词的个数并返回
firstword
1 | $(firstword <text>) |
- 名称:取首单词函数
- 功能:取字符串
<text>
中的首单词并返回
文件名操作函数
dir
1 | $(dir <names ...>) |
- 名称:取目录函数
- 功能:从一系列文件的全路径中取出路径部分并返回(也即最后一个斜杠
/
前的部分)
notdir
1 | $(nodir <names ...>) |
- 名称:取文件名函数
- 功能:从一系列文件的全路径中取出文件名并返回(也即最后一个斜杠
/
后的部分)
suffix
1 | $(suffix <names ...>) |
- 名称:取后缀函数
- 功能:从文件名序列中取出文件名后缀并返回,如果文件名无后缀则返回空字符串
basename
1 | $(basename <names ...>) |
- 名称:取文件名前缀函数
- 功能:从一系列文件名中取出文件名的前缀并返回
addsuffix
1 | $(addsuffix <suffix>,<names ...>) |
- 名称:添加后缀函数
- 功能:为
<names>
中的每个文件名添加后缀<suffix>
并返回
addprefix
1 | $(addprefix <prefix>,<names ...>) |
- 名称:添加前缀函数
- 功能:为
<names>
中的每个文件名添加前缀<prefix>
并返回
join
1 | $(join <list1>,<list2>) |
- 名称:连接函数
- 功能:把
<list2>
中的单词对应地加到<list1>
的单词后面。如果<list1>
的单词个数要比<list2>
的多,那么,<list1>
中的多出来的单词将保持原样。如果<list2>
的单词个数要比<list1>
多,那么,<list2>
多出来的单词将被复制到<list1>
中 - 返回:返回连接过后的字符串
其他函数
foreach 函数
1 | $(foreach <var>,<list>,<text>) |
- 功能:将参数
<list>
中的每个单词逐一去取出来放到参数<var>
所指定的变量中,然后执行<text>
所包含的表达式; - 返回:每次
<text>
的执行都会返回一个字符串,全部执行完毕后字符串用空格拼接后返回;
if 函数
1 | $(if <condition>,<then-part>) |
- 功能:
<condition>
成立则返回<then-part>
,否则,存在<else-part>
则返回<else-part>
,不存在则返回空字符串;
shell 函数
1 | $(shell <command>) |
- 功能:执行操作系统命令
<command>
,并将执行结果的输出返回;
Makefile 隐含规则
「隐含规则」也就是一种惯例,make 会按照这种惯例心照不宣地来运行,即使我们的 Makefile 中没有书写这样的规则。
隐含规则会使用一些我们的系统变量,我们可以设置这些系统变量的值来定制隐含规则在运行时使用的参数。例如,系统变量 CFLAGS
可以控制编译时使用的编译器参数。
如果要使用隐含规则生成我们需要的目标文件,我们需要做的就是不需要写出这个目标的完整规则链。此时,make 会试图去自动推导产生这个目标的规则和命令。例如:
1 | main : main.o math.o |
在上面的例子中,我们并没有写如何生成 main.o
和 math.o
文件的规则,make 的「隐含规则」功能会试图为我们自动推导生成这两个文件的依赖文件和生成命令。make 会在自己的隐含规则库中寻找可以用的规则,如果找到就会使用,找不到则会报错。
一些常用的隐含规则如下:
- 编译 C 程序的隐含规则:以
.o
结尾的目标文件的依赖文件会自动推导为对应的.c
文件,并且其生成命令为:$(CC) -c $(CPPFLAGS) $(CFLAGS)
; - 编译 C++程序的隐含规则:以
.o
结尾的目标文件的依赖文件会自动推导为对应的.cc
或.C
文件,并且其生成命令为:$(CXX) -c $(CPPFLAGS) $(CXXFLAGS)
;
隐含规则使用的变量
隐含规则的命令中大多使用了一些预置的变量,我们可以在 Makfile 中改变这些变量的值,或者在 make 的命令行中传入这些值,亦或是在我们的环境变量中设置这些值,无论采用哪种方法,只要设置了这些特定的变量,那么它们就会对隐含规则起作用。
我们可以把隐含规则中使用的变量分为两种:
- 一种是命令相关的,例如
CC
变量; - 另一种是参数相关的,例如
CFLAGS
变量;
命令相关的常用变量如下:
AR
:函数库打包程序,默认命令为ar
;AS
:汇编语言编译程序,默认命令为as
;CC
:C 语言编译程序,默认命令为cc
;CXX
:C++语言编译程序,默认命令为g++
;CPP
:C 程序的预处理器(输出为标准输出设备),默认命令为$(CC) -E
;RM
:文件删除命令,默认为rm -f
;
命令参数相关的常用变量如下:
ARFLAGS
:函数库打包程序AR
命令的参数;ASFLAGS
:汇编语言编译器参数;CFLAGS
:C 语言编译器参数;CXXFLAGS
:C++语言编译器参数;CPPFLAGS
:C 预处理器参数;LDFLAGS
:连接器参数;
模式规则
我们可以使用模式规则来定义一个隐含规则。一个模式规则就像一个普通的规则,区别在于模式规则中目标文件的定义中需要使用 %
字符。目标文件名中使用的 %
字符表示对文件名的匹配,%
标识匹配任意长度的非空字符串。例如 %.cc
表示以 .cc
结尾的文件名。
如果 %
出现在目标文件中,则依赖文件中的 %
所匹配的值决定了目标文件中 %
所代表的值。例如有如下模式规则:
1 | %.o : %.cc |
该模式规则定义了如何从所有的 .cc
文件生成对应名称的 .o
文件的规则,例如 main.cc
会生成 main.o
文件。
自动化变量
所谓的自动化变量,就是这种变量会把模式中所定义的一系列的文件自动地挨个取出,直至所有的符合模式的文件都取完。这种自动化变量只应该出现在规则的命令中。
下面为常用的自动化变量及其说明:
$@
:表示规则中的目标文件。$%
:当目标是函数库文件时,表示库文件中的一个成员名。例如,当目标为foo.a(bar.o)
时,$%
表示bar.o
而$@
表示foo.a
。$<
:表示依赖文件中的第一个文件名字。$?
:所有比目标文件新的依赖文件的集合,以空格分隔。$^
:所有依赖文件的集合,以空格分隔,该变量会去除重复的文件。$+
:类似$^
,也是依赖文件的集合,不过该变量不会去重。$*
:表示目标模式中,%
及其之前的部分
重载内建隐含规则
我们可以重载内建的隐含规则或事定义一个全新的隐含规则。例如,我们可以重新构造和内建隐含规则不同的命令:
1 | %.o : %.c |
或者也可以取消内建的隐含规则,只要不再后面写命令就行:
1 | %.o : %.s |
Makefile 更新函数库文件
函数库文件也就是对 Object 文件(程序编译的中间文件)的打包文件。在 UNIX 下,一般是通过 ar
命令来完成打包工作。
函数库文件的成员
一个函数库文件通常由多个中间文件组成,我们可以使用如下个是来指定函数库文件及其组成:
1 | archive(members ... ) |
这不是一个命令,而是一个目标和依赖的定义。一般来说,这种用法就是为了 ar
命令来服务的。例如:
1 | foolib(hack.o) : hack.o |
如果要指定多个 member 则使用空格分隔。
注意事项
在进行函数库打包文件时,需要小心使用 make 的并行机制(也即使用 -j
参数)。如果多个 ar
命令在同一时间运行在同一个函数库打包文件上,就很有可能损坏这个函数库文件。
make 的工作方式
GNU 的 make 工作时的执行步骤如下:
- 读入所有的 Makefile 文件;
- 读入被 include 的其他 Makefile 文件;
- 初始化文件中的变量;
- 推导隐晦规则,并分析所有规则;
- 为所有的目标文件创建依赖关系链;
- 根据依赖关系,决定哪些目标需要重新生成;
- 执行生成命令;