3. 第一个Makefile
变量是 Makefile 的灵魂。没有变量,你写的每一行路径、每一个编译器选项都得手敲,改一次就得改几十处。我刚开始用 Makefile 时,就吃过这个亏——一个项目里硬编码了十几处源文件路径,后来加了个模块,改得我头皮发麻。
说白了,变量就是给一段文本起个名字。Make 在解析时会把它展开成实际内容。但问题来了:什么时候展开?展开几次?不同赋值方式的结果完全不同。这一节,咱们就把这四种赋值运算符掰开揉碎讲清楚。
3.1 变量的定义与引用
先看最基本的用法:
# 定义变量
CC = gcc
CFLAGS = -Wall -O2
TARGET = myapp
# 引用变量
$(TARGET): main.o utils.o
$(CC) $(CFLAGS) -o $@ $^
引用变量用 $() 或 ${},我习惯用圆括号,看着更清爽。注意,变量名区分大小写,CC 和 cc 是两个不同的变量。
$(CC) 默认就是 cc,$(RM) 是 rm -f。你可以覆盖它们,但别滥用。
3.2 四种赋值运算符
这才是重点。四种赋值方式,每种都有自己的“展开时机”。我画了张图帮你理解:
3.2.1 =:递归展开赋值
这是 Make 的默认赋值方式。它不会在定义时展开变量,而是等到真正使用时才展开。听起来有点绕?看个例子:
A = hello
B = $(A) world
A = hi
all:
@echo $(B)
猜猜输出什么?是 hi world。因为 B 在定义时只记住了 $(A) world 这个文本,等到 echo 时才去查 A 的值,而此时 A 已经被改成 hi 了。
= 定义了一个变量,它引用了自身:LIST = $(LIST) new_item。结果 Make 直接报“无限递归”错误。这种自引用用 = 是死路一条。
3.2.2 :=:简单展开赋值
这个就好理解了——定义时立即展开,后续引用的是展开后的值。还是刚才的例子:
A := hello
B := $(A) world
A := hi
all:
@echo $(B)
输出是 hello world。因为 B 在定义时就把 $(A) 展开成了 hello,后面 A 怎么改都不影响 B。
我个人习惯:能用 := 的地方尽量用 :=。它让变量的值可预测,调试时少了很多“灵异事件”。
3.2.3 ?=:条件赋值
这个运算符的意思是:如果变量还没定义,就给它赋值;如果已经定义了,就跳过。说白了就是“安全默认值”。
CC ?= gcc
CFLAGS ?= -O2
all:
@echo $(CC) $(CFLAGS)
如果你在命令行传了 make CC=clang,那么 CC 就是 clang,?= 不会覆盖它。这在写通用 Makefile 时特别有用——用户可以通过环境变量或命令行参数覆盖默认值。
?= 定义工具链路径:CROSS_COMPILE ?= arm-linux-gnueabihf-。用户只要 export 自己的路径就能覆盖,不用改 Makefile。
3.2.4 +=:追加赋值
这个最简单——在变量原有值后面追加内容,自动加一个空格分隔。但有个细节要注意:追加的展开方式继承原变量的赋值方式。
CFLAGS := -Wall
CFLAGS += -O2
# 等价于 CFLAGS := -Wall -O2
如果原变量是用 = 定义的,那么 += 追加的内容也是延迟展开;如果原变量是用 := 定义的,追加的内容就是立即展开。嗯,这里要注意,别混用导致预期外的结果。
3.3 四种赋值方式对比表
| 运算符 | 名称 | 展开时机 | 自引用 | 推荐场景 |
|---|---|---|---|---|
= |
递归展开 | 使用时 | 不允许 | 需要延迟求值的场景 |
:= |
简单展开 | 定义时 | 允许 | 大多数场景,推荐使用 |
?= |
条件赋值 | 定义时(仅未定义) | 允许 | 提供默认值,允许外部覆盖 |
+= |
追加赋值 | 继承原变量 | 视原变量而定 | 逐步构建编译选项、源文件列表 |
3.4 实战中的选择策略
说了这么多理论,到底怎么选?我总结了几条经验:
- 默认用
:=:可预测、好调试。除非你有明确的延迟展开需求。 - 提供默认值时用
?=:让用户能通过环境变量或命令行覆盖。 - 逐步构建列表时用
+=:比如源文件列表、链接库列表。 - 只有明确需要延迟展开时才用
=:比如你想让一个变量根据最终状态决定值。
一个真实案例:我之前维护一个嵌入式项目,Makefile 里有几十个变量。最初全用 =,结果调试时经常搞不清某个变量到底展开成什么。后来我花了一天时间,把所有能用 := 的变量都改成了 :=,编译速度没变,但可读性和可调试性提升了一大截。
3.5 变量引用的高级技巧
除了基本的 $(VAR),还有几个实用技巧:
# 变量替换引用
SRCS = main.c utils.c
OBJS = $(SRCS:.c=.o)
# 等价于 OBJS = main.o utils.o
# 使用 patsubst 函数
OBJS2 = $(patsubst %.c, %.o, $(SRCS))
# 条件赋值与默认值
OUTPUT ?= build/output.bin
变量替换引用在编译规则里特别常用。你想想看,源文件列表和对象文件列表往往只有后缀不同,用 $(SRCS:.c=.o) 一行就搞定了,不用写两遍。
patsubst 函数更灵活。