Makefile
时间轴
2025-11-01
init
参考文档:
还有一篇翻译的也不错:
Makefile 简介
Makefiles 用于帮助决定一个大型程序的哪些部分需要重新编译。在绝大多数情况下,需要编译的只是 C 或 C++ 文件。其他语言通常有它们自己的一套与 Make 用途类似的工具。Make 的用途并不局限于编程。
除了 Make,还有一些比较流行的构建系统可选,像 SCon 、CMake 、Bazel 和 Ninja 等。一些代码编辑器,像 Microsoft Visual Studio ,内置了它们自己的构建工具。Java 语言的构建工具有 Ant 、Maven 和 Gradle 可选,其他语言像 Go 和 Rust 则都有它们自己的构建工具。
像 Python、Ruby 和 JavaScript 这样的解释型语言是不需要类似 Makefiles 的东西的。Makefiles 的目标是基于哪些文件发生了变化来编译需要被编译的一切文件。但是,当解释型语言的文件发生了变化,是不需要重新编译的,程序运行时会使用最新版的源码文件。
Makefile语法
Makefile 文件由一系列的 规则 (rules) 组成,一个规则类似下面这样:
1 | targets: prerequisites |
targets指的是文件名称,多个文件名以空格分隔。通常,一个规则只对应一个文件。commands通常是一系列用于制作(make)一个或多个目标(targets)的步骤。它们 需要以一个制表符开头,而不是空格。prerequisites也是文件名称,多个文件名以空格分隔。在运行目标(targets)的commands之前,要确保这些文件是存在的。它们也被称为 依赖。
例子:
1 | blah: blah.o |
-c选项只编译不链接,-o file将其前面命令的输出内容放在文件 file 中。
Targets
依赖
例子:
1 | some_file: other_file |
目标 some_file 依赖 other_file。当我们运行 make 时,默认目标(即 some_file,因为它是第一个)的构建会被调用。构建系统首先查看目标的 依赖 列表,若其中有旧的目标文件,构建系统首先会为这些依赖执行目标构建,此后才轮到默认目标。第二次执行 make 时,默认目标和依赖目标下的命令都不会再运行了,因为二者都存在了。
伪目标或虚拟目标
1 | some_file: other_file |
类似上面 other_file 这样的目标就是俗称的 伪目标 或 虚拟目标。
The All Targets
1 | all: one two three |
执行make命令默认执行的targets
Multiple Targets
1 | all: f1.o f2.o |
当一个规则(rule)有多个目标时,那么对于每个目标,这个规则下面的 commands 都会运行一次。
$@ 是一个指代目标名称的自动变量(Automatic Variables)。
Automatic Variables
通配符*
在 Make 中,% 和 * 都叫作通配符,但是它们是两个完全不同的东西。* 会搜索你的文件系统来匹配文件名。
建议应该一直使用 wildcard 函数来包裹它,要不然可能会掉入陷阱中。
不用 wildcard 包裹的 * 除了能给人徒增迷惑,毫无可取之处。
1 | # 打印出每个.c文件的文件信息 |
*不能直接用在变量定义中。当
*匹配不到文件时,它将保持原样(除非被wildcard函数包裹)。
1 | thing_wrong := *.o # 请不要这样做!'*.o' 将不会被替换为实际的文件名 |
通配符%
- 在“匹配”模式下使用时,它匹配字符串中的一个或多个字符,这种匹配被称为词干(stem)匹配。
- 在“替换”模式下使用时,它会替换匹配到的词干。
%大多用在规则定义以及一些特定函数中。
自动变量
automatic variables
虽然有很多自动变量,但常用的没有几个
$@
当前规则的目标文件名 (target)
$?
当前规则中比目标文件新的依赖文件列表
例子:对于规则:foo: a.c b.c,若 b.c 更新 → $? → b.c
$<
当前规则的第一个依赖文件 (prerequisite)
$^
当前规则的所有依赖文件(去重)
Fancy Rules
隐式规则
Make 钟爱 C 编译,它每次表达爱意时,都会做出一些“迷惑行为”。其中最令人迷惑的部分可能就是它的那些魔法般的规则了,Make 称之为“隐式规则”。并不推荐使用。
下面列出了隐式规则:
- 编译 C 程序时:使用
$(CC) -c $(CPPFLAGS) $(CFLAGS)形式的命令,n.o会由n.c自动生成。 - 编译 C++ 程序时:使用
$(CXX) -c $(CPPFLAGS) $(CXXFLAGS)形式的命令,n.o会由n.cc或n.pp自动生成。 - 链接单个目标文件时:通过运行
$(CC) $(LDFLAGS) n.o $(LOADLIBES) $(LDLIBS)命令,n会由n.o自动生成。
上述隐式规则使用的变量的含义如下所示:
CC:编译 C 程序的程序,默认是ccCXX:编译 C++ 程序的程序,默认是g++CFLAGS:提供给 C 编译器的额外标志CXXFLAGS:提供给 C++ 编译器的额外标志CPPFLAGS:提供给 C 预处理器的额外标志LDFLAGS:当编译器应该调用链接器时提供给编译器的额外标志
隐式规则的例子:
1 | CC = gcc # Flag for implicit rules |
静态模式规则
1 | targets ...: target-pattern: prereq-patterns ... |
它的本质是:给定的目标 target 由 target-pattern 在 targets 中匹配得到(利用通配符 %)。匹配到的内容被称为 词干 (stem)。然后,将词干替换到 prereq-pattern 中去,并以此生成目标的 prerequisites 部分。
静态模式规则的一个典型用例就是把 .c 文件编译为 .o 文件。
手动 方式:
1 | objects = foo.o bar.o all.o |
静态模式 方式:
1 | objects = foo.o bar.o all.o |
解释:
1 | $(objects): %.o: %.c |
这行很关键,它的意思是:
对于
$(objects)(即 foo.o、bar.o、all.o),如果有一个匹配规则%.o,则依赖于对应的%.c文件。
例如:
foo.o依赖foo.cbar.o依赖bar.call.o依赖all.c
这其实等价于:
1 | foo.o: foo.c |
但是用一行自动生成,体现了 通配符规则的强大性。
1 | all.c: |
当 make 发现 foo.o 需要 foo.c 而不存在时,
它会触发 %.c: 规则来创建空文件 foo.c
当 make 需要 all.o 时,会触发 all.c: 规则(生成有内容的文件)
也可以这样写:
1 | # 定义目标文件列表 |
静态模式规则与过滤器
例子:
1 | obj_files = foo.result bar.o lose.o |
声明伪目标
1 |
- 表示
all和clean不是实际文件,而是逻辑命令。 - 避免因为存在名为
all的文件而导致 make 混淆。
主目标(入口)
1 | all: $(obj_files) |
默认目标
all依赖于所有obj_files。当执行
make时,Make 会去构建:- foo.result
- bar.o
- lose.o
用 filter 筛选不同类型文件的规则
对 .o 文件的规则:
1 | $(filter %.o,$(obj_files)): %.o: %.c |
解释:
$(filter %.o,$(obj_files))
→ 从obj_files里筛选出.o结尾的文件:1
bar.o lose.o
展开后等价于:
1
2bar.o lose.o: %.o: %.c
@echo "target: $@ prereq: $<"%.o: %.c是 静态模式规则:- 目标形如
xxx.o,依赖于同名的xxx.c。 $@:表示目标文件名(例如bar.o)$<:表示第一个依赖文件(例如bar.c)
- 目标形如
这条规则不是真的编译,只是打印信息:
1 | target: bar.o prereq: bar.c |
对 .result 文件的规则:
1 | $(filter %.result,$(obj_files)): %.result: %.raw |
同理:
$(filter %.result,$(obj_files))→ 结果是foo.result展开后等价于:
1
2foo.result: %.result: %.raw
@echo "target: $@ prereq: $<"
意思是:要生成 foo.result,需要 foo.raw
模式规则
一个例子:
1 | # Define a pattern rule that compiles every .c file into a .o file |
模式规则在目标中包含了一个 %,这个 % 匹配任意非空字符串,其他字符匹配它们自己。一个模式规则的 prerequisite 中的 % 表示目标中 % 匹配到的同一个词干。
另一个例子:
1 | # Define a pattern rule that has no pattern in the prerequisites. |
双冒号规则
1 | target :: prerequisites |
含义:
- 目标可以被定义多次,每个规则都独立存在。
- 只要有一个规则的依赖文件更新,该规则的命令就会单独执行。
例子:
1 | foo :: a |
执行:
1 | $ make foo |
假设:
a比foo新(被更新)b比foo旧(未更新)
输出结果:
1 | rule 1 triggered by a |
如果 a、b 都比 foo 新,则输出:
1 | rule 1 triggered by a |
命令与执行
回显/静默命令
在一个命令前添加一个 @ 符号就会阻止该命令输出内容。
你也可以使用 make -s 在每个命令前添加 @。
1 | all: |
命令的执行
每个命令都运行在一个新的 shell 中(或者说运行效果等同于运行在一个新 shell 中)。
1 | all: |
更改默认Shell
系统默认的 shell 是 /bin/sh,可以通过改变 SHELL 变量的值来改变它:
1 | SHELL=/bin/bash |
错误处理:-k,-i 和 -
make -k 会使得即便遇到错误,构建也会继续执行下去。经常用于一次查看 Make 的所有错误。
在一个命令前添加 - 会抑制错误。
make -i 等同于在每个命令前添加 -。
1 | one: |
中断或杀死 make
在 make 的过程中,使用了 ctrl+c,那么刚刚制作的新目标会被删除。
make 的递归用法
为了递归应用一个 makefile,要使用 $(MAKE) 而不是 make,因为它会传递构建标志,而使用了 $(MAKE) 变量的这一行命令不会应用这些标志。
1 | new_contents = "hello:\n\ttouch inside_file" |
| 写法 | 行为 | 推荐 |
|---|---|---|
make |
普通命令,不继承 make 的选项 | ❌ |
$(MAKE) |
特殊变量,表示“当前 make 程序”本身,会自动继承选项 | ✅ |
环境变量
export
指令 export 携带了一个变量,并且对子 make 命令可见。
在下面的例子中,变量 cooly 被导出以便在子目录中的 makefile 可以使用它。
1 | new_contents = "hello:\n\\techo \$$(cooly)" |
向shell传递变量
向shell传递变量也要export导出
1 | one=this will only work locally |
.EXPORT_ALL_VARIABLES
.EXPORT_ALL_VARIABLES可以导出所有变量。
1 | .EXPORT_ALL_VARIABLES: |
给 make 传递参数
| 选项 / 用法 | 全写命令 | 功能说明 | 示例 |
|---|---|---|---|
-n / --dry-run |
make --dry-run |
只显示将要执行的命令,但不实际运行。用于调试 Makefile。 | make --dry-run all(查看执行流程) |
-t / --touch |
make --touch |
将目标文件标记为“最新”(更新时间戳),但不真正执行命令。常用于跳过实际构建。 | make --touch all |
-o <file> / --old-file=<file> |
make --old-file=foo.o |
指定某个文件视为“旧文件”,使依赖于它的目标不会被重建。 | make --old-file=main.o |
可以同时传递多个目标给
make,例如make clean run test会先后运行clean、run、test。
变量
变量的类型
- 递归变量(使用
=)- 只有在命令执行时才查找变量,而不是在定义时 - 简单的扩展变量(使用
:=)- 就像普通的命令式编程一样——只有当前已经定义的变量才会得到扩展
例子:
1 | # Recursive variable. 会打印出later |
1 | one = hello |
?=
当变量还没被设置值时给它设置值,反之则忽略。
1 | one = hello |
+=
用来追加变量的值:
1 | foo := start |
命令行变量
可以通过 override 来覆盖来自命令行的变量。
假使我们使用下面的 makefile 执行了这样一条命令 make option_one=hi,那么变量 option_one 的值就会被覆盖掉。
1 | # Overrides command line arguments |
#define 定义命令列表
它与函数 define 没有任何关系。
例子:
1 | one = export blah="I was set!"; echo $$blah |
这里请注意,它与用分号分隔多个命令的场景有点不同,因为前者如预期的那样,每条命令都是在一个单独的 shell 中运行的。
即:每一行命令(在 recipe 中)都是在一个新的 独立 shell 实例 中执行的。所以环境变量或当前目录等修改不会自动延续到下一行。
例如:
1 | all: |
不会打印任何东西,因为:
- 第1行在一个 shell 中运行,设置了环境变量。
- 第2行在另一个 shell 中运行,看不到前一个 shell 的环境。
特定目标的变量
我们可以为特定目标分配变量。
1 | all: one = cool |
特定模式的变量
我们可以为特定的目标 模式 分配变量。
1 | %.c: one = cool |
Makefile的条件判断
if/else
1 | foo = ok |
strip 检查变量是否为空
1 | nullstring = |
ifdef检查变量是否定义
ifdef 不会扩展变量引用,它只会查看变量的内容是否定义。
1 | bar = |
$(makeflags)
MAKEFLAGS 是 GNU Make 的内置变量,它会自动保存当前 make 的所有命令行选项。
| 调用方式 | MAKEFLAGS 的内容 |
|---|---|
make |
(空) |
make -i |
i |
make -k |
k |
make -ik |
ik |
make -n -s |
ns |
make -j4 |
j4(含数字参数) |
1 | bar = |
$(findstring i, $(MAKEFLAGS))
- 这是 GNU Make 内置函数之一。
- 功能:在第二个字符串中查找第一个字符串。
- 如果找到,就返回第一个字符串(这里是
"i"); - 如果没找到,就返回空字符串。
ifneq (,$(...))
- 语法:
ifneq (arg1, arg2)
表示“如果 arg1 和 arg2 不相等”则执行后续语句。 - 这里写成
(, $(findstring ...))
意思是 “如果 findstring 的结果 非空”。
因此整句逻辑等价于:
如果
MAKEFLAGS中包含字母i,则执行 echo。
函数
函数 主要用于文本处理。函数调用的语法是 $(fn, arguments) 或 ${fn, arguments}。你可以使用内置的函数 call 来制作自己的函数。Make 拥有数量众多的 内置函数 。
1 | bar := ${subst not, totally, "I am not superman"} |
如果想替换空格或逗号,需要使用变量:
1 | comma := , |
不要 在第一个参数之后的参数中包含空格,这将被视为字符串的一部分。
1 | comma := , |
patsubst 字符串替换
$(patsubst pattern,replacement,text) 做了下面这些事:
“在文本中查找匹配的以空格分隔的单词,用
replacement替换它们。这里的pattern可以包含一个%作为通配符以匹配单词中任意数量的任意字符。如果replacement中也包含了一个%,那它表示的内容将被pattern中的%匹配的内容替换。只有pattern和replacement中的第一个%才会采取这种行为,随后的任何%都将保持不变。
$(text:pattern=replacement) 是一个简化写法。
还有一个仅替换后缀的简写形式:$(text:suffix=replacement),这里没有使用通配符 %。
注意: 在简写形式中,不要添加额外的空格,它会被当作一个搜索或替换项。
1 | foo := a.o b.o l.a c.o |
foreach
函数 foreach 看起来像这样:$(foreach var,list,text),它用于将一个单词列表(空格分隔)转换为另一个。
var表示循环中的每一个单词list表示要循环的变量text用于扩展每个单词。
例子:在每个单词后追加一个感叹号:
1 | foo := who are you |
if
1 | $(if condition, then-part, else-part) |
if 函数用来检查它的第 1 个参数是否非空。如果非空,则运行第 2 个参数,否则运行第 3 个。
1 | foo := $(if this-is-not-empty,then!,else!) |
call
Make 支持创建基本的函数。你只需通过创建变量来“定义”函数,只是会用到参数 $(0)、$(1) 等。然后,你就可以使用专门的函数 call 来调用它了,语法是 $(call variable,param,param)。$(0) 是变量名,而 $(1)、$(1) 等则是参数。
1 | sweet_new_fn = Variable Name: $(0) First: $(1) Second: $(2) Empty Variable: $(3) |
shell
shell - 调用 shell,但它在输出中会用空格替代换行。
1 | all: |
其他特性
include
include 指令告诉 make 去读取其他 makefiles 文件,它是 makefile 中的一行,如下所示:
1 | include filenames... |
vpath
vpath 指令用来指定某些 prerequisites 的位置,使用格式是 vpath <pattern> <directories, space/colon separated>。
vpath 告诉 Make:当依赖文件不在当前目录时,应该到哪些目录去找。
<pattern> 中可以使用 %,用来匹配 0 个或多个字符。
你也可以使用变量 VPATH 全局执行此操作。
1 | vpath %.h ../headers ../other-directory |
1 | vpath %.h ../headers ../other-directory |
含义:对所有以 .h 结尾的文件(即头文件),如果当前目录找不到,就依次到../headers 和 ../other-directory去搜索。
多行处理
当命令过长时,反斜杠(\)可以让我们使用多行编写形式。
1 | some_file: |
.PHONY
向一个目标中添加 .PHONY 会避免把一个虚拟目标识别为一个文件名。
在下面这个例子中,即便文件 clean 被创建了,make clean 仍会运行。.PHONY 非常好用。
1 | some_file: |
.DELETE_ON_ERROR
如果一个命令返回了一个非 0 的退出码,那么 make 会停止运行相应的规则(并会传播到它的依赖中)。如果一个规则因为上述这种情况构建失败了,那么应用了 .DELETE_ON_ERROR 后,这个规则的目标文件就会被删除。
不像 .PHONY,.DELETE_ON_ERROR 对所有的目标都有效。始终使用 .DELETE_ON_ERROR 是个不错的选择,即使由于历史原因,make 不支持它。
1 | .DELETE_ON_ERROR: |
例子
1 | TARGET_EXEC := final_program |






