8、Makefile 进阶:自动变量 $@ $^ $<、模式规则、函数使用、多目录 Makefile
好,咱们接着聊 Makefile。上一章我们把基础语法过了一遍,能写一个单文件的编译脚本了。但说实话,那种写法在真实项目里根本不够用。你想想看,一个嵌入式项目少说几十个源文件,多则上百个,要是每个文件都手写一条编译规则,那不得写到天亮?
这一章,我就带你看看真正工程级的 Makefile 长什么样。我会把我在几个量产项目里沉淀下来的写法,掰开揉碎了讲给你听。
8.1 自动变量:偷懒的终极武器
先看一个最常见的场景。你写了一条规则:
main.o: main.c
gcc -c main.c -o main.o
这没问题。但如果有 20 个 .c 文件呢?你难道要写 20 条几乎一模一样的规则?
这时候自动变量就派上用场了。我当年第一次看到 $@ 和 $^ 的时候,心里想的是:卧槽,原来 Makefile 还能这么玩?
| 自动变量 | 含义 | 举例(规则 main.o: main.c head.h) |
|---|---|---|
$@ |
目标文件名 | main.o |
$^ |
所有依赖文件(去重) | main.c head.h |
$< |
第一个依赖文件 | main.c |
$? |
比目标新的依赖文件列表 | (按需) |
有了它们,上面的规则可以写成:
main.o: main.c head.h
gcc -c $< -o $@
$< 自动展开成 main.c,$@ 自动展开成 main.o。你看,代码量直接砍半。
$< 和 $@。$^ 更多用在链接阶段,因为链接需要把所有 .o 文件都传进去。
8.2 模式规则:一条规则管所有
自动变量解决了规则内部的重复,但规则本身还得一条条写。有没有办法用一条规则匹配所有 .c 文件?
有,模式规则。
%.o: %.c
gcc -c $< -o $@
这行代码的意思是:任何以 .o 结尾的目标,如果它对应的 .c 文件存在,就用这条规则编译。% 是通配符,匹配文件名中除去后缀的部分。
我在项目中遇到过一个问题:有人把模式规则里的 % 写成了 *,结果 Makefile 直接报错。记住,Makefile 里 % 才是模式匹配的通配符,* 是 shell 的通配符,只能在命令部分用。
%.o %.elf: %.c 是不行的)。如果你需要多目标,老老实实写多条规则,或者用静态模式规则。
8.3 Makefile 函数:让脚本活起来
光有模式规则还不够。你想想,源文件列表总不能手写吧?万一加了个文件还得改 Makefile,那也太 low 了。
Makefile 内置了一批函数,专门用来处理字符串和文件名。我常用的就三个:
8.3.1 wildcard:自动扫描源文件
SRC = $(wildcard src/*.c)
这行代码会把 src/ 目录下所有 .c 文件都抓出来,赋值给 SRC。加文件?不用改 Makefile,直接往目录里丢 .c 文件就行。
8.3.2 patsubst:批量改后缀
OBJ = $(patsubst %.c, %.o, $(SRC))
把 SRC 里所有 .c 结尾的字符串,换成 .o 结尾。说白了就是:从源文件列表生成目标文件列表。
8.3.3 notdir:去掉目录路径
OBJ = $(patsubst %.c, %.o, $(notdir $(SRC)))
如果源文件在子目录里,SRC 的值可能是 src/main.c src/gpio.c。直接做 patsubst 会得到 src/main.o src/gpio.o,但你可能想把 .o 文件统一放到 build 目录。这时候先用 notdir 去掉路径,再拼上 build 目录前缀。
SRC_DIR = src
BUILD_DIR = build
SRC = $(wildcard $(SRC_DIR)/*.c)
OBJ = $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR)/%.o, $(SRC))
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
gcc -c $< -o $@
这样源文件和目标文件就分开了,目录结构干干净净。
8.4 多目录 Makefile:项目就该这么组织
真实项目不可能所有文件堆在一个目录里。一般会按模块分目录:driver/、app/、kernel/ 等等。这时候 Makefile 怎么写?
我见过两种做法:
- 单 Makefile 递归扫描:用一个顶层的 Makefile,通过 wildcard 递归扫描所有子目录。
- 每个目录一个 Makefile:子目录各自编译成 .a 静态库,顶层 Makefile 再链接。
我个人更推荐第二种。为什么?因为模块独立编译,改一个模块只需要重新编译那个目录,不用全量编译。我在做一个车载项目时,全量编译要 15 分钟,用了分目录编译后,改驱动模块只需要 30 秒。
下面是一个典型的多目录 Makefile 结构:
# 顶层 Makefile
SUB_DIRS = driver app kernel
BUILD_DIR = build
.PHONY: all clean $(SUB_DIRS)
all: $(BUILD_DIR)/final.elf
$(BUILD_DIR)/final.elf: $(SUB_DIRS)
gcc $(BUILD_DIR)/*/lib*.a -o $@
$(SUB_DIRS):
make -C $@ BUILD_DIR=$(abspath $(BUILD_DIR))
clean:
rm -rf $(BUILD_DIR)
for dir in $(SUB_DIRS); do make -C $$dir clean; done
每个子目录的 Makefile 长这样:
# driver/Makefile
SRC = $(wildcard *.c)
OBJ = $(patsubst %.c, $(BUILD_DIR)/driver/%.o, $(SRC))
LIB = $(BUILD_DIR)/driver/libdriver.a
.PHONY: all clean
all: $(LIB)
$(LIB): $(OBJ)
ar rcs $@ $^
$(BUILD_DIR)/driver/%.o: %.c
gcc -c $< -o $@
clean:
rm -f $(OBJ) $(LIB)
BUILD_DIR 是从顶层传下来的。用 abspath 转成绝对路径,避免相对路径在不同目录下解析出错。这个坑我至少帮三个同事填过。
8.5 本章知识体系
下面这张图把本章的核心逻辑串起来了。你可以看到,从自动变量到模式规则,再到函数和多目录,是一条完整的进化链。
你看,从最基础的自动变量开始,一步步搭建成一个完整的多目录构建系统。每一步解决一个问题,没有一步是多余的。
$@是目标,$^是所有依赖,$<是第一个依赖 —— 这三兄弟能省你 80% 的重复代码。- 模式规则
%.o: %.c一条规则管所有,但注意%和*的区别。 - wildcard 自动扫描源文件,patsubst 批量改后缀,notdir 去掉路径 —— 这三个函数是处理文件列表的黄金组合。
- 多目录项目推荐每个子目录独立编译成 .a,顶层 Makefile 只负责链接和调度。
嗯,这一章的内容就到这。Makefile 这东西,光看是学不会的。我建议你找个真实项目,哪怕是个小工程,亲手把 Makefile 从单文件改到多目录。踩几个坑,你就真正掌握了。
公众号:蓝海资料掘金营,微信deep3321