4. 模式规则与静态模式
好,咱们今天聊聊 Makefile 里一个特别实用的东西——模式规则和静态模式。
说实话,我刚开始写 Makefile 的时候,也是一条一条规则手写。比如编译三个 .c 文件,我就写三条规则。后来项目大了,文件一多,手写就成噩梦了。你想想看,一个项目几十个源文件,每个都要写一条编译规则?那不得写到天亮。
这时候,模式规则就派上用场了。
4.1 %通配符:模式匹配的核心
模式规则里最核心的就是 % 这个通配符。它跟 shell 里的 * 有点像,但又不完全一样。% 在 Makefile 里专门用来做模式匹配。
它的工作方式是这样的:% 可以匹配任意长度的字符串(包括空字符串)。而且,同一个规则里,第一个 % 匹配到的内容,会传递给第二个 %。
举个例子:
%.o: %.c
gcc -c $< -o $@
这条规则的意思是:任何以 .o 结尾的目标文件,都依赖于同名的 .c 文件。比如你要生成 main.o,Make 就会去找 main.c,然后执行编译命令。
关键点: % 匹配的是文件名中除去后缀的部分。在 %.o 里,% 匹配的是 main;在 %.c 里,% 匹配的也是 main。这就是「同名匹配」的原理。
我个人习惯把模式规则放在 Makefile 的前面,这样 Make 会优先尝试匹配模式规则。如果匹配不上,再去找显式规则。
4.2 静态模式规则:精准控制
模式规则虽然好用,但它有个问题:太「泛」了。它会匹配所有符合模式的目标。有时候,我只想对特定的几个文件应用规则,而不是全部。
这时候,静态模式规则就登场了。
静态模式规则的语法是这样的:
目标列表 : 目标模式 : 依赖模式
命令
看个例子:
OBJS = main.o utils.o data.o
$(OBJS): %.o: %.c
gcc -c $< -o $@
这条规则的意思是:只对 OBJS 列表里的文件应用模式规则。其他 .o 文件,就算存在对应的 .c 文件,也不会被这条规则处理。
我的经验: 我在项目中遇到过这样的情况——项目里既有需要编译的源文件,又有一些预编译的 .o 文件。如果用普通模式规则,Make 会尝试去编译所有 .o 文件,包括那些不需要重新编译的。用静态模式规则,就能精确控制哪些文件走编译流程。
静态模式规则的好处是:
- 精准:只对指定目标生效
- 可读性强:一眼就能看出哪些文件受规则影响
- 避免误匹配:不会意外匹配到不该匹配的文件
4.3 多目标构建:一次搞定
有时候,我们需要同时构建多个目标。比如,一个库文件可能由多个 .o 文件组成。或者,一个项目需要生成多个可执行文件。
多目标构建,说白了就是让一条规则同时处理多个目标。
最直接的方式:
all: program1 program2 program3
program1: program1.o lib.a
gcc program1.o lib.a -o program1
program2: program2.o lib.a
gcc program2.o lib.a -o program2
program3: program3.o lib.a
gcc program3.o lib.a -o program3
嗯,这样写虽然能工作,但太啰嗦了。三个程序还好,要是三十个呢?
更好的做法是用模式规则配合多目标:
PROGRAMS = program1 program2 program3
all: $(PROGRAMS)
$(PROGRAMS): %: %.o lib.a
gcc $< lib.a -o $@
这里,%: %.o lib.a 是一个静态模式规则,它告诉 Make:对于 PROGRAMS 里的每个程序,都去找同名的 .o 文件,然后链接成可执行文件。
注意: 我曾经踩过一个坑——多目标规则里,如果多个目标共享同一个依赖文件,Make 可能会重复执行命令。比如上面的例子,如果 lib.a 更新了,Make 会重新链接所有程序。这没问题。但如果你不小心把命令写成了 gcc $< -o $@,而 $< 只代表第一个依赖,那就会出问题。
多目标构建的另一个常见场景是生成多个版本的库:
LIBS = libfoo.a libfoo.so libfoo.dylib
all: $(LIBS)
libfoo.a: foo.o
ar rcs $@ $<
libfoo.so: foo.o
gcc -shared -o $@ $<
libfoo.dylib: foo.o
gcc -dynamiclib -o $@ $<
当然,你也可以用模式规则来简化:
LIBS = libfoo.a libfoo.so libfoo.dylib
all: $(LIBS)
%.a: %.o
ar rcs $@ $<
%.so: %.o
gcc -shared -o $@ $<
%.dylib: %.o
gcc -dynamiclib -o $@ $<
4.4 知识体系图
下面这张图展示了模式规则、静态模式和多目标构建之间的关系:
4.5 避坑指南
最后,分享几个我踩过的坑:
- % 匹配的优先级:Make 会先尝试显式规则,再尝试模式规则。如果你同时写了显式规则和模式规则,显式规则优先。
- 静态模式中的空格:目标列表和冒号之间不要有多余空格。我见过有人写
$(OBJS) : %.o: %.c,结果 Make 报错。正确的写法是$(OBJS): %.o: %.c。 - 多目标命令的重复执行:如果多个目标共享同一个命令,Make 会为每个目标执行一次命令。这通常是期望的行为,但如果你不小心,可能会造成不必要的重复编译。
- 我曾经犯过一个错:在静态模式规则里用了
$^而不是$<,结果把所有依赖都链接进去了,导致链接失败。记住,$<是第一个依赖,$^是所有依赖。
小技巧: 如果你不确定模式规则是否匹配正确,可以用 make -p 查看 Make 的内部数据库,它会显示所有规则和变量的值。这招帮我排查过不少问题。
好了,模式规则和静态模式就讲到这里。这些东西看着简单,用好了能让你的 Makefile 精简不少。下次写 Makefile 的时候,试试用模式规则替代那些重复的显式规则,你会爱上这种感觉的。