4. Makefile进阶:条件判断、函数使用、多目标编译、伪目标
好,咱们接着聊。上一章我们把Makefile的基本规则捋了一遍,能写个简单的单文件编译了。但实际项目哪有这么简单?一个工程少说几十个源文件,还有各种配置开关。今天我就把Makefile里真正好用的几个进阶功能讲透。
4.1 条件判断:让Makefile“活”起来
条件判断,说白了就是让Makefile根据不同的情况走不同的分支。我最早接触这个功能,是因为一个项目要同时支持Debug和Release两种编译模式。
语法其实很简单:
ifeq ($(变量), 值)
# 成立时执行
else
# 不成立时执行
endif
举个例子,我习惯这样写:
DEBUG ?= 0
ifeq ($(DEBUG), 1)
CFLAGS = -g -O0 -DDEBUG
else
CFLAGS = -O2 -DNDEBUG
endif
all:
gcc $(CFLAGS) main.c -o main
你想想看,只要在命令行敲make DEBUG=1,就能切到调试模式。是不是很方便?
常用的判断还有:
ifneq— 不相等时执行ifdef— 变量已定义时执行ifndef— 变量未定义时执行
?= 赋值,这样用户可以在命令行覆盖默认值。我曾经见过同事用 = 赋值,结果想改个编译选项还得去改Makefile,太死板了。
4.2 函数使用:别重复造轮子
Makefile内置了不少实用函数。说实话,我刚开始也觉得记这些函数麻烦,但用熟了之后,真香。
4.2.1 字符串处理函数
| 函数 | 作用 | 示例 |
|---|---|---|
$(wildcard *.c) |
获取所有.c文件 | 返回 main.c foo.c bar.c |
$(patsubst %.c,%.o,$(SRC)) |
替换后缀 | 把.c换成.o |
$(notdir $(PATH)) |
去掉路径 | src/main.c → main.c |
我最常用的组合拳是这样的:
SRC = $(wildcard src/*.c)
OBJ = $(patsubst src/%.c, obj/%.o, $(SRC))
$(OBJ): obj/%.o: src/%.c
gcc -c $< -o $@
这段代码什么意思?自动扫描src/目录下所有.c文件,然后生成对应的.o文件到obj/目录。加一个新源文件?什么都不用改,直接make就行。
wildcard 自动收集源文件,用 patsubst 生成目标文件列表。这样Makefile就“自适应”了。
4.2.2 其他实用函数
$(foreach var, list, text)— 循环处理列表$(shell command)— 执行shell命令$(info text)— 打印调试信息
举个例子,我曾经需要为多个子目录分别编译:
SUBDIRS = src lib test
define build_subdir
$(MAKE) -C $(1)
endef
all:
$(foreach dir, $(SUBDIRS), $(call build_subdir, $(dir)))
嗯,这里要注意:foreach 和 call 配合使用,能写出很灵活的编译框架。
4.3 多目标编译:一次编译,到处生成
实际项目中,一个Makefile往往要生成多个目标。比如一个库文件加一个可执行文件,或者同时生成静态库和动态库。
写法其实很直接:
all: libfoo.a libfoo.so app
libfoo.a: foo.o bar.o
ar rcs $@ $^
libfoo.so: foo.o bar.o
gcc -shared -o $@ $^
app: main.o libfoo.a
gcc main.o -L. -lfoo -o $@
你想想看,一个make命令下去,静态库、动态库、可执行文件全出来了。是不是很爽?
app 依赖 libfoo.a,如果你先改了 foo.c 然后只 make app,Makefile会自动先编译 libfoo.a。这就是依赖追踪的魅力。
4.4 伪目标:别让文件名坑了你
伪目标这个概念,我第一次接触时觉得有点绕。说白了,就是告诉Make:“这个目标不是文件,别拿文件名跟我较真。”
最常见的伪目标就是 clean:
.PHONY: clean
clean:
rm -f *.o *.a *.so app
为什么要加 .PHONY?假设你的目录里恰好有一个叫 clean 的文件,不加伪目标的话,Make会认为 clean 已经是最新的了,什么都不做。我当年就因为这个bug排查了半天...
常用的伪目标还有:
all— 默认目标clean— 清理install— 安装test— 运行测试
我习惯把所有非文件目标都声明为伪目标:
.PHONY: all clean install test dist
.PHONY。宁可多写,不要漏写。这是血的教训换来的经验。
4.5 综合示例:一个完整的进阶Makefile
最后,我把今天讲的内容串起来,给你看一个实际项目里能用的Makefile:
CC = gcc
AR = ar
DEBUG ?= 0
# 条件判断:编译模式
ifeq ($(DEBUG), 1)
CFLAGS = -g -O0 -Wall -DDEBUG
else
CFLAGS = -O2 -Wall -DNDEBUG
endif
# 自动收集源文件
SRC_DIR = src
OBJ_DIR = obj
SRC = $(wildcard $(SRC_DIR)/*.c)
OBJ = $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(SRC))
# 多目标
TARGETS = libfoo.a app
.PHONY: all clean
all: $(TARGETS)
# 编译规则
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR)
$(CC) $(CFLAGS) -c $< -o $@
$(OBJ_DIR):
mkdir -p $@
# 静态库
libfoo.a: $(OBJ)
$(AR) rcs $@ $^
# 可执行文件
app: main.o libfoo.a
$(CC) main.o -L. -lfoo -o $@
clean:
rm -rf $(OBJ_DIR) *.o *.a app
这个Makefile包含了我们今天讲的所有内容:条件判断、函数使用、多目标编译、伪目标。你拿去改改就能用在真实项目里。
好了,这一章就到这。下一章我们聊聊Makefile的终极形态——自动依赖生成和模板化编译。到时候你会发现,原来Makefile可以写得这么优雅。