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就行。我在公司里推行这套写法后,团队里再也没出现过“编译不过去,原来是因为编译器版本不对”这种破事。

注意: 预定义变量可以被覆盖。如果你在Makefile里写了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构建时用优化选项。两不耽误。

核心原则: Makefile变量 > 命令行变量 > 环境变量。但?=是个例外,它只在变量未定义时才赋值。

3.4 一张图看懂变量体系

说了这么多,我画张图帮你理一理。这张图是我自己总结的,每次带新人时都会拿出来讲一遍。

Makefile 变量体系总览 自动变量 • $@ — 目标文件 • $< — 第一个依赖 • $^ — 所有依赖 • $? — 更新的依赖 在规则内部自动生效 预定义变量 • CC — C编译器 • CFLAGS — 编译选项 • LDFLAGS — 链接选项 • AR, RM 等 约定优于配置 环境变量 • PATH • CFLAGS (外部传入) • 通过 export 设置 • 优先级最低 CI/CD 中常用 用于规则命令 用于编译链接 可被覆盖 Makefile 规则 $(CC) $(CFLAGS) -c $< -o $@ 优先级:Makefile显式赋值 > 命令行参数 > 环境变量 ?= 仅在变量未定义时赋值,给环境变量留出空间

这张图把三种变量和它们的关系说清楚了。自动变量服务于规则内部,预定义变量是行业惯例,环境变量则用于外部传入。三者各司其职,互不冲突。

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时,别急着搞花哨的。先把自动变量和预定义变量用熟,再慢慢接触环境变量。一口吃不成胖子,Makefile这东西,用多了自然就熟了。

好了,这一节就到这里。变量这块儿,说白了就是“少写重复代码,多留扩展接口”。你写Makefile时多用用这些变量,慢慢就会形成肌肉记忆。