3. Makefile 进阶:自动变量、模式规则、函数与条件判断
Makefile 写多了你就会发现,重复劳动特别多。一个个写目标、写依赖、写编译命令,项目一膨胀,Makefile 比源码还长。我早期带团队时就吃过这个亏——一个嵌入式项目,200 多个 .c 文件,Makefile 写了 800 多行,改一次依赖能改到怀疑人生。
后来我悟了:Makefile 不是用来「写死」的,是用来「推导」的。自动变量、模式规则、内置函数、条件判断,这四样东西就是 Makefile 的「元编程」能力。今天我们就把它啃透。
3.1 自动变量:少写重复路径
先看一个最常见的场景:
obj/main.o: src/main.c
gcc -c src/main.c -o obj/main.o
obj/utils.o: src/utils.c
gcc -c src/utils.c -o obj/utils.o
你发现没有?目标和依赖的路径在命令里又写了一遍。改个目录名,得改三处。这就是自动变量要解决的问题。
Make 内置了这么几个自动变量,我几乎每天都在用:
| 变量 | 含义 | 示例值(目标 obj/main.o) |
|---|---|---|
$@ |
当前规则的目标文件 | obj/main.o |
$< |
第一个依赖文件 | src/main.c |
$^ |
所有依赖文件(去重) | src/main.c |
$+ |
所有依赖文件(不去重) | src/main.c |
$* |
目标中 % 匹配的部分 | main |
用自动变量重写上面的规则:
obj/main.o: src/main.c
gcc -c $< -o $@
obj/utils.o: src/utils.c
gcc -c $< -o $@
嗯,命令行干净多了。但规则还是两条,能不能合并?能,模式规则就是干这个的。
3.2 模式规则:一条规则匹配一类文件
模式规则用 % 做通配符,匹配文件名中相同的部分。上面两条规则可以合并成一条:
obj/%.o: src/%.c
gcc -c $< -o $@
这条规则的意思是:任何 obj/xxx.o 都由 src/xxx.c 编译而来。Make 会自动推导 % 匹配的部分,然后赋值给 $*。
模式规则还能处理更复杂的场景。比如我有个项目,头文件散落在 include/ 和 common/ 两个目录:
obj/%.o: src/%.c
gcc -c $< -I include -I common -o $@
一条规则,搞定所有 .c 到 .o 的编译。你想想看,如果不用模式规则,200 个文件就得写 200 条规则,那画面太美我不敢看。
3.3 函数使用:wildcard 与 patsubst
模式规则解决了「怎么写规则」的问题,但「依赖列表从哪来」还得靠函数。我最常用的两个函数是 wildcard 和 patsubst。
3.3.1 wildcard:自动收集文件
手动列出所有源文件?不存在的。用 wildcard 让 Make 自己去扫目录:
SRC = $(wildcard src/*.c)
这行代码会把 src/ 下所有 .c 文件都塞进 SRC 变量。新增文件?不用改 Makefile,重新 make 就行。
3.3.2 patsubst:批量替换路径
有了源文件列表,接下来要生成目标文件列表。把 src/xxx.c 变成 obj/xxx.o,patsubst 就是干这个的:
OBJ = $(patsubst src/%.c, obj/%.o, $(SRC))
说白了就是:从 $(SRC) 里拿出每个元素,把 src/%.c 模式替换成 obj/%.o。
这两个函数组合起来,一个完整的编译流程就出来了:
SRC = $(wildcard src/*.c)
OBJ = $(patsubst src/%.c, obj/%.o, $(SRC))
obj/%.o: src/%.c
gcc -c $< -o $@
app: $(OBJ)
gcc $^ -o $@
你看,整个 Makefile 只有 7 行。新增文件?不用改。改目录名?改两处变量就行。这就是函数化带来的维护红利。
3.4 条件判断:让 Makefile 有「脑子」
项目大了,不同环境、不同配置需要不同的编译行为。条件判断就是 Makefile 的 if-else。
基本语法长这样:
ifeq ($(CC), gcc)
CFLAGS += -Wall -Wextra
else ifeq ($(CC), clang)
CFLAGS += -Weverything
else
CFLAGS += -O2
endif
常用的条件关键字有四个:
| 关键字 | 含义 | 示例 |
|---|---|---|
ifeq |
相等判断 | ifeq ($(DEBUG), 1) |
ifneq |
不等判断 | ifneq ($(PLATFORM), arm) |
ifdef |
变量已定义 | ifdef CROSS_COMPILE |
ifndef |
变量未定义 | ifndef RELEASE |
我在实际项目中经常这样用:
# 调试模式:加 -g,不开优化
ifeq ($(DEBUG), 1)
CFLAGS = -g -O0
else
CFLAGS = -O2
endif
# 交叉编译:如果定义了 CROSS_COMPILE,就用它
ifdef CROSS_COMPILE
CC = $(CROSS_COMPILE)gcc
AR = $(CROSS_COMPILE)ar
endif
调用时传变量就行:
make DEBUG=1
make CROSS_COMPILE=arm-linux-gnueabihf-
ifdef 只检查变量是否被定义,不检查值是否为空。想检查空字符串,用 ifeq ($(VAR),)。
3.5 知识体系总览
下面这张图把今天讲的内容串起来了。你可以把它当作 Makefile 进阶的「思维导图」:
3.6 实战:一个完整的自动化构建 Makefile
最后,我把今天讲的所有知识点揉进一个完整的示例里。你可以直接拿去用:
# 配置区
CC = gcc
CFLAGS = -Wall -O2
LDFLAGS =
TARGET = app
SRC_DIR = src
OBJ_DIR = obj
# 调试模式
ifeq ($(DEBUG), 1)
CFLAGS = -g -O0
endif
# 自动收集源文件
SRC = $(wildcard $(SRC_DIR)/*.c)
OBJ = $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(SRC))
# 默认目标
all: $(TARGET)
# 模式规则:编译
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(OBJ_DIR)
$(CC) $(CFLAGS) -c $< -o $@
# 链接
$(TARGET): $(OBJ)
$(CC) $(LDFLAGS) $^ -o $@
# 清理
clean:
rm -rf $(OBJ_DIR) $(TARGET)
.PHONY: all clean
这个 Makefile 只有 30 行,但能自动处理任意数量的源文件。新增文件?重新 make 就行。切换调试模式?make DEBUG=1。想换编译器?改一行 CC 变量。
嗯,这就是 Makefile 进阶的威力。你想想看,如果每个项目都按这个套路来,维护成本能降多少?