Makefile 教程
1 Makefile 是什么
Makefile 是一种自动化构建脚本,其包含若干目标和对应的依赖和构建规则。 只需要在 Makefile 的目录下执行如下命令:
Make 就会根据 Makefile 中对应目标的依赖和构建规则一步一步构建出我们需要的目标。
2 Makefile 的规则
2.1 规则的形式
一个 Makefile 脚本文件的主体是规则。每条规则的形式如下:
其中,各占位符的含义如下:
- target:需要构建的目标,可以是一个可执行文件,也可以是一个标签,用来指定执行一系列命令;
- prerequisites:生成目标所依赖的文件或目标。如果依赖没有满足,那么会先根据依赖项的构建规则生成依赖项,然后再生成目标;
- command:是一个 Shell 命令,每条规则可以有多条命令,每条命令占一行。如果同一行有多条命令,那么需要使用 && 分隔;
- tab:若干个 tab。Makefile 规则的命令必须以 tab 字符开始。
这里我们给出一个 Makefile 规则示例:
这个规则表示:main.o 依赖于 main.c,若依赖满足,则执行命令 gcc -c main.c -o main.o
。
2.2 使用通配符的规则
Makefile 中使用 %
作为目标中的通配符。
例如:
就指定,对以 .o
作为后缀的目标,使用此条规则构建,所要求的依赖为同名的、以 .c
作为后缀名的文件。
规则中的 $<
和 $@
是 Makefile 中的自动变量,将会在后面的小节中介绍。
通配符可以使我们不用关心具体的文件名,对同一类目标执行相同的构建命令。
同一类目标中的特例?
若要对同一类中的某一个目标执行不同的命令,也可书写一条新的规则,这条规则有着更高的优先级。例如:
执行 make foo.echo
和 make bar.echo
的输出是不同的:
阅读 Makefile 的技巧
在阅读一个较为复杂的 Makefile 文件时,可以先查看 Makefile 中的目标规则,关注目标规则中的依赖关系和命令。 这样可以更快地理解 Makefile 的内容。
3 Makefile 的变量
3.1 变量的定义
Makefile 中的变量可以用于存放命令、选项、文件名等。变量的定义格式如下:
各占位符的含义如下:
- var-name:变量名,可以由字母、数字和下划线组成,但不能以数字开头;
- assignment:赋值运算符,这将在下一小节介绍;
- var-value:变量值,可以由任意字符组成,如果变量值中包含了空格,那么需要用引号将变量值括起来。
其中,变量名与赋值运算符、赋值运算符与变量值之间可以有若干空格。
3.2 变量的使用
变量使用的格式有两种:
这两种格式的效果完全相同。
3.3 示例
下面给出一个 Makefile 变量使用的示例,示例中的用法是较为常见的:
CC = gcc
CFLAGS = -Wall -g
MAIN_OBJS = main.o
$(MAIN_OBJS) : main.c
$(CC) $(CFLAGS) -c main.c -o $(MAIN_OBJS)
这个 Makefile 中定义了两个变量 CC 和 CFLAGS,然后在目标规则中使用了这两个变量。 这样做的好处是,如果我们需要更换编译器或者编译选项,那么只需要修改变量的值即可,而不需要修改目标规则。 在多个目标规则共用同一编译策略的情况下其优势会进一步凸显。
Makefile 变量的本质
Makefile 中所有的变量本质上都是字符串,即使使用了整型或其它类型字面量对其赋值,其仍然会被当作字符串存储。
例如,使用如下方式定义一个 STRING
变量:
则 STRING
中的值为 "This is a string."
,而并非 This is a string.
。
小心尾随空格
在定义变量和赋值时,Makefile 会裁剪掉赋值运算符后面,变量值前面的空格。 然而,行尾的空格并不会被裁剪掉。例如:
这里 HOME
变量会被赋值为 /Users/ubuntu
,$(HOME)/porject1
则会被替换成 /Users/ubuntu /project1
,这显然不是我们希望的结果。
因此,请务必小心变量赋值时的尾随空格。
4 Makefile 变量的赋值
Makefile 中的赋值运算符有四种,分别是 =
, :=
, ?=
和 +=
。其中,=
, :=
和 ?=
是覆盖变量值的赋值运算符,而 +=
则是用于给变量追加值的赋值运算符。
此外,也可以在命令中指定环境变量和参数变量的值。
4.1 =
赋值运算符
=
赋值运算符是最常用的赋值运算符,它的格式如下:
这个赋值会在 Makefile 全部展开后进行,对同一个变量的多次 =
赋值会使其值为最后一次所赋的值。例如:
这里 CC2
的值为 g++
,而不是 gcc
。因为 CC2
的赋值是在 Makefile 全部展开后进行的,此时 CC
的值为 g++
。
4.2 :=
赋值运算符
:=
赋值运算符的格式如下:
这个赋值和其在 Makefile 中的位置有关,比较符合一般的赋值逻辑。例如:
CC2
的赋值是在 Makefile 展开到当前时进行的,而此时 CC
的值为 gcc
,故 CC2
被赋值为 gcc
。
4.3 ?=
赋值运算符
?=
赋值运算符的格式如下:
这个赋值的特点是,如果变量已经被赋值,那么就不会重新赋值,否则就会赋值。例如:
这里的第一条赋值语句会覆盖第二条赋值语句,最终 CC
的值为 gcc
。
4.4 +=
赋值运算符
+=
赋值运算符的格式如下:
这个赋值会将新的值拼接到旧值的后面,并在中间补空格。例如:
则 CC
的值为 gcc -Wall
。
4.5 在命令行中对环境变量和参数变量赋值
在命令行中设定环境变量值的格式如下:
与之相对地,设定参数变量值的格式如下:
例如,对如下的 Makefile:
在其所在目录下执行下列命令:
会输出:
值得注意的是,在命令行中设定环境变量或参数变量的值时,若所赋的值中包含空格,则需要使用 "
包裹。
环境变量与参数变量的区别
咋一看,环境变量和参数变量是完全一样的,只是在执行时处在命令的不同位置。 事实上,它们还是有一定的区别:两者的覆盖性不同。
例如以下的 Makefile:
使用环境变量和使用参数变量对 VAR
赋值,输出的结果是不同的:
总体来说,可以将各种赋值方式按照覆盖性从高到低的顺序排列如下:
参数变量赋值 > =
/:=
/+=
赋值 > 环境变量赋值 > ?=
赋值
通常的做法是,将需要使用命令行传入的参数使用 ?=
赋值,并在命令行中使用环境变量修改为所需的值。
5 Makefile 中的自动变量
为了简便地书写规则,Makefile 提供了一些自动变量吗,常用的有 $@
, $*
, $<
, $^
和 $?
。
5.1 $@
$@
表示构建目标,例如:
此处 $@
就表示目标 main.o
。
5.2 $*
$*
表示构建目标去掉后缀的部分,例如:
此处 $*
就表示目标 build/main.o
去掉后缀的部分,即 build/main
。
5.3 $<
$<
表示第一个依赖文件,例如:
此处 $<
就表示第一个依赖文件,即 main.o
。
5.4 $^
$^
表示所有的依赖文件,例如:
这里的 $^
就表示所有的依赖文件,即 main.o
和 func.o
。
5.5 $?
$?
符号表示比目标文件更新的所有依赖文件,例如:
这里的 $?
就表示比目标文件 main
更新的所有依赖文件。
若 main.o
或 func.o
比 main
的更新,则会被包含在 $?
中。
6 Makefile的函数
Makefile 中的函数提供了丰富多样的功能,函数的格式如下:
其中,函数名和第一个参数之间用空格分隔,参数之间使用逗号分隔。 下面将介绍一些 Makefile 中常用的函数:
6.1 abspath
函数
abspath
函数用于取绝对路径,例如:
这里的 $(abspath ./build)
表示取 ./build
的绝对路径。
这个函数的主要用途是避免在不同位置使用同一个 Makefile 时,找不到目录或文件的问题。
6.2 addprefix
函数
addprefix
函数用于给一系列字符串添加前缀,例如:
OBJS := main.o func.o
BUILD_DIR := ./build
BUILD_OBJS := $(addprefix $(BUILD_DIR)/, $(OBJS))
这里的 $(addprefix $(BUILD_DIR)/, $(OBJS))
表示给OBJS中的每个字符串都添加前缀 $(BUILD_DIR)/
,结果为 ./build/main.o ./build/func.o
。
6.3 addsuffix
函数
addsuffix
函数用于给一系列字符串添加后缀,例如:
这里的 $(addsuffix .o, $(OBJS))
表示给OBJS中的每个字符串都添加后缀 .o
,结果为 main.o func.o
。
6.4 basename
函数
basename
函数用于去掉文件名中的后缀名,例如:
这里的 $(basename $(OBJS))
表示去除 OBJS
中每个文件名的后缀名,结果为 ./build/main ./build/func
。
6.5 dir
函数
dir
函数用于获取文件名中的目录部分,例如:
这里的 $(dir $(OBJS))
表示获取 OBJS
中每个字符串的目录部分,即 ./build/ ./build/
。
6.6 notdir
函数
notdir
函数用于获取文件名中的非目录部分,例如:
这里的 $(notdir \$(OBJS))
表示获取 OBJS
中每个字符串的非目录部分,即 main.o func.o
。
6.7 shell
函数
shell
函数用于执行 Shell 命令,例如:
这里的 $(shell pwd)
表示执行 pwd
命令,其结果为 pwd
命令输出的当前工作目录。
6.8 wildcard
函数
wildcard
函数用于获取符合通配符的所有文件,例如:
这里的 \$(wildcard *.c)
表示获取当前目录下的所有后缀名为 .c 的文件。
7 Makefile 的条件分支
我们可以使用 ifeq
, ifneq
, ifdef
等关键字控制 Makefile 的条件分支,例如:
此处的 ifeq (\$(CC), gcc)
表示如果 CC
的值等于 gcc
,那么就执行此分支的语句,否则执行 else
分支的语句。
与 C 语言不同的是,我们需要使用 endif
表示结束 if
语句。
8 Makefile 的互相包含
Makefile 可以互相包含,这样可以将一些常用的规则写在一个 Makefile 中,然后在其他 Makefile 中包含这个 Makefile,以提升代码的复用性。Makefile 的包含格式如下:
需要注意的是,include 命令会将被包含的 Makefile 在当前位置完全展开。 可以利用这个性质在 Makefile 使用一些没有定义的变量,而将其定义放在需要包含此 Makefile 的别的 Makefile 中相应的 include 命令前。 例如:
这样一来,compile.mk 中就可以直接使用此 Makefile 中的 CC
变量了。
9 Makefile 的运行
Makefile 可以用 make 命令运行。make 命令的格式如下:
环境变量、参数变量、构建选项、目标都是可选的。 当没有目标时,通常会自动构建 Makefile 中的第一个目标。
常用的构建选项有:
-f <file-path>
:指定 Makefile 文件。如果不指定,则默认使用当前目录下的 Makefile 文件;-C <directory>
:指定 Makefile 工作目录。此操作会相当于先将工作目录转移至目标目录,执行make
,再回到当前目录;-s
:静默模式,不输出 Makefile 规则中的命令;-n
:只输出要执行的命令,但是不执行。-B
:强制执行所有的目标。-j <nproc>
:指定并行执行的任务数。
如何关闭冗长的命令显示?
在不加 -s
选项时,构建 Makefile 中的目标时会将执行的命令也输出到命令行,这样会导致输出信息过多。
如果想要禁用此输出,可以在 Makefile 的目标规则的命令前添加一个 @
,例如对于如下的 Makefile:
执行 make foo
与执行 make bar
会有不同的输出:
前者输出了所执行的 echo
命令,而后者则没有输出。