29. Makefile 创建和使用变量
🧭 一、Makefile 中变量的创建与使用
1️⃣ 创建变量
在 Makefile 中,可以使用 = 或 := 来定义变量。
1.1 使用 = 定义变量(延迟求值)
这种赋值方式是 延迟求值,即变量的值只有在它被 引用时 才会被计算。这适用于你需要依赖其他变量值的情况。
CC = gcc
CFLAGS = -Wall -g
SRC = main.c utils.call:$(CC) $(CFLAGS) $(SRC) -o my_program
解释:
CC,CFLAGS和SRC使用=定义,只有在all规则中引用它们时,它们的值才会被计算。
优点: 它可以向后引用变量
缺点: 不能对该变量进行任何扩展,例如
CFLAGS = $(CFLAGS) -O 会造成死循环
1.2 使用 := 定义变量(立即求值)
这种赋值方式是 立即求值,即在变量定义时就计算出它的值。通常在需要 避免递归引用 的场景下使用。
SRC := $(wildcard *.c)
解释:
SRC := $(wildcard *.c)使用:=立即求值,这会在Makefile解析时立即计算SRC的值。
- 用这种方式定义的变量,会在变量的定义点,按照被引用的变量的当前值进行展开
- 这种定义变量的方式更适合在大的编程项目中使用,因为它更像我们一般的编程语言
1.3 ?= 定义变量(条件赋值)
?= 用于 条件赋值,只有在变量 未定义时 才会为其赋值。这个功能在配置文件中很常见,可以为变量提供默认值,但不会覆盖用户在命令行中指定的值。
CC ?= gcc
CFLAGS ?= -Wall -g
解释:
- 如果
CC和CFLAGS没有被定义,那么会将它们分别赋值为gcc和-Wall -g。 - 如果你在命令行执行
make CC=clang,则CC会被设置为clang,而不是默认值gcc。
用?=定义变量
dir := /foo/bar
FOO ?= bar
FOO是?
含义是,如果FOO没有被定义过,那么变量FOO的值就是“bar”,如果FOO先前被定义过,那么这条语将什么也不做,其等价于:
ifeq ($(origin FOO), undefined)
FOO = bar
endif
🧩 二、Makefile 中变量的使用
2️⃣ 引用变量
在 Makefile 中,变量的引用可以使用 $() 或 ${} 语法。
- 推荐使用
$():$(CC)和$(SRC)。 ${}是与$()等效的,但通常$()更常用。
示例:
CC = gcc
CFLAGS = -Wall -g
SRC = main.c utils.call:$(CC) $(CFLAGS) $(SRC) -o my_program
$(CC)会被替换为gcc,$(SRC)会被替换为main.c utils.c,最终的命令是:gcc -Wall -g main.c utils.c -o my_program。
🧩 三、Makefile 中的自动变量
好的!既然你对 Makefile 中的自动变量 完全没有了解,我将从基础开始,详细讲解这些变量的作用和用法,确保你能够理解并在实际的 Makefile 中灵活使用它们。
🧭 3.1 什么是 Makefile 中的自动变量?
自动变量 是 Makefile 中一类特殊的变量,它们由 make 自动提供,通常用于表示规则中的 目标文件、依赖文件、命令等内容。
自动变量通常在构建规则中使用,帮助你简化和动态化构建过程。
自动变量的作用:
- 目标文件(
$@) - 第一个依赖文件(
$<) - 所有依赖文件(
$^) - 更新过的依赖文件(
$?) - 目标文件的文件名部分(
$*) - 目标文件的目录部分(
$(@D)) - 目标文件的文件名部分(
$(@F))
这些变量的值是由 make 自动计算的,具体取决于 Makefile 的规则和依赖关系。
🧩 3.2、常见的自动变量及其用法
1️⃣ $@:目标文件的名称
$@ 代表当前规则的 目标文件,即你要生成的文件。在链接规则中,这通常是最终生成的目标文件名。
示例:
my_program: main.o utils.ogcc -o $@ $^
解释:
$@代表目标文件my_program。gcc -o $@ $^会变成gcc -o my_program main.o utils.o,用于生成最终的可执行文件my_program。
2️⃣ $<:第一个依赖文件的名称
$< 代表规则中 第一个依赖文件,它通常用于编译规则中,指向要编译的源文件。
示例:
%.o: %.cgcc -c $< -o $@
解释:
$<是源文件%.c,比如main.c,在这个规则中表示 第一个依赖文件。- 这里
gcc -c $< -o $@会变成gcc -c main.c -o main.o,用于编译源文件main.c成目标文件main.o。
3️⃣ $^:所有依赖文件的名称(去重)
$^ 代表规则中所有的依赖文件的名称,去重后用空格分隔。它常常用于链接阶段,表示所有需要链接的对象文件。
示例:
my_program: main.o utils.ogcc -o $@ $^
解释:
$^代表所有的依赖文件main.o和utils.o。gcc -o $@ $^会变成gcc -o my_program main.o utils.o,用于链接生成目标文件my_program。
4️⃣ $?:所有比目标文件新的依赖文件
$? 代表所有比目标文件 新的依赖文件,即 修改时间比目标文件更新的依赖文件。这是增量构建时非常有用的变量,只有更新过的依赖文件才会重新编译。
示例:
my_program: main.o utils.ogcc -o $@ $^echo "更新过的依赖文件: $?"
解释:
- 如果
main.o被修改了,而utils.o没有被修改,那么$?将返回main.o,而不是utils.o。
5️⃣ $*:目标文件的文件名部分(不包含扩展名)
$* 代表规则中目标文件的 文件名部分,去掉扩展名。它对于处理文件名而不考虑扩展名非常有用。
🧭$* 的真正含义
$*表示 目标文件的“stem”部分(即去掉扩展名之后的名字)。
它只在 模式规则(pattern rules) 中有意义。
✅ 举个最直观的例子:
%.o: %.cecho "目标文件名: $@"echo "目标文件名(不含扩展名): $*"echo "依赖文件: $<"
当你执行:
make main.o
输出可能是:
目标文件名: main.o
目标文件名(不含扩展名): main
依赖文件: main.c
🧩 理解 $* 是如何被“推导”的
在模式规则中:
%.o : %.c
% 匹配同一部分的字符串(称为 stem)。
👉 也就是说:
- 如果目标是
main.o - 那么
%对应的 “stem” 就是main - 所以
$*的值就是"main"
🧱 什么时候会用 $*
在一些高级自动生成规则中,$* 特别有用。
比如:
✅ 示例 1:用 $* 自动生成中间文件
假设我们编译 .c 文件时,还要生成 .d 依赖文件(记录头文件依赖)。
%.o: %.cgcc -c $< -o $@gcc -MM $< > $*.d
🔍 分析:
$<:源文件,例如main.c$@:目标文件,例如main.o$*:去掉扩展名后的文件名,例如main
于是第二行命令实际执行:
gcc -MM main.c > main.d
👉 这样我们自动生成了对应的依赖文件 main.d。
✅ 示例 2:编译不同后缀的文件但共用同一名字
假设我们有 .c 和 .s 文件(汇编),希望都编译成同名 .o 文件:
%.o: %.cgcc -c $< -o $@%.o: %.sas $< -o $@echo "汇编目标的基名:$*"
当执行:
make start.o
如果 start.s 存在,则:
$@=start.o$<=start.s$*=start
✅ 示例 3:批量操作同名文件
你可以用 $* 来生成多个文件:
%.bak: %.txtcp $< backup/$*.bak
当执行:
make hello.bak
实际命令执行:
cp hello.txt backup/hello.bak
💡 $* 让我们轻松拿到文件名的中间部分。
🧠 $* 的局限性与注意事项
-
只在“模式规则”中有效
如果你写的是普通规则(如main.o: main.c),那么$*为空。 -
**没有扩展名的目标无法推导出 ∗∗∗比如目标是‘Makefile‘(没有‘.‘),那么‘*** 比如目标是 `Makefile`(没有 `.`),那么 `∗∗∗比如目标是‘Makefile‘(没有‘.‘),那么‘*` 无法计算。
-
多后缀时取第一个匹配的规则
如果你有.c和.cpp同名文件,只会根据第一个匹配的规则计算$*。
🧩 3.3、Makefile 中自动变量的综合示例
为了更好地理解这些自动变量的用法,我们来看一个完整的 Makefile 示例:
示例:一个简单的 C 语言项目
假设你有以下目录结构:
project/├── src/├── main.c├── utils.c├── Makefile
在 Makefile 中,你需要编译源文件并链接生成可执行文件。我们使用自动变量来简化过程。
CC = gcc
CFLAGS = -Wall -g
SRC = src/main.c src/utils.c
OBJ = $(SRC:.c=.o)# 目标规则
my_program: $(OBJ)$(CC) $(CFLAGS) -o $@ $^# 编译规则
%.o: %.c$(CC) $(CFLAGS) -c $< -o $@
解释:
-
$(SRC:.c=.o):这是一个模式替换的用法,它将.c文件转换为.o文件,即将每个源文件.c映射为对应的目标文件.o。 -
my_program: $(OBJ):- 这是构建目标文件
my_program的规则。 - 目标文件
my_program依赖于.o文件(main.o和utils.o)。 $@会被替换为my_program。$^会被替换为main.o utils.o,表示所有依赖文件。
- 这是构建目标文件
-
编译规则:
%.o: %.c:- 这是一个模式规则,表示任何
.c文件都可以被编译成.o文件。 $<代表源文件%.c,比如main.c或utils.c。$@代表目标文件%.o,比如main.o或utils.o。
- 这是一个模式规则,表示任何
执行流程:
make会首先看到my_program规则,看到$(OBJ),它会找到main.o和utils.o作为依赖文件。- 对于
main.c和utils.c,make会根据%.o: %.c规则生成对应的.o文件。 - 一旦所有的
.o文件生成完成,make会使用gcc来链接生成my_program。
🧩 3.4、常见的自动变量错误与调试技巧
错误 1:混淆 $@ 和 $*
- 错误:在规则中错误地使用
$*作为目标文件。 - 原因:
$*是目标文件的 文件名部分(没有扩展名),而$@是完整的目标文件名。
正确用法:
%.o: %.cgcc -c $< -o $@
解释:$@ 是目标文件(如 main.o),而 $* 会返回 main(不包括 .o 扩展名)。
错误 2:忘记 $(CC) 或 $(CFLAGS) 的求值
- 错误:在变量中定义了
CC或CFLAGS,但没有在规则中正确引用它们。 - 原因:没有正确使用
$()来引用变量。
正确用法:
CC = gcc
CFLAGS = -Wall -g%.o: %.c$(CC) $(CFLAGS) -c $< -o $@
解释:$(CC) 和 $(CFLAGS) 被正确地引用来调用编译器和编译选项。
3.5 🧭 总结
自动变量总结:
| 自动变量 | 说明 | 使用场景 |
|---|---|---|
$@ | 目标文件的名称 | 用于表示当前规则的目标文件 |
$< | 第一个依赖文件 | 用于表示规则中的第一个依赖文件 |
$^ | 所有依赖文件(去重) | 用于表示所有的依赖文件 |
$? | 所有比目标文件新的依赖文件 | 用于增量构建时,更新过的依赖文件 |
$* | 目标文件的文件名部分 | 用于获取目标文件的文件名(不含扩展名) |
$(@D) | 目标文件的目录部分 | 用于获取目标文件所在的目录 |
$(@F) | 目标文件的文件名部分 | 用于获取目标文件的文件名部分 |
通过这节讲解,你应该能够理解和应用 Makefile 中的自动变量。掌握这些自动变量能大大简化构建规则,并且使你的 Makefile 更加灵活。如果你有任何问题或者需要更多的例子,随时告诉我!😊
🧩 四、环境变量
环境变量 是由操作系统或父进程设置的变量,它们存储系统配置信息。在 Makefile 中,环境变量通常用于 系统路径、工具链路径 或其他系统级的配置。
4️⃣ 访问环境变量
你可以直接通过 $(VARIABLE) 语法在 Makefile 中访问环境变量。例如,访问 PATH 或 HOME 环境变量:
all:echo "PATH: $(PATH)"echo "HOME: $(HOME)"
5️⃣ 设置环境变量
你还可以通过 export 将 Makefile 中的变量导出为环境变量,使得子进程也能访问它们:
export MY_VAR = valueall:echo "MY_VAR: $(MY_VAR)"
- 这将把
MY_VAR导出为环境变量,所有通过make调用的子进程都能访问到这个变量。
6️⃣ 在 Makefile 中使用环境变量
你可以在 Makefile 中引用环境变量来设置构建工具的路径或其他配置:
CC = $(MY_CC) # 如果 MY_CC 环境变量已定义
🧩 五、+= 和追加值
在 Makefile 中,+= 用于将新的值 追加到现有变量 的末尾。
示例:
CFLAGS = -Wall -g
CFLAGS += -O2all:echo "编译选项: $(CFLAGS)"
解释:
CFLAGS += -O2会将-O2追加到现有的CFLAGS后面。最终,CFLAGS的值将是-Wall -g -O2。
🧭 六、特殊变量和模式规则
1️⃣ 模式规则(Pattern Rules)
Makefile 允许通过模式规则(Pattern Rules)简化多文件的构建。你可以使用 % 通配符来匹配文件名。
示例:
%.o: %.cgcc -c $< -o $@
解释:
%.o: %.c是一个模式规则,表示所有的.c文件可以通过该规则生成.o文件。$<是源文件,$@是目标文件。
2️⃣ 内建函数
Makefile 中还提供了一些常见的内建函数,方便操作字符串、文件名等。
2.1 $(wildcard pattern):列出符合模式的文件
SRC := $(wildcard *.c) # 获取所有 .c 文件
2.2 $(patsubst pattern, replacement, text):替换模式
OBJ := $(patsubst %.c, %.o, $(SRC)) # 把 .c 后缀替换为 .o
2.3 $(basename names):去除文件扩展名
BASE := $(basename $(SRC)) # 去掉 .c 后缀,得到文件基名
🧭 七、总结与最佳实践
| 特性 | 说明 | 示例 |
|---|---|---|
| 变量定义 | 使用 =(延迟求值)、:=(立即求值)和 ?=(条件赋值)来定义变量 | CC ?= gcc,CFLAGS := -O2 |
| 自动变量 | 如 $@(目标文件)、$<(第一个依赖文件)等用于简化规则 | $@, $<, $^, $? |
| 环境变量 | 访问和设置系统级环境变量 | $(PATH), export MY_VAR = value |
| 追加变量值 | 使用 += 向现有变量添加新值 | CFLAGS += -O3 |
| 模式规则 | 用 % 定义模式规则,用于简化多文件规则 | %.o: %.c |
| 内建函数 | 如 $(wildcard)、$(patsubst) 等用于文件操作 | $(wildcard *.c), $(patsubst %.c, %.o, $(SRC)) |
- VPATH : 虚路径
- 在一些大的工程中,有大量的源文件,我们通常的做法是把这许多的源文件分类,并存放在不同的目录中。所以,当make需要去找寻文件的依赖关系时,你可以在文件前加上路径,但最好的方法是把一个路径告诉make,让make在自动去找。
- Makefile文件中的特殊变量“VPATH”就是完成这个功能的,如果没有指明这个变量,make只会在当前的目录中去找寻依赖文件和目标文件。如果定义了这个变量,那么,make就会在当当前目录找不到的情况下,到所指定的目录中去找寻文件了。
- VPATH = src:…/headers
- 上面的的定义指定两个目录,“src”和“…/headers”,make会按照这个顺序进行搜索。目录由“冒号”分隔。(当然,当前目录永远是最高优先搜索的地方)
