3. Makefile基础语法
上一章我们聊了变量和自动变量,算是入了Makefile的门。这一章咱们继续深入,把模式规则、伪目标这几个硬骨头啃下来。说实话,这些才是Makefile真正好用的地方。我刚开始用Makefile时,写了一大堆重复规则,后来才发现模式规则能省掉80%的代码量。
3.1 模式规则:%.o : %.c 的魔力
模式规则,说白了就是通配符规则。它用%来匹配文件名,一个规则就能处理所有同类文件。
基本语法很简单:
%.o : %.c
$(CC) -c $(CFLAGS) $< -o $@
这个规则的意思是:任何.o文件都依赖于同名的.c文件。%匹配文件名中除去后缀的部分。$<是第一个依赖(.c文件),$@是目标(.o文件)。
# 匹配所有 .o 目标,这样三个月后回来看代码,一眼就能明白。
来看个完整例子:
CC = gcc
CFLAGS = -Wall -O2
# 模式规则:所有 .o 都由同名的 .c 编译而来
%.o : %.c
$(CC) -c $(CFLAGS) $< -o $@
# 最终目标
app : main.o utils.o io.o
$(CC) $^ -o $@
这个Makefile里,main.o、utils.o、io.o都会自动匹配到模式规则,不需要为每个文件单独写规则。
%只能匹配文件名的一部分,不能匹配路径中的目录分隔符。比如%.o : %.c不会匹配src/main.o,因为%不能跨目录。这个问题我踩过坑,后面会讲怎么解决。
3.2 静态模式规则:更精准的匹配
模式规则虽然好用,但它太"泛"了——它会匹配所有符合模式的目标。有时候你只想对特定列表中的目标应用规则,这时候就该静态模式规则上场了。
语法:
<目标列表> : <目标模式> : <依赖模式>
命令
举个例子:
# 只对这三个目标应用规则
objects = main.o utils.o io.o
$(objects) : %.o : %.c
$(CC) -c $(CFLAGS) $< -o $@
这里只有main.o、utils.o、io.o会匹配这个规则。如果目录下还有test.o,它不会受影响。
| 特性 | 模式规则 | 静态模式规则 |
|---|---|---|
| 匹配范围 | 所有符合模式的目标 | 仅限指定的目标列表 |
| 适用场景 | 通用编译规则 | 对特定文件定制规则 |
| 灵活性 | 高,自动匹配 | 可控,精确匹配 |
| 性能 | 可能匹配过多目标 | 只处理指定目标 |
我在项目中遇到过这样一个场景:大部分.c文件用默认编译选项,但有几个文件需要特殊优化参数。静态模式规则就派上用场了:
# 普通文件用默认选项
common_objs = file1.o file2.o file3.o
$(common_objs) : %.o : %.c
$(CC) -c $(CFLAGS) $< -o $@
# 性能关键文件用-O3
perf_objs = fast_math.o dsp_core.o
$(perf_objs) : %.o : %.c
$(CC) -c $(CFLAGS) -O3 $< -o $@
3.3 伪目标 .PHONY:别把命令当文件
伪目标是个容易忽略但极其重要的概念。你想想看,如果Makefile里有个目标叫clean,恰巧目录下也有个叫clean的文件,会发生什么?
Make会检查clean文件是否存在,如果存在且没有依赖更新,它就什么都不做。这就是为什么有时候执行make clean没反应——目录里有个同名文件。
解决办法就是声明伪目标:
.PHONY: clean all install
clean:
rm -f *.o app
all: app
install: app
cp app /usr/local/bin/
.PHONY告诉Make:这些目标不是真实文件,每次都执行命令。
all、clean、install、test、distclean、help等。养成习惯,能避免很多诡异的问题。
我曾经在一个项目里,因为忘了加.PHONY,导致make clean在某个分支上完全失效。排查了半天,才发现是有人提交了一个叫clean的空文件。从那以后,我写Makefile第一件事就是列伪目标。
3.4 常用函数:wildcard、patsubst、notdir、foreach
Makefile的函数是它的"瑞士军刀"。掌握了这几个,大部分文件操作都能搞定。
3.4.1 wildcard:文件通配符展开
wildcard函数用来获取匹配模式的文件列表:
SRCS = $(wildcard *.c)
# 结果:SRCS = main.c utils.c io.c test.c
它比直接写*.c更可靠。直接写*.c是在变量定义时展开,而wildcard是在使用时展开,更灵活。
我常用的模式:
# 递归查找所有子目录的.c文件
SRCS = $(wildcard src/*.c) $(wildcard lib/*.c)
# 或者用递归通配符(需要自己实现)
3.4.2 patsubst:模式替换
patsubst用来做模式化的字符串替换,语法是$(patsubst 模式,替换,文本):
SRCS = $(wildcard *.c)
# 把 .c 替换成 .o
OBJS = $(patsubst %.c, %.o, $(SRCS))
# 结果:OBJS = main.o utils.o io.o test.o
还有个简写形式:$(SRCS:.c=.o),效果一样,但只能做后缀替换。
patsubst,简单的后缀替换用简写形式。比如OBJS = $(SRCS:.c=.o),代码更简洁。
3.4.3 notdir:去掉目录路径
notdir从文件路径中提取文件名:
SRCS = $(wildcard src/*.c lib/*.c)
# SRCS = src/main.c src/utils.c lib/io.c
FILES = $(notdir $(SRCS))
# FILES = main.c utils.c io.c
这个函数在组织多目录项目时特别有用。比如你想把所有源文件编译到同一个输出目录:
SRCS = $(wildcard src/*.c lib/*.c)
OBJS = $(addprefix build/, $(notdir $(SRCS:.c=.o)))
# OBJS = build/main.o build/utils.o build/io.o
3.4.4 foreach:循环处理
foreach是Makefile里最强大的函数之一,它让你能遍历列表做处理:
DIRS = src lib test
# 为每个目录生成编译目标
OBJS = $(foreach dir, $(DIRS), $(wildcard $(dir)/*.c))
# 等价于:$(wildcard src/*.c) $(wildcard lib/*.c) $(wildcard test/*.c)
语法:$(foreach 变量, 列表, 表达式)。它会遍历列表,把每个元素赋给变量,然后计算表达式,最后把所有结果拼起来。
来看个更实用的例子——多目录项目的完整构建:
SRC_DIRS = src lib test
# 收集所有源文件
SRCS = $(foreach dir, $(SRC_DIRS), $(wildcard $(dir)/*.c))
# 生成目标文件列表(放在build目录下)
OBJS = $(patsubst %.c, build/%.o, $(notdir $(SRCS)))
# 模式规则:编译到build目录
build/%.o : src/%.c
$(CC) -c $(CFLAGS) $< -o $@
build/%.o : lib/%.c
$(CC) -c $(CFLAGS) $< -o $@
build/%.o : test/%.c
$(CC) -c $(CFLAGS) $< -o $@
这里有个问题——每个目录都要写一条规则。有没有更优雅的方式?嗯,可以用vpath指令,但那是后面章节的内容了。
3.5 知识结构图:核心语法关系
下面这张图展示了本章各个知识点之间的关系:
3.6 综合实战:把这些串起来
最后,咱们把本章所有知识点组合成一个完整的Makefile。这个例子来自我去年做的一个嵌入式项目,稍微简化了一下:
# 编译器配置
CC = arm-none-eabi-gcc
CFLAGS = -mcpu=cortex-m4 -mthumb -Wall -O2
# 目录结构
SRC_DIRS = src/driver src/kernel src/app
BUILD_DIR = build
# 收集源文件
SRCS = $(foreach dir, $(SRC_DIRS), $(wildcard $(dir)/*.c))
# 生成目标文件路径
OBJS = $(patsubst %.c, $(BUILD_DIR)/%.o, $(notdir $(SRCS)))
# 伪目标声明
.PHONY: all clean flash
# 默认目标
all: $(BUILD_DIR)/firmware.elf
# 静态模式规则:编译所有源文件
$(OBJS) : $(BUILD_DIR)/%.o : %.c
@mkdir -p $(BUILD_DIR)
$(CC) -c $(CFLAGS) $< -o $@
# 链接
$(BUILD_DIR)/firmware.elf : $(OBJS)
$(CC) $^ -o $@ -T linker.ld
# 清理
clean:
rm -rf $(BUILD_DIR)
# 烧录
flash: $(BUILD_DIR)/firmware.elf
openocd -f board/stm32f4discovery.cfg -c "program $< verify reset exit"
这个Makefile里,foreach + wildcard自动发现所有源文件,patsubst + notdir生成目标文件路径,静态模式规则精确控制编译,.PHONY保护了clean和flash。你看,这些语法组合起来,威力就出来了。
$(BUILD_DIR)/前缀,结果目标文件路径不匹配,规则死活不执行。排查了半小时才发现是路径问题。记住:静态模式规则里的模式必须和目标的实际路径完全一致。