make 基础教程
make 是一个 Linux 环境下的构建工具,主要用于管理 C 语言项目。但是实际上,make 不仅仅用来编译源代码,也可以完成一些其他功能。任何只要某个文件发生变化,就需要重新构建的项目,都可以使用 make 来管理。
make 介绍
新建项目后进入到项目目录,在命令行中执行 make
,并不会有任何作用。make 本身只是一个命令行工具,它不知道该如何构建项目,需要明确告诉 make 以何种规则来编排项目,比如需要将文件 main.c 编译为 main.o 文件,make 需要知道如下规则:
1
2main.o: main.c
cc -c main.c
- 确认 main.c 文件存在
- 如果存在 main.o 文件,且 main.o 文件最后修改的时间戳要比 main.c 晚,不会重新编译
- 如果不存在 main.o 文件,或者 main.o 文件最后修改的时间戳要比 main.c 早,进行编译
一个工程可能有很多这样的规则,这些规则会被写到一个文件中,make 依赖这个文件来进行构建。make 默认会按照 GNUmakefile、makefile、Makefile 的名称顺序来查找该文件,推荐使用 Makefile 来命名。如果需要自定义名称,也可以通过命令行参数来指定:
1
2
3make -f otherName.txt
# 或者
make --file=otherName.txt
Makefile规则
Makefile 文件包含一系列规则。每条规则的格式如下:
1
2<target>: <prerequisites>
[tab] <command>
目标(target)
规则的目标通常是最后需要生成的文件名,比如上述的 main.o。当然除了文件名,目标也可以是某个执行操作的名称,我们称这样的目标为“伪目标”。比如:
1
2clean:
rm *.omake clean
,如果工作目录中存在文件 clean
,这条命令将不会执行。因为 make 发现 clean 文件已经存在,就不会重新进行构建了。为了避免出现这种情况,我们可以明确声明 clean 是“伪目标”,写法如下:
1
2
3
clean:
rm *.o
前置条件(prerequisites)
前置条件通常是一组用空格分隔的文件名。它表明了两件事情:一是此规则目标依赖哪些目标文件来触发重新构建,二是重建此规则目标需要先重建哪些规则目标。比如有以下规则:
1
2
3
4
5
6
7
8main: main.o util.o
cc main.o util.o -o main
main.o: main.c
cc -c main.c
util.o: util.c util.h
cc -c util.c
命令(command)
规则的命令由一些 shell 命令组成,它们被一条一条的执行。命令表明了规则具体要做哪些操作,通常是更新目标文件。每条命令必须以 [tab] 字符开始,多个命令行之间可以有空行和注释行(空行是不包括任何字符的一行),在执行规则时空行会被忽略。如果希望用其他字符来代替 [tab],可以指定 .RECIPEPREFIX:
1
2
3
4# 使用 > 来代替 [tab] 字符
.RECIPEPREFIX = >
main.o: main.c
> cc -c main.c1
2
3
4
5
6
7
8
9
10
11
12
13
14# 方法一:将命令写在一行,用分号分隔
var-kept:
export foo=bar; echo "foo=[$$foo]"
# 方法二:使用反斜杠(\)来连接
var-kept:
export foo=bar; \
echo "foo=[$$foo]"
# 方法三:使用 .ONESHELL,该方式在GNU Make 3.82以上支持
.ONESHELL:
var-kept:
export foo=bar
echo "foo=[$$foo]"1
2clean:
-rm *.o
Makefile 变量
Makefile 允许变量定义,变量是使用一个字符或字符串来代表一段字符串,有点类似于 C 语言的宏。Makefile 中规则的目标、前置条件和命令都可引用变量。变量的引用比较简单,引用一个变量的形式是:$(VAR_NAME)
或 ${VAR_NAME}
。
在定义一个变量时,变量名不能包含 :
、#
、=
、前置空白和尾空白。通常使用字母、数字和下划线来定义变量。同时,变量名是大小写敏感的。
变量定义
Makefile 支持几种变量定义的方式,我们来一一说明。
递归展开式变量
使用等号来定义变量,格式为:VAR_NAME = value
。这种变量在引用的地方是严格的文本替换过程,变量值的字符串会原样的出现在引用它的地方。比如下面例子:
1
2
3
4
5
6a = $(b)
b = $(c)
c = c
main:
echo $(a)$(a)
被替换为 $(b)
,接着 $(b)
被替换为 $(c)
,最后 $(c)
被替换为 c。整个替换过程是在执行 echo $(a)
时完成的。这种方式定义的变量可以引用其后定义的变量,并且支持递归展开,所以也称之为递归展开变量。也正因为如此,此风格的变量定义会导致 make 陷入到无限的变量展开过程中。比如:
1
2a = $(b)
b = $(a)
直接展开式变量
另外一种变量定义是使用 :=
,格式为:VAR_NAME := value
。这种变量的变量值是在定义变量时被展开的,所以变量被定义后就是一个实际需要的文本串,其中不再包含任何变量的引用。比如:
1
2
3a := a
b := $(a) b
a := after1
2b := a b
a := after
多行定义
指示符 define 可以定义一个包含多行字符串的变量,格式为:
1
2
3
4define VAR_NAME
command
command
endef
条件赋值
Makefile 可以使用 ?=
来进行条件赋值,格式为 VAR_NAME ?= value
。只有当变量在之前没有赋值的情况下才会对这个变量进行赋值。比如:
1
a ?= b
追加变量值
Makefile 使用 +=
来追加变量值,格式为 VAR_NAME += value
。这使得我们可以先定义一个通用变量,然后在别的地方追加值。比如:
1
2objects = a.o b.o
objects += c.o1
2objects = a.o b.o
objects := $(objects) c.o
override 指示符
在执行 make 时,如果通过命令行定义了一个变量,它将替代在 Makefile 中出现的同名变量。比如:
1
2
3a = a
main:
echo $(a)make a=c
,终端会输出 c。如果不希望发生这种替代,只需要在变量定义前加上 override 即可。
Makefile 条件判断
Makefile 使用 ifeq
、ifneq
、ifdef
、ifndef
来进行条件判断,格式为:
1
2
3
4
5CONDITIONAL-DIRECTIVE
TEXT-IF-TRUE
else
TEXT-IF-FALSE
endif1
2
3
4
5
6
7
8
9foo = bar
ifeq ($(foo),bar)
foo_defined = yes
else
foo_defined = no
endif
main:
echo $(foo_defined)make
,终端会输出 yes。这里有几个需要注意地方:
- 条件判断
($(foo),bar)
中的逗号两边没有空格 - 条件体中的开头不能是 tab 字符,否则会被当作 Makefile 规则的一部分发送到 shell。这里的
foo_defined
前面使用了两个空格符来进行缩进
Makefile 函数
Makefile 可以定义函数,函数调用的展开和变量引用的展开方式相同。函数的调用语法为:
1
2
3$(FUNCTION ARGUMENTS)
# 或
${FUNCTION ARGUMENTS}FUNCTION
是函数名称,如果是自定义函数,需要使用内置的 call
来间接调用。ARGUMENTS
是函数参数,多个参数用逗号隔开。Makefile 提供了很多内置函数,我们这里只是列出常用的几个。
subst函数
subst 是字符串替换函数,其函数签名为 $(subst from,to,text)
。比如:
1
$(subst ee,EE,feet on the street)
patsubst函数
patsubst 函数用于模式匹配的替换,函数签名为 $(patsubst pattern,replacement,text)
。比如下面的例子将字符串 “x.c.c bar.c” 替换成 “x.c.o bar.o”。
1
$(patsubst %.c,%.o,x.c.c bar.c)
$(wildcard pattern)
。比如:
1
$(wildcard *.c)
Makefile 自动化变量
Makefile 除了常规的规则之外,还支持模式规则,模式规则最大的特点就是目标名中包含有模式字符”%”,该字符用来模糊匹配文件名。比如模式规则 %.o: %.c
,它表示的含义是所有的 .o 文件依赖于对应的 .c 文件。因为不能使用具体的文件名,规则命令中将无法引用需要操作的文件,为了解决这个问题,Makefile 内置了一些自动化变量。
$@
表示 make 命令当前构建的目标名。比如:
1
2a.txt b.txt:
touch $@1
2
3
4a.txt:
touch a.txt
b.txt:
touch b.txtmain.o: main.c util.c
,$< 指代的是 main.c。
$?
所有比目标文件更新的依赖文件列表,空格分隔。比如规则 main.o: main.c util.c
,如果 util.c 比 main.o 更新,$? 就指代 util.c。
$^
规则的所有依赖文件列表,空格分隔。比如规则 main.o: main.c util.c
,$^ 指代 “main.c util.c”。
$* $* 指代匹配符 % 匹配的部分,比如 % 匹配 main.c 中的 main,$* 就指代 main。
一个例子
一个简单的 C 语言项目,分别有 main.c、util.c、util.h 三个文件,文件内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// util.h
void hello();
// util.c
void hello() {
printf("Hello, Make!");
}
// main.c
int main() {
hello();
return 0;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17# 定义变量
objects = main.o util.o
main: $(objects)
cc $(objects) -o main
# 使用 Makefile 的隐含规则,同下面的写法:
# main.o:
# cc -c main.c
main.o: main.c
util.o: util.c
# 定义伪目标
clean:
# 命令前的减号表示忽略rm命令出现的错误
-rm main $(objects)make
,会编译输出 main,运行 ./main
,终端会输出 Hello, Make!
。