跳转到内容

make

Makefile 本质上是一个构建规则文件,用于指导 make 工具自动化完成编译、打包、安装等任务。通过 Makefile,开发者可以避免手动输入繁琐的命令,只需执行 make 命令,系统就会根据 Makefile 中的规则自动完成构建过程。

Makefile 是软件开发中用于自动化构建过程的重要工具,尤其在 C/C++ 等编译型语言的项目中应用广泛。在大型项目开发中,源文件众多且依赖关系复杂,手动编译不仅效率低下,还容易出错。Makefile 能够清晰地描述文件之间的依赖关系和构建规则,实现增量编译(只重新编译修改过的文件及其依赖文 件),大大提高开发效率。

本文档旨在为自己提供一个系统、全面的 Makefile 学习查阅资料,整理学习过程中的重点知识。使用时,可根据需要查阅相应章节,快速获取所需信息。

Makefile 主要由一系列规则组成,每条规则通常包含目标(target)、依赖(prerequisites)和命令 (recipe)三部分,基本结构如下:

Terminal window
target ... : prerequisites ...
recipe
  • 目标(target):目标是规则的核心,表明要构建的对象,可以是文件目标,也可以是伪目标
  • 依赖(prerequisites):依赖是生成目标所依赖的文件或目标,当依赖文件的修改时间比目标文件晚时 ,make 会执行命令重新生成目标
  • 命令(recipe):命令是构建目标的具体操作,命令可以是任意的 shell 命令,也可以是其他可执行程序的命令

这是一个文件的依赖关系,也就是说,target 这一个或多个的目标文件依赖于 prerequisites 中的文件,其生成规则定义在 command 中,下面区分一下文件目标和伪目标

1. 文件目标

文件目标是对应实际文件的 Makefile 目标,目标名就是生成的文件名,比如下面的例子:

main.o: main.c main.h
gcc -c main.c -o main.o

main.o 就是文件目标,make 会检查依赖文件的时间戳,如果依赖更新或目标不存在,则执行规则中的命令

2. 伪目标

伪目标不对应实际文件,只用来执行命令或动作,比如下面这个例子:

clean:
rm *.o temp

因为 rm 命令不会创建一个名为 clean 的文件,而且很可能这个文件永远也不会存在,所以每次你执行 make clean 时,rm 命令都会被执行,如果目录中某天意外创建了一个名为 clean 的文件,clean 目标就无法正常工作。因为它没有依赖文件,make 会认为 clean 总是最新的,所以命令就不会被执行,为避免这个问题,可以显式地将目标声明为伪目标(phony),方法是让它成为特殊目标 .PHONY 的依赖,如下所示:

.PHONY: clean
clean:
rm *.o temp

这样可以保证 make clean 每次都会执行命令,而不受是否存在同名文件的影响,伪目标常用于执行任务,常用伪目标命名约定如下:

目标名 作用 备注
all 默认构建全部目标 放在 Makefile 开头,一般作为默认目标
clean 删除中间文件和生成文件 .o、可执行文件、临时文件等
distclean 更彻底的清理 包括配置文件、生成的依赖文件等
install 安装程序或库到系统目录 常用 PREFIX 变量控制安装路径
uninstall 卸载安装的文件 配合 install 使用
check / test 运行测试 单元测试或集成测试
docs 生成文档 如 doxygen 或 man 页
allclean 彻底清理,包括版本控制生成的文件 某些项目会区分 clean 与 allclean
dist / package 打包源码或二进制 生成 tar.gz、zip 等发行包
update 更新依赖或子模块 如 git submodule update
help 打印 Makefile 可用目标列表 提供给用户查看

make 的工作原理核心是 依赖管理 + 时间戳比较 + 执行命令,详细工作流程如下:

1. 读取并解析 Makefile

make 会在当前目录下按顺序寻找文件名为 GNUmakefile 、 makefile 和 Makefile 的文件,解析 Makefile 中的内容,包括变量定义、规则、条件判断、函数调用等,make 遇到 include 指令,会把其他位置的 Makefile 包含进来,被包含的文件内容会像写在主 Makefile 一样被解析

2. 确定构建目标

make 会把 Makefile 中的第一个规则中的目标作为默认目标,也可以通过命令行指定目标:make <target>,目标文件可以:

  • 文件目标(如 main、foo.o)
  • 伪目标(如clean、install)

3. 构建依赖关系链

make 从目标出发,递归解析所有的依赖项,如果依赖本身是目标,递归生成其依赖关系链,最终构建完整的依赖关系,确保依赖先于目标生成

4. 检查文件状态

对每个目标,make 通过文件修改时间(mtime)判断是否需要重新构建,规则是:

  • 目标不存在:必须执行命令生成目标
  • 目标存在:对比目标与所有依赖的修改时间
    • 如果所有依赖的修改时间都早于目标:目标是最新的,无需操作
    • 如果任一依赖的修改时间晚于目标(或依赖本身需要更新):必须执行命令更新目标

伪目标不受文件时间影响,make 构建伪目标时总会运行其命令

5. 执行命令构建目标

当确定需要更新目标时,make 会按照依赖关系链中的顺序执行规则中的命令,保证依赖先生成。执行完成后,目标文件的修改时间会被更新,确保下次检查时能正确判断状态

变量本质上是一个字符串,在使用时会被替换为对应的字符串内容,类似 C 语言中的宏,与宏不同的是 Makefile 中的变量可以修改值

1. 变量定义方式

(1) 简单赋值 =

  • 定义方式:使用 = 给变量赋值,所得到的变量称为简单变量
  • 特点:
    • 变量值在被使用时才会被展开,且会递归展开其中包含的其他变量
    • 变量的值会被后续的赋值所覆盖

示例:延迟展开

# 使用 = 定义变量
A = $(B)
B = hello
test:
@echo "A = $(A)" # A 在使用时展开,此时 B 已定义为 hello
# 输出
A = hello

示例:变量修改的影响

# 使用 = 定义
X = foo
Y = $(X)
X = bar # 后续修改 X
test:
@echo "Y = $(Y)" # Y 在使用时展开,使用时 X 已变为 bar
# 输出
Y = bar

(2) 立即赋值 :=

  • 定义方式:使用 := 赋值,所得到的变量称为立即变量
  • 特点:
    • 变量值在定义时立即展开
    • 后续对其他变量的修改不会影响当前变量

示例:立即展开

# 使用 := 定义变量(立即展开)
C := $(D)
D = world
test:
@echo "C = $(C)" # C 在定义时展开,此时 D 尚未定义,所以为空
# 输出
C =

示例:变量修改的影响

# 使用 := 定义
P := foo
Q := $(P)
P := bar # 后续修改 P
test:
@echo "Q = $(Q)" # Q 是立即展开,定义时 P 是 foo,后续修改不影响
# 输出
Q = foo

(3) 条件赋值 ?=

  • 定义方式:使用 ?= 赋值

  • 特点:

    • 如果变量已经定义,则保持原值
    • 如果未定义,则赋值

示例:条件定义变量

# 使用 ?= 定义
M = foo
M ?= bar
test:
@echo "M = $(M)" # M 之前定义过,所以 M 依旧是 foo
# 输出
M = foo

(4) 追加赋值 +=

  • 定义方式:使用 += 赋值
  • 特点:
    • 如果变量之前没有定义过,那么, += 会自动变成 =
    • 如果变量之前定义过,那么,,那么 += 会继承于前次操作的赋值符(如果前一次的是 := ,那么 += 会以 := 作为其赋值符),在已有变量值的末尾追加内容,自动添加空格分隔

示例:追加赋值变量

# 使用 += 追加
N := foo
N += bar
# 等价于
# N := foo
# N := $(N) bar
test:
@echo "N = $(N)" # 追加 foo
# 输出
N = foo bar

2. 变量引用方式

使用 $(变量名) 或 ${变量名} 引用变量,建议在 Makefile 中统一使用一种形式

3. 反斜杠 \ 换行

当定义的字符串太长,可以选择使用 \ 换行定义,如下所示:

SOURCES = main.c \
util.c \
foo.c
  • 每行末尾加 \ 表示续行,最后一行不要加 \

  • 在引用变量时,make 会把换行符替换为空格,所以 $(SOURCES) –> main.c util.c foo.c

Makefile 中有一些预定义的变量,通常与编译器、链接器相关,用户可以直接使用或修改。常见的预定义变量如下:

变量名 类型 默认值 / 描述
CC 有默认值 C 编译程序,默认是 cc
CXX 有默认值 C++ 编译程序,默认是 g++
AR, AS 有默认值 默认分别是 ar(归档工具)、as(汇编器)
CFLAGS 默认未定义 用于 C 编译器选项,默认空
CXXFLAGS 默认未定义 用于 C++ 编译器选项,默认空
LDFLAGS 默认未定义 传递给链接器的选项,默认空
LDLIBS 默认未定义 链接时使用的库,默认空
ARFLAGS, ASFLAGS 默认未定义 用于归档或汇编的附加选项,默认空

目标专属变量绑定到某个具体目标,该变量仅对该目标及其依赖(包括递归依赖)生效,不会影响规则链以外的变量,格式定义为:

<target ...> : <variable-assignment>
  • 可以给同一个目标设置多个专属变量
  • 可以给多个目标设置相同的专属变量

举例说明:

# 给目标 taget 定义两个目标专属变量 var1 和 var2
# 分开定义(推荐,更清晰)
target: var1 = value1
target: var2 = value2
# 给目标 taget 定义两个目标专属变量 var1 和 var2
# 合并定义(等价效果)
target: var1 = value1 var2 = value2
# 给目标 target1 和 target2 设置共享变量 common
target1 target2: common = "共享内容"

模式专属变量绑定到符合某种模式的目标,所有目标中匹配该模式的目标都会使用这个变量值,如果一个目标同时匹配多个模式,会使用最后一个定义的值(后定义覆盖先定义),格式定义为:

<pattern ...> : <variable-assignment>

举例说明:

%.o: CFLAGS = -O3
debug/%.o: CFLAGS = -g -O0
main.o: main.c
$(CC) $(CFLAGS) -c $< -o $@
debug/util.o: util.c
$(CC) $(CFLAGS) -c $< -o $@
  • main.o 会匹配 %.o –> CFLAGS = -O3

  • debug/util.o 会匹配 debug/%.o –> CFLAGS = -g -O0

当需要定义包含换行符、多个命令或复杂文本的变量时,使用define 关键字可以定义 包含多行文本(可包含命令、文本、变量引用等) 的变量, 保持内容的格式完整性,该变量和其他变量一样使用 $(变量名) 或 ${变量名} 引用,格式如下:

define <变量名>
# 多行内容(可包含命令、文本、变量引用等)
内容行 1
内容行 2
...
endef

多行变量的特点:

  • 保留换行:与使用 \ 续行不同,define 内的换行不会被替换为空格

  • 可写复杂脚本:适合多行 shell 命令、配置模板、生成文件内容等

  • 可引用其他变量:支持 $(VAR)、$(OTHER_VAR),在展开时替换为变量值

  • 展开方式:默认递归展开,类似 VAR = value,如果希望立即展开,可以写作 define VARIABLE := ...

环境变量是在 Makefile 外部(通常是操作系统、Shell)定义的变量,可以被 Makefile 读取和使用,也可以在 Makefile 中定义并传递给子进程

  • 读取系统环境变量:make 在启动时会自动读取系统环境变量,并将它们作为 make 变量使用
  • 在 Makefile 中定义环境变量:使用 export 关键字可以将变量导出为环境变量,供 子 shell 或子 make 使用,也可以使用 unexport 关键字防止变量传递给子 shell 或子 make

举例说明:

# 读取系统环境变量
all:
@echo "系统 PATH: $(PATH)"
@echo "用户 HOME: $(HOME)"
# 定义并导出环境变量
export MY_VAR = "hello from makefile"
show_var:
@echo "Make 中: $(MY_VAR)"
@echo "Shell 中: $$MY_VAR" # 在 shell 命令中引用需要用 $$

命令行变量是指在执行 make 命令时通过命令行传递给 make 的变量,格式如下:

Terminal window
make VAR1=<value1> VAR2=<value2> ...

该变量仅在当前 make 执行过程中有效,不会修改 Makefile 文件

自动变量是 Makefile 预定义的特殊变量,它们在规则的命令中自动获取特定值(与当前规则的目标、依赖相关),无需手动定义。常用的自动变量如下:

变量 含义 示例值(假设规则为 main.o: main.c defs.h)
$@ 规则的目标文件名 main.o
$< 第一个依赖文件(prerequisite) main.c
$^ 所有依赖文件(去重) main.c defs.h
$+ 所有依赖文件(不去重) main.c defs.h
$? 所有比目标文件新的依赖文件 如果 defs.h 更新过 → defs.h
$* 目标文件的主名(去掉扩展名) main(来自 main.o)
  • $@ 、 $<、 $* 在扩展时只会有一个文件,$^、$+、$? 在扩展时是一个文件列表

(1) 多目标时,$@ 的行为分析

假设:

foo bar: common.c
@echo "Target is $@"

当执行:

Terminal window
make foo

会输出:

Terminal window
Target is foo

当执行:

Terminal window
make bar

输出:

Terminal window
Target is bar

虽然 foo bar 共享同一条规则和命令,但 make 会为每个目标分别执行一次命令,$@ 在每次执行时只代表当前那个目标

(2) 不同规则下,$* 的行为分析

  • 隐式规则 / 静态模式 中$* 表示目标中与匹配模式中% 匹配的部分(% 如果匹配见后文的模式匹配)
  • 显示规则 中 $* 表示为去掉后缀的目标名,前提是目标名以一个已识别的后缀结尾(GNU Make 的旧式后缀规则里定义的,比如 .c, .o 等)

$* 最好只在隐式规则或静态模式里使用,因为它的行为在那里是稳定且符合直觉的

在显式规则中依赖 $* 可能会出现意料之外的结果,尤其是没有后缀时会是空字符串

在 make 里,变量的作用域指的是变量在什么范围内可见、可用,以及它的生命周期。

(1) 作用域优先级

┌───────────────────────────┐
│ 命令行变量 │ ← 优先级最高
│ make VAR=value │
└───────────────────────────┘
┌───────────────────────────┐
│ 目标专属变量 │
│ target: VAR = value │
└───────────────────────────┘
┌───────────────────────────┐
│ 模式专属变量 │
│ %.o: VAR = value │
└───────────────────────────┘
┌───────────────────────────┐
│ 全局变量 │
│ 自定义变量 VAR = value |
│ 多行变量 define ... endef │
└───────────────────────────┘
┌───────────────────────────┐
│ 预定义变量(内置) │
│ CC, CFLAGS, RM 等 │
└───────────────────────────┘
┌───────────────────────────┐
│ 环境变量 │ ← 优先级最低
│ export VAR / shell ENV │
└───────────────────────────┘
  • 顶层:命令行变量覆盖一切,是临时优先级最高的变量。
  • 中层:目标专属 > 模式专属 > 全局变量(包括自定义变量和多行变量)
  • 预定义变量:作为全局默认值存在,但可被覆盖
  • 底层:环境变量优先级最低,可被 Makefile 中的变量覆盖

(2) override 关键字

override 关键字用于强制覆盖已有变量的值,无论该变量是如何定义的(包括命令行传入的变量),格式定义如下:

override VAR = <value>
  • 将 VAR 的值强制设置为 value

  • 即使 VAR 是通过命令行传入的,也会被覆盖

override 是唯一能覆盖命令行变量的方式

(1) 变量替换

变量替换用于对变量的值进行文本替换、删除或模式替换,常用于生成文件列表或修改路径,Makefile 提供了丰富的变量替换语法,用于对变量值进行灵活处理,下面是两种替换用法:

基础替换

语法:$(var:pattern=replacement)

功能:该替换并不是“任意位置的匹配”,而是 末尾匹配,make 按空格(或换行)将 var 的值拆成单词列表,仅对每个单词末尾匹配 pattern 的部分进行替换,如果 pattern 前面带 .,就是最常见的文件扩展名替换用法(如果只是替换后缀,不带 .,该替换也能实现)

文件扩展名替换:

src = foo.c bar.c
obj_1 = $(src:.c=.o) # 带 . 进行文件扩展名替换
obj_2 = $(src:c=o) # 不带 . 进行文件扩展名替换
test:
@echo $(obj)
# 输出
foo.o bar.o
foo.o bar.o

普通末尾替换:

str = Hello World
substr = $(str:lo=LO)
test:
@echo $(substr)
# 输出
HelLo World

模式替换

语法:$(var:%.from=%.to)

功能:该替换可以匹配任意位置,按空格(或换行)把 var 的值拆成单词列表,对每个单词,判断是否符合 pattern(这里的 pattern 必须包含 % 通配符),如果匹配,则用 replacement 里的 % 替换成对应的匹配部分

批量改扩展名

src = main.c util.c src/test.c
obj = $(src:%.c=%.o)
test:
@echo $(src)
# 输出
main.o util.o src/test.o

改路径

src = main.c util.c src/test.c
dist = $(src:src/%.c=build/%.o)
test:
@echo $(src)
# 输出
build/test.o

2. 变量嵌套

变量嵌套允许用一个变量的值作为另一个变量的名字来访问,格式如下:

$($(VAR))

当然上述变量嵌套也可以嵌套的更深

构造动态变量名

debug_FLAGS = -g -O0
release_FLAGS = -O2
MODE = debug
CFLAGS = $($(MODE)_FLAGS)
# $(MODE) = debug
# $($(MODE)_FLAGS) = $(debug_FLAGS) = -g -O0

按目标选择编译选项

prog1_SRC = main1.c util1.c
prog2_SRC = main2.c util2.c
TARGET = prog1
SRC = $($(TARGET)_SRC)

make 支持 Shell 通配符(GLOB),功能如下:

GLOB 语法 匹配含义 示例匹配
* 匹配任意长度的任意字符(包括空) *.c 匹配 main.c、test.c
? 匹配任意一个字符 a?.c 匹配 ab.c、ac.c
[abc] 匹配括号内任意一个字符 file[12].c 匹配 file1.c、file2.c
[a-z] 匹配一个范围内的字符 file[a-d].c 匹配 filea.c 到 filed.c

% 是 Makefile 中最特殊的通配符,主要用于模式规则和静态模式规则中目标和依赖的模式中,% 在目标和依赖中必须成对出现,表示 “任意长度的字符串”

(1) 模式和茎

  • 模式(pattern)是带有一个 % 的字符串,可以有前缀、后缀,也可以只是 % 本身,举例:

    • %.c (前缀为空,后缀为 .c)

    • lib%.a (前缀 lib,后缀 .a)

    • % (没有前后缀)

  • 茎(stem)是% 匹配到的那部分字符串,举例:

    • 模式 %.c 匹配 test.c → 茎是 test

    • 模式 lib%.a 匹配 libhello.a → 茎是 hello

(3) 茎的传递

  • 如果目标和依赖目标都带 %,目标模式的 % 匹配目标文件名得到了茎,而每一个目标的依赖文件是使用此目标的茎代替依赖模式中的模式字符 % 而得到,这就是茎的传递,举例:

    objects = foo.o bar.o
    all: $(objects)
    $(objects): %.o: %.c
    $(CC) -c $(CFLAGS) $< -o $@
    • 目标:foo.o

    • 匹配规则 %.o → % 匹配 foo → 茎 = foo

    • 依赖:%.c → 替换 % → foo.c

    • 执行命令:$(CC) -c foo.c -o foo.o

    • 同理,bar.o 会生成 bar.o,依赖 bar.c

  • 当模式匹配包含有斜杠(包含目录名)的文件时,那么在进行模式匹配时,目录部分会首先被移开,然后进行匹配,成功后,再把目录加回去,举例:

    • 目标模式 e%t | 依赖模式 c%r | 文件 src/eat
    • 首先剥离文件的目录名,得到 eat 和模式 e%t 进行匹配,得到了 %=a,之后再把目录加回去,于是 %=src/a,得到了茎
    • 然后茎传给依赖模式的 % 进行替换,得到依赖就是 src/car
维度 Shell 通配符(*、?、[...]) Makefile % 通配符
解析工具 Shell 解释器(如 Bash) Make 工具
核心作用 匹配文件系统中的实际文件 建立目标与依赖的关联关系(模式规则)
匹配逻辑 直接匹配文件名(如 *.c 匹配所有 .c 文件) 通过 “主干” 关联目标与依赖(如 %.o 关联 %.c)
使用场景 1. Shell 命令中批量操作文件
2. Makefile 中通过 $(wildcard) 匹配文件
1. 模式规则和静态模式规则
2. patsubst 等函数的模式替换
独立性 可单独使用(如 rm *.log) 必须成对出现(目标和依赖中同时有 %)

定义方式:

target: prerequisites
recipe
  • 基础规则是最常见的规则类型,用来直接说明某个文件是如何由其他文件生成的

  • 如果依赖中有文件比目标新,就会执行 recipe

定义方式:

target1 target2 ... : prerequisites
recipe
  • 多个目标共享同一套规则,执行时会对每个目标都运行一次命令

  • 自动变量 $@ 会分别取当前目标的名字

  • 常用于多个文件生成方式完全相同的情况

示例:

foo bar:
@echo $@

执行:

Terminal window
make foo

输出:

Terminal window
foo

执行:

Terminal window
make bar

输出:

Terminal window
bar

模式规则是 预定义或自定义的通用规则,通过通配符 % 匹配一类目标和依赖,自动应用于所有符合模式的文件。它不需要显式指定目标列表,make 会自动寻找匹配的文件并应用规则

定义方式:

# 格式:目标模式: 依赖模式
%.目标后缀: %.依赖后缀
recipe
  • % 是通配符,表示 “任意长度的字符串”,在目标和依赖中必须成对出现(代表同一部分)。
  • 命令中通常使用自动变量
  • 无需显式列出目标,make 会自动为所有符合 %.目标后缀 的文件应用规则
  • 自定义模式规则可以覆盖 make 内置的默认模式规则

示例:

# 链接所有 .o 文件生成可执行程序
app: main.o utils.o log.o
$(CC) $^ -o $@
# 将所有 .c 文件编译为 .o 文件
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@

静态模式规则是 显式指定目标列表 的模式规则,它为一组已知的目标绑定统一的模式匹配逻辑。与模式规则的 “自动匹配” 不同,静态模式规则的目标是明确列出的,仅对这些目标生效

定义方式:

# 格式:目标列表: 目标模式: 依赖模式
目标1 目标2 ...: %.目标后缀: %.依赖后缀
recipe
  • 目标列表:显式列出要应用规则的目标(如 main.o utils.o)
  • 目标模式:用 % 表示目标名称中与依赖对应的部分(如 %.o)
  • 依赖模式:用 % 关联到目标模式中的 %(如 %.c,表示 main.o 依赖 main.c)

静态模式规则和模式规则不同之处是模式规则是从全局目标中匹配符合的目标,而静态模式规则是从目标列表中匹配符合的目标

在 Makefile 中,隐含规则是 make 工具预定义的一组规则和变量,无需用户显式编写即可生效,主要用于简化常见的构建任务,它们是模式规则的一种特殊形式,覆盖了大多数通用的文件转换场景

定义方式:

# 格式:目标模式: 依赖模式
%.目标后缀: %.依赖后缀
  • 使用隐含规则生成需要的目标,可以不显示写出这个目标的规则,make 会试图去自动推导产生这个目标的规则和命令(make 中预定义了这些规则)
  • make 预定义的规则:
    • 编译规则:.c –> .o | .cpp/.cc –> .o | .f –> .o | …
    • 链接规则:.o –> 可执行文件 | .c –> 可执行文件 | …
    • ……

示例:

CC = gcc
CFLAGS = -Wall -g
LDLIBS = -lm
# 仅定义最终目标,依赖由隐含规则处理
app: main.o utils.o # 无需写链接规则,隐含规则会处理

下面列举了常见函数,具体详情查看 GUN make 手册

函数 用法 说明 示例
$(subst from,to,text) 替换字符串 将 text 中所有 from 替换成 to $(subst a,b,cat) → cbt
$(patsubst pattern,replacement,text) 模式替换 用 % 模式匹配 text 中单词并替换 $(patsubst %.c,%.o,main.c) → main.o
$(strip string) 去掉空格 删除首尾和多余空格 $(strip a b c) → a b c
$(findstring find,in) 查找子串 如果 find 在 in 中,返回 find,否则空 $(findstring a,abc) → a
$(filter pattern...,text) 过滤匹配 返回 text 中匹配 % 模式的部分 $(filter %.o,main.o test.c) → main.o
$(filter-out pattern...,text) 排除匹配 返回 text 中不匹配 % 模式的部分 $(filter-out %.c,main.c test.o) → test.o
函数 用法 说明 示例
$(join list1,list2) 拼接列表 将 list1 和 list2 对应元素拼接 $(join a b,c d) → ac bd
$(word n,text) 取单词 返回 text 中第 n 个空格分隔单词 $(word 2,a b c) → b
$(words text) 单词数 返回 text 中单词个数 $(words a b c) → 3
$(wordlist s,e,text) 单词子集 返回 text 中第 s 到 e 个单词 $(wordlist 2,3,a b c d) → b c
函数 用法 说明 示例
$(basename names…) 去掉后缀 返回文件名不带扩展名 $(basename main.c) → main
$(notdir names…) 去掉目录 返回文件名不带路径 $(notdir src/main.c) → main.c
$(dir names…) 取目录 返回路径部分 $(dir src/main.c) → src/
$(suffix names…) 取后缀 返回文件扩展名 $(suffix main.c) → .c
$(addsuffix suffix,names…) 添加后缀 给每个名字添加后缀 $(addsuffix .o,main test) → main.o test.o
$(addprefix prefix,names…) 添加前缀 给每个名字添加前缀 $(addprefix src/,main.o test.o) → src/main.o src/test.o
函数 用法 说明 示例
$(wildcard pattern) 文件匹配 使用 glob 匹配实际存在的文件 $(wildcard src/*.c) → src/main.c src/test.c
函数 用法 说明 示例
$(or a,b,…) 逻辑或 返回第一个非空参数 $(or ,x,y) → x
$(and a,b,…) 逻辑与 所有非空才返回最后一个参数,否则空 $(and x,y) → y
函数 用法 说明 示例
$(if condition,then[,else]) 条件判断 如果 condition 非空,则返回 then,否则返回 else $(if $(CC),gcc,cc) → gcc
$(foreach var,list,text) 循环 对 list 中每个单词,设置 var 并展开 text,返回拼接结果 $(foreach f,main.c test.c,$(basename $(f))) → main test
$(origin var) 查询变量来源 返回变量来源类型:file, command line, environment, default 等 $(origin CC) → file
$(shell command) 调用命令 执行 shell 命令并返回结果 $(shell ls *.c) → main.c test.c
函数类别 pattern 类型 匹配对象
patsubst / filter / filter-out % 单词列表(逻辑匹配)
wildcard glob (* ? [..]) 文件系统中的实际文件

(1) 空变量

在 Makefile 中空变量 "" 有两种形式:

  • 未定义的变量:如果直接引用未定义的变量,结果是空值,使用 $(origin VAR) 查看变量来源会输出 undefined
  • 显式定义空变量:使用 VAR = 显式定义变量 VAR 是空变量,使用 $(origin VAR) 查看变量来源会输出 file

(2) 空格

在 Makefile 中空格可以这样定义:

nullstring :=
space := $(nullstring) $(nullstring)

借助空串界定了一个空格,还可以借助注释符的特性来定义空格,注释符 # 可以表示变量定义的终止,于是

dir := /foo/bar # directory to put the frobs in

dir 这个变量的值是 /foo/bar ,后面还跟了4个空格,借助这个特性,也可以定义空格:

nullstring :=
space := $(nullstring) # end of the line

借助 # 和空串定义一个值空格的变量

在 Makefile 中,条件语句用于根据变量的值或是否定义来决定执行不同的构建逻辑,实现构建过程的动态适配(如区分 debug/release 模式、不同平台适配等)。条件语句的核心是在解析 Makefile 时进行判断,而非执行命令时

语法:

<conditional-directive>
<text-if-true>
endif

以及

<conditional-directive>
<text-if-true>
else
<text-if-false>
endif

其中 <conditional-directive> 表示条件关键字,这个关键字有四个:ifeq、ifneq、ifdef、ifndef

(1) 关键字 ifeq

语法:

ifeq (<arg1>, <arg2>)
ifeq '<arg1>' '<arg2>'
ifeq "<arg1>" "<arg2>"
ifeq "<arg1>" '<arg2>'
ifeq '<arg1>' "<arg2>"

比较参数 arg1 和 arg2 的值是否相同,如果相同则为真

参数可以是函数

示例:

ifeq ($(strip, $foo),)
echo foo is empty
endif

(2) 关键字 ifneq

语法:

fneq (<arg1>, <arg2>)
ifneq '<arg1>' '<arg2>'
ifneq "<arg1>" "<arg2>"
ifneq "<arg1>" '<arg2>'
ifneq '<arg1>' "<arg2>"

比较参数 arg1 和 arg2 的值是否不同,如果不同则为真

参数可以是函数

(3) 关键字 ifdef

语法:

ifdef <variable-name>

如果变量 <variable-name> 已经定义,那到表达式为真。否则,表达式为假

注意, ifdef 只是测试一个变量是否有值,其并不会把变量扩展到当前位置

示例:

bar =
foo = $(bar)
ifdef foo
frobozz = yes
else
frobozz = no
endif

(4) 关键字 ifndef

语法:

ifndef <variable-name>

如果变量 <variable-name> 没有定义,那到表达式为真。否则,表达式为假

注意, ifdef 只是测试一个变量是否有值,其并不会把变量扩展到当前位置

(1) VAPTH 变量

在 Makefile 中,VPATH 是一个特殊变量,用于指定 文件搜索路径。当 Make 需要查找目标文件或依赖文件(如源文件、头文件)而该文件不在当前目录时,会自动到 VPATH 定义的路径中查找,这对于多目录项目的文件管理非常有用

VAPTH 变量定义:

VAPTH 的值是一个以冒号(:)分隔的目录列表

VAPTH = path1:path2:...

当 make 解析规则时,若目标或依赖文件在当前目录不存在,会依次到 VPATH 列出的目录中查找:

(2) vapth 指令

在 Makefile 中,vpath是一个指令关键字,用于为特定模式的文件(如 .c 源文件、.h 头文件)指定搜索路径。它比 VPATH 变量更灵活,能针对不同类型的文件设置独立的搜索规则,是多目录项目中管理文件路径的重要工具

vapth 指令定义:

vpath 指令有三种用法,核心是 “模式匹配 + 路径指定”

  • 设置路径:为匹配 “模式” 的文件指定搜索目录

    vpath <模式> <目录列表> # 目录列表用冒号(:)分隔
  • 清除特定模式的路径:移除之前为 “模式” 设置的所有搜索路径

    vpath <模式>
  • 清除所有路径:移除通过 vpath 指令设置的所有搜索路径

    vpath

示例说明:

假设项目结构如下:

project/
├── Makefile
├── src/ # 存放 .c 源文件(main.c、tool.c)
├── include/ # 存放 .h 头文件(common.h)
└── test/ # 存放测试相关 .c 文件(test.c)

为不同类型文件设置不同搜索路径

# 1. 为所有 .c 文件设置搜索路径:先找 src 目录,再找 test 目录
vpath %.c src:test
# 2. 为所有 .h 文件设置搜索路径:仅找 include 目录
vpath %.h include
# 编译 main.o:依赖 main.c(在 src 目录)和 common.h(在 include 目录)
main.o: main.c common.h
gcc -c $< -o $@ -Iinclude # -I 是编译器选项,用于查找头文件
# 编译 test.o:依赖 test.c(在 test 目录)
test.o: test.c
gcc -c $< -o $@
  • 执行 make main.o 时,make 会自动从 src/main.c 找到源文件,从 include/common.h 找到头文件;
  • 执行 make test.o 时,make 会从 test/test.c 找到源文件,无需写全路径

清除特定模式的路径

# 先为 .c 文件设置路径
vpath %.c src:test
# 清除 .c 文件的搜索路径(之后找不到 src 或 test 中的 .c 文件)
vpath %.c
# 此时编译 main.o 会报错(找不到 main.c)
main.o: main.c
gcc -c $< -o $@

清除所有 vpath 路径

# 设置路径
vpath %.c src
vpath %.h include
# 清除所有通过 vpath 设置的路径
vpath
# 此时 .c 和 .h 文件的搜索路径均失效
main.o: main.c common.h # 会报错(找不到文件)
gcc -c $< -o $@

(3) VAPTH & vpath 对比

特性 vpath 指令 VPATH 变量
作用对象 仅匹配 “模式” 的文件(如 %.c) 所有文件(不区分类型)
灵活性 可针对不同文件类型设置不同路径 对所有文件生效,无法区分类型
语法形式 指令,需按 vpath 模式 目录 书写 变量,直接赋值(VPATH = 目录1:目录2)

在 Makefile 中,命令修饰符是放在命令前的特殊符号,用于改变命令的执行行为。它们能帮助处理错误、控制输出或调整执行逻辑,一共三种:

修饰符 作用 常见用法 备注
@ 隐藏命令本身的输出 美化构建日志 不影响命令执行结果
- 忽略该命令的错误 清理文件、可选步骤 错误不会中断构建
+ 即使启用了 -n(仅打印),也强制执行该命令;并且允许并行模式下运行 常用于调用子 make 在递归 make 时几乎必用

上述修饰符可以互相组合,比如:-@

(1) @(静默)

make 默认会输出执行的指令,@ 可以隐藏命令本身的打印,但是不影响命令执行结果

示例:

all:
@echo "Building..."

输出:

Terminal window
Building...

(2) -(容错)

命令前加 - 表示即使该命令执行失败(返回非 0 状态码),make 也会继续执行后续命令,不会终止构建过程

示例:

clean:
-rm -f *.o
echo "清理完成"

如果不加 -,当 rm 因找不到文件报错时,make 会立即停止,不会执行后续的 echo 命令,加了 -,即使没有 .o 文件(rm 报错),也继续执行

(3) + (强制执行)

使用 make -n(模拟执行)时,普通命令模拟执行(实际不执行),但是带 + 的命令会强制执行(实际执行)

示例:

all:
+$(MAKE) -C src

无论是否 -n,都能正确执行子 make

在 Makefile 使用 include 指令可以把其他的 Makefile 包含进来,这很像C语言的 #include ,被包含的文件会原模原样的放在当前文件的包含位置。 include 的语法是:

include <filenames>...
  • include 前面可以有一些空字符,但是绝不能是 Tab 键开始
  • include 和 <filenames> 可以用一个或多个空格隔开
  • <filenames> 是 Shell 所支持的文件名(可以使用通配符)

举例:

有这样几个Makefile: a.mk 、 b.mk 、 c.mk ,还有一个文件叫 foo.make ,以及一个变量 bar = fish bash ,下面的语句:

include foo.make *.mk $(bar)

等价于:

include foo.make a.mk b.mk c.mk fish bash

make 命令开始时,会找寻 include 所指出的其它 Makefile,并把其内容安置在当前的位置。如果 include 指定的文件不是以斜线开始(绝对路径,如/usr/src/Makefile...), 而且当前目录下也不存在此文件;make 将根据文件名试图在以下几个目录下查找:

  1. 首先,查找使用命令行选项 -I 或者 --include-dir指定的目录,如果找到指定的文件,则使用这个文件;

  2. 否则依此搜索以下几个目录(如果其存在): /usr/gnu/include、/usr/local/includ 和 /usr/include

当在这些目录下都没有找到 include 指定的文件时,make 将会提示一个包含文件未找到的警告提示,但是不会立刻退出。而是继续处理 Makefile 的内容。当完成读取所有的 Makefile 文件后, make 将试图使用规则来创建通过 include 指定的但未找到的文件,当不能创建它时(没有创建这个文件的规则),make 将提示致命错误并退出,如果想让 make 不理那些无法读取的文件,而继续执行,可以使用 + 修饰命令:

-include <filenames>...

这样无论 include 过程中出现什么错误,都不会报错,而是继续执行

在 Makefile 中,嵌套执行指的是在一个 Makefile 中调用另一个 Make 进程来执行子目录中的 Makefile,常用于管理多目录、多模块的大型项目(如源码目录、测试目录、库目录分离的项目)。通过嵌套执行,可以将复杂项目的构建逻辑拆分到各个子目录的 Makefile 中,实现模块化管理

嵌套执行语法:

在父 Makefile 中,通过 $(MAKE) 命令(而非直接使用 make)调用子目录的 Makefile,语法如下:

# 进入子目录并执行 make
subtarget:
$(MAKE) -C [target] [VAR=val]
  • $(MAKE):推荐使用这个变量(而非直接写 make),它会自动传递当前 make 的选项(如 -j 并行构建、-k 忽略错误等)
  • -C subdir:切换到指定的子目录后再执行 make
  • [target]:可选,指定子 Makefile 要执行的目标(如 all、clean)
  • [VAR=val]:可选,向子 Makefile 传递变量(覆盖子 Makefile 中的默认定义)

上述语法等价于:

# 进入子目录并执行 make
subtarget:
cd subdir && $(MAKE) [target] [VAR=val]

父 Makefile 的变量可以传递到子 Makefile 中(通过 export 声明),但是不会覆盖下层的Makefile中所定义的变量,除非指定了 -e 参数

示例说明:

假设项目结构如下(多目录拆分):

project/
├── Makefile # 父 Makefile
├── src/ # 源码目录
│ ├── Makefile # 子 Makefile(编译源码生成 libsrc.a)
│ ├── main.c
│ └── utils.c
└── test/ # 测试目录
├── Makefile # 子 Makefile(编译测试程序)
└── test.c

子目录 Makefile(src/Makefile)

# 编译源码为静态库
LIB = libsrc.a
OBJS = main.o utils.o
all: $(LIB)
$(LIB): $(OBJS)
ar rcs $@ $^ # 打包为静态库
%.o: %.c
gcc -c $< -o $@
clean:
rm -f $(OBJS) $(LIB)

子目录 Makefile(test/Makefile)

# 编译测试程序,依赖 src 目录的库
TEST = test_app
OBJS = test.o
LIBS = ../src/libsrc.a # 依赖 src 生成的库
all: $(TEST)
$(TEST): $(OBJS) $(LIBS)
gcc $< $(LIBS) -o $@
%.o: %.c
gcc -c $< -o $@
clean:
rm -f $(OBJS) $(TEST)

父 Makefile(project/Makefile)

# 定义子目录
SUBDIRS = src test
# 默认目标:构建所有子目录
all:
# 遍历子目录,依次执行 make
for dir in $(SUBDIRS); do \
$(MAKE) -C $$dir; \
done
# 清理所有子目录
clean:
for dir in $(SUBDIRS); do \
$(MAKE) -C $$dir clean; \
done
# 单独构建 src 目录
src:
$(MAKE) -C src
# 单独构建 test 目录(依赖 src 先构建)
test: src
$(MAKE) -C test
  • 执行 make(或 make all):父 make 会先进入 src 目录编译静态库,再进入 test 目录编译测试程序
  • 执行 make clean:父 make 会清理 src 和 test 目录的目标文件和产物
  • 执行 make test:父 make 会先确保 src 目录构建完成,再构建 test 目录(体现依赖关系)

C/C++ 源文件之间通常通过头文件(.h)相互依赖,当某个头文件修改时,需要重新编译依赖它的源文件,手动维护依赖关系很容易出错,尤其是大型项目。自动生成依赖可以让 make 自动追踪头文件变化,如果头文件发生变化可以及时更新目标

自动生成依赖的实现方式:

最常用的方法是利用编译器的 -M 系列选项(以 GCC 为例)生成依赖规则,再通过 Makefile 的 include 指令引入这些规则

(1) GCC 生成依赖选项

GCC 提供了一系列用于生成依赖关系的选项,核心功能及区别如下:

  • -M:分析源码并输出完整依赖关系,包含用户自定义头文件和系统头文件(如 stdio.h)
  • -MM:分析源码并生成仅包含用户自定义头文件的依赖关系(忽略系统头文件,推荐使用)
  • -MF 文件名: 将分析得到的依赖规则输出到指定文件(通常使用 .d 作为后缀的依赖文件)
  • -MP:为每个依赖的头文件生成伪目标,避免头文件被删除时出现构建错误
  • -MT 目标:自定义依赖规则中的目标名称(默认目标为与源文件同名的 .o 文件)
  • -MD:编译源码时自动生成 .d 依赖文件,包含完整依赖(含系统头文件),同时生成 .o 目标文件
  • -MMD:编译源码时自动生成 .d 依赖文件,仅包含用户自定义头文件(忽略系统头文件,推荐使用),同时生成 .o 目标文件

-M / -MM

  • 直接传入源码(如gcc -MM main.c)进行依赖分析,此时分析结果直接输出到标准输出

  • 编译源码(如gcc -MM -MF main.d -c main.c -o main.o)过程中进行依赖分析,此时需要指定输出文件,否则不输出信息

-MD/-MMD

  • 直接传入源码(如gcc -MMD main.c)进行依赖分析,此时分析结果输出到文件 a-main.d
  • 编译源码(如gcc -MMD -c main.c -o main.o)过程中进行依赖分析,此时分析结果输出到文件 main.d

示例:

为 main.c 生成依赖,指定目标名为 main.o 并输出到 main.d

gcc -MM -MF main.d -MT main.o main.c

执行后,main.d 内容可能为:

main.o: main.c header.h common.h

表示 main.o 依赖 main.c、header.h 和 common.h(自动分析所有包含的头文件)

(2) 在 Makefile 中集成自动依赖

通过模式规则批量生成 .d 依赖文件,并使用 include 引入这些文件,实现依赖的自动管理

假设目录结构:

project/
├── main.c # 主程序源文件
├── utils.c # 工具函数源文件
├── include/ # 头文件目录
│ ├── main.h # main.c 的头文件
│ └── utils.h # utils.c 的头文件
└── Makefile # 构建脚本

假设源码内容(简化):

  • main.c 包含 include/main.h 和 include/utils.h
  • utils.c 包含 include/utils.h

在 Makefile 中集成自动依赖管理:

# 编译器与选项
CC = gcc
CFLAGS = -Wall -g -I./include # -I 指定头文件搜索目录
LDFLAGS =
# 源文件、目标文件、依赖文件
SRCS = main.c utils.c
OBJS = $(SRCS:.c=.o) # 转换为:main.o utils.o
DEPS = $(OBJS:.o=.d) # 转换为:main.d utils.d
# 最终可执行文件
TARGET = app
# 默认目标:构建可执行文件
all: $(TARGET)
# 链接目标文件生成可执行文件
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) $(OBJS) -o $@ $(LDFLAGS)
# 模式规则:编译 .c 文件生成 .o 文件,并自动生成 .d 依赖文件
# -MMD:编译时生成 .d 依赖(仅用户头文件)
# -MP:为头文件生成伪目标
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@ -MMD -MP
# 引入所有 .d 依赖文件(-include 忽略不存在的文件)
-include $(DEPS)
# 清理构建产物
clean:
rm -f $(OBJS) $(DEPS) $(TARGET)
# 伪目标:避免与同名文件冲突
.PHONY: all clean

假设 main.d 和 utlis.d 内容如下:

main.d
main.o: main.c include/main.h include/utils.h
include/main.h: # -MP 生成的伪目标
include/utils.h: # -MP 生成的伪目标
# utlis.d
utils.o: utils.c include/utils.h
include/utils.h: # -MP 生成的伪目标

于是会被插入到上述 Makefile 中,使得 make 可以及时跟踪头文件的变化,以生成新的目标文件

1. -f <file> / --file=<file>

指定要使用的 Makefile 文件(默认寻找 Makefile 或 makefile)

示例:

make -f MyMakefile # 使用 MyMakefile 而非默认文件

2. -j <n> / --jobs=<n>

并行执行任务(多进程构建),n 为最大并行任务数,如果不带 n,那么 make 会自动根据 CPU 核心数决定并行数

示例:

make -j4 # 同时运行 4 个任务
make -j # 自动根据 CPU 核心数决定并行数(推荐)

注意:需确保目标间依赖关系正确,避免并行冲突

3. -k / --keep-going

遇到错误时不立即停止,继续执行其他不依赖于错误目标的任务。适合批量构建时找出所有错误。

示例:

make -k # 即使部分目标失败,仍尽可能执行其他目标

4. -n / --just-print / --dry-run

模拟执行,只打印将要执行的命令,不实际执行。用于调试 Makefile,检查命令是否正确。

示例:

make -n clean # 显示 clean 目标会执行的命令,但不真正删除文件

5. -t / --touch

不执行命令,仅更新目标文件的时间戳(让目标看起来是最新的)。用于避免重复构建未修改的文件。

示例:

make -t # 标记所有目标为"已更新",下次 make 会认为无需重新构建

6. -B / --always-make

强制重新构建所有目标,无论文件是否有更新。

示例:

make -B # 忽略时间戳检查,强制重新编译所有文件

1. -d / --debug

输出详细的调试信息,包括依赖关系解析、目标是否需要更新等。适合排查复杂的 Makefile 逻辑问题。

示例:

make -d # 显示完整的调试日志(内容较多)

2. -p / --print-data-base

打印 make 的内部数据库,包括所有变量、规则、隐式规则等。用于了解 make 的默认行为。

示例:

make -p # 输出所有内置规则和变量(可重定向到文件查看)

3. -q / --question

检查目标是否需要更新,不执行任何命令,仅通过返回码表示结果:

  • 0:所有目标都是最新的(无需构建)
  • 1:有目标需要更新
  • 2:出错

示例:

make -q && echo "已是最新" || echo "需要构建"

4. --warn-undefined-variables

当使用未定义的变量时,输出警告信息。帮助发现拼写错误或未初始化的变量。

示例:

make --warn-undefined-variables # 检测未定义的变量

1. -I <dir> / --include-dir=<dir>

添加 include 指令的搜索目录(用于寻找被包含的 Makefile 片段)

示例:

make -I ./config # 在 ./config 目录中搜索 include 的文件

2. -C <dir> / --directory=<dir>

进入指定目录后再执行 make(常用于嵌套构建)

示例:

make -C src # 进入 src 目录,执行该目录下的 Makefile

1. -s / --silent / --quiet

静默模式:不显示执行的命令(类似在所有命令前加 @ 修饰符)

示例:

make -s # 只显示命令输出,不显示命令本身

2. -o <file> / --old-file=<file>

将 <file> 视为 “旧文件”(即使它实际是新的),不触发依赖它的目标重建

示例:

make -o main.c # 即使 main.c 被修改,也不重新编译依赖它的目标

3. -W <file> / --what-if=<file>

将 <file> 视为 “新文件”(即使它实际未修改),强制重建依赖它的目标

示例:

make -W main.h # 强制重新编译所有依赖 main.h 的文件

4. -h / --help

显示帮助信息,列出所有选项的简要说明

示例:

make -h