3、Makefile变量进阶:自动变量、预定义变量与环境变量
说实话,刚学Makefile那会儿,我觉得变量这玩意儿没啥好讲的。不就是个替换嘛?直到我在一个嵌入式项目里,手写了二十多个类似的编译规则,改一个源文件就要改三处地方……嗯,那次之后我才真正明白,用好变量,才是Makefile从“能用”到“好用”的分水岭。
这一节,我带你看看Makefile里真正实用的变量玩法。不是教科书式的罗列,而是我这些年踩坑踩出来的经验。
3.1 自动变量:写规则时的偷懒神器
自动变量是什么?说白了,就是Make帮你提前定义好的一些“代号”。你在规则里直接用就行,不用自己再费劲去写文件名。
我最常用的四个自动变量:$@、$<、$^、$?。一个一个说。
$@ —— 目标文件
$@代表当前规则的目标。比如你写一条规则生成main.o,那$@就是main.o。我在项目里经常这么用:
main.o: main.c
gcc -c $< -o $@
你看,不用写两遍main.o了。改目标名时,只改一处就行。
$< —— 第一个依赖文件
$<代表依赖列表里的第一个文件。上面那个例子里,$<就是main.c。我个人习惯在编译单个源文件时,几乎必用$<。
$^ —— 所有依赖文件
$^代表所有依赖文件,用空格隔开。这个在链接阶段特别好用:
app: main.o utils.o
gcc $^ -o $@
等价于:
app: main.o utils.o
gcc main.o utils.o -o app
少写好多重复内容,对吧?
$? —— 比目标新的依赖文件
$?这个变量,说实话,我一开始没太在意。直到有一次做增量编译优化,才发现它真香。它只列出那些“比目标文件更新”的依赖。什么意思?就是你改了哪个源文件,$?就只包含哪个。
我曾经在一个大型C项目里,用$?实现了只重新编译修改过的文件,编译时间从3分钟降到了10秒。用法大概是这样:
lib.a: foo.o bar.o baz.o
ar rcs $@ $?
只有被修改过的.o文件才会被重新打包进静态库。效率提升很明显。
$^和$<。比如:gcc -c $< -o $@ 然后 gcc $^ -o app。各司其职。
3.2 预定义变量:别自己造轮子
Make自带了一些预定义变量,比如CC、CFLAGS、LDFLAGS。很多人不知道,或者知道了也不爱用。我建议你养成习惯——用它们。
为什么?因为约定大于配置。别人看你的Makefile,一眼就知道编译器是CC,编译选项是CFLAGS。不用猜。
| 变量名 | 含义 | 默认值 |
|---|---|---|
| CC | C编译器 | cc |
| CFLAGS | C编译选项 | 空 |
| LDFLAGS | 链接选项 | 空 |
| AR | 归档工具 | ar |
| RM | 删除命令 | rm -f |
举个例子,一个规范的Makefile开头通常是这样的:
CC = gcc
CFLAGS = -Wall -O2 -I./include
LDFLAGS = -lm
app: main.o utils.o
$(CC) $^ -o $@ $(LDFLAGS)
%.o: %.c
$(CC) -c $< -o $@ $(CFLAGS)
你看,清晰明了。想换编译器?改CC就行。想加调试选项?改CFLAGS就行。我在公司里推行这套写法后,团队里再也没出现过“编译不过去,原来是因为编译器版本不对”这种破事。
CC = clang,那后面所有用到$(CC)的地方都会用clang。但如果你在命令行传参make CC=gcc,那命令行优先级更高。这个机制我吃过亏——有一次我在Makefile里写了CC = arm-linux-gcc,结果同事直接make CC=gcc,编译出一堆x86的二进制……嗯,从那以后我都在Makefile开头加一句注释:# 请使用make CC=xxx 来指定交叉编译器。
3.3 环境变量与Makefile变量:谁听谁的?
这个问题,我当年也迷糊过。环境变量和Makefile变量,到底谁覆盖谁?
简单说:Makefile里显式赋值的变量,优先级高于环境变量。但如果你在Makefile里用?=赋值,那环境变量就有机会了。
举个例子:
# 假设环境变量 CFLAGS 已经设置为 "-O2"
CFLAGS ?= -g -Wall
app: main.o
$(CC) -o $@ $< $(CFLAGS)
如果环境变量CFLAGS存在,那?=不会覆盖它,最终用的是-O2。如果环境变量不存在,那就用-g -Wall。
这个特性在持续集成里特别有用。我在CI脚本里设置export CFLAGS="-O3 -march=native",然后Makefile里用?=给个默认值。本地开发时用默认的调试选项,CI构建时用优化选项。两不耽误。
?=是个例外,它只在变量未定义时才赋值。
3.4 一张图看懂变量体系
说了这么多,我画张图帮你理一理。这张图是我自己总结的,每次带新人时都会拿出来讲一遍。
这张图把三种变量和它们的关系说清楚了。自动变量服务于规则内部,预定义变量是行业惯例,环境变量则用于外部传入。三者各司其职,互不冲突。
3.5 实战:一个完整的例子
最后,我放一个我实际项目里用过的Makefile片段。不算复杂,但把今天讲的变量都用上了:
CC ?= gcc
CFLAGS ?= -Wall -O2
LDFLAGS ?= -lm
SRCS = main.c utils.c
OBJS = $(SRCS:.c=.o)
TARGET = app
$(TARGET): $(OBJS)
$(CC) $^ -o $@ $(LDFLAGS)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJS) $(TARGET)
.PHONY: clean
你看,$^、$<、$@都用上了。CC、CFLAGS、LDFLAGS也用了,而且用的是?=,方便外部覆盖。这个Makefile我用了好几年,从个人项目到团队项目,基本没出过问题。
好了,这一节就到这里。变量这块儿,说白了就是“少写重复代码,多留扩展接口”。你写Makefile时多用用这些变量,慢慢就会形成肌肉记忆。