3、Makefile函数应用:字符串处理函数、文件名函数与条件判断
说实话,Makefile 里最容易被忽略的就是函数。很多人写 Makefile 就是堆变量、写规则,遇到复杂点的路径处理就傻眼了。我早年带团队时,见过一个项目,光路径拼接就写了三十多行重复代码——其实几个函数就能搞定。
这一节,咱们把 Makefile 里最常用的三类函数掰开揉碎讲清楚。字符串处理、文件名操作、条件判断,这三板斧用好了,你的 Makefile 能瘦身一半。
3.1 字符串处理函数
字符串处理是 Makefile 里最基础也最常用的。说白了,就是让你能灵活地操作文本内容。
3.1.1 subst —— 文本替换
subst 函数做的是简单的字符串替换。它的语法是:
$(subst 被替换的字符串, 替换成的字符串, 原始文本)
举个例子:
SRC := main.c utils.c driver.c
OBJ := $(subst .c,.o,$(SRC))
# OBJ 的值是 main.o utils.o driver.o
我在项目中遇到过一个问题:有个同事用 sed 在 Makefile 里做替换,结果不同平台上的 sed 行为不一致,导致编译失败。换成 subst 后,干干净净,跨平台无压力。
3.1.2 patsubst —— 模式替换
patsubst 比 subst 更灵活。它支持通配符 %,可以匹配任意长度的字符串。
$(patsubst 模式, 替换模式, 文本列表)
看个实际例子:
SRC := src/main.c src/utils.c src/driver.c
OBJ := $(patsubst src/%.c, build/%.o, $(SRC))
# OBJ 的值是 build/main.o build/utils.o build/driver.o
嗯,这里要注意:% 匹配的部分会被保留,然后塞到替换模式里。这个特性在做目录映射时特别有用。
3.1.3 strip —— 去空格
strip 函数的作用很简单:去掉字符串首尾的空格,并把中间连续的空格压缩成一个。
STR := " main.c utils.c driver.c "
CLEAN := $(strip $(STR))
# CLEAN 的值是 "main.c utils.c driver.c"
你想想看,如果从某个配置文件里读出来的路径带了一堆多余空格,编译时就会报"文件找不到"。用 strip 处理一下,问题就解决了。
3.2 文件名函数
文件名函数是专门用来处理文件路径的。我当年从 Windows 转到 Linux 做嵌入式开发时,最头疼的就是路径分隔符。Makefile 里这些函数帮我省了不少事。
3.2.1 dir —— 取目录
dir 从文件路径中提取目录部分:
$(dir src/main.c) # 返回 "src/"
$(dir /home/user/a.c) # 返回 "/home/user/"
3.2.2 notdir —— 取文件名
notdir 和 dir 正好相反,它只保留文件名:
$(notdir src/main.c) # 返回 "main.c"
$(notdir /home/user/a.c) # 返回 "a.c"
3.2.3 basename —— 取主文件名
basename 去掉文件扩展名:
$(basename main.c) # 返回 "main"
$(basename src/utils.o) # 返回 "src/utils"
3.2.4 suffix —— 取扩展名
suffix 提取文件后缀:
$(suffix main.c) # 返回 ".c"
$(suffix src/utils.o) # 返回 ".o"
这几个函数组合起来用,威力很大。比如自动生成依赖文件时:
SRC := src/main.c src/utils.c
OBJ := $(patsubst %.c, %.o, $(SRC))
DEP := $(patsubst %.o, %.d, $(OBJ))
# 或者一步到位:
DEP := $(addsuffix .d, $(basename $(SRC)))
3.3 条件判断函数
条件判断让 Makefile 有了"脑子"。不同平台、不同配置下,可以执行不同的逻辑。
3.3.1 if 系列函数
Makefile 里有三种条件判断写法:
# 方式一:条件关键字
ifeq ($(OS),Linux)
CC := gcc
else
CC := arm-none-eabi-gcc
endif
# 方式二:if 函数
CC = $(if $(filter Linux,$(OS)), gcc, arm-none-eabi-gcc)
# 方式三:条件赋值
CC ?= gcc # 如果 CC 没定义过,才赋值
我个人习惯用 if 函数,因为它可以嵌在表达式里,写起来更紧凑。但如果是多分支判断,还是 ifeq/ifneq 更清晰。
3.3.2 and/or 函数
and 和 or 用于组合多个条件:
# 两个条件都满足才执行
$(and $(CONFIG_A), $(CONFIG_B))
# 任一条件满足就执行
$(or $(CONFIG_A), $(CONFIG_B))
举个例子,我在做交叉编译时经常这么用:
# 只有定义了 CROSS_COMPILE 且不是空字符串,才用交叉编译器
ifneq ($(strip $(CROSS_COMPILE)),)
CC = $(CROSS_COMPILE)gcc
else
CC = gcc
endif
3.4 知识体系总览
下面这张图把本章的核心内容串起来了。你可以把它当作一个速查表:
3.5 实战组合拳
光讲理论没意思。咱们看一个实际场景:自动收集源码、生成目标文件、根据平台选择编译器。
# 自动收集所有 .c 文件
SRC_DIR := src
SRC := $(wildcard $(SRC_DIR)/*.c)
# 生成目标文件路径(保持目录结构)
OBJ_DIR := build
OBJ := $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(SRC))
# 根据平台选择编译器
ifeq ($(OS),Windows_NT)
CC := gcc
RM := del /Q
else
CC := gcc
RM := rm -f
endif
# 生成依赖文件
DEP := $(patsubst %.o, %.d, $(OBJ))
# 编译规则
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(dir $@)
$(CC) -c $< -o $@
# 包含依赖文件
-include $(DEP)
这段代码里,wildcard、patsubst、dir、条件判断全用上了。你想想看,如果没有这些函数,你得写多少行?
好了,这一节的内容就到这儿。函数这东西,光看没用,得动手敲。下次写 Makefile 时,试着用 patsubst 替换掉你手写的路径拼接,你会爱上它的。