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