makefile基础介绍
简介
Makefile 是一个文本文件,描述了一个或者多个目标-依赖关系以及目标对应的生成/更新(后续统一称为更新)规则。
Makefile 由 make 命令一键解析并处理。
通过 Makefile+make 的支持,我们可以以层级的方式组织项目的成百上千个代码文件,并达到增量编译的目的、以减少编译时间。
本文的阅读方式
本文只列出了我们工作中常用的 Makefile 语法,还有一些高级语法及特性没有没有讲解。
本文出现的示例代码中:
$为命令行提示符,意为等待用户输入#开头的为注释,也是 Makefile中 的注释符号- 部分示例可以直接保存到文件中运行,行首的的空格需要改为 Tab,否则编译会提示
missing separator. Stop.
Makefile 基本组成
Makefile 由以下的基本结构外加其他一些特性(如变量定义、文件包含等)组成:
1 | target(s) : prerequisite(s) |
我们通过以下一个简单的Makefile来讲解这个基本的结构:
Makefile
1 | main: main.o libtest.a test.h |
main.c
1 | #include <stdio.h> |
test.c
1 | #include <stdio.h> |
test.h
1 | #ifndef _TEST_H_ |
该 Makefile支 持的功能有:
- 执行
make或make main:编译 main.c 更新可执行程序 main - 执行
make libtest.a:编译 test.c 更新静态库 libtest.a - 执行
make clean:删除中间产物 main.o、test.o、libtest.a 及可执行程序 main
目标列表 targets
什么可以作为目标:
- 一个文件,如可执行程序 main、静态库 libtest.a
- 一个动作,如删除动作 clean
- clean 动作未对应实际文件,因此也称作伪目标
- 为避免当磁盘上存在一个名为 clean 文件时,目标 clean 对应规则无法执行,可通过
.PHONY特殊目标将 clean 目标声明为伪目标
Makefile 可支持多个目标,Makefile 中出现的第一个目标,是 make 命令默认更新的目标。
所以我们在更新 main 的时候,可以不用指定 main 目标,但要执行 clean 动作或者更新 libtest.a 时,就要在 make 时明确指定目标了:
1 | $ make # 更新main无需指定 |
依赖列表 prerequisites
什么可以作为目标的依赖:
- 一个或多个文件,如 main.o
- 另外的一个或多个目标,如 libtest.a
- 依赖可以为空
make 命令通过目标-依赖这个组合达到增量更新的目的:
- 依赖的文件修改时间比目标文件更新(包含目标文件不存在),才触发执行规则
- main.o、libtest.a 或 test.h 有一个比可执行文件 main 更新,则触发执行
cc -o main main.o -ltest命令 - test.c 比 libtest.a 新,则触发执行对应
cc和ar命令
- main.o、libtest.a 或 test.h 有一个比可执行文件 main 更新,则触发执行
- 如依赖是一个或多个目标,且依赖需要更新,则按书写顺序先递归更新依赖
- 判断是否需要更新 main 时,要先依次判断依赖 main.o 和 libtest.a 是否需要更新
- 特别的,伪目标没有对应文件,不管依赖文件是否更新,必然触发执行规则
- clean 是伪目标,更新该目标时必然触发
rm操作
- clean 是伪目标,更新该目标时必然触发
注意:C/C++ 头文件未出现在依赖中可能引发运行错误
对于 C/C++ 代码,编写的 Makefile 中依赖关系通常没有包含头文件,如果涉及头文件更新(如结构体新增一个字段),会导致包含该头文件的源文件没有被重新编译,进而导致运行时出错(不同源文件中结构体的大小不一致)。
如果涉及有头文件更新,保险起见建议 clean 清除最终及中间产物后重新编译(或通过 -B 选项强制重建所有目标)。
我们修改 Makefile 删除第一行 main 对 test.h 的依赖,TEST 结构体新增一个变量,然后 touch test.c,然后更新并执行 main 看会出现什么问题:
修改后 Makefile:
1 | main: main.o libtest.a |
修改后 test.h:
1 | ... |
执行结果:
1 | $ make |
执行命令 commands
执行命令是更新目标所执行的动作,由一组Shell命令组成,它们被顺序执行。需要注意:
- 除了第一条命令可以跟在依赖列表之后、以分号分隔之外,命令必须以水平制表符
TAB开头,否则会引发语法错误:
libtest.a 更新规则可改写为(但一般不会这么做,命令单独书写看起来更美观)
1 | libtest.a: test.c test.h; cc -c -o test.o test.c |
将 Makefile 第9行编译 test.c 命令的 cc 前的 TAB 替换成空格:
1 | libtest.a: test.c test.h |
执行结果:
1 | $ make |
每一行命令都在一个子 Shell 中执行,每一个子 Shell 都通过
fork一个新的进程来执行,各个子Shell的运行结果不互相影响,因此特别注意:- 相互依赖的命令需要写在同一行
看如下 Makefile 的执行的结果是否符合你的预期:
我想在 /home/tp 目录下的 Makefile 中,获取到 /home/tp/test 目录的绝对路径:
1
2
3
4
5
6
7
8.PHONY: test
test:
# 正确做法
@cd test; pwd
# 错误做法
@cd test
@pwd运行结果:
1
2
3$ make
/home/tp/test
/home/tp- Shell 的
if、for等结构通过行连接符\连接成一行,否则会有语法错误:
我想在 1 小于 2 的情况下打印”1 < 2”,但如下 Makefile 将在
make时报错:1
2
3
4
5.PHONY: test
test:
if [ 1 -lt 2 ]; then
echo "1 < 2";
fi执行结果:
1
2
3
4
5$ make
if true; then
/bin/sh: -c: line 1: syntax error: unexpected end of file # if true; then和fi孤立存在,Shell if结构不完整导致报错
Makefile:3: recipe for target 'test' failed # 报错在Makefile第三行的Shell if
make: *** [test] Error 1修改后的 Makefile:
1
2
3
4
5.PHONY: test
test:
if [ 1 -lt 2 ]; then \
echo "1 < 2"; \
fi执行结果:
1
2$ make
1 < 2命令组不能孤立存在,必须置于目标下或命令包内,否则会出现语法错误
我们在 Makefile 的开始加上一行 echo,看会出现什么问题
1 | 1 echo "test" |
运行结果:
1 | $ make |
定义命令包 - 中阶
书写 Makefile 时,可能有多个目标的生成会使用相同的一组命令。我们可以把这一组命令定义成一个命令包,然后各个目标的执行规则中通过 $(cmd-pack) 调用对应命令包就可以。
命令包的定义结构:
1 | define cmd-pack |
示例:
1 | define cc_compile |
执行结果:
1 | $ make |
Makefile 的执行过程
当我们在命令行中执行 make 命令时,后台执行了以下过程:
- 读入 Makefile
- 读入被
include的其它 Makefile - 初始化文件中的变量
- 推导隐含规则,并分析所有规则
- 为所有的目标文件创建依赖关系链
- 根据依赖关系,决定哪些目标要重新生成
- 执行规则更新目标。
我们以上的介绍中只介绍了 4-7 步骤相关的内容,接下来介绍 1-3 相关的内容。
文件包含 - include命令 - 中阶
我们使用 include命令包含其他文件,包括且不限于其他形式的Makefile,也可以是普通文本文件,一般我们会以 .mk 为后缀命名。
这些文件一般包含变量定义及函数定义,类似于C语言的 #include 操作。
include file 之后我们就可以使用 file 中的定义了。
变量定义与使用 - 中阶
变量定义
变量定义常用有两种方式,他们的区别是:
- variable = value,递归展开式变量定义
递归展开的含义是:在定义变量时,如果其包含对其它变量或函数的引用,则在引用此变量时才对其递归展开、进行文本替换。
例:
1 | a = foo |
执行结果:
1 | $ make |
- variable := value,直接展开式变量定义
直接展开的意思是,在定义变量时,变量值中对其他量或者函数的引用在定义变量时就已经被展开、进行文本替换了。
例:
1 | a := foo |
执行结果:
1 | $ make |
更多的赋值方式(条件赋值 ?= 和 追加赋值 +=)可参考
变量使用
如以上例子显示,通过 $(val) 或 ${var} 方式引用变量值。
由于 Makefile 的执行规则中大部分都是 Shell 相关命令,Shell 命令中也可能包含变量定义。在 Makefile 的 Shell 语句中访问 Shell 变量时,需要对 $ 进行转义,即通过 $$var 来访问Shell变量。
如果要使用
$的字面值,需要通过$$转义。
例如,通过Shell for循环一次打印Makefile的list变量中的单词:
1 | list = 1 2 3 |
变量的高级用法
变量的替换引用
对于一个已经定义的变量,可以使用替换引用将其值中的后缀字符串使用指定的字符字符串替换。
格式为 $(VAR:A=B) ,意为替换变量 VAR 中所有 A 字符串的字为 B 结尾的字符串。
例如,将二进制文件名替换成源文件名:
1 | obj_files := a.o b.o c |
执行结果:
1 | $ make |
变量作用域
当前文件中定义的变量默认只在当前 Makefile 生效。
如果要生效在子 Makefile,需要通过 export 声明,或者通过命令行 make var=val 命令传入。当然也可通过 include 命令将变量定义引入到其他文件。
例如:
被下一个 Makefile 调用的 Makefile:
1 | all: |
调用上面的 Makefile,并通过 export 和命令行传入变量:
1 | export export_var = "export variable" |
运行结果:
1 | $ make |
流程控制 - 中阶
Shell 流程控制只支持ifeq/ifneq/ifdef/ifndef,其他流程控制如循环、case等需要函数或 Shell 命令支持。
ifeq/ifneq结构,判断 ARG1 与 ARG2 是否相等或不等:
1 | ifeq (ARG1, ARG2) |
ifdef/ifndef结构,判断变量是否定义或未定义:
1 | ifdef var |
ifeq/ifdef结构均可嵌套使用。else 是可选分支。
注意:ifeq/ifneq/ifdef/ifndef不是 Shell 命令,在执行规则中无需以制表符 TAB 开头;若最外层 if 以制表符开头,将会引发语法错误。
示例:
以下 Makefile 判断 var 是否定义,以及定义的情况下变量值是否为空:
1 | var = " " # 空格等空白字符必须由引号包含 |
执行结果:
1 | $ make |
内置函数 - 中/高阶
Makefile内置了一些常用函数,常用函数按功能分类主要有以下:
- 字符串处理函数
| 函数 | 调用方式及作用 |
|---|---|
patsubst |
$(patsubst pattern, replacement, text),模式字符串替换,将 text 末尾的 pattern 替换为 replacement |
subst |
$(subst from, to, text),字符串替换,将 text 中 from 替换为 to |
strip |
$(strip text),去掉字符串 text 的开头和结尾的空格,并且将其中的多个连续的空格合并成为一个空格 |
findstring |
$(findstring sub, text),字符串查找,在 text 中查找 sub 字符串 |
filter |
$(filter pattern, text),模式过滤,在 text 中过滤出符合 pattern 模式的字符串,pattern 可包含多个模式 |
filter-out |
$(filter-out pattern, text),模式反过滤,在 text 中过滤出不符合 pattern 模式的字符串,pattern 可包含多个模式 |
sort |
$(sort list),单词排列,对 list 中包含的单词去重后、按照升序排列 |
注意:以上 pattern中 一般需要包含模式字符
%,如 %.c 匹配C源文件。
示例:
1 | ori = 1.o 3.o 3.o 2.c |
运行结果:
1 | $ make |
- 文件名操作函数
| 函数 | 调用方式及作用 |
|---|---|
wildcard |
$(wildcard pattern),列出当前目录下所有符合模式 pattern 格式的文件名(patter 中包含有通配符;pattern 中也可包含目录名) |
dir |
$(dir file_paths),取 file_paths 文件路径目录名部分(最后一个 /之前的内容) |
notdir |
$(notdir file_paths),取 file_paths 文件路径目录名部分(最后一个 / 之后的内容) |
basename |
$(notdir file_paths),取 file_paths 文件路径名的前缀部分(文件路径名中最后一个点号之前的部分) |
*是我们经常使用的文件名通配符,代表符合某一规则的所有文件,如以下 src/*. c代表 src 目录下的所有 C 源文件
示例:
/home/tp/src 文件夹下有一 1.c 文件,通过 Makefile 获取其相关信息:
1 | SRC = $(wildcard src/*.c) |
执行结果:
1 | $ make |
注意:wildcard 并不会自动帮你补全绝对路径,你需要自己处理。
- 控制函数
| 函数 | 调用方式及作用 |
|---|---|
if |
$(if conditon(s), then-part[, else-part]),类似于 Shell if 功能,conditon(s) 展开结果非空时执行 then-part、否则执行 else-part,[] 代表 else-part 可选 |
foreach |
$(foreach var, list, op),类似于 Shell for 功能,依次取 list 中单词赋值给 var ,然后在 var 上执行 op 操作 |
warning |
$(warning text),输出一条警告信息 |
error |
$(error text),输出一条错误信息、并退出 make 命令 |
示例一:
1 | texts = foo bar baz |
运行结果:
1 | $ make |
示例二:
1 | all: |
执行结果:
1 | $ make |
- 其他常用函数
| 函数 | 调用方式及作用 |
|---|---|
shell |
$(shell shell-cmd),执行一条 Shell 命令、并返回执行结果,也可以用 Shell 的操作符 ``替代 |
Shell 函数示例:
1 | PWD = $(Shell pwd) |
执行结果:
1 | $ make |
call函数示例:
1 | reverse = $(2) $(1) |
执行结果:
1 | $ make |
其他常用特性 - 中阶
隐含规则
隐含规则为 make 提供了重建一类符合某一模式的目标文件的固定及通用方法,针对这类某的目标文件,无需显示的在Makefile中定义规则,make 命令会自动帮我们生成。
make命令根据文件名后缀自动推导的规则,跟我们工作相关的常用隐含规则有:
- 编译 C 程序:file.o 自动由 file.c 生成,执行命令为
$(CC) -c $(CPPFLAGS) $(CFLAGS) - 编译 C++ 程序:file.o 自动由 file.cc 或 file.C 生成,执行命令为
$(CXX) -c $(CPPFLAGS) $(CFLAGS) - 链接单一的 object 文件:exe 自动由 exe.o 生成,执行命令是
$(CC) $(LDFLAGS) N.o $(LOADLIBES) $(LDLIBS)
建议针对所有目标显示定义执行规则,隐式规则在 Makefile 中不可见,并且可能与我们的需求不符
从文章开头的示例 Makefile 的执行结果可以看到,虽然我们并没有在 Makefile 中为 main.o 显示的定义规则,但 make 命令还是执行了 cc 编译命令:
Makefile 截取片段:
1 | main: main.o libtest.a test.h |
执行结果截取片段:
1 | $ make # 更新main无需指定 |
静态模式规则
静态模式规则的基本语法:
1 | target(s): target-pattern: prereq-pattern(s) |
- targets,列出了静态规则适用的所有目标;可以省略,省略时意味着对该文件中所有符合 target-pattern 的目标生效
- target-pattern,描述了目标文件的模式、需包含模式字符
% - prereq-patterns,描述了依赖文件所包含的模式组合、需包含模式字符
%
以以下编译当前目录下 main.c test.c other.c 生成对应目标文件的 Makefile 为例,说明该语法:
1 | OBJS = main.o test.o # OBJS中可以包含非.o文件,但make时会报警,不建议这么做 |
执行结果:
1 | $ make |
该 Makefile 的执行过程是:
make命令默认生成 all,检查其依赖目标 main.o test.o 是否需要更新;- 根据静态规则的描述,目标文件应以 .o 为后缀,并依赖于同名的后缀为 .c 的源文件,即 main.o 依赖于 main.c、test.o 依赖于 test.c;
- 查找 main.c 文件是否存在,且比 main.o 更新,是则执行
cc命令更新 main.o;test.o 也是同样处理; - main.o test.o 更新完毕,all 是伪目标,每次
make都需要更新,但执行规则为空,所以没有执行什么指令。
自动变量
关于自动化变量可以理解为由 Makefile 自动产生的变量。
在以上描述的静态模式规则中,依赖文件和目标文件可能是变动的,显然在命令中不能出现具体的文件名称,否则模式规则将失去意义。
那么模式规则命令中该如何表示文件呢?就需要使用自动化变量,常用的自动化变量有:
| 自动化变量 | 说明 |
|---|---|
$@ |
表示规则的目标文件名,在多目标模式规则中,它代表的是触发规则被执行的文件名。 |
$< |
规则的第一个依赖的文件名。如果是一个目标文件使用隐含的规则来重建,则它代表由隐含规则加入的第一个依赖文件。 |
$^ |
代表的是所有依赖文件列表,使用空格分隔 |
简单改动下静态模式规则的示例来查看这些自动变量:
1 | OBJS = main.o test.o |
执行结果:
1 | $ make |
常用命令前缀
可以使用一些命令前缀控制命令的行为,常用的有:
@cmd,命令执行时不回显(回显为默认行为)-cmd,cmd 执行错误时、忽略错误
示例:
1 | all: $(OBJS) |
执行结果:
1 | $ make |
make命令常用选项
| 参数 | 释义 |
|---|---|
-B |
强制重建所有目标 |
-C dir |
切换到 dir 目录执行 make 命令 |
-f file |
指定要读入并执行的 Makefile ,make 命令默认读入并执行当前目录下的名为 Makefile、makefile 的文件 |
-j [N] |
指定并行 make 的任务数,不带 N 时不限制并行任务数 |
-n |
只打印、不执行命令,做调试用 |
-d |
在正常处理信息之外打印调试信息,做调试用 |
通过 -C 选项实现代码层级结构组织
一般情况下,我们单个工程的代码文件还是比较多的。如果按照模块将文件组织在不同的目录及子目录当中,那么我们在查找及管理文件时效率就比较高了。
通过在工程每个父目录的 Makefile 中添加 make -C sub_dir 这样一条指令,我们可以达到编译整个工程目录树的目的。
我们将开头的示例改造一下以展示该功能:
首先将功能相关的文件组织成一下方式:
1 | $ tree |
然后分别在当前目录及 test 子目录添加 Makefile:
当前目录 Makefile:
1 | main: main.o |
test 子目录 Makefile:
1 | libtest.a: test.c |
在当前目录执行 make 命令:
1 | $ make |
Makefile调试
我们调试 Makefile 一般采用两种方式:
- 通过
echo在关键位置添加打印信息 - 通过以上
-n或-d命令查看make命令输出的信息
附录
参考书目
《GNU+makefile中文手册》
《跟我一起写Makefile》

