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 知识结构图:核心语法关系

下面这张图展示了本章各个知识点之间的关系:

Makefile核心语法(二)知识结构 核心语法体系 模式规则 %.o:%.c 静态模式规则 伪目标 .PHONY 常用函数 wildcard patsubst notdir foreach 核心要点 模式规则减少重复代码 | 静态模式规则精确控制 | .PHONY避免文件冲突 | 函数组合实现自动化

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)/前缀,结果目标文件路径不匹配,规则死活不执行。排查了半小时才发现是路径问题。记住:静态模式规则里的模式必须和目标的实际路径完全一致。