make 基础教程

make 是一个 Linux 环境下的构建工具,主要用于管理 C 语言项目。但是实际上,make 不仅仅用来编译源代码,也可以完成一些其他功能。任何只要某个文件发生变化,就需要重新构建的项目,都可以使用 make 来管理。

make 介绍

新建项目后进入到项目目录,在命令行中执行 make,并不会有任何作用。make 本身只是一个命令行工具,它不知道该如何构建项目,需要明确告诉 make 以何种规则来编排项目,比如需要将文件 main.c 编译为 main.o 文件,make 需要知道如下规则:

1
2
main.o: main.c
cc -c main.c
当使用 make 来进行构建时,实际上 make 做了以下几件事情:

  • 确认 main.c 文件存在
  • 如果存在 main.o 文件,且 main.o 文件最后修改的时间戳要比 main.c 晚,不会重新编译
  • 如果不存在 main.o 文件,或者 main.o 文件最后修改的时间戳要比 main.c 早,进行编译

一个工程可能有很多这样的规则,这些规则会被写到一个文件中,make 依赖这个文件来进行构建。make 默认会按照 GNUmakefile、makefile、Makefile 的名称顺序来查找该文件,推荐使用 Makefile 来命名。如果需要自定义名称,也可以通过命令行参数来指定:

1
2
3
make -f otherName.txt
# 或者
make --file=otherName.txt

Makefile规则

Makefile 文件包含一系列规则。每条规则的格式如下:

1
2
<target>: <prerequisites>
[tab] <command>
target 是规则的目标,prerequisites 是前置条件,command 是要执行的命令。目标是必需的,前置条件和命令都是可选的,但是两者之中必须至少存在一个。每条规则就是描述在什么前置条件下,该如何构建目标。

目标(target)

规则的目标通常是最后需要生成的文件名,比如上述的 main.o。当然除了文件名,目标也可以是某个执行操作的名称,我们称这样的目标为“伪目标”。比如:

1
2
clean:
rm *.o
clean 不是一个文件名,而是一个执行操作的名称,它是一个伪目标,它的作用是删除工作目录中所有后缀为 o 的对象文件。当然,上面的声明仅仅是概念上的区分。在工作目录中执行 make clean,如果工作目录中存在文件 clean,这条命令将不会执行。因为 make 发现 clean 文件已经存在,就不会重新进行构建了。为了避免出现这种情况,我们可以明确声明 clean 是“伪目标”,写法如下:
1
2
3
.PHONY: clean
clean:
rm *.o
默认情况下,make 执行 Makefile 中的第一个规则,此规则的第一个目标称之为“最终目的”或者“终极目标”,有点像是 grunt 或者 gulp 中的 default。

前置条件(prerequisites)

前置条件通常是一组用空格分隔的文件名。它表明了两件事情:一是此规则目标依赖哪些目标文件来触发重新构建,二是重建此规则目标需要先重建哪些规则目标。比如有以下规则:

1
2
3
4
5
6
7
8
main: 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
main 的前置条件是 main.o 和 util.o。在构建目标 main 前,make 会先构建目标 main.o 和 util.o。如果无需构建 main.o 和 util.o,换句话说,也就是存在文件 main.o 和 util.o,且它们的最后修改时间戳最新,如果文件 main 存在,规则 main 也无需被构建。

命令(command)

规则的命令由一些 shell 命令组成,它们被一条一条的执行。命令表明了规则具体要做哪些操作,通常是更新目标文件。每条命令必须以 [tab] 字符开始,多个命令行之间可以有空行和注释行(空行是不包括任何字符的一行),在执行规则时空行会被忽略。如果希望用其他字符来代替 [tab],可以指定 .RECIPEPREFIX

1
2
3
4
# 使用 > 来代替 [tab] 字符
.RECIPEPREFIX = >
main.o: main.c
> cc -c main.c
需要注意的是每行命令的执行是在一个独立的 shell 进程中完成的。因此多行命令之间的执行是相互独立,不存在依赖的。当规则的命令中存在使用 cd 来改变工作目录时,其结果并不会对往后的命令产生任何影响。有以下几种方式来解决:
1
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]"
命令在运行结束之后,make 会检测命令执行的返回状态,如果返回成功,make 会启动下一个子 shell 来执行下一条命令。如果某条命令出错了(返回非0状态),make 就会放弃对当前规则往后命令的执行。可以在命令之前加一个减号“-”来告诉 make 忽略此命令的执行失败,比如:
1
2
clean:
-rm *.o

Makefile 变量

Makefile 允许变量定义,变量是使用一个字符或字符串来代表一段字符串,有点类似于 C 语言的宏。Makefile 中规则的目标、前置条件和命令都可引用变量。变量的引用比较简单,引用一个变量的形式是:$(VAR_NAME)${VAR_NAME}

在定义一个变量时,变量名不能包含 :#=、前置空白和尾空白。通常使用字母、数字和下划线来定义变量。同时,变量名是大小写敏感的。

变量定义

Makefile 支持几种变量定义的方式,我们来一一说明。

递归展开式变量

使用等号来定义变量,格式为:VAR_NAME = value。这种变量在引用的地方是严格的文本替换过程,变量值的字符串会原样的出现在引用它的地方。比如下面例子:

1
2
3
4
5
6
a = $(b)
b = $(c)
c = c

main:
echo $(a)
执行 make 会输出 c 。整个变量的替换过程是这样的:首先 $(a) 被替换为 $(b),接着 $(b) 被替换为 $(c),最后 $(c) 被替换为 c。整个替换过程是在执行 echo $(a) 时完成的。这种方式定义的变量可以引用其后定义的变量,并且支持递归展开,所以也称之为递归展开变量。也正因为如此,此风格的变量定义会导致 make 陷入到无限的变量展开过程中。比如:
1
2
a = $(b)
b = $(a)

直接展开式变量

另外一种变量定义是使用 := ,格式为:VAR_NAME := value。这种变量的变量值是在定义变量时被展开的,所以变量被定义后就是一个实际需要的文本串,其中不再包含任何变量的引用。比如:

1
2
3
a := a
b := $(a) b
a := after
其等价于:
1
2
b := a b
a := after
和递归式展开变量不同,此风格变量在定义时就完成了对所引用变量的展开,因此不能引用其后定义的变量。

多行定义

指示符 define 可以定义一个包含多行字符串的变量,格式为:

1
2
3
4
define VAR_NAME
command
command
endef

条件赋值

Makefile 可以使用 ?= 来进行条件赋值,格式为 VAR_NAME ?= value。只有当变量在之前没有赋值的情况下才会对这个变量进行赋值。比如:

1
a ?= b
其含义是:如果变量 a 在之前没有定义,就给它赋值 b,否则不改变它的值。

追加变量值

Makefile 使用 += 来追加变量值,格式为 VAR_NAME += value。这使得我们可以先定义一个通用变量,然后在别的地方追加值。比如:

1
2
objects = a.o b.o
objects += c.o
相当于:
1
2
objects = a.o b.o
objects := $(objects) c.o
执行操作之后,objects 的值为 “a.o b.o c.o”。

override 指示符

在执行 make 时,如果通过命令行定义了一个变量,它将替代在 Makefile 中出现的同名变量。比如:

1
2
3
a = a
main:
echo $(a)
执行命令 make a=c,终端会输出 c。如果不希望发生这种替代,只需要在变量定义前加上 override 即可。

Makefile 条件判断

Makefile 使用 ifeqifneqifdefifndef 来进行条件判断,格式为:

1
2
3
4
5
CONDITIONAL-DIRECTIVE
TEXT-IF-TRUE
else
TEXT-IF-FALSE
endif
比如以下例子:
1
2
3
4
5
6
7
8
9
foo = 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)
替换 “feet on the street” 中的 “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函数 wildcard 函数用于获取匹配模式文件名,函数签名为 $(wildcard pattern)。比如:
1
$(wildcard *.c)
返回值为当前目录下所有 .c 源文件列表。

Makefile 自动化变量

Makefile 除了常规的规则之外,还支持模式规则,模式规则最大的特点就是目标名中包含有模式字符”%”,该字符用来模糊匹配文件名。比如模式规则 %.o: %.c,它表示的含义是所有的 .o 文件依赖于对应的 .c 文件。因为不能使用具体的文件名,规则命令中将无法引用需要操作的文件,为了解决这个问题,Makefile 内置了一些自动化变量。

$@ 表示 make 命令当前构建的目标名。比如:

1
2
a.txt b.txt:
touch $@
在构建过程中,$@ 分别指代 a.txt 和 b.txt。等同于:
1
2
3
4
a.txt:
touch a.txt
b.txt:
touch b.txt
$< 规则的第一个依赖文件名。比如 main.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
#include <stdio.h>
#include "util.h"

void hello() {
printf("Hello, Make!");
}

// main.c
#include "util.h"

int main() {
hello();
return 0;
}
编写 Makefile 内容为:
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

# 定义伪目标
.PHONY: clean
clean:
# 命令前的减号表示忽略rm命令出现的错误
-rm main $(objects)
执行 make,会编译输出 main,运行 ./main,终端会输出 Hello, Make!

参考