2、变量与宏:Makefile中的变量定义、自动变量、环境变量、递归展开与简单展开变量
说实话,变量和宏是Makefile里最容易被低估的东西。
很多人写Makefile,上来就是硬编码路径、硬编码编译器参数。我刚开始做嵌入式开发时也这样,直到有一次项目要同时编译三个平台——ARM、MIPS、x86——我才发现,不改变量设计,Makefile根本没法复用。
这一章,咱们就把变量这块彻底讲透。
2.1 变量定义:你其实每天都在用
Makefile里的变量,说白了就是个名字替身。你定义一个变量,后面用到的地方自动替换成它的值。
CC = gcc
CFLAGS = -Wall -O2
hello: hello.c
$(CC) $(CFLAGS) -o hello hello.c
这里 CC 和 CFLAGS 就是变量。用 $() 或者 ${} 引用它们。
我个人习惯用 $(),因为跟Shell的 ${} 区分开,看着更清晰。
MY_PROJECT_DIR,而不是 myProjectDir。这是Makefile社区的惯例,一眼就能认出是变量。
2.2 自动变量:Makefile里的"魔法符号"
自动变量这东西,我第一次接触时觉得挺玄乎。后来用多了才发现,它其实就是Make帮你预定义好的一些快捷方式。
最常用的几个:
| 自动变量 | 含义 | 示例值 |
|---|---|---|
$@ |
目标文件名 | hello.o |
$< |
第一个依赖文件 | hello.c |
$^ |
所有依赖文件(去重) | hello.c utils.c |
$? |
比目标新的依赖文件 | utils.c |
$* |
去掉后缀的目标文件名 | hello |
举个例子你就明白了:
%.o: %.c
$(CC) -c $< -o $@
这条规则的意思是:
$< 是 hello.c,$@ 是 hello.o。
实际执行的就是 gcc -c hello.c -o hello.o。
我在项目中遇到过一个问题:有人用 $^ 代替 $<,结果链接时把同一个.o文件传了两遍。嗯,$^ 虽然去重,但用在编译阶段会出问题。记住:编译用 $<,链接用 $^。
$@,那玩意儿是未定义的。
2.3 环境变量:Makefile与系统的桥梁
Makefile能读取系统的环境变量。比如 PATH、HOME、USER 这些。
all:
@echo "当前用户:$(USER)"
@echo "家目录:$(HOME)"
但这里有个坑——环境变量可以被Makefile里的同名变量覆盖。
# 假设系统环境变量 CC=gcc
CC = clang
all:
@echo "编译器:$(CC)" # 输出 clang,不是 gcc
我曾经在一个团队项目里踩过这个坑。有人把 CC 在Makefile里硬编码成 gcc,但CI环境里 CC 指向的是 clang。结果本地编译没问题,CI上死活过不了。
我的建议是:不要依赖环境变量来做关键决策。如果一定要用,用 ?= 来赋值:
CC ?= gcc # 只有没定义时才用 gcc
这样既保留了环境变量的灵活性,又有默认值兜底。
2.4 递归展开与简单展开:两种变量,两种命运
这是Makefile变量里最核心、也最容易搞混的概念。
简单说:
- 递归展开变量(用
=定义):使用时才展开,延迟求值 - 简单展开变量(用
:=定义):定义时就展开,立即求值
看个例子:
# 递归展开
A = hello
B = $(A) world
A = hi
all:
@echo $(B) # 输出 "hi world"
为什么会这样?因为 B 在定义时并没有展开 $(A),它只是记下了"等我被用到时,再去查 A 的值"。等到 echo 执行时,A 已经被改成 hi 了。
再看简单展开:
# 简单展开
A = hello
B := $(A) world
A = hi
all:
@echo $(B) # 输出 "hello world"
B 在定义时就把 $(A) 展开成了 hello,后面 A 怎么变都跟它没关系。
=是"引用",用的时候才去查:=是"复制",定义时就定死了
我个人习惯:能用 := 就用 :=。原因很简单——避免意外。递归展开虽然灵活,但容易产生循环引用,而且调试时很难追踪值的变化。
你想想看,如果一个变量被引用了三层,每层都是递归展开,中间某个地方改了值,你找bug得找到什么时候?
- 路径、文件名等固定信息用
:= - 需要延迟求值的(比如根据目标平台动态变化)用
= - 条件赋值用
?= - 追加赋值用
+=
2.5 知识体系总览
下面这张图,把变量与宏的核心脉络串起来了:
这张图把变量定义、自动变量、环境变量、展开方式串在了一起。你写Makefile时,脑子里要有这个结构——变量不是孤立的,它们互相影响。
2.6 避坑指南
最后,分享几个我踩过的坑:
- 递归展开的循环引用:
A = $(B)和B = $(A)会导致无限递归。Make会报错,但排查起来很痛苦。 - 自动变量在依赖列表里无效:我见过有人写
$@.c当依赖,结果Make把它当成空字符串处理。 - 环境变量污染:CI环境里经常有各种环境变量,一不小心就覆盖了你的Makefile变量。用
?=能规避大部分问题。 - 空格陷阱:
CC = gcc和CC=gcc不一样。前者CC的值是gcc(带前导空格),后者是gcc。我因为这个浪费过半天时间。
:=,除非有明确的延迟求值需求。
变量这东西,看着简单,用好了能让你的Makefile优雅十倍。用不好,就是给自己挖坑。记住:明确、简单、可预测,这是变量设计的三个原则。
可可精品资料库